feat(260606-tv8-01): add guarded GET /api/login route + tests
- Register app.get('/api/login', redirect to '/') in protected-routes block
- Route placed after OIDC guard so unauthenticated nav triggers auth flow
- Add login.test.ts covering bypass and OIDC-passthrough redirect paths
This commit is contained in:
@@ -46,6 +46,16 @@ if (!devBypassActive) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Protected API routes (behind oidcAuthMiddleware)
|
// Protected API routes (behind oidcAuthMiddleware)
|
||||||
|
|
||||||
|
// GET /api/login — login entry point for the PWA.
|
||||||
|
// Flow (production): unauthenticated top-level nav hits the OIDC guard above,
|
||||||
|
// which 302-redirects to Authelia. After login, Authelia POSTs to /callback,
|
||||||
|
// the middleware sets a `continue` cookie pointing back to /api/login, and the
|
||||||
|
// browser follows it here — now authenticated. The handler then redirects to /
|
||||||
|
// so the SPA boots with a valid session. Under DEV_AUTH_BYPASS the guard is not
|
||||||
|
// mounted, so /api/login reaches this handler directly and still redirects to /.
|
||||||
|
app.get('/api/login', (c) => c.redirect('/'))
|
||||||
|
|
||||||
app.route('/api/me', meRouter)
|
app.route('/api/me', meRouter)
|
||||||
app.route('/api/events', eventsRouter)
|
app.route('/api/events', eventsRouter)
|
||||||
app.route('/api/sse', sseRouter)
|
app.route('/api/sse', sseRouter)
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* GET /api/login — tests for the top-level-nav login entry point.
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* 1. DEV_AUTH_BYPASS=true: GET /api/login returns 302 → '/'.
|
||||||
|
* Guard is not mounted; handler fires directly.
|
||||||
|
* 2. Without DEV_AUTH_BYPASS: oidcAuthMiddleware passthrough mock lets the
|
||||||
|
* request through; handler returns 302 → '/' (mirroring me.test.ts OIDC path).
|
||||||
|
*
|
||||||
|
* Flow (production):
|
||||||
|
* Unauthenticated top-level nav → guard 302 → Authelia → /callback → `continue`
|
||||||
|
* cookie returns browser to /api/login (now authenticated) → handler 302 → /.
|
||||||
|
* Under DEV_AUTH_BYPASS, guard is not mounted, so /api/login reaches the handler
|
||||||
|
* directly and redirects to '/'.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared mock: DB — avoids real DB connections across all tests in this file.
|
||||||
|
// Hoisted so it applies to every dynamic import below.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
vi.mock('../../src/db/client.js', () => ({
|
||||||
|
db: {
|
||||||
|
execute: vi.fn().mockResolvedValue([[{ '1': 1 }]]),
|
||||||
|
select: vi.fn().mockReturnValue({
|
||||||
|
from: vi.fn().mockReturnValue({
|
||||||
|
where: vi.fn().mockResolvedValue([]),
|
||||||
|
innerJoin: vi.fn().mockReturnValue({
|
||||||
|
innerJoin: vi.fn().mockReturnValue({
|
||||||
|
where: vi.fn().mockResolvedValue([]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Track whether oidcAuthMiddleware was registered on the app.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const oidcMiddlewareSpy = vi.fn(
|
||||||
|
() => async (_c: unknown, next: () => Promise<void>) => next(),
|
||||||
|
)
|
||||||
|
|
||||||
|
vi.mock('@hono/oidc-auth', () => ({
|
||||||
|
oidcAuthMiddleware: () => oidcMiddlewareSpy(),
|
||||||
|
processOAuthCallback: () => async (c: { json: (v: unknown) => unknown }) =>
|
||||||
|
c.json({ ok: true }),
|
||||||
|
getAuth: vi.fn().mockResolvedValue(null),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Env snapshot — restored after each test.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const originalNodeEnv = process.env.NODE_ENV
|
||||||
|
const originalBypassFlag = process.env.DEV_AUTH_BYPASS
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.NODE_ENV = originalNodeEnv
|
||||||
|
if (originalBypassFlag === undefined) {
|
||||||
|
delete process.env.DEV_AUTH_BYPASS
|
||||||
|
} else {
|
||||||
|
process.env.DEV_AUTH_BYPASS = originalBypassFlag
|
||||||
|
}
|
||||||
|
vi.resetModules()
|
||||||
|
oidcMiddlewareSpy.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('GET /api/login — dev-auth bypass (DEV_AUTH_BYPASS=true)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.NODE_ENV = 'test'
|
||||||
|
process.env.DEV_AUTH_BYPASS = 'true'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 302 with location "/" (bypass active, guard not mounted)', async () => {
|
||||||
|
const { app } = await import('../../src/index.js')
|
||||||
|
|
||||||
|
const res = await app.request('/api/login')
|
||||||
|
expect(res.status).toBe(302)
|
||||||
|
expect(res.headers.get('location')).toBe('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not invoke oidcAuthMiddleware when bypass is active', async () => {
|
||||||
|
const { app } = await import('../../src/index.js')
|
||||||
|
|
||||||
|
await app.request('/api/login')
|
||||||
|
|
||||||
|
expect(oidcMiddlewareSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /api/login — OIDC path (no DEV_AUTH_BYPASS)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.NODE_ENV = 'test'
|
||||||
|
delete process.env.DEV_AUTH_BYPASS
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 302 with location "/" when OIDC passthrough allows the request', async () => {
|
||||||
|
const { app } = await import('../../src/index.js')
|
||||||
|
|
||||||
|
// oidcAuthMiddleware is mocked as a passthrough — request reaches the handler.
|
||||||
|
const res = await app.request('/api/login')
|
||||||
|
expect(res.status).toBe(302)
|
||||||
|
expect(res.headers.get('location')).toBe('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user