test(ux): integration coverage for search + stats + due session

This commit is contained in:
2026-05-21 07:02:13 +02:00
parent 9bfcb02c25
commit 65dcd185b8

View File

@@ -0,0 +1,90 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import { createApp } from '../app.js';
import { makeTestDb, createUserDirect } from './dbHelper.js';
import { hashPassword } from '../services/auth/passwords.js';
import { setMailerForTests, type Mailer } from '../services/auth/email.js';
class StubMailer implements Mailer { async send() {} }
async function login(app: ReturnType<typeof createApp>, email: string, password = 'secretpass') {
const r = await request(app).post('/api/auth/login').send({ email, password });
if (r.status !== 200) throw new Error(`login failed: ${r.status}`);
const cookies = r.headers['set-cookie'] as unknown as string[];
const csrf = cookies.find((c) => c.startsWith('flashcard_csrf='))!.split(';')[0]!.split('=')[1]!;
return { cookies, csrf };
}
async function makeUser(env: ReturnType<typeof makeTestDb>, email: string, role: 'user'|'sysadmin' = 'user') {
return createUserDirect(env.db, {
email, role, isActive: true,
passwordHash: await hashPassword('secretpass'),
emailVerifiedAt: Math.floor(Date.now() / 1000),
});
}
let env: ReturnType<typeof makeTestDb>;
let app: ReturnType<typeof createApp>;
beforeEach(async () => {
env = makeTestDb();
setMailerForTests(new StubMailer());
app = createApp(env.db);
});
describe('UX integration', () => {
it('GET /api/search filters by visibility', async () => {
await makeUser(env, 'a@example.com');
await makeUser(env, 'b@example.com');
const aAuth = await login(app, 'a@example.com');
await request(app).post('/api/lessons').set('Cookie', aAuth.cookies).set('x-csrf-token', aAuth.csrf)
.send({ name: 'Spaans private' });
const lShared = (await request(app).post('/api/lessons').set('Cookie', aAuth.cookies).set('x-csrf-token', aAuth.csrf)
.send({ name: 'Spaans public' })).body;
await request(app).patch(`/api/lessons/${lShared.id}/visibility`).set('Cookie', aAuth.cookies).set('x-csrf-token', aAuth.csrf)
.send({ visibility: 'shared' });
const bAuth = await login(app, 'b@example.com');
const r = await request(app).get('/api/search?q=spaans').set('Cookie', bAuth.cookies);
expect(r.status).toBe(200);
const names = r.body.lessons.map((l: { name: string }) => l.name);
expect(names).toContain('Spaans public');
expect(names).not.toContain('Spaans private');
});
it('GET /api/stats/lessons-progress returns only roots', async () => {
await makeUser(env, 'u@example.com');
const uAuth = await login(app, 'u@example.com');
const root = (await request(app).post('/api/lessons').set('Cookie', uAuth.cookies).set('x-csrf-token', uAuth.csrf)
.send({ name: 'Root' })).body;
await request(app).post('/api/lessons').set('Cookie', uAuth.cookies).set('x-csrf-token', uAuth.csrf)
.send({ name: 'Child', parentId: root.id });
const r = await request(app).get('/api/stats/lessons-progress').set('Cookie', uAuth.cookies);
expect(r.status).toBe(200);
expect(r.body.rows).toHaveLength(1);
expect(r.body.rows[0].name).toBe('Root');
});
it('GET /api/stats/due returns counts', async () => {
await makeUser(env, 'u@example.com');
const uAuth = await login(app, 'u@example.com');
const r = await request(app).get('/api/stats/due').set('Cookie', uAuth.cookies);
expect(r.status).toBe(200);
expect(r.body).toHaveProperty('overdue');
expect(r.body).toHaveProperty('today');
expect(r.body).toHaveProperty('tomorrow');
expect(r.body).toHaveProperty('thisWeek');
});
it('POST /api/sessions/due creates a session', async () => {
await makeUser(env, 'u@example.com');
const uAuth = await login(app, 'u@example.com');
const lesson = (await request(app).post('/api/lessons').set('Cookie', uAuth.cookies).set('x-csrf-token', uAuth.csrf)
.send({ name: 'L' })).body;
await request(app).post(`/api/lessons/${lesson.id}/cards`).set('Cookie', uAuth.cookies).set('x-csrf-token', uAuth.csrf)
.send({ question: 'q', answer: 'a' });
const r = await request(app).post('/api/sessions/due').set('Cookie', uAuth.cookies).set('x-csrf-token', uAuth.csrf);
expect(r.status).toBe(201);
expect(r.body.session.id).toBeGreaterThan(0);
});
});