import express from 'express' import cookieParser from 'cookie-parser' import jwt from 'jsonwebtoken' import request from 'supertest' import { afterEach, describe, expect, it, vi } from 'vitest' import { requireLabAccess } from '../src/labAuth.js' const secret = 'test-lab-secret' function testApp(config: Parameters[0]) { const app = express() app.use(cookieParser()) app.use(express.urlencoded({ extended: false })) app.use(requireLabAccess(config)) app.get('/admin', (_request, response) => response.send('ok')) app.get('/api/ca/decks', (_request, response) => response.json([{ id: 'deck' }])) return app } function authCookie(claims: object = {}) { return `auth_token=${jwt.sign({ sub: '42', email: 'student@glitch.university', ...claims }, secret)}` } describe('lab auth middleware', () => { afterEach(() => { vi.restoreAllMocks() }) it('allows requests when lab auth is disabled', async () => { const app = testApp({ enabled: false, gnommowebUrl: 'https://glitch.university', jwtSecret: '', keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app).get('/admin').expect(200, 'ok') }) it('shows the local lab login form for browser requests without a shared auth cookie', async () => { const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google?returnTo={returnTo}', userProfileUrl: 'https://glitch.university/api/user/profile' }) const response = await request(app) .get('/admin') .set('accept', 'text/html') .set('host', 'lab.glitch.university') .set('x-forwarded-proto', 'https') .expect(401) expect(response.text).toContain('Insert lab keycard') expect(response.text).toContain('Temporary secret key') }) it('allows browser requests with the temporary lab secret key', async () => { const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: '', keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', temporarySecretKey: 'supersecret', userProfileUrl: 'https://glitch.university/api/user/profile' }) const unlock = await request(app) .post('/admin/lab-login') .type('form') .send({ secret_key: 'supersecret' }) .expect(302) const cookie = unlock.headers['set-cookie'] expect(String(cookie)).toContain('lab_temp_key=supersecret') await request(app) .get('/admin') .set('cookie', cookie) .expect(200, 'ok') }) it('rejects the temporary lab secret key when it is wrong', async () => { const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', temporarySecretKey: 'supersecret', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app) .post('/admin/lab-login') .type('form') .send({ secret_key: 'not-it' }) .expect(401) .expect((response) => { expect(response.text).toContain('Enter a JWT token or the temporary secret key') }) }) it('rejects API requests with an invalid shared auth cookie', async () => { const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app) .get('/api/ca/decks') .set('cookie', 'auth_token=not-a-real-jwt') .set('accept', 'application/json') .expect(401) .expect((response) => { expect(response.body.error).toBe('Invalid or expired token') }) }) it('allows valid JWTs when keycard checks are disabled', async () => { const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: false, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app).get('/api/ca/decks').set('cookie', authCookie()).expect(200) }) it('allows valid JWTs that include a lab keycard claim', async () => { const fetchSpy = vi.fn() vi.stubGlobal('fetch', fetchSpy) const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app) .get('/api/ca/decks') .set('cookie', authCookie({ merit_slug: 'lab-keycard' })) .expect(200) expect(fetchSpy).not.toHaveBeenCalled() }) it('allows valid JWTs when the gnommoweb user profile includes the merit slug', async () => { vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ merits: [{ slug: 'lab-keycard' }] }), { headers: { 'content-type': 'application/json' }, status: 200 }))) const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app) .get('/api/ca/decks') .set('cookie', authCookie()) .set('accept', 'application/json') .expect(200) }) it('returns a locked response when the gnommoweb user profile lacks the merit slug', async () => { vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ merits: [{ slug: 'other_merit' }] }), { headers: { 'content-type': 'application/json' }, status: 200 }))) const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app) .get('/admin') .set('cookie', authCookie()) .set('accept', 'text/html') .expect(403) .expect((response) => { expect(response.text).toContain('Lab keycard required') expect(response.text).toContain('does not have the CA Lab Keycard yet') }) }) it('reports a missing gnommoweb user profile endpoint clearly', async () => { vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ error: 'Not found' }), { headers: { 'content-type': 'application/json' }, status: 404 }))) const app = testApp({ enabled: true, gnommowebUrl: 'https://glitch.university', jwtSecret: secret, keycardMeritSlug: 'lab-keycard', keycardRequired: true, signInUrl: 'https://glitch.university/auth/google', userProfileUrl: 'https://glitch.university/api/user/profile' }) await request(app) .get('/api/ca/decks') .set('cookie', authCookie()) .set('accept', 'application/json') .expect(403) .expect((response) => { expect(response.body.reason).toContain('HTTP 404') }) }) })