forked from wangziqi/gongxue-base
refactor(classes): preserve student membership history
This commit is contained in:
95
apps/server/src/classes/classes.membership.spec.ts
Normal file
95
apps/server/src/classes/classes.membership.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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 })]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user