Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 90 additions & 2 deletions src/courses/courses.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Repository } from 'typeorm';
import { DataSource, Repository } from 'typeorm';
import { CoursesService } from './courses.service';
import { Course, CourseStatus } from './entities/course.entity';
import { CourseReview } from './entities/course-review.entity';
Expand Down Expand Up @@ -40,6 +40,21 @@ const mockEventEmitter = {
emit: jest.fn(),
};

const mockDataSource = {
transaction: jest.fn((cb: (manager: any) => Promise<any>) => {
const manager = {
getRepository: jest.fn((entity: any) => {
if (entity === Course) return mockCourseRepo;
if (entity === CourseVersion) return mockVersionRepo;
if (entity === CourseReview) return mockReviewRepo;
if (entity === BulkOperation) return mockBulkOpRepo;
return null;
}),
};
return cb(manager);
}),
};

const instructor: User = {
id: 'instr-1',
role: UserRole.INSTRUCTOR,
Expand Down Expand Up @@ -67,6 +82,7 @@ describe('CoursesService', () => {
{ provide: getRepositoryToken(CourseVersion), useValue: mockVersionRepo },
{ provide: getRepositoryToken(BulkOperation), useValue: mockBulkOpRepo },
{ provide: EventEmitter2, useValue: mockEventEmitter },
{ provide: DataSource, useValue: mockDataSource },
],
}).compile();

Expand All @@ -86,7 +102,6 @@ describe('CoursesService', () => {

mockCourseRepo.create.mockReturnValue(savedCourse);
mockCourseRepo.save.mockResolvedValue(savedCourse);
mockVersionRepo.findOne.mockResolvedValue(null);
mockVersionRepo.create.mockReturnValue({});
mockVersionRepo.save.mockResolvedValue({ ...savedCourse, versionNumber: 1 });

Expand Down Expand Up @@ -185,4 +200,77 @@ describe('CoursesService', () => {
);
});
});

describe('concurrent updates', () => {
it('should produce versions 2 and 3 for two sequential updates', async () => {
const existingCourse = {
...baseCourse,
instructorId: 'instr-1',
};
const updated1 = { ...existingCourse, title: 'Update A' };
const updated2 = { ...existingCourse, title: 'Update B' };

mockCourseRepo.findOne
.mockResolvedValueOnce(existingCourse)
.mockResolvedValueOnce(existingCourse);

mockCourseRepo.save.mockResolvedValueOnce(updated1).mockResolvedValueOnce(updated2);

mockVersionRepo.findOne
.mockResolvedValueOnce({ versionNumber: 1 } as CourseVersion)
.mockResolvedValueOnce({ versionNumber: 2 } as CourseVersion);

mockVersionRepo.create.mockReturnValueOnce({}).mockReturnValueOnce({});

mockVersionRepo.save
.mockResolvedValueOnce({ versionNumber: 2 })
.mockResolvedValueOnce({ versionNumber: 3 });

await service.update('course-1', { title: 'Update A' } as any, instructor);
await service.update('course-1', { title: 'Update B' } as any, instructor);

expect(mockVersionRepo.create).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ versionNumber: 2 }),
);
expect(mockVersionRepo.create).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ versionNumber: 3 }),
);
});

it('should retry once on unique violation and succeed', async () => {
const existingCourse = {
...baseCourse,
instructorId: 'instr-1',
};
const updatedCourse = { ...existingCourse, title: 'Retried update' };

const uniqueViolationError = new Error('duplicate key value violates unique constraint');
(uniqueViolationError as any).code = '23505';

mockCourseRepo.findOne.mockResolvedValue(existingCourse);
mockCourseRepo.save.mockResolvedValue(updatedCourse);
mockVersionRepo.findOne.mockResolvedValue({ versionNumber: 1 } as CourseVersion);
mockVersionRepo.create.mockReturnValue({});

let saveCallCount = 0;
mockVersionRepo.save.mockImplementation(() => {
saveCallCount++;
if (saveCallCount === 1) {
return Promise.reject(uniqueViolationError);
}
return Promise.resolve({ versionNumber: 2 });
});

const result = await service.update(
'course-1',
{ title: 'Retried update' } as any,
instructor,
);

expect(result).toEqual(updatedCourse);
expect(mockVersionRepo.save).toHaveBeenCalledTimes(2);
});
});
});
Loading
Loading