diff --git a/packages/backend/src/tests/ux.integration.test.ts b/packages/backend/src/tests/ux.integration.test.ts new file mode 100644 index 0000000..b9c4569 --- /dev/null +++ b/packages/backend/src/tests/ux.integration.test.ts @@ -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, 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, 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; +let app: ReturnType; +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); + }); +});