import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import * as TestApp from '../../../../test/App.js' import * as EarlyAccess from '../../../db/tables/earlyAccess.js' import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js' import * as Organizations from '../../../db/tables/organizations.js' import * as Projects from '../../../db/tables/projects.js' import * as Users from '../../../db/tables/users.js' import type * as Log from '../../../internal/Log.js' import * as Orgs from './orgs.js' /** Origin pinned for SIWE domain binding; Hono test requests use this host. */ const origin = 'http://localhost' /** Super admin secret configured on the test app. */ const secret = 'tempo:sk:b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1' /** Wildcard-scoped key: full API access, still not a super admin. */ const wildcard = { id: 'key_wildcard', orgId: 'org_a', scopes: ['*'], token: 'secret_wildcard_key', } satisfies TestApp.kvStore.Key /** Key limited to management reads for Org A. */ const managementRead = { id: 'key_management_read', orgId: 'org_a', scopes: ['management:read'], token: 'secret_management_read_key', } satisfies TestApp.kvStore.Key /** Key limited to management writes for Org A. */ const managementWrite = { id: 'key_management_write', orgId: 'org_a', scopes: ['management:write'], token: 'secret_management_write_key', } satisfies TestApp.kvStore.Key /** RequestInit presenting the given token. */ function as(token: string) { return { headers: { 'tempo-api-key': token } } as const } /** RequestInit for a JSON mutation carrying the session cookie. */ function json(method: 'PATCH' | 'POST', cookie: string, body: unknown) { return { body: JSON.stringify(body), headers: { 'content-type': 'application/json', cookie }, method, } } function createApp(db = TestApp.database()) { return TestApp.create({ auth: { keys: [TestApp.key, managementRead, managementWrite, wildcard], superAdmin: { secret }, }, db, session: { wallet: { origin } }, }) } /** App over a database seeded with two organizations created by the super admin. */ async function seeded() { const db = TestApp.database() await Organizations.create(db, { id: 'org_a', name: 'Org A' }) await Organizations.create(db, { id: 'org_b', name: 'Org B' }) return createApp(db) } /** Signs in a fresh scripted account; returns its session cookie. */ async function session(app: TestApp.signIn.App) { const account = privateKeyToAccount(generatePrivateKey()) const { cookie } = await TestApp.signIn(app, account) return cookie! } test('publishes generator-ready OpenAPI contracts', async () => { const spec = await (await createApp().request('/openapi.json')).json() const collection = spec.paths['/v1/orgs'] const detail = spec.paths['/v1/orgs/{orgId}'] expect({ components: [ 'CreateOrganizationRequest', 'DeleteOrganizationResponse', 'Organization', 'OrganizationList', 'UpdateOrganizationRequest', ].filter((name) => spec.components.schemas[name]), create: { errors: { 400: collection.post.responses[400].content['application/json'].schema, 403: collection.post.responses[403].content['application/json'].schema, }, operationId: collection.post.operationId, request: collection.post.requestBody.content['application/json'].schema, response: collection.post.responses[200].content['application/json'].schema, }, delete: { errors: { 400: detail.delete.responses[400].content['application/json'].schema, 403: detail.delete.responses[403].content['application/json'].schema, 404: detail.delete.responses[404].content['application/json'].schema, 409: detail.delete.responses[409].content['application/json'].schema, }, operationId: detail.delete.operationId, response: detail.delete.responses[200].content['application/json'].schema, }, get: { errors: { 400: detail.get.responses[400].content['application/json'].schema, 403: detail.get.responses[403].content['application/json'].schema, 404: detail.get.responses[404].content['application/json'].schema, }, operationId: detail.get.operationId, response: detail.get.responses[200].content['application/json'].schema, }, list: { errors: { 400: collection.get.responses[400].content['application/json'].schema, 403: collection.get.responses[403].content['application/json'].schema, }, operationId: collection.get.operationId, response: collection.get.responses[200].content['application/json'].schema, }, timestamps: { createdAt: spec.components.schemas.Organization.properties.createdAt.format, updatedAt: spec.components.schemas.Organization.properties.updatedAt.format, }, update: { errors: { 400: detail.patch.responses[400].content['application/json'].schema, 403: detail.patch.responses[403].content['application/json'].schema, 404: detail.patch.responses[404].content['application/json'].schema, }, operationId: detail.patch.operationId, request: detail.patch.requestBody.content['application/json'].schema, response: detail.patch.responses[200].content['application/json'].schema, }, }).toMatchInlineSnapshot(` { "components": [ "CreateOrganizationRequest", "DeleteOrganizationResponse", "Organization", "OrganizationList", "UpdateOrganizationRequest", ], "create": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, }, "operationId": "createOrganization", "request": { "$ref": "#/components/schemas/CreateOrganizationRequest", }, "response": { "$ref": "#/components/schemas/Organization", }, }, "delete": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, "409": { "$ref": "#/components/schemas/SponsorshipsUnreportedError", }, }, "operationId": "deleteOrganization", "response": { "$ref": "#/components/schemas/DeleteOrganizationResponse", }, }, "get": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, }, "operationId": "getOrganization", "response": { "$ref": "#/components/schemas/Organization", }, }, "list": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, }, "operationId": "listOrganizations", "response": { "$ref": "#/components/schemas/OrganizationList", }, }, "timestamps": { "createdAt": "date-time", "updatedAt": "date-time", }, "update": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidOrParamInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, }, "operationId": "updateOrganization", "request": { "$ref": "#/components/schemas/UpdateOrganizationRequest", }, "response": { "$ref": "#/components/schemas/Organization", }, }, } `) }) describe('GET /orgs', () => { test('rejects malformed API key credentials', async () => { const response = await createApp().request('/v1/orgs', { headers: { authorization: 'Basic wrong' }, }) expect(response.status).toBe(400) expect(await response.json()).toMatchObject({ error: { code: 'api_key_malformed' } }) }) test('lists all organizations for the super admin', async () => { const response = await (await seeded()).request('/v1/orgs', as(secret)) expect(response.status).toBe(200) const body = await TestApp.json(response, Orgs.schema.listOrganizations.Response) // Timestamps are nondeterministic; two same-millisecond rows make the // newest-first order itself unstable, so sort by id before snapshotting. const data = body.data .map(({ id, name }) => ({ id, name })) .sort((a, b) => a.id.localeCompare(b.id)) expect(data).toMatchInlineSnapshot(` [ { "id": "org_a", "name": "Org A", }, { "id": "org_b", "name": "Org B", }, ] `) }) test('accepts a management API key in the query parameter', async () => { const app = await seeded() const response = await app.request(`/v1/orgs?key=${encodeURIComponent(managementRead.token)}`) const body = await TestApp.json(response, Orgs.schema.listOrganizations.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body.data.map(({ id }) => id)).toMatchInlineSnapshot(` [ "org_a", ] `) }) test('lists only owned organizations for a session', async () => { const app = await seeded() const cookie = await session(app) const empty = await app.request('/v1/orgs', { headers: { cookie } }) expect(empty.status).toBe(200) expect((await TestApp.json(empty, Orgs.schema.listOrganizations.Response)).data).toEqual([]) const created = await app.request('/v1/orgs', json('POST', cookie, { name: 'Acme' })) expect(created.status).toBe(200) const listed = await app.request('/v1/orgs', { headers: { cookie } }) const body = await TestApp.json(listed, Orgs.schema.listOrganizations.Response) expect(body.data.map(({ name }) => name)).toEqual(['Acme']) }) test('rejects missing credentials', async () => { const response = await (await seeded()).request('/v1/orgs') expect(response.status).toBe(401) const { requestId, ...body } = (await response.json()) as { requestId?: string } expect(requestId).toBeDefined() expect(body).toMatchInlineSnapshot(` { "error": { "code": "api_key_missing", "message": "Missing API key", }, } `) }) test('rejects API keys without the management read scope', async () => { const response = await (await seeded()).request('/v1/orgs', as(TestApp.key.token)) expect(response.status).toBe(403) }) test('lists only the owning organization for scoped and wildcard keys', async () => { const app = await seeded() const scoped = await app.request('/v1/orgs', as(managementRead.token)) expect(scoped.status).toBe(200) expect( (await TestApp.json(scoped, Orgs.schema.listOrganizations.Response)).data.map(({ id }) => id), ).toEqual(['org_a']) const response = await (await seeded()).request('/v1/orgs', as(wildcard.token)) expect(response.status).toBe(200) expect( (await TestApp.json(response, Orgs.schema.listOrganizations.Response)).data.map( ({ id }) => id, ), ).toEqual(['org_a']) }) test('rejects the secret when super admin is unconfigured', async () => { const client = TestApp.client({ db: TestApp.database() }) const response = await client.v1.orgs.$get({}, as(secret)) expect(response.status).toBe(401) }) test('logs the super admin actor, never the secret', async () => { const entries: Log.Entry[] = [] const client = TestApp.client({ auth: { superAdmin: { secret } }, db: TestApp.database(), logger: (entry) => { entries.push(entry) }, }) await client.v1.orgs.$get({}, as(secret)) expect(entries[0]?.principal).toMatchInlineSnapshot(` { "actor": "super_admin", "id": "super_admin", "type": "super_admin", } `) expect(JSON.stringify(entries)).not.toContain(secret) }) }) describe('POST /orgs', () => { test('creates a session-owned organization', async () => { const db = TestApp.database() const app = createApp(db) const cookie = await session(app) const response = await app.request('/v1/orgs', json('POST', cookie, { name: 'Acme' })) expect(response.status).toBe(200) const body = await TestApp.json(response, Orgs.schema.Organization) expect(body.id.startsWith('org_')).toBe(true) expect(body.name).toBe('Acme') expect(await EnabledBillingSources.listByOrg(db, body.id)).toEqual([]) }) test.each([ { email: 'Dev@Example.NET', entry: 'dev@example.net', kind: 'email' }, { email: 'Dev@Example.ORG', entry: 'example.org', kind: 'domain' }, ])('enables Stripe for a session matching an early-access $kind', async ({ email, entry }) => { const db = TestApp.database() const app = createApp(db) await EarlyAccess.add(db, { createdBy: 'test', entry }) const account = privateKeyToAccount(generatePrivateKey()) const { cookie } = await TestApp.signIn(app, account) const user = await Users.getByAddress(db, account.address) if (!user) throw new Error('sign-in did not create a user') await Users.setEmail(db, user.id, email) const response = await app.request('/v1/orgs', json('POST', cookie!, { name: 'Acme' })) expect(response.status).toBe(200) const org = await TestApp.json(response, Orgs.schema.Organization) const sources = await EnabledBillingSources.listByOrg(db, org.id) expect(sources.map(({ createdBy, source }) => ({ createdBy, source }))).toEqual([ { createdBy: user.id, source: 'stripe' }, ]) }) test('does not enable Stripe without an early-access grant', async () => { const db = TestApp.database() const app = createApp(db) const account = privateKeyToAccount(generatePrivateKey()) const { cookie } = await TestApp.signIn(app, account) const user = await Users.getByAddress(db, account.address) if (!user) throw new Error('sign-in did not create a user') await Users.setEmail(db, user.id, 'dev@example.net') const response = await app.request('/v1/orgs', json('POST', cookie!, { name: 'Acme' })) const org = await TestApp.json(response, Orgs.schema.Organization) expect(await EnabledBillingSources.listByOrg(db, org.id)).toEqual([]) }) test('creates a memberless org as the super admin', async () => { const db = TestApp.database() const app = createApp(db) const response = await app.request('/v1/orgs', { body: JSON.stringify({ name: 'Ops' }), headers: { 'content-type': 'application/json', 'tempo-api-key': secret }, method: 'POST', }) expect(response.status).toBe(200) const org = await TestApp.json(response, Orgs.schema.Organization) expect(org.name).toBe('Ops') expect(await EnabledBillingSources.listByOrg(db, org.id)).toEqual([]) // Memberless: invisible to sessions until an owner is seated. const cookie = await session(app) const foreign = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie } }) expect(foreign.status).toBe(404) const superAdmin = await app.request(`/v1/orgs/${org.id}`, as(secret)) expect(superAdmin.status).toBe(200) }) test('rejects invalid bodies', async () => { const app = createApp() const cookie = await session(app) const response = await app.request('/v1/orgs', json('POST', cookie, { name: '' })) expect(response.status).toBe(400) }) }) describe('GET /orgs/:orgId', () => { test('binds API keys to their owning organization', async () => { const app = await seeded() expect((await app.request('/v1/orgs/org_a', as(managementRead.token))).status).toBe(200) expect((await app.request('/v1/orgs/org_b', as(managementRead.token))).status).toBe(404) expect((await app.request('/v1/orgs/org_a', as(managementWrite.token))).status).toBe(403) }) test('hides foreign organizations behind 404', async () => { const app = createApp() const owner = await session(app) const created = await app.request('/v1/orgs', json('POST', owner, { name: 'Acme' })) const org = await TestApp.json(created, Orgs.schema.Organization) const owned = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie: owner } }) expect(owned.status).toBe(200) const stranger = await session(app) const foreign = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie: stranger } }) expect(foreign.status).toBe(404) const superAdmin = await app.request(`/v1/orgs/${org.id}`, as(secret)) expect(superAdmin.status).toBe(200) }) }) describe('PATCH /orgs/:orgId', () => { test('requires the management write scope', async () => { const app = await seeded() const write = await app.request( `/v1/orgs/org_a?key=${encodeURIComponent(managementWrite.token)}`, { body: JSON.stringify({ name: 'Managed' }), headers: { 'content-type': 'application/json' }, method: 'PATCH', }, ) expect(write.status).toBe(200) expect((await TestApp.json(write, Orgs.schema.Organization)).name).toBe('Managed') const readOnly = await app.request('/v1/orgs/org_a', { body: JSON.stringify({ name: 'Denied' }), headers: { 'content-type': 'application/json', 'tempo-api-key': managementRead.token }, method: 'PATCH', }) expect(readOnly.status).toBe(403) }) test('renames an owned organization', async () => { const app = createApp() const cookie = await session(app) const created = await app.request('/v1/orgs', json('POST', cookie, { name: 'Acme' })) const org = await TestApp.json(created, Orgs.schema.Organization) const renamed = await app.request(`/v1/orgs/${org.id}`, json('PATCH', cookie, { name: 'Acme, Inc.' })) // prettier-ignore expect(renamed.status).toBe(200) expect((await TestApp.json(renamed, Orgs.schema.Organization)).name).toBe('Acme, Inc.') const stranger = await session(app) const foreign = await app.request(`/v1/orgs/${org.id}`, json('PATCH', stranger, { name: 'Hijack' })) // prettier-ignore expect(foreign.status).toBe(404) }) }) describe('DELETE /orgs/:orgId', () => { test('deletes an owned organization and cascades its projects', async () => { const db = TestApp.database() const app = createApp(db) const cookie = await session(app) const created = await app.request('/v1/orgs', json('POST', cookie, { name: 'Acme' })) const org = await TestApp.json(created, Orgs.schema.Organization) const createdProject = await app.request( `/v1/orgs/${org.id}/projects`, json('POST', cookie, { name: 'Checkout' }), ) expect(createdProject.status).toBe(200) const project = (await createdProject.json()) as { id: string } const deleted = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie }, method: 'DELETE', }) expect(deleted.status).toBe(200) expect(await TestApp.json(deleted, Orgs.schema.deleteOrganization.Response)).toEqual({ id: org.id, }) const gone = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie } }) expect(gone.status).toBe(404) // Cascade: the org's project rows are deleted, not orphaned. expect(await Projects.get(db, project.id)).toBeUndefined() }) test('hides foreign organizations behind 404', async () => { const app = createApp() const owner = await session(app) const created = await app.request('/v1/orgs', json('POST', owner, { name: 'Acme' })) const org = await TestApp.json(created, Orgs.schema.Organization) const stranger = await session(app) const foreign = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie: stranger }, method: 'DELETE', }) expect(foreign.status).toBe(404) const still = await app.request(`/v1/orgs/${org.id}`, { headers: { cookie: owner } }) expect(still.status).toBe(200) }) test('deletes any organization as the super admin', async () => { const app = await seeded() const response = await app.request('/v1/orgs/org_a', { ...as(secret), method: 'DELETE' }) expect(response.status).toBe(200) const gone = await app.request('/v1/orgs/org_a', as(secret)) expect(gone.status).toBe(404) }) }) describe('ownership', () => { test('hides organizations without owning users from every session verb', async () => { const app = await seeded() const cookie = await session(app) const read = await app.request('/v1/orgs/org_a', { headers: { cookie } }) expect(read.status).toBe(404) const renamed = await app.request('/v1/orgs/org_a', json('PATCH', cookie, { name: 'Steal' })) expect(renamed.status).toBe(404) const deleted = await app.request('/v1/orgs/org_a', { headers: { cookie }, method: 'DELETE' }) expect(deleted.status).toBe(404) // Untouched by the attempts, and still visible to the super admin. const superAdmin = await app.request('/v1/orgs/org_a', as(secret)) expect(superAdmin.status).toBe(200) expect((await TestApp.json(superAdmin, Orgs.schema.Organization)).name).toBe('Org A') }) })