From cdbb0fe827a4f4f48e73451ed6187040480d689f Mon Sep 17 00:00:00 2001 From: Sanjay Date: Tue, 28 Jul 2026 09:42:34 -0400 Subject: [PATCH 1/2] test: add missing control-plane integration cases Co-authored-by: Cursor --- .../integration/attestation-flow.test.ts | 105 ++++++++++++++++++ .../integration/tenant-isolation.test.ts | 79 +++++++++++++ 2 files changed, 184 insertions(+) diff --git a/control-plane/src/__tests__/integration/attestation-flow.test.ts b/control-plane/src/__tests__/integration/attestation-flow.test.ts index b3bb485..263ad86 100644 --- a/control-plane/src/__tests__/integration/attestation-flow.test.ts +++ b/control-plane/src/__tests__/integration/attestation-flow.test.ts @@ -2,6 +2,7 @@ process.env.DATABASE_URL ||= 'postgresql://verilink:verilink@127.0.0.1:15432/verilink_test'; process.env.API_KEY_HMAC_SECRET ||= 'test-hmac-secret-for-integration'; +import crypto from 'node:crypto'; import { describe, it, before, after, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import type pg from 'pg'; @@ -88,6 +89,110 @@ describe('Attestation Flow Integration', () => { assert.equal(body.data.items.length, 1); }); + it('duplicate submission returns 409', async () => { + const issuer = await seedIssuer(pool, tenantId); + const subject = await seedSubject(pool, tenantId); + const token = await signAttestationToken({ + issuerId: issuer.id, + subjectId: subject.id, + privateKey: issuer.privateKey, + keyId: issuer.keyId, + }); + + const submitResp1 = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(apiKey), + }, + body: JSON.stringify({ token }), + }); + assert.equal(submitResp1.status, 201, await submitResp1.text()); + + const submitResp2 = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(apiKey), + }, + body: JSON.stringify({ token }), + }); + assert.equal(submitResp2.status, 409); + }); + + it('invalid signature rejection returns 400', async () => { + const issuer = await seedIssuer(pool, tenantId); + const subject = await seedSubject(pool, tenantId); + const signer = await seedIssuer(pool, tenantId); + + // The JWS is signed with `signer.privateKey`, but claims issuerId=issuer.id and kid=issuer.keyId. + // Signature verification must fail against the issuer's stored public key. + const token = await signAttestationToken({ + issuerId: issuer.id, + subjectId: subject.id, + privateKey: signer.privateKey, + keyId: issuer.keyId, + visibility: 'participants', + }); + + const submitResp = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(apiKey), + }, + body: JSON.stringify({ token }), + }); + assert.equal(submitResp.status, 400); + }); + + it('unknown issuer rejection returns 400', async () => { + const issuerId = `vrl:p:${crypto.randomUUID()}`; + const subject = await seedSubject(pool, tenantId); + const issuer = await seedIssuer(pool, tenantId); + + const token = await signAttestationToken({ + issuerId, + subjectId: subject.id, + privateKey: issuer.privateKey, + keyId: issuer.keyId, + }); + + const submitResp = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(apiKey), + }, + body: JSON.stringify({ token }), + }); + assert.equal(submitResp.status, 400); + }); + + it('schema violation rejection returns 400', async () => { + const issuer = await seedIssuer(pool, tenantId); + const subject = await seedSubject(pool, tenantId); + + const token = await signAttestationToken({ + issuerId: issuer.id, + subjectId: subject.id, + privateKey: issuer.privateKey, + keyId: issuer.keyId, + // Not in SUPPORTED_TYPES; validateSchema must reject with 400. + attestationType: 'not_a_real_type', + }); + + const submitResp = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(apiKey), + }, + body: JSON.stringify({ token }), + }); + assert.equal(submitResp.status, 400); + }); + it('unauthorized request returns 401', async () => { const resp = await fetch(`${harness.url}/v1/attestations?issuer_id=vrl:p:dummy`, { headers: { 'Content-Type': 'application/json' }, diff --git a/control-plane/src/__tests__/integration/tenant-isolation.test.ts b/control-plane/src/__tests__/integration/tenant-isolation.test.ts index 8baeee3..1dddece 100644 --- a/control-plane/src/__tests__/integration/tenant-isolation.test.ts +++ b/control-plane/src/__tests__/integration/tenant-isolation.test.ts @@ -77,4 +77,83 @@ describe('Tenant Isolation Integration', () => { assert.equal(data.data.total, 0, 'Tenant B should not see Tenant A participants attestations'); assert.equal(data.data.items.length, 0); }); + + it('api-key tenant binding: tenant A cannot see tenant B attestations', async () => { + const tenantA = await seedTenant(pool, `tenant-a-${Date.now()}`); + const tenantB = await seedTenant(pool, `tenant-b-${Date.now()}`); + + const tenantAKey = await seedApiKey(pool, tenantA.id, ['attest:write', 'attest:read']); + const tenantBKey = await seedApiKey(pool, tenantB.id, ['attest:write', 'attest:read']); + + const issuerA = await seedIssuer(pool, tenantA.id); + const subjectA = await seedSubject(pool, tenantA.id); + + const issuerB = await seedIssuer(pool, tenantB.id); + const subjectB = await seedSubject(pool, tenantB.id); + + const tokenA = await signAttestationToken({ + issuerId: issuerA.id, + subjectId: subjectA.id, + privateKey: issuerA.privateKey, + keyId: issuerA.keyId, + visibility: 'participants', + }); + + const tokenB = await signAttestationToken({ + issuerId: issuerB.id, + subjectId: subjectB.id, + privateKey: issuerB.privateKey, + keyId: issuerB.keyId, + visibility: 'participants', + }); + + const submitA = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(tenantAKey), + }, + body: JSON.stringify({ token: tokenA }), + }); + assert.equal(submitA.status, 201, await submitA.text()); + + const submitB = await fetch(`${harness.url}/v1/attestations/submit`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authHeaders(tenantBKey), + }, + body: JSON.stringify({ token: tokenB }), + }); + assert.equal(submitB.status, 201, await submitB.text()); + + // Tenant A should not be able to read Tenant B data, even when filtering by issuer_id. + const listTenantBWithTenantAKey = await fetch( + `${harness.url}/v1/attestations?issuer_id=${encodeURIComponent(issuerB.id)}`, + { + headers: authHeaders(tenantAKey), + } + ); + assert.equal(listTenantBWithTenantAKey.status, 200); + const dataB = (await listTenantBWithTenantAKey.json()) as { + ok: boolean; + data: { items: unknown[]; total: number }; + }; + assert.equal(dataB.ok, true); + assert.equal(dataB.data.total, 0); + assert.equal(dataB.data.items.length, 0); + + // Unfiltered list should still only return Tenant A attestations. + const listUnfilteredWithTenantAKey = await fetch(`${harness.url}/v1/attestations`, { + headers: authHeaders(tenantAKey), + }); + assert.equal(listUnfilteredWithTenantAKey.status, 200); + const dataUnfiltered = (await listUnfilteredWithTenantAKey.json()) as { + ok: boolean; + data: { items: unknown[]; total: number }; + }; + assert.equal(dataUnfiltered.ok, true); + assert.equal(dataUnfiltered.data.total, 1); + assert.equal(dataUnfiltered.data.items.length, 1); + }); }); From 2c17278d7075bc5fe753dbf7353551638f4767e4 Mon Sep 17 00:00:00 2001 From: Sanjay Date: Tue, 28 Jul 2026 09:47:09 -0400 Subject: [PATCH 2/2] test: assert tenant A issuer in unfiltered list Co-authored-by: Cursor --- .../src/__tests__/integration/tenant-isolation.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/control-plane/src/__tests__/integration/tenant-isolation.test.ts b/control-plane/src/__tests__/integration/tenant-isolation.test.ts index 1dddece..415e441 100644 --- a/control-plane/src/__tests__/integration/tenant-isolation.test.ts +++ b/control-plane/src/__tests__/integration/tenant-isolation.test.ts @@ -150,10 +150,12 @@ describe('Tenant Isolation Integration', () => { assert.equal(listUnfilteredWithTenantAKey.status, 200); const dataUnfiltered = (await listUnfilteredWithTenantAKey.json()) as { ok: boolean; - data: { items: unknown[]; total: number }; + data: { items: Array<{ issuer_id: string }>; total: number }; }; assert.equal(dataUnfiltered.ok, true); assert.equal(dataUnfiltered.data.total, 1); assert.equal(dataUnfiltered.data.items.length, 1); + assert.equal(dataUnfiltered.data.items[0].issuer_id, issuerA.id); + assert.notEqual(dataUnfiltered.data.items[0].issuer_id, issuerB.id); }); });