feat(auth): /api/auth routes + integration tests (pending wiring)
This commit is contained in:
95
packages/backend/src/tests/auth.integration.test.ts
Normal file
95
packages/backend/src/tests/auth.integration.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { createApp } from '../app.js';
|
||||
import { makeTestDb } from './dbHelper.js';
|
||||
import { setMailerForTests, type Mailer } from '../services/auth/email.js';
|
||||
|
||||
class CaptureMailer implements Mailer {
|
||||
sent: { to: string; subject: string; text: string; html: string }[] = [];
|
||||
async send(to: string, m: { subject: string; html: string; text: string }) {
|
||||
this.sent.push({ to, ...m });
|
||||
}
|
||||
}
|
||||
|
||||
let env: ReturnType<typeof makeTestDb>;
|
||||
let mailer: CaptureMailer;
|
||||
let app: ReturnType<typeof createApp>;
|
||||
|
||||
beforeEach(() => {
|
||||
env = makeTestDb();
|
||||
mailer = new CaptureMailer();
|
||||
setMailerForTests(mailer);
|
||||
app = createApp(env.db);
|
||||
});
|
||||
|
||||
function tokenFromMail(text: string): string {
|
||||
const m = text.match(/token=([^\s&"]+)/);
|
||||
if (!m) throw new Error('no token in mail');
|
||||
return decodeURIComponent(m[1]!);
|
||||
}
|
||||
|
||||
function extractCookieValue(cookies: string[], name: string): string {
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith(`${name}=`)) {
|
||||
const v = c.split(';')[0]!.slice(name.length + 1);
|
||||
return decodeURIComponent(v);
|
||||
}
|
||||
}
|
||||
throw new Error(`cookie ${name} not found`);
|
||||
}
|
||||
|
||||
describe('auth integration', () => {
|
||||
it('register → verify → login → me → logout', async () => {
|
||||
const reg = await request(app).post('/api/auth/register').send({
|
||||
email: 'alice@example.com', displayName: 'Alice', password: 'secretpass',
|
||||
});
|
||||
expect(reg.status).toBe(201);
|
||||
expect(mailer.sent).toHaveLength(1);
|
||||
const verifyToken = tokenFromMail(mailer.sent[0]!.text);
|
||||
|
||||
const bad = await request(app).post('/api/auth/login').send({ email: 'alice@example.com', password: 'secretpass' });
|
||||
expect(bad.status).toBe(403);
|
||||
expect(bad.body.error.code).toBe('EMAIL_NOT_VERIFIED');
|
||||
|
||||
const ver = await request(app).post('/api/auth/verify-email').send({ token: verifyToken });
|
||||
expect(ver.status).toBe(200);
|
||||
|
||||
const ok = await request(app).post('/api/auth/login').send({ email: 'alice@example.com', password: 'secretpass' });
|
||||
expect(ok.status).toBe(200);
|
||||
const cookies = ok.headers['set-cookie'] as unknown as string[];
|
||||
expect(cookies.find((c) => c.startsWith('flashcard_sid='))).toBeDefined();
|
||||
expect(cookies.find((c) => c.startsWith('flashcard_csrf='))).toBeDefined();
|
||||
|
||||
const me = await request(app).get('/api/auth/me').set('Cookie', cookies);
|
||||
expect(me.status).toBe(200);
|
||||
expect(me.body.email).toBe('alice@example.com');
|
||||
|
||||
const csrf = extractCookieValue(cookies, 'flashcard_csrf');
|
||||
const logout = await request(app).post('/api/auth/logout').set('Cookie', cookies).set('x-csrf-token', csrf);
|
||||
expect(logout.status).toBe(204);
|
||||
|
||||
const me2 = await request(app).get('/api/auth/me').set('Cookie', cookies);
|
||||
expect(me2.status).toBe(401);
|
||||
});
|
||||
|
||||
it('marks first registered user as sysadmin', async () => {
|
||||
const r = await request(app).post('/api/auth/register').send({
|
||||
email: 'first@example.com', displayName: 'First', password: 'secretpass',
|
||||
});
|
||||
expect(r.status).toBe(201);
|
||||
const token = tokenFromMail(mailer.sent[0]!.text);
|
||||
await request(app).post('/api/auth/verify-email').send({ token });
|
||||
|
||||
const login = await request(app).post('/api/auth/login').send({ email: 'first@example.com', password: 'secretpass' });
|
||||
const cookies = login.headers['set-cookie'] as unknown as string[];
|
||||
const me = await request(app).get('/api/auth/me').set('Cookie', cookies);
|
||||
expect(me.body.role).toBe('sysadmin');
|
||||
});
|
||||
|
||||
it('rejects duplicate email', async () => {
|
||||
await request(app).post('/api/auth/register').send({ email: 'a@example.com', displayName: 'A', password: 'secretpass' });
|
||||
const dup = await request(app).post('/api/auth/register').send({ email: 'A@example.com', displayName: 'A2', password: 'secretpass' });
|
||||
expect(dup.status).toBe(409);
|
||||
expect(dup.body.error.code).toBe('EMAIL_TAKEN');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user