mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
chore: move oidc backend to proper DI
This commit is contained in:
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user