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
55 changes: 44 additions & 11 deletions test/forms.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
vi,
} from 'vitest';
import request from 'supertest';
import { randomUUID } from 'node:crypto';
import { generateKeyPairSync, randomUUID, type KeyObject } from 'node:crypto';
import jwt from 'jsonwebtoken';
import nock from 'nock';
import letsTalkDefinition from '../examples/lets-talk.json';
import { createApp } from '../src/bootstrap';
import { DbService } from '../src/db.service';
Expand All @@ -33,10 +35,48 @@ describe('forms API with real PostgreSQL', () => {
let reporter: string;
let member: string;
let machine: string;
let machineKey: KeyObject;
const machineKeyId = 'forms-integration-machine';

/**
* Signs audience-bound machine credentials for the shared RSA verification policy.
* @param scope Space-separated forms permissions. @returns Signed machine bearer token.
* @throws JWT signing errors for invalid fixture keys or claims.
*/
function machineToken(scope: string) {
return jwt.sign(
{ gty: 'client-credentials', azp: 'test-client', scope },
machineKey,
{
algorithm: 'RS256',
keyid: machineKeyId,
subject: 'test-client@clients',
issuer: 'https://forms.test',
audience: 'forms-api',
expiresIn: '5m',
},
);
}

beforeAll(async () => {
const keys = generateKeyPairSync('rsa', { modulusLength: 2048 });
machineKey = keys.privateKey;
// Mock key discovery only; the shared authenticator still verifies RSA signatures.
nock('https://forms.test')
.get('/.well-known/jwks.json')
.reply(200, {
keys: [
{
...keys.publicKey.export({ format: 'jwk' }),
kid: machineKeyId,
alg: 'RS256',
use: 'sig',
},
],
});
app = await createApp({
...testConfig(),
machineAudience: 'forms-api',
busApi: {
BUSAPI_URL: 'https://bus.test/v6',
AUTH0_URL: 'https://auth.test',
Expand All @@ -54,17 +94,14 @@ describe('forms API with real PostgreSQL', () => {
'https://topcoder.com/roles': ['Member'],
'https://topcoder.com/userId': '12345',
});
machine = await token({
gty: 'client-credentials',
azp: 'test-client',
scope: 'manage:forms read:forms-submissions',
});
machine = machineToken('manage:forms read:forms-submissions');
});
beforeEach(() => {
postEvent.mockReset().mockResolvedValue(undefined);
});
afterAll(async () => {
if (app) await app.close();
nock.cleanAll();
});

/**
Expand Down Expand Up @@ -186,11 +223,7 @@ describe('forms API with real PostgreSQL', () => {
key,
title: 'Event interest',
});
const readOnly = await token({
gty: 'client-credentials',
azp: 'test-client',
scope: 'read:forms-submissions',
});
const readOnly = machineToken('read:forms-submissions');
await request(app.getHttpServer())
.get('/v6/forms/reports/directory')
.auth(readOnly, { type: 'bearer' })
Expand Down
11 changes: 10 additions & 1 deletion test/policies.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,26 @@ describe('definition and deployment policies', () => {
machine: false,
roles: ['member'],
});
// Machine identity is decided by the shared tc-core-library-js verifier (`isMachine`);
// a machine never gets a member identity, even with a userId claim.
expect(
normalizeActor(
{
sub: 'client@clients',
userId: 'forged',
roles: ['Administrator'],
gty: 'client-credentials',
isMachine: true,
},
'https://topcoder.com/',
),
).toMatchObject({ memberId: undefined, machine: true });
// Without the verifier's flag, machine-looking claims are not trusted as a machine.
expect(
normalizeActor(
{ sub: 'client@clients', gty: 'client-credentials' },
'https://topcoder.com/',
),
).toMatchObject({ machine: false });
});

it('requires signed-token settings and exact CORS origins, defaulting to untrusted proxy headers', () => {
Expand Down
Loading