import test from 'node:test'; import assert from 'node:assert/strict'; import { JwtService } from '@nestjs/jwt'; import { ForbiddenException, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { AuthService } from '../src/auth/auth.service.js'; import type { JwtUser } from '../src/common/auth.types.js'; import { KeyResultsService } from '../src/key-results/key-results.service.js'; import { ObjectivesService } from '../src/objectives/objectives.service.js'; import { PrismaService } from '../src/prisma/prisma.service.js'; // Seeded user identities (from prisma/seed.ts insertion order) const managerUser: JwtUser = { sub: 2, email: 'manager@okr.local', role: 'MANAGER', name: 'Nguyen Van Manager' }; const employeeUser: JwtUser = { sub: 3, email: 'employee@okr.local', role: 'EMPLOYEE', name: 'Nguyen Van A' }; const employee2User: JwtUser = { sub: 4, email: 'employee2@okr.local', role: 'EMPLOYEE', name: 'Tran Thi B' }; // ─── Auth ─────────────────────────────────────────────────────────────────── test('AuthService rejects invalid password with UnauthorizedException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new AuthService(prisma, new JwtService({ secret: 'test-secret' })); try { await assert.rejects(() => service.login('employee', 'wrong-password'), UnauthorizedException); } finally { await prisma.$disconnect(); } }); test('AuthService rejects non-existent user with UnauthorizedException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new AuthService(prisma, new JwtService({ secret: 'test-secret' })); try { await assert.rejects(() => service.login('nobody@ghost.local', 'Password@123'), UnauthorizedException); } finally { await prisma.$disconnect(); } }); test('AuthService accepts correct credentials and returns signed JWT', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new AuthService(prisma, new JwtService({ secret: 'test-secret' })); try { const result = await service.login('employee', 'Password@123'); assert.equal(result.user.email, 'employee@okr.local'); assert.equal(result.user.role, 'EMPLOYEE'); assert.ok(result.token.length > 20, 'JWT must be non-trivially long'); // Token has 3 dot-separated segments assert.equal(result.token.split('.').length, 3); } finally { await prisma.$disconnect(); } }); test('AuthService accepts login by email as well as username', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new AuthService(prisma, new JwtService({ secret: 'test-secret' })); try { const byUsername = await service.login('manager', 'Password@123'); const byEmail = await service.login('manager@okr.local', 'Password@123'); assert.equal(byUsername.user.id, byEmail.user.id); } finally { await prisma.$disconnect(); } }); // ─── ObjectivesService.list ────────────────────────────────────────────────── test('ObjectivesService.list employee sees only own objectives', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { const results = await service.list(employeeUser); assert.ok(results.length > 0, 'Employee must have at least one objective'); assert.ok( results.every((o) => o.ownerId === employeeUser.sub), 'Every returned objective must belong to the employee', ); } finally { await prisma.$disconnect(); } }); test('ObjectivesService.list manager sees all objectives', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { const managerResults = await service.list(managerUser); const employeeResults = await service.list(employeeUser); assert.ok(managerResults.length > employeeResults.length, 'Manager must see more objectives than employee'); } finally { await prisma.$disconnect(); } }); test('ObjectivesService.list quarter filter narrows results to matching quarter only', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { const all = await service.list(managerUser); const filtered = await service.list(managerUser, 'Q2/2026'); const empty = await service.list(managerUser, 'Q1/1900'); assert.ok(filtered.length <= all.length); assert.ok(filtered.every((o) => o.quarter === 'Q2/2026'), 'All filtered results must match the requested quarter'); assert.equal(empty.length, 0, 'No objectives should match a future non-existent quarter'); } finally { await prisma.$disconnect(); } }); // ─── ObjectivesService.getById ─────────────────────────────────────────────── test('ObjectivesService.getById returns objective with computedProgress', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { // Objective 1: KR1=33, KR2=60 → avg = Math.round((33+60)/2) = 47 const result = await service.getById(1, managerUser); assert.equal(result.id, 1); assert.equal(result.computedProgress, 47); assert.ok(result.keyResults.length === 2); assert.ok(result.owner !== undefined, 'Owner relation must be loaded'); } finally { await prisma.$disconnect(); } }); test('ObjectivesService.getById employee accessing own objective succeeds', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { // Objective 1 is owned by employee (id=3) const result = await service.getById(1, employeeUser); assert.equal(result.ownerId, employeeUser.sub); } finally { await prisma.$disconnect(); } }); test('ObjectivesService.getById employee accessing another user objective throws ForbiddenException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { // Objective 3 belongs to manager (id=2), not employee (id=3) await assert.rejects(() => service.getById(3, employeeUser), ForbiddenException); } finally { await prisma.$disconnect(); } }); test('ObjectivesService.getById non-existent id throws NotFoundException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { await assert.rejects(() => service.getById(999999, managerUser), NotFoundException); } finally { await prisma.$disconnect(); } }); // ─── ObjectivesService.create ──────────────────────────────────────────────── test('ObjectivesService.create sets status to NOT_STARTED and loads relations', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); let createdId: number | undefined; try { const result = await service.create( { title: 'Test objective for create', ownerId: employeeUser.sub, quarter: 'Q1/2099' }, managerUser, ); createdId = result.id; assert.equal(result.status, 'NOT_STARTED'); assert.equal(result.ownerId, employeeUser.sub); assert.equal(result.quarter, 'Q1/2099'); assert.ok(result.owner !== undefined, 'Owner relation must be eagerly loaded'); assert.deepEqual(result.keyResults, [], 'New objective must have no key results'); } finally { if (createdId) await prisma.objective.delete({ where: { id: createdId } }); await prisma.$disconnect(); } }); test('ObjectivesService.create employee creating for another user throws ForbiddenException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { await assert.rejects( () => service.create( { title: 'Sneaky objective', ownerId: employee2User.sub, quarter: 'Q1/2099' }, employeeUser, ), ForbiddenException, ); } finally { await prisma.$disconnect(); } }); test('ObjectivesService.create with non-existent owner throws NotFoundException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new ObjectivesService(prisma); try { await assert.rejects( () => service.create({ title: 'Ghost objective', ownerId: 999999, quarter: 'Q1/2099' }, managerUser), NotFoundException, ); } finally { await prisma.$disconnect(); } }); // ─── KeyResultsService.getById ─────────────────────────────────────────────── test('KeyResultsService.getById returns KR with nested objective and owner', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); try { // KR 1 belongs to objective 1 which is owned by employee const result = await service.getById(1, employeeUser); assert.equal(result.id, 1); assert.ok(typeof result.progress === 'number'); assert.ok(result.objective !== undefined, 'Objective relation must be loaded'); assert.ok(result.objective.owner !== undefined, 'Owner relation must be loaded inside objective'); } finally { await prisma.$disconnect(); } }); test('KeyResultsService.getById employee reading another owner KR throws ForbiddenException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); try { // KR 3 belongs to objective 2 (ownerId=employee2/id=4), not employee (id=3) await assert.rejects(() => service.getById(3, employeeUser), ForbiddenException); } finally { await prisma.$disconnect(); } }); test('KeyResultsService.getById non-existent id throws NotFoundException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); try { await assert.rejects(() => service.getById(999999, managerUser), NotFoundException); } finally { await prisma.$disconnect(); } }); // ─── KeyResultsService.updateProgress ──────────────────────────────────────── test('KeyResultsService.updateProgress blocks employee updating KR from another owner', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); try { // KR 3 belongs to employee2's objective — employee cannot update it await assert.rejects( () => service.updateProgress(3, { progress: 50, comment: 'Not mine' }, employeeUser), ForbiddenException, ); } finally { await prisma.$disconnect(); } }); test('KeyResultsService.updateProgress records new progress value and creates ProgressUpdate', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); try { const updated = await service.updateProgress(1, { progress: 100, comment: 'Completed KR' }, employeeUser); assert.equal(updated.progress, 100); const progressUpdates = await prisma.progressUpdate.findMany({ where: { keyResultId: 1, progress: 100 } }); assert.ok(progressUpdates.length > 0, 'A ProgressUpdate record must be created'); } finally { await prisma.keyResult.update({ where: { id: 1 }, data: { progress: 33 } }); await prisma.progressUpdate.deleteMany({ where: { keyResultId: 1 } }); await prisma.$disconnect(); } }); test('KeyResultsService.updateProgress non-existent KR throws NotFoundException', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); try { await assert.rejects( () => service.updateProgress(999999, { progress: 50, comment: 'ghost' }, managerUser), NotFoundException, ); } finally { await prisma.$disconnect(); } }); // ─── Status recalculation (full NOT_STARTED → IN_PROGRESS → COMPLETED cycle) ─ test('KeyResultsService status recalculation: NOT_STARTED → IN_PROGRESS → COMPLETED → IN_PROGRESS', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); let testObjectiveId: number | undefined; let testKrId: number | undefined; try { // Create isolated objective + KR owned by employee const obj = await prisma.objective.create({ data: { title: 'status-cycle-test', ownerId: employeeUser.sub, quarter: 'Q4/2099', status: 'NOT_STARTED' }, }); testObjectiveId = obj.id; const kr = await prisma.keyResult.create({ data: { objectiveId: obj.id, title: 'single KR for status test', progress: 0, startValue: 0, targetValue: 10, deadline: new Date('2099-12-31'), }, }); testKrId = kr.id; // 0% → IN_PROGRESS await service.updateProgress(kr.id, { progress: 50, comment: 'halfway' }, employeeUser); const afterHalf = await prisma.objective.findUniqueOrThrow({ where: { id: obj.id } }); assert.equal(afterHalf.status, 'IN_PROGRESS'); // 100% → COMPLETED await service.updateProgress(kr.id, { progress: 100, comment: 'done' }, employeeUser); const afterFull = await prisma.objective.findUniqueOrThrow({ where: { id: obj.id } }); assert.equal(afterFull.status, 'COMPLETED'); // Regression back → IN_PROGRESS await service.updateProgress(kr.id, { progress: 80, comment: 'regressed' }, employeeUser); const afterRegress = await prisma.objective.findUniqueOrThrow({ where: { id: obj.id } }); assert.equal(afterRegress.status, 'IN_PROGRESS'); // Back to 0 → NOT_STARTED await service.updateProgress(kr.id, { progress: 0, comment: 'reset' }, employeeUser); const afterZero = await prisma.objective.findUniqueOrThrow({ where: { id: obj.id } }); assert.equal(afterZero.status, 'NOT_STARTED'); } finally { if (testKrId) await prisma.keyResult.delete({ where: { id: testKrId } }); if (testObjectiveId) await prisma.objective.delete({ where: { id: testObjectiveId } }); await prisma.$disconnect(); } }); test('KeyResultsService averageProgress: multi-KR completion drives COMPLETED status', async () => { const prisma = new PrismaService(); await prisma.$connect(); const service = new KeyResultsService(prisma); let testObjectiveId: number | undefined; let kr1Id: number | undefined; let kr2Id: number | undefined; try { const obj = await prisma.objective.create({ data: { title: 'multi-kr-test', ownerId: employeeUser.sub, quarter: 'Q4/2099', status: 'NOT_STARTED' }, }); testObjectiveId = obj.id; const kr1 = await prisma.keyResult.create({ data: { objectiveId: obj.id, title: 'kr1', progress: 0, startValue: 0, targetValue: 1, deadline: new Date('2099-12-31') }, }); kr1Id = kr1.id; const kr2 = await prisma.keyResult.create({ data: { objectiveId: obj.id, title: 'kr2', progress: 0, startValue: 0, targetValue: 1, deadline: new Date('2099-12-31') }, }); kr2Id = kr2.id; // Set KR1 to 100%, KR2 stays 0 → avg=50 → IN_PROGRESS await service.updateProgress(kr1.id, { progress: 100, comment: '' }, employeeUser); const afterFirst = await prisma.objective.findUniqueOrThrow({ where: { id: obj.id } }); assert.equal(afterFirst.status, 'IN_PROGRESS'); // Set KR2 to 100% → avg=100 → COMPLETED await service.updateProgress(kr2.id, { progress: 100, comment: '' }, employeeUser); const afterBoth = await prisma.objective.findUniqueOrThrow({ where: { id: obj.id } }); assert.equal(afterBoth.status, 'COMPLETED'); } finally { if (kr1Id) await prisma.keyResult.delete({ where: { id: kr1Id } }); if (kr2Id) await prisma.keyResult.delete({ where: { id: kr2Id } }); if (testObjectiveId) await prisma.objective.delete({ where: { id: testObjectiveId } }); await prisma.$disconnect(); } });