import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import request from 'supertest'; import { createTestApp, loginToken } from './helpers.js'; // ─── Auth guard ─────────────────────────────────────────────────────────────── test('unauthenticated requests are rejected with 401', async () => { const { app } = await createTestApp(); try { await request(app.getHttpServer()).get('/api/v1/objectives').expect(401); await request(app.getHttpServer()).get('/api/v1/objectives/1').expect(401); await request(app.getHttpServer()).get('/api/v1/key-results/1').expect(401); } finally { await app.close(); } }); // ─── GET /objectives (list) ─────────────────────────────────────────────────── test('GET /objectives employee sees only own objectives', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); const res = await request(app.getHttpServer()) .get('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .expect(200); assert.equal(res.body.success, true); assert.ok(Array.isArray(res.body.data)); // Every returned objective must belong to employee (id=3) assert.ok(res.body.data.every((o: { ownerId: number }) => o.ownerId === 3)); assert.equal(res.body.meta.total, res.body.data.length); } finally { await app.close(); } }); test('GET /objectives manager sees all objectives', async () => { const { app } = await createTestApp(); try { const managerToken = await loginToken(app, 'manager'); const employeeToken = await loginToken(app, 'employee'); const managerRes = await request(app.getHttpServer()) .get('/api/v1/objectives') .set('Authorization', `Bearer ${managerToken}`) .expect(200); const employeeRes = await request(app.getHttpServer()) .get('/api/v1/objectives') .set('Authorization', `Bearer ${employeeToken}`) .expect(200); assert.ok(managerRes.body.data.length > employeeRes.body.data.length); assert.equal(managerRes.body.data.length, 3); } finally { await app.close(); } }); test('GET /objectives?quarter= filters to matching quarter only', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); const matching = await request(app.getHttpServer()) .get('/api/v1/objectives?quarter=Q2/2026') .set('Authorization', `Bearer ${token}`) .expect(200); const empty = await request(app.getHttpServer()) .get('/api/v1/objectives?quarter=Q1/1900') .set('Authorization', `Bearer ${token}`) .expect(200); assert.ok(matching.body.data.every((o: { quarter: string }) => o.quarter === 'Q2/2026')); assert.equal(empty.body.data.length, 0); assert.equal(empty.body.meta.total, 0); } finally { await app.close(); } }); // ─── GET /objectives/:id ────────────────────────────────────────────────────── test('GET /objectives/:id returns objective with computedProgress and relations', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); const res = await request(app.getHttpServer()) .get('/api/v1/objectives/1') .set('Authorization', `Bearer ${token}`) .expect(200); const data = res.body.data; assert.equal(data.id, 1); assert.equal(typeof data.computedProgress, 'number'); // KR1=33, KR2=60 → avg = Math.round(46.5) = 47 assert.equal(data.computedProgress, 47); assert.ok(data.owner !== undefined); assert.ok(Array.isArray(data.keyResults)); } finally { await app.close(); } }); test('GET /objectives/:id returns 404 for non-existent objective', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); await request(app.getHttpServer()) .get('/api/v1/objectives/999999') .set('Authorization', `Bearer ${token}`) .expect(404); } finally { await app.close(); } }); test('GET /objectives/:id returns 403 when employee accesses another owner objective', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); // Objective 3 belongs to manager (id=2), not employee (id=3) await request(app.getHttpServer()) .get('/api/v1/objectives/3') .set('Authorization', `Bearer ${token}`) .expect(403); } finally { await app.close(); } }); // ─── POST /objectives ───────────────────────────────────────────────────────── test('POST /objectives returns 400 for empty title', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); await request(app.getHttpServer()) .post('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .send({ title: '', ownerId: 3, quarter: 'Q1/2026' }) .expect(400); } finally { await app.close(); } }); test('POST /objectives returns 400 for invalid quarter format', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); // DTO requires Q[1-4]/YYYY format await request(app.getHttpServer()) .post('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .send({ title: 'Valid title', ownerId: 3, quarter: '2026-Q2' }) .expect(400); } finally { await app.close(); } }); test('POST /objectives manager creates objective and receives it with NOT_STARTED status', async () => { const { app, prisma } = await createTestApp(); let createdId: number | undefined; try { const token = await loginToken(app, 'manager'); const res = await request(app.getHttpServer()) .post('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .send({ title: 'E2E created objective', ownerId: 3, quarter: 'Q1/2099' }) .expect(201); createdId = res.body.data.id; assert.equal(res.body.data.status, 'NOT_STARTED'); assert.equal(res.body.data.ownerId, 3); assert.ok(typeof createdId === 'number'); } finally { if (createdId) await prisma.objective.delete({ where: { id: createdId } }); await app.close(); } }); test('POST /objectives returns 403 when employee creates for another user', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); // employee (id=3) trying to create objective for employee2 (id=4) await request(app.getHttpServer()) .post('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .send({ title: 'Sneaky objective', ownerId: 4, quarter: 'Q1/2099' }) .expect(403); } finally { await app.close(); } }); test('POST /objectives returns 404 for non-existent owner', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); await request(app.getHttpServer()) .post('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .send({ title: 'Ghost owner objective', ownerId: 999999, quarter: 'Q1/2099' }) .expect(404); } finally { await app.close(); } }); // ─── GET /key-results/:id ───────────────────────────────────────────────────── test('GET /key-results/:id returns KR with nested objective', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); const res = await request(app.getHttpServer()) .get('/api/v1/key-results/1') .set('Authorization', `Bearer ${token}`) .expect(200); assert.equal(res.body.success, true); assert.equal(res.body.data.id, 1); assert.ok(res.body.data.objective !== undefined); } finally { await app.close(); } }); test('GET /key-results/:id returns 404 for non-existent KR', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); await request(app.getHttpServer()) .get('/api/v1/key-results/999999') .set('Authorization', `Bearer ${token}`) .expect(404); } finally { await app.close(); } }); test('GET /key-results/:id returns 403 when employee reads another owner KR', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); // KR 3 belongs to objective 2 (ownerId=employee2/id=4) await request(app.getHttpServer()) .get('/api/v1/key-results/3') .set('Authorization', `Bearer ${token}`) .expect(403); } finally { await app.close(); } }); // ─── POST /key-results ──────────────────────────────────────────────────────── test('POST /key-results employee creates KR on own objective', async () => { const { app, prisma } = await createTestApp(); let createdId: number | undefined; try { const token = await loginToken(app, 'employee'); const res = await request(app.getHttpServer()) .post('/api/v1/key-results') .set('Authorization', `Bearer ${token}`) .send({ objectiveId: 1, title: 'E2E created key result', progress: 0, startValue: 0, targetValue: 5, deadline: '2099-12-31', }) .expect(201); createdId = res.body.data.id; assert.equal(res.body.data.progress, 0); assert.equal(res.body.data.objectiveId, 1); } finally { if (createdId) await prisma.keyResult.delete({ where: { id: createdId } }); await app.close(); } }); test('POST /key-results returns 403 when employee creates KR on another owner objective', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); // Objective 3 belongs to manager (id=2) await request(app.getHttpServer()) .post('/api/v1/key-results') .set('Authorization', `Bearer ${token}`) .send({ objectiveId: 3, title: 'Sneaky KR', progress: 0, startValue: 0, targetValue: 1, deadline: '2099-12-31', }) .expect(403); } finally { await app.close(); } }); // ─── PATCH /key-results/:id/progress ───────────────────────────────────────── test('PATCH /key-results/:id/progress updates progress and returns updated KR', async () => { const { app, prisma } = await createTestApp(); try { const token = await loginToken(app, 'employee'); const res = await request(app.getHttpServer()) .patch('/api/v1/key-results/2/progress') .set('Authorization', `Bearer ${token}`) .send({ progress: 70, comment: 'E2E progress update' }) .expect(200); assert.equal(res.body.data.progress, 70); } finally { await prisma.keyResult.update({ where: { id: 2 }, data: { progress: 60 } }); await prisma.progressUpdate.deleteMany({ where: { keyResultId: 2, progress: 70 } }); await app.close(); } }); test('PATCH /key-results/:id/progress returns 400 for out-of-range values', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); await request(app.getHttpServer()) .patch('/api/v1/key-results/1/progress') .set('Authorization', `Bearer ${token}`) .send({ progress: -1 }) .expect(400); await request(app.getHttpServer()) .patch('/api/v1/key-results/1/progress') .set('Authorization', `Bearer ${token}`) .send({ progress: 101 }) .expect(400); } finally { await app.close(); } }); test('PATCH /key-results/:id/progress returns 403 when employee updates another owner KR', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); // KR 3 belongs to employee2's objective await request(app.getHttpServer()) .patch('/api/v1/key-results/3/progress') .set('Authorization', `Bearer ${token}`) .send({ progress: 50 }) .expect(403); } finally { await app.close(); } }); test('PATCH /key-results/:id/progress returns 404 for non-existent KR', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); await request(app.getHttpServer()) .patch('/api/v1/key-results/999999/progress') .set('Authorization', `Bearer ${token}`) .send({ progress: 50 }) .expect(404); } finally { await app.close(); } }); // ─── Golden regression ──────────────────────────────────────────────────────── test('golden: manager objective list does not drift', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); const response = await request(app.getHttpServer()) .get('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .expect(200); const canonical = JSON.stringify( { success: response.body.success, data: response.body.data.map((o: { id: number; title: string; quarter: string; status: string; keyResults: { id: number; title: string; progress: number }[] }) => ({ id: o.id, title: o.title, quarter: o.quarter, status: o.status, keyResults: o.keyResults.map((kr) => ({ id: kr.id, title: kr.title, progress: kr.progress })), })), meta: response.body.meta, }, null, 2, ); const fixture = await fs.readFile(path.resolve('test/golden/objectives.manager.json'), 'utf8'); assert.equal(`${canonical}\n`, fixture, 'Manager objective list response drifted from golden fixture'); } finally { await app.close(); } }); test('golden: employee objective list does not drift', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'employee'); const response = await request(app.getHttpServer()) .get('/api/v1/objectives') .set('Authorization', `Bearer ${token}`) .expect(200); const canonical = JSON.stringify( { success: response.body.success, data: response.body.data.map((o: { id: number; title: string; quarter: string; status: string; keyResults: { id: number; title: string; progress: number }[] }) => ({ id: o.id, title: o.title, quarter: o.quarter, status: o.status, keyResults: o.keyResults.map((kr) => ({ id: kr.id, title: kr.title, progress: kr.progress })), })), meta: response.body.meta, }, null, 2, ); const fixture = await fs.readFile(path.resolve('test/golden/objectives.employee.json'), 'utf8'); assert.equal(`${canonical}\n`, fixture, 'Employee objective list response drifted from golden fixture'); } finally { await app.close(); } }); test('golden: objective detail (GET /objectives/1) does not drift', async () => { const { app } = await createTestApp(); try { const token = await loginToken(app, 'manager'); const response = await request(app.getHttpServer()) .get('/api/v1/objectives/1') .set('Authorization', `Bearer ${token}`) .expect(200); const data = response.body.data; const canonical = JSON.stringify( { success: response.body.success, data: { id: data.id, title: data.title, quarter: data.quarter, status: data.status, computedProgress: data.computedProgress, keyResults: data.keyResults.map((kr: { id: number; title: string; progress: number }) => ({ id: kr.id, title: kr.title, progress: kr.progress, })), }, }, null, 2, ); const fixture = await fs.readFile(path.resolve('test/golden/objective-detail.json'), 'utf8'); assert.equal(`${canonical}\n`, fixture, 'Objective detail response drifted from golden fixture'); } finally { await app.close(); } });