diff --git a/dashcaddy-api/__tests__/routes/auth.totp.routes.test.js b/dashcaddy-api/__tests__/routes/auth.totp.routes.test.js index e88866f..d0f6643 100644 --- a/dashcaddy-api/__tests__/routes/auth.totp.routes.test.js +++ b/dashcaddy-api/__tests__/routes/auth.totp.routes.test.js @@ -4,8 +4,8 @@ * Covers the BACKLOG.md DC-006 acceptance criteria: * - no code → 400 (ValidationError) * - wrong code → 401 (AuthenticationError) - * - valid TOTP → 200 + session cookie + CSRF token - * - check-session with valid session → 200 { authenticated: true } + * - valid TOTP → 200 + session cookie + CSRF token + SSO handoff token + * - check-session with valid session → 200 { success: true, authenticated: true } * - check-session without session → 401 (AuthenticationError) * * Uses real otplib for code generation (so we exercise the actual TOTP math) @@ -79,6 +79,7 @@ function createApp(depsOverride = {}) { sessionStore.delete(ip); }), clearCookie: jest.fn(), + createHandoffToken: jest.fn(() => 'mock-sso-handoff-token'), isValid: jest.fn((req) => { const ip = session.getClientIP(req); const entry = sessionStore.get(ip); @@ -303,8 +304,10 @@ describe('TOTP Auth Routes — DC-006 Integration Test', () => { expect(res.body.success).toBe(true); expect(res.body.message).toMatch(/Authenticated successfully/); expect(res.body.csrfToken).toBe('mock-csrf-token'); + expect(res.body.ssoToken).toBe('mock-sso-handoff-token'); expect(deps.session.create).toHaveBeenCalled(); expect(deps.session.setCookie).toHaveBeenCalled(); + expect(deps.session.createHandoffToken).toHaveBeenCalledTimes(1); expect(deps.renewCSRFToken).toHaveBeenCalled(); }); }); @@ -350,7 +353,7 @@ describe('TOTP Auth Routes — DC-006 Integration Test', () => { deps.session._grantSession('127.0.0.1'); const res = await request(app).get('/api/totp/check-session').set('X-Forwarded-For', '127.0.0.1'); expect(res.status).toBe(200); - expect(res.body).toEqual({ authenticated: true }); + expect(res.body).toEqual({ success: true, authenticated: true }); }); }); @@ -450,24 +453,23 @@ describe('TOTP Auth Routes — DC-006 Integration Test', () => { const loginRes = await request(app).post('/api/totp/verify').send({ code: loginCode }); expect(loginRes.status).toBe(200); expect(loginRes.body.csrfToken).toBeDefined(); + expect(loginRes.body.ssoToken).toBe('mock-sso-handoff-token'); + expect(deps.session.createHandoffToken).toHaveBeenCalledTimes(1); // 5. Check-session — should now be authenticated (the BACKLOG "→ endpoint succeeds" step) const checkRes = await request(app).get('/api/totp/check-session'); expect(checkRes.status).toBe(200); - expect(checkRes.body).toEqual({ authenticated: true }); + expect(checkRes.body).toEqual({ success: true, authenticated: true }); // 6. Logout / disable const disableCode = authenticator.generate(secret); const disableRes = await request(app).post('/api/totp/disable').send({ code: disableCode }); expect(disableRes.status).toBe(200); - // 7. After disable, check-session should be 401 (bypass removed for security) - // unless the user still holds a valid session, in which case it's 200. - // The login step (4) may or may not have granted one depending on test order. + // 7. After disable, check-session deterministically rejects before + // checking session validity because TOTP protection is disabled. const afterRes = await request(app).get('/api/totp/check-session'); - // After disable, TOTP is off AND we may or may not have an active session. - // The new contract: bypass is gone, but a valid session still authenticates. - expect([200, 401]).toContain(afterRes.status); + expect(afterRes.status).toBe(401); }); it('proves otplib is real (not stubbed) by using a totally bogus code', async () => { diff --git a/dashcaddy-api/__tests__/self-updater-version.test.js b/dashcaddy-api/__tests__/self-updater-version.test.js index dcb7952..f561281 100644 --- a/dashcaddy-api/__tests__/self-updater-version.test.js +++ b/dashcaddy-api/__tests__/self-updater-version.test.js @@ -78,23 +78,19 @@ describe('SelfUpdater.getLocalVersion() — DC-033 regression', () => { } }); - test('commit is a git SHA (7-40 hex chars), not null', () => { + test('commit contains a git SHA and is not null', () => { expect(result.commit).not.toBeNull(); - expect(result.commit).toMatch(/^[0-9a-f]{7,40}$/); + expect(result.commit).toMatch(/(?:^|-)[0-9a-f]{7,40}$/); }); }); - describe('candidate-path resolution survives missing sibling files', () => { - // If we shadow __dirname by requiring the module through a different - // require() chain, the function should still find package.json via its - // candidate-list fallback. This catches the case where someone refactors - // the file to a deeper subdirectory and forgets to update the candidates. - test('getLocalVersion works regardless of how the module is required', () => { + describe('repeat construction uses the same resolved metadata', () => { + test('a second instance resolves the same non-fallback version metadata', () => { const mod = require(path.join(API_ROOT, 'src', 'docker', 'self-updater.js')); const Cls = mod.SelfUpdater || mod.default || mod; const result = new Cls({}).getLocalVersion(); expect(result.version).not.toBe('0.0.0'); - expect(result.commit).toMatch(/^[0-9a-f]{7,40}$/); + expect(result.commit).toMatch(/(?:^|-)[0-9a-f]{7,40}$/); }); }); }); \ No newline at end of file