import { BadRequestException, NotFoundException } from '@nestjs/common'; import { ClassesService } from './classes.service'; import { ClassStudent } from '../entities'; function createService( classStudentRepo: Record, classRepo: Record = { findOne: jest.fn().mockResolvedValue({ id: 3 }) }, studentRepo: Record = { find: jest.fn().mockResolvedValue([{ id: 8 }, { id: 9 }, { id: 10 }]), }, ) { return new ClassesService( classRepo as never, classStudentRepo as never, {} as never, {} as never, {} as never, {} as never, studentRepo as never, {} as never, ); } describe('ClassesService — student membership lifecycle', () => { it('marks an active membership as left instead of deleting it', async () => { const membership = { classId: 3, studentId: 8, status: 'active', leaveDate: null, } as ClassStudent; const repo = { findOne: jest.fn().mockResolvedValue(membership), save: jest.fn().mockImplementation(async (value: ClassStudent) => value), }; const service = createService(repo); await service.removeStudent(3, 8); expect(membership.status).toBe('left'); expect(membership.leaveDate).toMatch(/^\d{4}-\d{2}-\d{2}$/); expect(repo.save).toHaveBeenCalledWith(membership); }); it('rejects removing an already-left membership', async () => { const repo = { findOne: jest.fn().mockResolvedValue({ status: 'left' }), save: jest.fn(), }; const service = createService(repo); await expect(service.removeStudent(3, 8)).rejects.toBeInstanceOf(BadRequestException); expect(repo.save).not.toHaveBeenCalled(); }); it('rejects removing a student without a membership', async () => { const repo = { findOne: jest.fn().mockResolvedValue(null), save: jest.fn(), }; const service = createService(repo); await expect(service.removeStudent(3, 8)).rejects.toBeInstanceOf(NotFoundException); }); it('reactivates left memberships, creates new ones, and skips active ones', async () => { const left = { id: 1, classId: 3, studentId: 8, status: 'left', joinDate: '2026-01-01', leaveDate: '2026-02-01', } as ClassStudent; const active = { id: 2, classId: 3, studentId: 9, status: 'active' } as ClassStudent; const repo = { find: jest.fn().mockResolvedValue([left, active]), create: jest.fn().mockImplementation((value: Partial) => value), save: jest.fn().mockImplementation(async (value: ClassStudent[]) => value), }; const service = createService(repo); const result = await service.addStudents(3, [8, 9, 10, 10]); expect(result).toEqual({ added: 2, skipped: 1 }); expect(left).toMatchObject({ status: 'active', leaveDate: null }); expect(left.joinDate).toMatch(/^\d{4}-\d{2}-\d{2}$/); expect(repo.create).toHaveBeenCalledWith( expect.objectContaining({ classId: 3, studentId: 10, status: 'active' }), ); expect(repo.save).toHaveBeenCalledWith( expect.arrayContaining([left, expect.objectContaining({ studentId: 10 })]), ); }); });