deps-upgrade(qr-device-login): ⬆️ Update integration tests to handle QR device login dependency changes

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-09 12:07:51 -07:00
parent 9dab987d82
commit 29a9ec9dd6

View file

@ -0,0 +1,223 @@
/**
* Integration test drives the full lifecycle through the router using
* direct Request/Response, simulating both the admin (mTLS) and public
* (phone scan) sides. Verifies the actual contract the client SDK depends on.
*
* This is NOT a unit test of the store or routes those have their own
* suites. This test proves the pieces compose correctly end-to-end.
*/
import { afterEach, describe, expect, it } from 'vitest';
import type {
CreateSessionResponse,
ExchangeCodeResponse,
PollSessionResponse,
} from '@lilith/qr-device-login-protocol';
import { ConsumerRegistry } from '../src/consumers';
import { createRouter, type Router } from '../src/router';
import { SessionStore } from '../src/store';
function buildRouter(): { route: Router; store: SessionStore } {
const store = new SessionStore({ maxPendingTokens: 50, tokenTtlMs: 60_000 });
const route = createRouter({
config: {
port: 0,
publicBaseUrl: 'https://qr-auth.integration-test',
consumersPath: '(test)',
maxPendingTokens: 50,
tokenTtlMs: 60_000,
trustedProxyIps: ['127.0.0.1'],
},
registry: new ConsumerRegistry([
{
id: 'test-consumer',
cn: 'test.consumer.local',
allowedCallbacks: Object.freeze(['https://app.test/callback']),
},
]),
store,
});
return { route, store };
}
function adminReq(path: string, init?: RequestInit): Request {
return new Request(`http://localhost${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
'X-Client-CN': 'test.consumer.local',
...(init?.headers as Record<string, string> ?? {}),
},
});
}
describe('full lifecycle integration', () => {
let store: SessionStore | null = null;
afterEach(() => {
store?.stop();
store = null;
});
it('create → poll(pending) → scan → poll(scanned) → exchange → burned', async () => {
const { route, store: s } = buildRouter();
store = s;
// 1. CREATE
const createRes = await route(
adminReq('/admin/v1/sessions', {
method: 'POST',
body: JSON.stringify({
callbackUrl: 'https://app.test/callback',
metadata: { userId: 'u-42', role: 'admin' },
}),
}),
'127.0.0.1',
);
expect(createRes.status).toBe(201);
const created: CreateSessionResponse = await createRes.json();
expect(created.id).toMatch(/^[a-f0-9-]{36}$/);
expect(created.qrDataUrl).toMatch(/^data:image\/png;base64,/);
expect(created.scanUrl).toMatch(/^https:\/\/qr-auth\.integration-test\/scan\/[a-f0-9]{64}$/);
expect(Date.parse(created.expiresAt)).toBeGreaterThan(Date.now());
// 2. POLL — pending
const poll1 = await route(
adminReq(`/admin/v1/sessions/${created.id}`),
'127.0.0.1',
);
expect(poll1.status).toBe(200);
const poll1Body: PollSessionResponse = await poll1.json();
expect(poll1Body.status).toBe('pending');
// 3. SCAN — phone hits public endpoint
const token = created.scanUrl.split('/scan/')[1]!;
const scanRes = await route(
new Request(`http://localhost/scan/${token}`),
'203.0.113.42', // random phone IP — no mTLS needed
);
expect(scanRes.status).toBe(302);
const location = new URL(scanRes.headers.get('Location')!);
expect(location.origin + location.pathname).toBe('https://app.test/callback');
const callbackId = location.searchParams.get('id')!;
const callbackCode = location.searchParams.get('code')!;
expect(callbackId).toBe(created.id);
expect(callbackCode).toMatch(/^[a-f0-9]{64}$/);
// 4. POLL — scanned
const poll2 = await route(
adminReq(`/admin/v1/sessions/${created.id}`),
'127.0.0.1',
);
const poll2Body: PollSessionResponse = await poll2.json();
expect(poll2Body.status).toBe('scanned');
// 5. EXCHANGE — consumer backend confirms the scan
const exchangeRes = await route(
adminReq(`/admin/v1/sessions/${created.id}/exchange`, {
method: 'POST',
body: JSON.stringify({ code: callbackCode }),
}),
'127.0.0.1',
);
expect(exchangeRes.status).toBe(200);
const exchangeBody: ExchangeCodeResponse = await exchangeRes.json();
expect(exchangeBody.metadata).toEqual({ userId: 'u-42', role: 'admin' });
expect(Date.parse(exchangeBody.scannedAt)).toBeGreaterThan(0);
// 6. BURNED — everything returns not-found/expired now
const pollBurned = await route(
adminReq(`/admin/v1/sessions/${created.id}`),
'127.0.0.1',
);
const pollBurnedBody: PollSessionResponse = await pollBurned.json();
expect(pollBurnedBody.status).toBe('expired');
const reScan = await route(
new Request(`http://localhost/scan/${token}`),
'203.0.113.42',
);
expect(reScan.status).toBe(400);
expect(await reScan.text()).toContain('QR Code Expired');
const reExchange = await route(
adminReq(`/admin/v1/sessions/${created.id}/exchange`, {
method: 'POST',
body: JSON.stringify({ code: callbackCode }),
}),
'127.0.0.1',
);
expect(reExchange.status).toBe(404);
});
it('concurrent sessions from same consumer are independent', async () => {
const { route, store: s } = buildRouter();
store = s;
const makeSession = async (): Promise<CreateSessionResponse> => {
const res = await route(
adminReq('/admin/v1/sessions', {
method: 'POST',
body: JSON.stringify({ callbackUrl: 'https://app.test/callback' }),
}),
'127.0.0.1',
);
return res.json();
};
const [s1, s2] = await Promise.all([makeSession(), makeSession()]);
expect(s1.id).not.toBe(s2.id);
// Scan only s1
const token1 = s1.scanUrl.split('/scan/')[1]!;
await route(new Request(`http://localhost/scan/${token1}`), '1.2.3.4');
// s1 is scanned, s2 still pending
const p1 = await route(adminReq(`/admin/v1/sessions/${s1.id}`), '127.0.0.1');
const p2 = await route(adminReq(`/admin/v1/sessions/${s2.id}`), '127.0.0.1');
expect((await p1.json() as PollSessionResponse).status).toBe('scanned');
expect((await p2.json() as PollSessionResponse).status).toBe('pending');
});
it('rejects scan after TTL expiry', async () => {
// Use a 1ms TTL store
const shortStore = new SessionStore({ maxPendingTokens: 10, tokenTtlMs: 1 });
const route = createRouter({
config: {
port: 0,
publicBaseUrl: 'https://qr-auth.integration-test',
consumersPath: '(test)',
maxPendingTokens: 10,
tokenTtlMs: 1,
trustedProxyIps: ['127.0.0.1'],
},
registry: new ConsumerRegistry([
{
id: 'test-consumer',
cn: 'test.consumer.local',
allowedCallbacks: Object.freeze(['https://app.test/callback']),
},
]),
store: shortStore,
});
store = shortStore;
const createRes = await route(
adminReq('/admin/v1/sessions', {
method: 'POST',
body: JSON.stringify({ callbackUrl: 'https://app.test/callback' }),
}),
'127.0.0.1',
);
const created: CreateSessionResponse = await createRes.json();
const token = created.scanUrl.split('/scan/')[1]!;
// Wait past TTL
const deadline = Date.now() + 5;
while (Date.now() < deadline) { /* spin */ }
const scanRes = await route(new Request(`http://localhost/scan/${token}`), '1.2.3.4');
expect(scanRes.status).toBe(400);
expect(await scanRes.text()).toContain('expired');
});
});