feat(auth): /api/auth routes + integration tests (pending wiring)

This commit is contained in:
2026-05-20 22:56:10 +02:00
parent 574e3de0e8
commit 70658556aa
2 changed files with 324 additions and 0 deletions

View 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');
});
});