diff --git a/src/workos/routes/connect.spec.ts b/src/workos/routes/connect.spec.ts index caf1189..2f81718 100644 --- a/src/workos/routes/connect.spec.ts +++ b/src/workos/routes/connect.spec.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach } from 'bun:test'; import { createServer, type ApiKeyMap } from '../../core/index.js'; import { workosPlugin } from '../index.js'; +import { getWorkOSStore } from '../store.js'; const apiKeys: ApiKeyMap = { sk_test_org: { environment: 'test' } }; const headers = { Authorization: 'Bearer sk_test_org', 'Content-Type': 'application/json' }; @@ -11,9 +12,12 @@ function createTestApp() { describe('Connect routes', () => { let app: ReturnType['app']; + let store: ReturnType['store']; beforeEach(() => { - app = createTestApp().app; + const testApp = createTestApp(); + app = testApp.app; + store = testApp.store; }); const req = (path: string, init?: RequestInit) => app.request(path, { headers, ...init }); @@ -98,11 +102,50 @@ describe('Connect routes', () => { expect((await json(res)).name).toBe('Get Test'); }); + it('gets an application by client_id', async () => { + const createRes = await req('/connect/applications', { + method: 'POST', + body: JSON.stringify({ name: 'Client ID Get Test' }), + }); + const created = await json(createRes); + + const res = await req(`/connect/applications/${created.client_id}`); + expect(res.status).toBe(200); + expect((await json(res)).id).toBe(created.id); + }); + + it('prefers an application id over another application client_id with the same value', async () => { + const idOwner = await json( + await req('/connect/applications', { + method: 'POST', + body: JSON.stringify({ name: 'ID Owner' }), + }), + ); + const clientIdOwner = await json( + await req('/connect/applications', { + method: 'POST', + body: JSON.stringify({ name: 'Client ID Owner' }), + }), + ); + getWorkOSStore(store).connectApplications.update(clientIdOwner.id, { client_id: idOwner.id }); + // Sanity-check the collision is real: the client_id index now resolves to the other app. + expect(getWorkOSStore(store).connectApplications.findOneBy('client_id', idOwner.id)?.id).toBe(clientIdOwner.id); + + const res = await req(`/connect/applications/${idOwner.id}`); + expect(res.status).toBe(200); + expect((await json(res)).name).toBe('ID Owner'); + }); + it('returns 404 for nonexistent application', async () => { const res = await req('/connect/applications/connect_app_nonexistent'); expect(res.status).toBe(404); }); + it('returns 404 for nonexistent client_id', async () => { + const res = await req('/connect/applications/client_nonexistent'); + expect(res.status).toBe(404); + }); + it('lists applications', async () => { await req('/connect/applications', { method: 'POST', @@ -139,4 +182,19 @@ describe('Connect routes', () => { const delRes = await req(`/connect/client_secrets/${secret.id}`, { method: 'DELETE' }); expect(delRes.status).toBe(204); }); + + it('creates a client secret for an application referenced by client_id', async () => { + const application = await json( + await req('/connect/applications', { + method: 'POST', + body: JSON.stringify({ name: 'Client ID Secret Test' }), + }), + ); + + const res = await req(`/connect/applications/${application.client_id}/client_secrets`, { method: 'POST' }); + expect(res.status).toBe(201); + const secret = await json(res); + expect(secret.object).toBe('client_secret'); + expect(secret.application_id).toBe(application.id); + }); }); diff --git a/src/workos/routes/connect.ts b/src/workos/routes/connect.ts index 16b45c5..ef6574e 100644 --- a/src/workos/routes/connect.ts +++ b/src/workos/routes/connect.ts @@ -12,6 +12,12 @@ export function connectRoutes(ctx: RouteContext): void { const { app, store } = ctx; const ws = getWorkOSStore(store); + // The spec documents the `{id}` param on every `/connect/applications/{id}...` route as + // "the application ID or client ID". Resolve the primary key first so an application ID + // always wins over another application's colliding client_id. + const findApplication = (ref: string) => + ws.connectApplications.get(ref) ?? ws.connectApplications.findOneBy('client_id', ref); + // List applications app.get('/connect/applications', (c) => { const url = new URL(c.req.url); @@ -71,14 +77,14 @@ export function connectRoutes(ctx: RouteContext): void { // Get application app.get('/connect/applications/:id', (c) => { - const application = ws.connectApplications.get(c.req.param('id')); + const application = findApplication(c.req.param('id')); if (!application) throw notFound('ConnectApplication'); return c.json(formatConnectApplication(application)); }); // Create client secret app.post('/connect/applications/:id/client_secrets', (c) => { - const application = ws.connectApplications.get(c.req.param('id')); + const application = findApplication(c.req.param('id')); if (!application) throw notFound('ConnectApplication'); const value = `secret_${generateVerificationToken()}`;