chore: move oidc backend to proper DI

This commit is contained in:
jubnl
2026-08-01 18:25:28 +02:00
parent d892b61a59
commit ec0709741a
9 changed files with 1306 additions and 1379 deletions
+34 -34
View File
@@ -1,8 +1,11 @@
/**
* OIDC integration tests — OIDC-001 through OIDC-010.
* OIDC integration tests — OIDC-001 through OIDC-014.
* Covers /api/auth/oidc/login, /callback, /exchange.
* HTTP calls (discover, exchangeCodeForToken, getUserInfo) are mocked.
* State management, auth codes, and findOrCreateUser run against the real test DB.
* HTTP calls (discover, exchangeCodeForToken, getUserInfo, verifyIdToken) are
* stubbed with vi.spyOn on the container's OidcService instance (the domain is
* DI-native since the oidc fold — a services/oidcService path mock would
* silently miss). State management, auth codes, and findOrCreateUser run for
* real on that same instance against the real test DB.
*/
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
import request from 'supertest';
@@ -42,33 +45,13 @@ vi.mock('../../src/config', () => ({
}));
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
// ── Mock only the HTTP-calling functions from oidcService ────────────────────
vi.mock('../../src/services/oidcService', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../src/services/oidcService')>();
return {
...actual,
discover: vi.fn(),
exchangeCodeForToken: vi.fn(),
getUserInfo: vi.fn(),
// Bypass real JWKS fetch + signature verification in tests. Callers
// that exercise the security of verifyIdToken should unit-test the
// function directly instead; integration tests here focus on the
// callback flow, not the crypto.
verifyIdToken: vi.fn(),
};
});
import type { MockInstance } from 'vitest';
import { buildApp } from '../../src/bootstrap';
import { createTables } from '../../src/db/schema';
import { runMigrations } from '../../src/db/migrations';
import { resetTestDb, resetRateLimits } from '../helpers/test-db';
import { createUser } from '../helpers/factories';
import * as oidcService from '../../src/services/oidcService';
const mockDiscover = vi.mocked(oidcService.discover);
const mockExchangeCode = vi.mocked(oidcService.exchangeCodeForToken);
const mockGetUserInfo = vi.mocked(oidcService.getUserInfo);
const mockVerifyIdToken = vi.mocked(oidcService.verifyIdToken);
import { OidcService } from '../../src/nest/oidc/oidc.service';
const MOCK_DISCOVERY_DOC = {
authorization_endpoint: 'https://oidc.example.com/auth',
@@ -78,12 +61,29 @@ const MOCK_DISCOVERY_DOC = {
let nestApp: INestApplication;
let app: Application;
let oidcSvc: OidcService;
let mockDiscover: MockInstance<OidcService['discover']>;
let mockExchangeCode: MockInstance<OidcService['exchangeCodeForToken']>;
let mockGetUserInfo: MockInstance<OidcService['getUserInfo']>;
let mockVerifyIdToken: MockInstance<OidcService['verifyIdToken']>;
beforeAll(async () => {
createTables(testDb);
runMigrations(testDb);
nestApp = await buildApp();
app = nestApp.getHttpAdapter().getInstance();
// Stub only the HTTP-calling methods on the container's OidcService — the
// default implementation resolves undefined so an unexpected call fails the
// flow (matching the old vi.fn() mocks) instead of hitting the network.
// Bypasses real JWKS fetch + signature verification: callers that exercise
// the security of verifyIdToken unit-test the method directly; integration
// tests here focus on the callback flow, not the crypto. State management,
// auth codes, and findOrCreateUser run for real on this same instance.
oidcSvc = nestApp.get(OidcService);
mockDiscover = vi.spyOn(oidcSvc, 'discover').mockImplementation(async () => undefined as never);
mockExchangeCode = vi.spyOn(oidcSvc, 'exchangeCodeForToken').mockImplementation(async () => undefined as never);
mockGetUserInfo = vi.spyOn(oidcSvc, 'getUserInfo').mockImplementation(async () => undefined as never);
mockVerifyIdToken = vi.spyOn(oidcSvc, 'verifyIdToken').mockImplementation(async () => undefined as never);
});
beforeEach(() => {
@@ -169,7 +169,7 @@ describe('GET /api/auth/oidc/callback', () => {
});
// Create a valid state token
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=authcode123&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -187,7 +187,7 @@ describe('GET /api/auth/oidc/callback', () => {
name: 'New User',
});
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=code999&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -224,7 +224,7 @@ describe('GET /api/auth/oidc/callback', () => {
mockDiscover.mockResolvedValueOnce(MOCK_DISCOVERY_DOC);
mockExchangeCode.mockResolvedValueOnce({ _ok: false, _status: 400 });
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=badcode&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -236,7 +236,7 @@ describe('GET /api/auth/oidc/callback', () => {
mockDiscover.mockResolvedValueOnce(MOCK_DISCOVERY_DOC);
mockExchangeCode.mockResolvedValueOnce({ access_token: 'tok', _ok: true, _status: 200 }); // no id_token
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=anycode&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -249,7 +249,7 @@ describe('GET /api/auth/oidc/callback', () => {
mockExchangeCode.mockResolvedValueOnce({ access_token: 'tok', id_token: 'bad.id.token', _ok: true, _status: 200 });
mockVerifyIdToken.mockResolvedValueOnce({ ok: false, error: 'signature_or_claim_mismatch: invalid signature' });
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=anycode&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -267,7 +267,7 @@ describe('GET /api/auth/oidc/callback', () => {
name: 'Alice',
});
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=anycode&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -290,7 +290,7 @@ describe('GET /api/auth/oidc/callback', () => {
name: 'Blocked',
});
const { state } = oidcService.createState('http://localhost:3001/api/auth/oidc/callback');
const { state } = oidcSvc.createState('http://localhost:3001/api/auth/oidc/callback');
const res = await request(app).get(`/api/auth/oidc/callback?code=anycode&state=${state}`).set('Cookie', `trek_oidc_state=${state}`);
@@ -304,7 +304,7 @@ describe('GET /api/auth/oidc/callback', () => {
describe('GET /api/auth/oidc/exchange', () => {
it('OIDC-011: valid auth code returns JWT and sets cookie', async () => {
const fakeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.sig';
const code = oidcService.createAuthCode(fakeToken);
const code = oidcSvc.createAuthCode(fakeToken);
const res = await request(app).get(`/api/auth/oidc/exchange?code=${code}`);
@@ -331,7 +331,7 @@ describe('GET /api/auth/oidc/exchange', () => {
it('OIDC-014: auth code is single-use (second use returns 400)', async () => {
const fakeToken = 'test.token.here';
const code = oidcService.createAuthCode(fakeToken);
const code = oidcSvc.createAuthCode(fakeToken);
// First use: success
const res1 = await request(app).get(`/api/auth/oidc/exchange?code=${code}`);