Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion src/workos/routes/connect.spec.ts
Original file line number Diff line number Diff line change
@@ -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' };
Expand All @@ -11,9 +12,12 @@ function createTestApp() {

describe('Connect routes', () => {
let app: ReturnType<typeof createTestApp>['app'];
let store: ReturnType<typeof createTestApp>['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 });
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
});
});
10 changes: 8 additions & 2 deletions src/workos/routes/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()}`;
Expand Down
Loading