Files
gongxue-base/apps/server/src/classes/classes.membership.spec.ts

96 lines
3.1 KiB
TypeScript

import { BadRequestException, NotFoundException } from '@nestjs/common';
import { ClassesService } from './classes.service';
import { ClassStudent } from '../entities';
function createService(
classStudentRepo: Record<string, jest.Mock>,
classRepo: Record<string, jest.Mock> = { findOne: jest.fn().mockResolvedValue({ id: 3 }) },
studentRepo: Record<string, jest.Mock> = {
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<ClassStudent>) => 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 })]),
);
});
});