From 5e5985c92c13ee8b9611ab8067e046b71504109e Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Sat, 10 Jan 2026 19:23:08 -0300 Subject: [PATCH] test: Updated AuthController and SongController to test the current codebase --- apps/backend/src/auth/auth.controller.spec.ts | 30 +++++++-- .../src/song/my-songs/my-songs.controller.ts | 7 ++- apps/backend/src/song/song.controller.spec.ts | 63 ++++++++++++------- apps/backend/src/song/song.controller.ts | 3 + apps/backend/src/song/song.service.spec.ts | 40 +++++++++--- packages/database/tsconfig.build.json | 4 +- 6 files changed, 105 insertions(+), 42 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index d6bae4fa..0a68ba35 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -22,6 +22,9 @@ describe('AuthController', () => { let authService: AuthService; beforeEach(async () => { + // Clear all mocks before each test to ensure test isolation + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ controllers: [AuthController], providers: [ @@ -68,8 +71,13 @@ describe('AuthController', () => { describe('githubLogin', () => { it('should call AuthService.githubLogin', async () => { - await controller.githubLogin(); - expect(authService.githubLogin).toHaveBeenCalled(); + // githubLogin is just a Passport guard entry point - it doesn't call authService + // The actual login is handled by the callback endpoint (githubRedirect) + controller.githubLogin(); + // Verify the method exists and can be called without errors + expect(controller.githubLogin).toBeDefined(); + // Verify authService was NOT called (since this is just a guard entry point) + expect(authService.githubLogin).not.toHaveBeenCalled(); }); }); @@ -97,8 +105,13 @@ describe('AuthController', () => { describe('googleLogin', () => { it('should call AuthService.googleLogin', async () => { - await controller.googleLogin(); - expect(authService.googleLogin).toHaveBeenCalled(); + // googleLogin is just a Passport guard entry point - it doesn't call authService + // The actual login is handled by the callback endpoint (googleRedirect) + controller.googleLogin(); + // Verify the method exists and can be called without errors + expect(controller.googleLogin).toBeDefined(); + // Verify authService was NOT called (since this is just a guard entry point) + expect(authService.googleLogin).not.toHaveBeenCalled(); }); }); @@ -126,8 +139,13 @@ describe('AuthController', () => { describe('discordLogin', () => { it('should call AuthService.discordLogin', async () => { - await controller.discordLogin(); - expect(authService.discordLogin).toHaveBeenCalled(); + // discordLogin is just a Passport guard entry point - it doesn't call authService + // The actual login is handled by the callback endpoint (discordRedirect) + controller.discordLogin(); + // Verify the method exists and can be called without errors + expect(controller.discordLogin).toBeDefined(); + // Verify authService was NOT called (since this is just a guard entry point) + expect(authService.discordLogin).not.toHaveBeenCalled(); }); }); diff --git a/apps/backend/src/song/my-songs/my-songs.controller.ts b/apps/backend/src/song/my-songs/my-songs.controller.ts index c1e5b8ec..ece24049 100644 --- a/apps/backend/src/song/my-songs/my-songs.controller.ts +++ b/apps/backend/src/song/my-songs/my-songs.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; @@ -12,7 +12,10 @@ import { SongService } from '../song.service'; @Controller('my-songs') @ApiTags('song') export class MySongsController { - constructor(public readonly songService: SongService) {} + constructor( + @Inject(SongService) + public readonly songService: SongService, + ) {} @Get('/') @ApiOperation({ diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index 51865a09..ad035e5a 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -31,6 +31,7 @@ const mockSongService = { getSongEdit: jest.fn(), patchSong: jest.fn(), getSongDownloadUrl: jest.fn(), + getSongFileBuffer: jest.fn(), deleteSong: jest.fn(), uploadSong: jest.fn(), getRandomSongs: jest.fn(), @@ -46,6 +47,9 @@ describe('SongController', () => { let songService: SongService; beforeEach(async () => { + // Clear all mocks before each test + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ controllers: [SongController], providers: [ @@ -66,8 +70,8 @@ describe('SongController', () => { songController = module.get(SongController); songService = module.get(SongService); - // Clear all mocks - jest.clearAllMocks(); + // Verify the service is injected + expect(songController.songService).toBeDefined(); }); it('should be defined', () => { @@ -79,7 +83,12 @@ describe('SongController', () => { const query: SongListQueryDTO = { page: 1, limit: 10 }; const songList: SongPreviewDto[] = []; - mockSongService.getSongByPage.mockResolvedValueOnce(songList); + mockSongService.querySongs.mockResolvedValueOnce({ + content: songList, + page: 1, + limit: 10, + total: 0, + }); const result = await songController.getSongList(query); @@ -88,7 +97,7 @@ describe('SongController', () => { expect(result.page).toBe(1); expect(result.limit).toBe(10); expect(result.total).toBe(0); - expect(songService.getSongByPage).toHaveBeenCalled(); + expect(songService.querySongs).toHaveBeenCalled(); }); it('should handle search query', async () => { @@ -357,7 +366,7 @@ describe('SongController', () => { it('should handle errors', async () => { const query: SongListQueryDTO = { page: 1, limit: 10 }; - mockSongService.getSongByPage.mockRejectedValueOnce(new Error('Error')); + mockSongService.querySongs.mockRejectedValueOnce(new Error('Error')); await expect(songController.getSongList(query)).rejects.toThrow('Error'); }); @@ -437,7 +446,7 @@ describe('SongController', () => { expect(result.total).toBe(5); expect(result.page).toBe(1); expect(result.limit).toBe(10); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); it('should handle search with empty query string', async () => { @@ -457,7 +466,7 @@ describe('SongController', () => { expect(result).toBeInstanceOf(PageDto); expect(result.content).toEqual(songList); expect(result.total).toBe(0); - expect(songService.querySongs).toHaveBeenCalledWith(query, '', undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, ''); }); it('should handle search with null query string', async () => { @@ -476,7 +485,7 @@ describe('SongController', () => { expect(result).toBeInstanceOf(PageDto); expect(result.content).toEqual(songList); - expect(songService.querySongs).toHaveBeenCalledWith(query, '', undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, ''); }); it('should handle search with multiple pages', async () => { @@ -499,7 +508,7 @@ describe('SongController', () => { expect(result.content).toHaveLength(10); expect(result.total).toBe(25); expect(result.page).toBe(2); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); it('should handle search with large result set', async () => { @@ -521,7 +530,7 @@ describe('SongController', () => { expect(result).toBeInstanceOf(PageDto); expect(result.content).toHaveLength(50); expect(result.total).toBe(500); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); it('should handle search on last page with partial results', async () => { @@ -561,7 +570,7 @@ describe('SongController', () => { const result = await songController.searchSongs(query, q); expect(result).toBeInstanceOf(PageDto); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); it('should handle search with very long query string', async () => { @@ -579,7 +588,7 @@ describe('SongController', () => { const result = await songController.searchSongs(query, q); expect(result).toBeInstanceOf(PageDto); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); it('should handle search with custom limit', async () => { @@ -627,7 +636,7 @@ describe('SongController', () => { expect(result).toBeInstanceOf(PageDto); expect(result.content).toHaveLength(10); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); it('should return correct pagination info with search results', async () => { @@ -699,7 +708,7 @@ describe('SongController', () => { const result = await songController.searchSongs(query, q); expect(result).toBeInstanceOf(PageDto); - expect(songService.querySongs).toHaveBeenCalledWith(query, q, undefined); + expect(songService.querySongs).toHaveBeenCalledWith(query, q ?? ''); }); }); @@ -803,23 +812,31 @@ describe('SongController', () => { const res = { set: jest.fn(), - redirect: jest.fn(), + send: jest.fn(), } as unknown as Response; - const url = 'test-url'; + const buffer = Buffer.from('test-song-data'); + const filename = 'test-song.nbs'; - mockSongService.getSongDownloadUrl.mockResolvedValueOnce(url); + mockSongService.getSongFileBuffer.mockResolvedValueOnce({ + buffer, + filename, + }); await songController.getSongFile(id, src, user, res); expect(res.set).toHaveBeenCalledWith({ - 'Content-Disposition': 'attachment; filename="song.nbs"', + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${filename.replace( + /[/"]/g, + '_', + )}"`, 'Access-Control-Expose-Headers': 'Content-Disposition', }); - expect(res.redirect).toHaveBeenCalledWith(HttpStatus.FOUND, url); + expect(res.send).toHaveBeenCalledWith(Buffer.from(buffer)); - expect(songService.getSongDownloadUrl).toHaveBeenCalledWith( + expect(songService.getSongFileBuffer).toHaveBeenCalledWith( id, user, src, @@ -836,16 +853,16 @@ describe('SongController', () => { const res = { set: jest.fn(), - redirect: jest.fn(), + send: jest.fn(), } as unknown as Response; - mockSongService.getSongDownloadUrl.mockRejectedValueOnce( + mockSongService.getSongFileBuffer.mockRejectedValueOnce( new Error('Error'), ); await expect( songController.getSongFile(id, src, user, res), - ).rejects.toThrow('Error'); + ).rejects.toThrow('An error occurred while retrieving the song file'); }); }); diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index d50f2a8f..2dc2b7a2 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -8,6 +8,7 @@ import { Headers, HttpException, HttpStatus, + Inject, Logger, Param, Patch, @@ -65,7 +66,9 @@ export class SongController { }; constructor( + @Inject(SongService) public readonly songService: SongService, + @Inject(FileService) public readonly fileService: FileService, ) {} diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index 4a944f7f..7c140daa 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -24,6 +24,7 @@ import { SongService } from './song.service'; const mockFileService = { deleteSong: jest.fn(), getSongDownloadUrl: jest.fn(), + getSongFile: jest.fn(), }; const mockSongUploadService = { @@ -39,6 +40,16 @@ const mockSongWebhookService = { syncSongWebhook: jest.fn(), }; +const mockSongModel = { + create: jest.fn(), + findOne: jest.fn(), + find: jest.fn(), + deleteOne: jest.fn(), + countDocuments: jest.fn(), + aggregate: jest.fn(), + populate: jest.fn(), +}; + describe('SongService', () => { let service: SongService; let fileService: FileService; @@ -46,6 +57,9 @@ describe('SongService', () => { let songModel: Model; beforeEach(async () => { + // Clear all mocks before each test + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ providers: [ SongService, @@ -55,7 +69,7 @@ describe('SongService', () => { }, { provide: getModelToken(SongEntity.name), - useValue: mongoose.model(SongEntity.name, SongSchema), + useValue: mockSongModel, }, { provide: FileService, @@ -1014,13 +1028,13 @@ describe('SongService', () => { { _id: 'category2', count: 5 }, ]; - jest.spyOn(songModel, 'aggregate').mockResolvedValue(categories); + mockSongModel.aggregate.mockResolvedValue(categories); const result = await service.getCategories(); expect(result).toEqual({ category1: 10, category2: 5 }); - expect(songModel.aggregate).toHaveBeenCalledWith([ + expect(mockSongModel.aggregate).toHaveBeenCalledWith([ { $match: { visibility: 'public' } }, { $group: { _id: '$category', count: { $sum: 1 } } }, { $sort: { count: -1 } }, @@ -1211,8 +1225,8 @@ describe('SongService', () => { exec: jest.fn().mockResolvedValue(songList), }; - jest.spyOn(songModel, 'aggregate').mockReturnValue(mockAggregate as any); - jest.spyOn(songModel, 'populate').mockResolvedValue(songList); + mockSongModel.aggregate.mockReturnValue(mockAggregate as any); + mockSongModel.populate.mockResolvedValue(songList); const result = await service.getRandomSongs(count); @@ -1220,10 +1234,14 @@ describe('SongService', () => { songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)), ); - expect(songModel.aggregate).toHaveBeenCalledWith([ + expect(mockSongModel.aggregate).toHaveBeenCalledWith([ { $match: { visibility: 'public' } }, { $sample: { size: count } }, ]); + expect(mockSongModel.populate).toHaveBeenCalledWith(songList, { + path: 'uploader', + select: 'username profileImage -_id', + }); }); it('should return random songs with category filter', async () => { @@ -1235,8 +1253,8 @@ describe('SongService', () => { exec: jest.fn().mockResolvedValue(songList), }; - jest.spyOn(songModel, 'aggregate').mockReturnValue(mockAggregate as any); - jest.spyOn(songModel, 'populate').mockResolvedValue(songList); + mockSongModel.aggregate.mockReturnValue(mockAggregate as any); + mockSongModel.populate.mockResolvedValue(songList); const result = await service.getRandomSongs(count, category); @@ -1244,10 +1262,14 @@ describe('SongService', () => { songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)), ); - expect(songModel.aggregate).toHaveBeenCalledWith([ + expect(mockSongModel.aggregate).toHaveBeenCalledWith([ { $match: { visibility: 'public', category: 'pop' } }, { $sample: { size: count } }, ]); + expect(mockSongModel.populate).toHaveBeenCalledWith(songList, { + path: 'uploader', + select: 'username profileImage -_id', + }); }); }); }); diff --git a/packages/database/tsconfig.build.json b/packages/database/tsconfig.build.json index 0fc0958a..472e06a3 100644 --- a/packages/database/tsconfig.build.json +++ b/packages/database/tsconfig.build.json @@ -8,8 +8,8 @@ "declaration": false, "emitDeclarationOnly": false, - // Module target for Node runtime - "module": "CommonJS", + // Module target for Node runtime (ESM to match package.json "type": "module") + "module": "ES2022", "moduleResolution": "node", "target": "ES2021",