test(ux): integration coverage for search + stats + due session
This commit is contained in:
90
packages/backend/src/tests/ux.integration.test.ts
Normal file
90
packages/backend/src/tests/ux.integration.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user