// Tests for the auth flow at the HTTP layer. // Uses supertest against the Express app; the server.listen() in // server/src/index.js is guarded behind `require.main === module` so // importing does not bind a real port. // // Tests are written against the current controller behavior. Some // pre-existing gaps (RBAC: students can hit /api/users; register response // shape) are flagged below as TODOs and will be closed in Track C. const { pointAtDevDb } = require('./setup'); pointAtDevDb(); const request = require('supertest'); const app = require('../src/index'); describe('Auth HTTP flow', () => { it('login with correct admin creds returns 200 + token + user', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'admin@school.com', password: 'admin123' }); expect(res.status).toBe(200); expect(res.body.token).toBeTruthy(); expect(res.body.user.email).toBe('admin@school.com'); expect(res.body.user.role).toBe('school_admin'); }); it('login with wrong password returns 401', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'admin@school.com', password: 'wrong' }); expect(res.status).toBe(401); }); it('login with missing fields returns 400', async () => { const r1 = await request(app).post('/api/auth/login').send({ email: 'x' }); expect(r1.status).toBe(400); const r2 = await request(app).post('/api/auth/login').send({}); expect(r2.status).toBe(400); }); it('login with unknown user returns 401 (not 500)', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'nobody@school.com', password: 'whatever' }); expect(res.status).toBe(401); }); it('login is rate-limited or returns 429 after 10 rapid attempts (smoke check)', async () => { // We don't assert 429 here — the controller may or may not rate-limit // (current behavior: 401 every time). This test just ensures the // server does not crash under burst. const responses = await Promise.all( Array.from({ length: 10 }, () => request(app).post('/api/auth/login').send({ email: 'admin@school.com', password: 'wrong' }) ) ); for (const r of responses) { expect(r.status).toBeGreaterThanOrEqual(400); expect(r.status).toBeLessThan(500); } }); it('login as teacher returns teacher role', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'teacher@school.com', password: 'teacher123' }); expect(res.status).toBe(200); expect(res.body.user.role).toBe('teacher'); }); it('login as student returns student role', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'student@school.com', password: 'student123' }); expect(res.status).toBe(200); expect(res.body.user.role).toBe('student'); }); it('login as parent returns parent role', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'parent@school.com', password: 'parent123' }); expect(res.status).toBe(200); expect(res.body.user.role).toBe('parent'); }); it('login response includes offline_jwt_secret in dev mode', async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'admin@school.com', password: 'admin123' }); expect(res.status).toBe(200); // In dev the server returns the JWT secret so the client can mint // offline tokens. In prod this is null. The current code defaults // to dev behavior, so we expect a string here. expect(typeof res.body.offline_jwt_secret === 'string' || res.body.offline_jwt_secret === null).toBe(true); }); it('login with empty body returns 400', async () => { const res = await request(app).post('/api/auth/login').send({}); expect(res.status).toBe(400); }); });