diff --git a/test/ai.controller.spec.ts b/test/ai.controller.spec.ts new file mode 100644 index 0000000..3085167 --- /dev/null +++ b/test/ai.controller.spec.ts @@ -0,0 +1,150 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; + +import { AiController } from '@/presentation/controllers/ai.controller'; +import { IAiService } from '@/application/ports/ai.port'; +import { SessionGuard } from '@/presentation/guards/session.guard'; +import { + PredictionDto, + PredictionResponseDto, +} from '@/presentation/dtos/ai.dto'; +import { + AiService, + PredictionWithModules, +} from '@/application/services/ai.service'; + +// ============================ +// Mock SessionGuard +// ============================ +jest.mock('@/presentation/guards/session.guard', () => ({ + SessionGuard: jest.fn().mockImplementation(() => ({ + canActivate: () => true, + })), +})); + +describe('AiController (Integration)', () => { + let app: INestApplication; + let aiService: jest.Mocked; + + const mockSession = { + user: { id: 'user-1', role: 'STUDENT' }, + }; + + const predictionDto: PredictionDto = { + currentStudy: 'CS', + interests: ['AI'], + wantedStudyCreditRange: [5, 10], + locationPreference: ['Amsterdam'], + learningGoals: ['ML'], + levelPreference: ['HBO'], + preferredLanguage: 'EN', + preferredPeriod: ['P2'], + }; + + const mockPrediction: PredictionWithModules = { + predictions: [ + { + module: { + id: 1, + name: 'AI Basics', + shortDescription: 'Intro', + studyCredits: 5, + level: 'HBO', + location: [{ id: 1, name: 'Breda' }], + startDate: '2025-02-01', + }, + score: 0.9, + motivation: 'Perfect match', + }, + ], + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AiController], + providers: [ + { + provide: 'SERVICE.AI', + useValue: { + getPrediction: jest.fn(), + }, + }, + ], + }) + .overrideGuard(SessionGuard) + .useValue({ canActivate: () => true }) + .compile(); + + app = module.createNestApplication(); + + // Fake session middleware + app.use((req: any, _res: any, next: any) => { + req.session = mockSession; + next(); + }); + + await app.init(); + + aiService = module.get('SERVICE.AI'); + }); + + afterAll(async () => { + await app.close(); + }); + + // ============================ + // POST /ai/predict → Success + // ============================ + it('POST /ai/predict → retourneert voorspellingen', async () => { + aiService.getPrediction.mockResolvedValue({ + _tag: 'Success', + data: mockPrediction, + }); + + const response = await request(app.getHttpServer()) + .post('/ai/predict') + .send(predictionDto) + .expect(200); + + expect(response.body.predictions).toBeDefined(); + expect(response.body.predictions).toHaveLength(1); + expect(response.body.predictions[0].module.name).toBe('AI Basics'); + }); + + // ============================ + // POST /ai/predict → Failure + // ============================ + it('POST /ai/predict → faalt bij error', async () => { + aiService.getPrediction.mockResolvedValue({ + _tag: 'Failure', + error: new Error('AI service unavailable'), + }); + + const response = await request(app.getHttpServer()) + .post('/ai/predict') + .send(predictionDto) + .expect(400); + + expect(response.body.message).toBe('Prediction generation failed'); + expect(response.body.details).toBe('AI service unavailable'); + }); + + // ============================ + // POST /ai/predict → lege predictions + // ============================ + it('POST /ai/predict → retourneert lege array', async () => { + aiService.getPrediction.mockResolvedValue({ + _tag: 'Success', + data: { predictions: [] }, + }); + + const response = await request(app.getHttpServer()) + .post('/ai/predict') + .send(predictionDto) + .expect(200); + + expect(response.body.predictions).toBeDefined(); + expect(response.body.predictions).toHaveLength(0); + }); +}); diff --git a/test/ai.intergration.spec.ts b/test/ai.intergration.spec.ts new file mode 100644 index 0000000..a57b194 --- /dev/null +++ b/test/ai.intergration.spec.ts @@ -0,0 +1,125 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AiController } from '@/presentation/controllers/ai.controller'; +import { AiService } from '@/application/services/ai.service'; +import { BadRequestException } from '@nestjs/common'; +import { PredictionDto } from '@/presentation/dtos/ai.dto'; +import { LoggerService } from '@/logger.service'; +import { SessionGuard } from '@/presentation/guards/session.guard'; + +describe('AI Integration Test', () => { + let aiController: AiController; + let aiService: AiService; + + const mockLogger = { + setContext: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const mockPredictionClient = { + getPrediction: jest.fn(), + }; + + const mockModuleService = { + findManyByIds: jest.fn(), + }; + + const mockPredictionResult = [ + { id: 1, similarity_score: 0.95, motivation: 'Great match' }, + { id: 2, similarity_score: 0.87, motivation: 'Good fit' }, + ]; + + const mockModules = [ + { + id: 1, + name: 'Math 101', + shortDescription: 'Basic Math', + studyCredits: 30, + level: 'NFQL5', + location: ['Breda', 'Roosendaal'], + startDate: new Date(), + }, + { + id: 2, + name: 'Physics 101', + shortDescription: 'Basic Physics', + studyCredits: 15, + level: 'Beginner', + location: ['Breda', 'Roosendaal'], + startDate: new Date(), + }, + ]; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AiController], + providers: [ + { provide: 'SERVICE.AI', useClass: AiService }, + { provide: 'CLIENT.PREDICTION', useValue: mockPredictionClient }, + { provide: 'SERVICE.MODULE', useValue: mockModuleService }, + { provide: LoggerService, useValue: mockLogger }, + ], + }) + .overrideGuard(SessionGuard) // <-- guard overschrijven + .useValue({ canActivate: () => true }) + .compile(); + + aiController = module.get(AiController); + aiService = module.get('SERVICE.AI'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return predictions successfully', async () => { + mockPredictionClient.getPrediction.mockResolvedValue(mockPredictionResult); + mockModuleService.findManyByIds.mockResolvedValue(mockModules); + + const session: any = { user: { id: 'user-1', role: 'STUDENT' } }; + const wantedCredits: [number, number] = [15, 30]; + + const dto: PredictionDto = { + currentStudy: 'Science', + interests: ['Math', 'Physics'], + wantedStudyCreditRange: wantedCredits, + locationPreference: ['Breda', 'Roosendaal'], + learningGoals: ['Understand basics'], + levelPreference: ['NFQL5'], + preferredLanguage: 'EN', + preferredPeriod: ['P1', 'P2'], + }; + + const result = await aiController.createPrediction(session, dto); + + expect(result.predictions.length).toBe(2); + expect(result.predictions[0].module.name).toBe('Math 101'); + + expect(mockPredictionClient.getPrediction).toHaveBeenCalled(); + expect(mockModuleService.findManyByIds).toHaveBeenCalledWith([1, 2]); + }); + + it('should throw BadRequestException on failure', async () => { + mockPredictionClient.getPrediction.mockRejectedValue( + new Error('AI service down'), + ); + const session: any = { user: { id: 'user-1', role: 'STUDENT' } }; + const wantedCredits: [number, number] = [15, 30]; + + const dto: PredictionDto = { + currentStudy: 'Science', + interests: [], + wantedStudyCreditRange: wantedCredits, + locationPreference: ['Breda', 'Roosendaal'], + learningGoals: [], + levelPreference: ['NFQL5'], + preferredLanguage: 'EN', + preferredPeriod: ['P1', 'P2'], + }; + + await expect(aiController.createPrediction(session, dto)).rejects.toThrow( + BadRequestException, + ); + }); +}); diff --git a/test/ai.service.spec.ts b/test/ai.service.spec.ts new file mode 100644 index 0000000..b534d39 --- /dev/null +++ b/test/ai.service.spec.ts @@ -0,0 +1,177 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { + AiService, + PredictionWithModules, +} from '@/application/services/ai.service'; +import { IPredictionClient } from '@/domain/prediction/prediction-client.interface'; +import { IModuleService } from '@/application/ports/module.port'; +import { PredictionDto } from '@/presentation/dtos/ai.dto'; +import { Result } from '@/result'; +import { LoggerService } from '@/logger.service'; + +describe('AiService', () => { + let aiService: AiService; + let predictionClient: jest.Mocked; + let moduleService: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AiService, + { + provide: 'CLIENT.PREDICTION', + useValue: { + getPrediction: jest.fn(), + }, + }, + { + provide: 'SERVICE.MODULE', + useValue: { + findManyByIds: jest.fn(), + }, + }, + { + provide: LoggerService, // ✅ voeg mock LoggerService toe + useValue: { + log: jest.fn(), + error: jest.fn(), + setContext: jest.fn(), + }, + }, + ], + }).compile(); + + aiService = module.get(AiService); + predictionClient = module.get('CLIENT.PREDICTION'); + moduleService = module.get('SERVICE.MODULE'); + }); + + it('should return predictions with full modules (success)', async () => { + predictionClient.getPrediction.mockResolvedValue([ + { id: 1, similarity_score: 0.9, motivation: 'Perfect match' }, + { id: 2, similarity_score: 0.8, motivation: 'Good fit' }, + ]); + + moduleService.findManyByIds.mockResolvedValue([ + { + id: 1, + name: 'AI Basics', + shortDescription: 'Intro', + studyCredits: 5, + level: 'HBO', + location: [{ id: 1, name: 'Breda' }], + moduleTags: [{ id: 1, name: 'AI' }], + learningOutcomes: 'AI begrijpen', + startDate: '2025-02-01', + description: '', + content: '', + availableSpots: 0, + }, + { + id: 2, + name: 'Web Dev', + shortDescription: 'Frontend & Backend', + studyCredits: 5, + level: 'HBO', + location: [{ id: 1, name: 'Breda' }], + moduleTags: [], + learningOutcomes: '', + startDate: '2025-02-01', + description: '', + content: '', + availableSpots: 0, + }, + ]); + + const dto: PredictionDto = { + currentStudy: 'CS', + interests: ['AI'], + wantedStudyCreditRange: [5, 10], + locationPreference: ['Amsterdam'], + learningGoals: ['ML'], + levelPreference: ['HBO'], + preferredLanguage: 'EN', + preferredPeriod: ['P2'], + }; + + const result = await aiService.getPrediction(dto); + + expect(result._tag).toBe('Success'); + if (result._tag === 'Success') { + expect(result.data.predictions).toHaveLength(2); + expect(result.data.predictions[0].module.name).toBe('AI Basics'); + } + }); + + it('should fail if a module is missing', async () => { + predictionClient.getPrediction.mockResolvedValue([ + { id: 1, similarity_score: 0.9, motivation: 'Perfect match' }, + ]); + moduleService.findManyByIds.mockResolvedValue([]); // simulate missing module + + const dto: PredictionDto = { + currentStudy: 'CS', + interests: ['AI'], + wantedStudyCreditRange: [5, 10], + locationPreference: ['Amsterdam'], + learningGoals: ['ML'], + levelPreference: ['HBO'], + preferredLanguage: 'EN', + preferredPeriod: ['P2'], + }; + + const result = await aiService.getPrediction(dto); + + expect(result._tag).toBe('Failure'); + if (result._tag === 'Failure') { + expect(result.error.message).toContain('Kan de aanbevolen module'); + } + }); + + it('should fail if prediction client throws', async () => { + predictionClient.getPrediction.mockRejectedValue( + new Error('Service unavailable'), + ); + + const dto: PredictionDto = { + currentStudy: 'CS', + interests: ['AI'], + wantedStudyCreditRange: [5, 10], + locationPreference: ['Amsterdam'], + learningGoals: ['ML'], + levelPreference: ['HBO'], + preferredLanguage: 'EN', + preferredPeriod: ['P2'], + }; + + const result = await aiService.getPrediction(dto); + + expect(result._tag).toBe('Failure'); + if (result._tag === 'Failure') { + expect(result.error.message).toBe('Service unavailable'); + } + }); + + it('should return empty predictions array if client returns empty', async () => { + predictionClient.getPrediction.mockResolvedValue([]); + moduleService.findManyByIds.mockResolvedValue([]); + + const dto: PredictionDto = { + currentStudy: 'CS', + interests: ['AI'], + wantedStudyCreditRange: [5, 10], + locationPreference: ['Amsterdam'], + learningGoals: ['ML'], + levelPreference: ['HBO'], + preferredLanguage: 'EN', + preferredPeriod: ['P2'], + }; + + const result = await aiService.getPrediction(dto); + + expect(result._tag).toBe('Success'); + if (result._tag === 'Success') { + expect(result.data.predictions).toHaveLength(0); + } + }); +}); diff --git a/test/auth.controller.spec.ts b/test/auth.controller.spec.ts new file mode 100644 index 0000000..48512f5 --- /dev/null +++ b/test/auth.controller.spec.ts @@ -0,0 +1,127 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from '@/presentation/controllers/auth.controller'; +import { IAuthService } from '@/application/ports/auth.port'; +import { LoginDto } from '@/presentation/dtos/auth.dto'; +import { User } from '@/domain/user/user.model'; +import { fail, succeed } from '@/result'; +import { AuthenticatedSession, SessionData } from '@/types/session.types'; +import { ConflictException, BadRequestException } from '@nestjs/common'; +import { SessionGuard } from '@/presentation/guards/session.guard'; + +describe('AuthController', () => { + let authController: AuthController; + let authService: jest.Mocked; + + const mockUser: User = { + id: 'user-1', + name: 'Test User', + email: 'test@example.com', + hashedPassword: 'hashed-password', + role: 'STUDENT', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockSession = () => + ({ + user: undefined, + lastActivity: undefined, + destroy: jest.fn((cb) => cb()), + }) as unknown as SessionData; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: 'SERVICE.AUTH', + useValue: { + login: jest.fn(), + }, + }, + ], + }) + // Guards mocken zodat ze altijd 'pass' geven + .overrideGuard(SessionGuard) + .useValue({ canActivate: () => true }) + .compile(); + + authController = module.get(AuthController); + authService = module.get('SERVICE.AUTH'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('login', () => { + it('should login successfully', async () => { + const session = mockSession(); + const dto: LoginDto = { email: mockUser.email, password: 'password' }; + + authService.login.mockResolvedValue(succeed({ user: mockUser })); + + const result = await authController.login(dto, session); + + expect(result.user.email).toBe(mockUser.email); + expect(session.user).toEqual(mockUser); + expect(session.lastActivity).toBeDefined(); + expect(authService.login).toHaveBeenCalledWith(dto.email, dto.password); + }); + + it('should throw BadRequestException on login failure', async () => { + const session = mockSession(); + const dto: LoginDto = { email: 'wrong@example.com', password: '1234' }; + + authService.login.mockResolvedValue( + fail(new Error('Invalid credentials')), + ); + + await expect(authController.login(dto, session)).rejects.toThrow( + BadRequestException, + ); + }); + + it('should throw ConflictException if already logged in', async () => { + const session = mockSession(); + session.user = mockUser; + const dto: LoginDto = { email: mockUser.email, password: 'password' }; + + await expect(authController.login(dto, session)).rejects.toThrow( + ConflictException, + ); + }); + }); + + describe('logout', () => { + it('should logout successfully', async () => { + const destroyMock = jest.fn((cb) => cb()); + const session = { + user: mockUser, + destroy: destroyMock, + } as unknown as AuthenticatedSession; + + const res = { clearCookie: jest.fn() } as unknown as any; + + const result = await authController.logout(session, res); + + expect(result.message).toBe('Successfully logged out'); + expect(destroyMock).toHaveBeenCalled(); + expect(res.clearCookie).toHaveBeenCalledWith('connect.sid'); + }); + + it('should throw BadRequestException if destroy fails', async () => { + const destroyMock = jest.fn((cb) => cb(new Error('fail'))); + const session = { + user: mockUser, + destroy: destroyMock, + } as unknown as AuthenticatedSession; + + const res = { clearCookie: jest.fn() } as unknown as any; + + await expect(authController.logout(session, res)).rejects.toThrow( + BadRequestException, + ); + }); + }); +}); diff --git a/test/auth.intergration.spec.ts b/test/auth.intergration.spec.ts new file mode 100644 index 0000000..c25335f --- /dev/null +++ b/test/auth.intergration.spec.ts @@ -0,0 +1,116 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from '@/presentation/controllers/auth.controller'; +import { AuthService } from '@/application/services/auth.service'; +import { IUserRepository } from '@/domain/user/user-repository.interface'; +import { LoginDto } from '@/presentation/dtos/auth.dto'; +import { fail, succeed } from '@/result'; +import { User } from '@/domain/user/user.model'; +import { verify } from '@node-rs/argon2'; +import { LoggerService } from '@/logger.service'; +import { SessionGuard } from '@/presentation/guards/session.guard'; +import { ExecutionContext } from '@nestjs/common'; + +jest.mock('@node-rs/argon2'); + +describe('Auth Integration Test', () => { + let authController: AuthController; + let userRepository: jest.Mocked; + + const mockUser: User = { + id: 'user-1', + name: 'Test User', + email: 'test@example.com', + hashedPassword: 'hashed-password', + role: 'STUDENT', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockLogger = { + setContext: jest.fn(), + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + // hier registreren we AuthService onder dezelfde token als de controller verwacht + { + provide: 'SERVICE.AUTH', + useClass: AuthService, + }, + { + provide: 'REPO.USER', + useValue: { + findByEmail: jest.fn(), + }, + }, + { + provide: LoggerService, + useValue: mockLogger, + }, + ], + }) + // override SessionGuard zodat hij altijd true returned + .overrideGuard(SessionGuard) + .useValue({ + canActivate: (context: ExecutionContext) => true, + }) + .compile(); + + authController = module.get(AuthController); + userRepository = module.get('REPO.USER'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should login successfully through controller → service → repository', async () => { + userRepository.findByEmail.mockResolvedValue(succeed(mockUser)); + (verify as jest.Mock).mockResolvedValue(true); + + const session: any = {}; + const dto: LoginDto = { email: mockUser.email, password: 'password' }; + + const result = await authController.login(dto, session); + + expect(result.user.email).toBe(mockUser.email); + expect(session.user).toEqual(mockUser); + expect(session.lastActivity).toBeDefined(); + expect(userRepository.findByEmail).toHaveBeenCalledWith(mockUser.email); + expect(verify).toHaveBeenCalledWith( + mockUser.hashedPassword, + 'password', + expect.any(Object), + ); + }); + + it('should fail login if credentials are invalid', async () => { + userRepository.findByEmail.mockResolvedValue(succeed(mockUser)); + (verify as jest.Mock).mockResolvedValue(false); + + const session: any = {}; + const dto: LoginDto = { email: mockUser.email, password: 'wrong-password' }; + + await expect(authController.login(dto, session)).rejects.toThrow( + 'Failed to login: Invalid credentials', + ); + }); + + it('should fail login if user does not exist', async () => { + userRepository.findByEmail.mockResolvedValue( + fail(new Error('User not found')), + ); + + const session: any = {}; + const dto: LoginDto = { email: 'noone@test.com', password: 'password' }; + + await expect(authController.login(dto, session)).rejects.toThrow( + 'Failed to login: User not found', + ); + }); +}); diff --git a/test/auth.service.spec.ts b/test/auth.service.spec.ts new file mode 100644 index 0000000..40293cc --- /dev/null +++ b/test/auth.service.spec.ts @@ -0,0 +1,109 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from '@/application/services/auth.service'; +import { IUserRepository } from '@/domain/user/user-repository.interface'; +import { fail, succeed } from '@/result'; +import { verify } from '@node-rs/argon2'; +import { User } from '@/domain/user/user.model'; +import { LoggerService } from '@/logger.service'; + +jest.mock('@node-rs/argon2'); + +describe('AuthService', () => { + let authService: AuthService; + let userRepository: jest.Mocked; + + const mockUser: User = { + id: 'user-1', + name: 'Test User', + email: 'test@example.com', + hashedPassword: 'hashed-password', + role: 'STUDENT', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockLogger = { + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + setContext: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide: 'REPO.USER', + useValue: { + findByEmail: jest.fn(), + }, + }, + { + provide: LoggerService, + useValue: mockLogger, + }, + ], + }).compile(); + + authService = module.get(AuthService); + userRepository = module.get('REPO.USER'); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should login successfully with correct credentials', async () => { + userRepository.findByEmail.mockResolvedValue(succeed(mockUser)); + (verify as jest.Mock).mockResolvedValue(true); + + const result = await authService.login(mockUser.email, 'password'); + + expect(result._tag).toBe('Success'); + if (result._tag === 'Success') { + expect(result.data.user.email).toBe(mockUser.email); + } + expect(userRepository.findByEmail).toHaveBeenCalledWith(mockUser.email); + expect(verify).toHaveBeenCalledWith( + mockUser.hashedPassword, + 'password', + expect.any(Object), + ); + }); + + it('should fail login if password is incorrect', async () => { + userRepository.findByEmail.mockResolvedValue(succeed(mockUser)); + (verify as jest.Mock).mockResolvedValue(false); + + const result = await authService.login(mockUser.email, 'wrong-password'); + + expect(result._tag).toBe('Failure'); + if (result._tag === 'Failure') { + expect(result.error.message).toBe('Invalid credentials'); + } + }); + + it('should fail login if user does not exist', async () => { + userRepository.findByEmail.mockResolvedValue( + fail(new Error('User not found')), + ); + + const result = await authService.login( + 'nonexistent@example.com', + 'password', + ); + + expect(result._tag).toBe('Failure'); + if (result._tag === 'Failure') { + expect(result.error.message).toBe('User not found'); + } + }); + + it('logout should succeed', () => { + const result = authService.logout(); + expect(result._tag).toBe('Success'); + if (result._tag === 'Success') { + expect(result.data).toBeUndefined(); + } + }); +}); diff --git a/test/module.controller.spec.ts b/test/module.controller.spec.ts new file mode 100644 index 0000000..4549bb5 --- /dev/null +++ b/test/module.controller.spec.ts @@ -0,0 +1,216 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; + +import { ModulesController } from '../src/presentation/controllers/modules.controller'; +import { IModuleService } from '@/application/ports/module.port'; +import { SessionGuard } from '@/presentation/guards/session.guard'; + +describe('ModulesController (Integration)', () => { + let app: INestApplication; + let moduleService: jest.Mocked; + + // ============================ + // Mock Guard + // ============================ + + const mockSessionGuard = { + canActivate: jest.fn(() => true), + }; + + // ============================ + // Mock data + // ============================ + + const mockModule = { + id: 1, + name: 'Web Development', + shortDescription: 'Leer frontend en backend', + studyCredits: 5, + level: 'HBO', + location: [{ id: 1, name: 'Amsterdam' }], + startDate: '2025-02-01', + }; + + const mockModuleDetail = { + ...mockModule, + description: 'Volledige beschrijving', + content: 'HTML, CSS, JS', + moduleTags: [{ id: 1, name: 'Web' }], + learningOutcomes: 'Je kunt webapps bouwen', + availableSpots: 20, + }; + + // ============================ + // Setup + // ============================ + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + controllers: [ModulesController], + providers: [ + { + provide: 'SERVICE.MODULE', + useValue: { + getAllModules: jest.fn(), + findById: jest.fn(), + createModule: jest.fn(), + updateModule: jest.fn(), + deleteModule: jest.fn(), + getAllLocations: jest.fn(), + getAllModuleTags: jest.fn(), + createModuleTag: jest.fn(), + }, + }, + ], + }) + .overrideGuard(SessionGuard) + .useValue(mockSessionGuard) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + + moduleService = moduleFixture.get('SERVICE.MODULE'); + }); + + afterAll(async () => { + await app.close(); + }); + + // ============================ + // GET /modules + // ============================ + + it('GET /modules → retourneert modules', async () => { + moduleService.getAllModules.mockResolvedValue([mockModule]); + + const response = await request(app.getHttpServer()) + .get('/modules') + .expect(200); + + expect(response.body.modules).toHaveLength(1); + expect(response.body.modules[0].name).toBe('Web Development'); + }); + + // ============================ + // GET /modules/:id + // ============================ + + it('GET /modules/:id → retourneert module detail', async () => { + moduleService.findById.mockResolvedValue(mockModuleDetail as any); + + const response = await request(app.getHttpServer()) + .get('/modules/1') + .expect(200); + + expect(response.body.module.id).toBe(1); + expect(response.body.module.name).toBe('Web Development'); + }); + + // ============================ + // POST /modules + // ============================ + + it('POST /modules → maakt module aan', async () => { + moduleService.createModule.mockResolvedValue(mockModuleDetail as any); + + const response = await request(app.getHttpServer()) + .post('/modules') + .send({ + name: 'AI Basics', + shortDescription: 'Introductie AI', + description: 'AI uitleg', + content: 'ML, AI', + level: 'HBO', + studyCredits: 5, + location: [{ id: 1, name: 'Utrecht' }], + moduleTags: [{ id: 2, name: 'AI' }], + learningOutcomes: 'AI begrijpen', + availableSpots: 30, + startDate: '2025-03-01', + }) + .expect(201); + + expect(response.body.module.name).toBe('Web Development'); + }); + + // ============================ + // PUT /modules/:id + // ============================ + + it('PUT /modules/:id → werkt module bij', async () => { + moduleService.updateModule.mockResolvedValue(mockModuleDetail as any); + + const response = await request(app.getHttpServer()) + .put('/modules/1') + .send({ + name: 'Updated Module', + }) + .expect(200); + + expect(response.body.module.id).toBe(1); + }); + + // ============================ + // DELETE /modules/:id + // ============================ + + it('DELETE /modules/:id → verwijdert module', async () => { + moduleService.deleteModule.mockResolvedValue(undefined); + + const response = await request(app.getHttpServer()) + .delete('/modules/1') + .expect(200); + + expect(response.body.success).toBe(true); + }); + + // ============================ + // GET /modules/locations + // ============================ + + it('GET /modules/locations → retourneert locaties', async () => { + moduleService.getAllLocations.mockResolvedValue([ + { id: 1, name: 'Amsterdam' }, + ]); + + const response = await request(app.getHttpServer()) + .get('/modules/locations') + .expect(200); + + expect(response.body.locations).toHaveLength(1); + }); + + // ============================ + // GET /modules/moduletags + // ============================ + + it('GET /modules/moduletags → retourneert module tags', async () => { + moduleService.getAllModuleTags.mockResolvedValue([{ id: 1, name: 'Web' }]); + + const response = await request(app.getHttpServer()) + .get('/modules/moduletags') + .expect(200); + + expect(response.body.moduleTags[0].name).toBe('Web'); + }); + + // ============================ + // POST /modules/moduletags + // ============================ + + it('POST /modules/moduletags → maakt module tag aan', async () => { + moduleService.createModuleTag.mockResolvedValue({ + id: 2, + name: 'Backend', + }); + + const response = await request(app.getHttpServer()) + .post('/modules/moduletags') + .send({ tag: 'Backend' }) + .expect(201); + + expect(response.body.moduleTag.name).toBe('Backend'); + }); +}); diff --git a/test/module.service.spec.ts b/test/module.service.spec.ts new file mode 100644 index 0000000..920be18 --- /dev/null +++ b/test/module.service.spec.ts @@ -0,0 +1,208 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ModuleService } from '../src/application/services/module.service'; + +import { IModuleRepository } from '@/domain/module/module-repository.interface'; +import { ILocationRepository } from '@/domain/location/location-repository.interface'; +import { IModuleTagRepository } from '@/domain/moduletag/moduletag-repository.interface'; + +import { + Module, + ModuleDetail, + CreateModule, + UpdateModule, +} from '@/domain/module/module.model'; + +describe('ModuleService (Unit tests)', () => { + let service: ModuleService; + let moduleRepo: jest.Mocked; + let locationRepo: jest.Mocked; + let moduleTagRepo: jest.Mocked; + + // ============================ + // Mock data + // ============================ + + const mockModuleDetail: ModuleDetail = { + id: 1, + name: 'Web Development', + shortDescription: 'Leer frontend en backend', + description: 'Uitgebreide beschrijving', + content: 'HTML, CSS, JavaScript, API’s', + level: 'NFQL5', + studyCredits: 15, + location: [{ id: 1, name: 'Breda' }], + moduleTags: [{ id: 1, name: 'Web' }], + learningOutcomes: 'Je kunt een webapplicatie bouwen', + availableSpots: 20, + startDate: '2026-02-01', + }; + + const mockModuleList: Module[] = [ + { + id: 1, + name: 'Web Development', + shortDescription: 'Leer frontend en backend', + studyCredits: 5, + level: 'HBO', + location: [{ id: 1, name: 'Amsterdam' }], + startDate: '2025-02-01', + }, + ]; + + const createModuleDto: CreateModule = { + name: 'AI Basics', + shortDescription: 'Introductie AI', + description: 'Basis van kunstmatige intelligentie', + content: 'AI, ML, Data', + level: 'HBO', + studyCredits: 5, + location: [{ id: 2, name: 'Utrecht' }], + moduleTags: [{ id: 2, name: 'AI' }], + learningOutcomes: 'Je begrijpt de basis van AI', + availableSpots: 30, + startDate: '2025-03-01', + }; + + // ============================ + // Test setup + // ============================ + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ModuleService, + { + provide: 'REPO.MODULE', + useValue: { + getAllModules: jest.fn(), + findById: jest.fn(), + findManyByIds: jest.fn(), + createModule: jest.fn(), + updateModule: jest.fn(), + deleteModule: jest.fn(), + }, + }, + { + provide: 'REPO.LOCATION', + useValue: { + getAllLocations: jest.fn(), + }, + }, + { + provide: 'REPO.MODULETAG', + useValue: { + getAllModuleTags: jest.fn(), + createModuleTag: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(ModuleService); + moduleRepo = module.get('REPO.MODULE'); + locationRepo = module.get('REPO.LOCATION'); + moduleTagRepo = module.get('REPO.MODULETAG'); + }); + + // ============================ + // Module tests + // ============================ + + it('haalt alle modules op', async () => { + moduleRepo.getAllModules.mockResolvedValue(mockModuleList); + + const result = await service.getAllModules(); + + expect(result).toEqual(mockModuleList); + expect(moduleRepo.getAllModules).toHaveBeenCalled(); + }); + + it('haalt een module op op basis van id', async () => { + moduleRepo.findById.mockResolvedValue(mockModuleDetail); + + const result = await service.findById(1); + + expect(result).toEqual(mockModuleDetail); + expect(moduleRepo.findById).toHaveBeenCalledWith(1); + }); + + it('maakt een nieuwe module aan', async () => { + moduleRepo.createModule.mockResolvedValue({ + id: 2, + ...createModuleDto, + } as ModuleDetail); + + const result = await service.createModule(createModuleDto); + + expect(result.name).toBe('AI Basics'); + expect(moduleRepo.createModule).toHaveBeenCalledWith(createModuleDto); + }); + + it('werkt een module bij', async () => { + const updateDto: UpdateModule = createModuleDto; + + moduleRepo.updateModule.mockResolvedValue({ + id: 1, + ...updateDto, + } as ModuleDetail); + + const result = await service.updateModule(1, updateDto); + + expect(result.id).toBe(1); + expect(moduleRepo.updateModule).toHaveBeenCalledWith(1, updateDto); + }); + + it('verwijdert een module', async () => { + moduleRepo.deleteModule.mockResolvedValue(undefined); + + await service.deleteModule(1); + + expect(moduleRepo.deleteModule).toHaveBeenCalledWith(1); + }); + + it('gooit een fout wanneer repository faalt', async () => { + moduleRepo.findById.mockRejectedValue(new Error('Database error')); + + await expect(service.findById(999)).rejects.toThrow('Database error'); + }); + + // ============================ + // Location tests + // ============================ + + it('haalt alle locaties op', async () => { + locationRepo.getAllLocations.mockResolvedValue([ + { id: 1, name: 'Amsterdam' }, + ]); + + const result = await service.getAllLocations(); + + expect(result.length).toBe(1); + expect(locationRepo.getAllLocations).toHaveBeenCalled(); + }); + + // ============================ + // ModuleTag tests + // ============================ + + it('haalt alle module tags op', async () => { + moduleTagRepo.getAllModuleTags.mockResolvedValue([{ id: 1, name: 'Web' }]); + + const result = await service.getAllModuleTags(); + + expect(result[0].name).toBe('Web'); + expect(moduleTagRepo.getAllModuleTags).toHaveBeenCalled(); + }); + + it('maakt een module tag aan', async () => { + moduleTagRepo.createModuleTag.mockResolvedValue({ + id: 3, + name: 'Backend', + }); + + const result = await service.createModuleTag('Backend'); + + expect(result.name).toBe('Backend'); + expect(moduleTagRepo.createModuleTag).toHaveBeenCalledWith('Backend'); + }); +}); diff --git a/test/modules.intergration.spec.ts b/test/modules.intergration.spec.ts new file mode 100644 index 0000000..40228f1 --- /dev/null +++ b/test/modules.intergration.spec.ts @@ -0,0 +1,195 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ModulesController } from '@/presentation/controllers/modules.controller'; +import { ModuleService } from '@/application/services/module.service'; +import { SessionGuard } from '@/presentation/guards/session.guard'; +import { Module, ModuleDetail } from '@/domain/module/module.model'; +import { Location } from '@/domain/location/location.model'; +import { ModuleTag } from '@/domain/moduletag/moduletag.model'; +import { CreateModuleTagDto } from '@/presentation/dtos/moduleTag.dto'; +import { + CreateModuleDTO, + UpdateModuleDTO, +} from '@/presentation/dtos/module.dto'; + +describe('Modules Integration Test', () => { + let modulesController: ModulesController; + let moduleService: ModuleService; + + const mockModuleService = { + getAllModules: jest.fn(), + findById: jest.fn(), + findManyByIds: jest.fn(), + createModule: jest.fn(), + updateModule: jest.fn(), + deleteModule: jest.fn(), + getAllLocations: jest.fn(), + getAllModuleTags: jest.fn(), + createModuleTag: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ModulesController], + providers: [{ provide: 'SERVICE.MODULE', useValue: mockModuleService }], + }) + .overrideGuard(SessionGuard) + .useValue({ canActivate: () => true }) // Guard overschrijven + .compile(); + + modulesController = module.get(ModulesController); + moduleService = module.get('SERVICE.MODULE'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all modules', async () => { + const mockModules: Module[] = [ + { + id: 1, + name: 'Math 101', + shortDescription: 'Basic Math', + studyCredits: 0, + level: '', + location: [], + startDate: '', + }, + { + id: 2, + name: 'Physics 101', + shortDescription: 'Basic Physics', + studyCredits: 0, + level: '', + location: [], + startDate: '', + }, + ]; + mockModuleService.getAllModules.mockResolvedValue(mockModules); + + const result = await modulesController.getModules(); + expect(result.modules).toEqual(mockModules); + expect(mockModuleService.getAllModules).toHaveBeenCalled(); + }); + + it('should return all locations', async () => { + const mockLocations: Location[] = [ + { id: 1, name: 'Breda' }, + { id: 2, name: 'Roosendaal' }, + ]; + mockModuleService.getAllLocations.mockResolvedValue(mockLocations); + + const result = await modulesController.getAllLocations(); + expect(result.locations).toEqual(mockLocations); + expect(mockModuleService.getAllLocations).toHaveBeenCalled(); + }); + + it('should return all module tags', async () => { + const mockTags: ModuleTag[] = [ + { id: 1, name: 'Math' }, + { id: 2, name: 'Physics' }, + ]; + mockModuleService.getAllModuleTags.mockResolvedValue(mockTags); + + const result = await modulesController.getAllModuleTags(); + expect(result.moduleTags).toEqual(mockTags); + expect(mockModuleService.getAllModuleTags).toHaveBeenCalled(); + }); + + it('should create a module tag', async () => { + const dto: CreateModuleTagDto = { tag: 'Chemistry' }; + const createdTag: ModuleTag = { id: 3, name: 'Chemistry' }; + mockModuleService.createModuleTag.mockResolvedValue(createdTag); + + const result = await modulesController.createModuleTag(dto); + expect(result.moduleTag).toEqual(createdTag); + expect(mockModuleService.createModuleTag).toHaveBeenCalledWith('Chemistry'); + }); + + it('should return module by id', async () => { + const moduleDetail: ModuleDetail = { + id: 1, + name: 'Math 101', + shortDescription: 'Basic Math', + studyCredits: 15, + level: 'NFQL5', + location: [{ id: 1, name: 'Breda' }], + startDate: new Date().toISOString(), + moduleTags: [{ id: 1, name: 'Math' }], + description: 'Math module description', + content: 'Math content', + learningOutcomes: 'You will learn Math', + availableSpots: 13, + }; + mockModuleService.findById.mockResolvedValue(moduleDetail); + + const result = await modulesController.getModuleById(1); + expect(result.module).toEqual(moduleDetail); + expect(mockModuleService.findById).toHaveBeenCalledWith(1); + }); + + it('should create a module', async () => { + const dto: CreateModuleDTO = { + name: 'Chemistry 101', + shortDescription: 'Basic Chemistry', + studyCredits: 30, + level: 'NFQL6', + location: [{ id: 1, name: 'Breda' }], + startDate: new Date().toISOString(), + moduleTags: [{ id: 2, name: 'Science' }], + description: 'Chemistry module description', + content: 'Chemistry content', + learningOutcomes: 'You will learn Chemistry', + availableSpots: 10, + }; + const createdModule: ModuleDetail = { id: 4, ...dto }; + mockModuleService.createModule.mockResolvedValue(createdModule); + + const result = await modulesController.createModule(dto); + expect(result.module).toEqual(createdModule); + expect(mockModuleService.createModule).toHaveBeenCalledWith(dto); + }); + + it('should update a module', async () => { + const dto: UpdateModuleDTO = { + name: 'Advanced Math', + description: 'Math module description', + content: 'Math content', + level: 'NFQL5', + studyCredits: 15, + location: [{ id: 1, name: 'Breda' }], + moduleTags: [{ id: 1, name: 'Math' }], + learningOutcomes: 'You will learn Math', + availableSpots: 12, + startDate: new Date().toISOString(), + shortDescription: 'Basic Math', + }; + const updatedModule: ModuleDetail = { + id: 1, + name: 'Advanced Math', + shortDescription: 'Basic Math', + studyCredits: 30, + level: 'Beginner', + location: [{ id: 1, name: 'Breda' }], + startDate: new Date().toISOString(), + moduleTags: [{ id: 1, name: 'Math' }], + description: 'Math module description', + content: 'Math content', + learningOutcomes: 'You will learn Math', + availableSpots: 12, + }; + mockModuleService.updateModule.mockResolvedValue(updatedModule); + + const result = await modulesController.updateModule(1, dto); + expect(result.module).toEqual(updatedModule); + expect(mockModuleService.updateModule).toHaveBeenCalledWith(1, dto); + }); + + it('should delete a module', async () => { + mockModuleService.deleteModule.mockResolvedValue(undefined); + + const result = await modulesController.deleteModule(1); + expect(result.success).toBe(true); + expect(mockModuleService.deleteModule).toHaveBeenCalledWith(1); + }); +}); diff --git a/test/user.controller.spec.ts b/test/user.controller.spec.ts index 6711a09..2685900 100644 --- a/test/user.controller.spec.ts +++ b/test/user.controller.spec.ts @@ -1,41 +1,199 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigService } from '@nestjs/config'; -import { UserController } from '../src/presentation/controllers/user.controller'; -import { IUserService } from '../src/application/ports/user.port'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; -const mockUserService: Partial = { - findById: jest.fn(), -}; +import { UserController } from '@/presentation/controllers/user.controller'; +import { IUserService } from '@/application/ports/user.port'; +import { SessionGuard } from '@/presentation/guards/session.guard'; +import { UserRole } from '@/domain/user/user.model'; -const mockConfigService = { - get: jest.fn((key: string) => { - if (key === 'INACTIVITY_TIMEOUT') return '1800000'; - return null; - }), -}; +// ============================ +// Mock SessionGuard +// ============================ +jest.mock('@/presentation/guards/session.guard', () => ({ + SessionGuard: jest.fn().mockImplementation(() => ({ + canActivate: () => true, + })), +})); -describe('UserController', () => { - let controller: UserController; +describe('UserController (Integration)', () => { + let app: INestApplication; + let userService: jest.Mocked; - beforeEach(async () => { + // ============================ + // Mock session + // ============================ + const mockSession = { + user: { + id: 'user-1', + role: 'STUDENT', + }, + }; + + // ============================ + // Mock data + // ============================ + const mockUser = { + id: 'user-1', + name: 'Test Student', + email: 'student@test.nl', + hashedPassword: 'hashed-password', + role: 'STUDENT' as UserRole, + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], providers: [ { provide: 'SERVICE.USER', - useValue: mockUserService, - }, - { - provide: ConfigService, - useValue: mockConfigService, + useValue: { + findById: jest.fn(), + findFavorites: jest.fn(), + isModuleFavorited: jest.fn(), + favoriteModule: jest.fn(), + unfavoriteModule: jest.fn(), + setRecommendedModules: jest.fn(), + getRecommendedModules: jest.fn(), + }, }, ], - }).compile(); + }) + .overrideGuard(SessionGuard) + .useValue({ canActivate: () => true }) + .compile(); + + app = module.createNestApplication(); + + // Fake session middleware + app.use((req: any, _res: any, next: any) => { + req.session = mockSession; + next(); + }); + + await app.init(); + + userService = module.get('SERVICE.USER'); + }); + + afterAll(async () => { + await app.close(); + }); + + // ============================ + // GET /user/profile + // ============================ + + it('GET /user/profile → retourneert gebruikersprofiel', async () => { + userService.findById.mockResolvedValue({ + _tag: 'Success', + data: mockUser, + }); + + const response = await request(app.getHttpServer()) + .get('/user/profile') + .expect(200); + + expect(response.body.user.id).toBe('user-1'); + expect(response.body.user.email).toBe('student@test.nl'); + }); + + // ============================ + // GET /user/favorites + // ============================ + + it('GET /user/favorites → retourneert favorieten', async () => { + userService.findFavorites.mockResolvedValue([ + { userId: 'user-1', moduleId: 1, id: 1, createdAt: new Date() }, + ]); + + const response = await request(app.getHttpServer()) + .get('/user/favorites') + .expect(200); + + expect(response.body.favorites).toHaveLength(1); + expect(response.body.favorites[0].moduleId).toBe(1); + }); - controller = module.get(UserController); + // ============================ + // GET /user/favorites/:moduleId + // ============================ + + it('GET /user/favorites/:moduleId → controleert favoriet', async () => { + userService.isModuleFavorited.mockResolvedValue(true); + + const response = await request(app.getHttpServer()) + .get('/user/favorites/1') + .expect(200); + + expect(response.body.isFavorited).toBe(true); }); - it('should be defined', () => { - expect(controller).toBeDefined(); + // ============================ + // POST /user/favorites/:moduleId + // ============================ + + it('POST /user/favorites/:moduleId → voegt favoriet toe', async () => { + userService.favoriteModule.mockResolvedValue(undefined); + + const response = await request(app.getHttpServer()) + .post('/user/favorites/1') + .expect(201); + + expect(response.body.success).toBe(true); + }); + + // ============================ + // DELETE /user/favorites/:moduleId + // ============================ + + it('DELETE /user/favorites/:moduleId → verwijdert favoriet', async () => { + userService.unfavoriteModule.mockResolvedValue(undefined); + + const response = await request(app.getHttpServer()) + .delete('/user/favorites/1') + .expect(200); + + expect(response.body.success).toBe(true); + }); + + // ============================ + // GET /user/recommended + // ============================ + + it('GET /user/recommended → retourneert max 5 modules', async () => { + userService.getRecommendedModules.mockResolvedValue([ + { moduleId: 1 }, + { moduleId: 2 }, + { moduleId: 3 }, + { moduleId: 4 }, + { moduleId: 5 }, + { moduleId: 6 }, + ]); + + const response = await request(app.getHttpServer()) + .get('/user/recommended') + .expect(200); + + expect(response.body.recommended).toHaveLength(5); + }); + + // ============================ + // POST /user/recommended + // ============================ + + it('POST /user/recommended → slaat aanbevelingen op', async () => { + userService.setRecommendedModules.mockResolvedValue(undefined); + + const response = await request(app.getHttpServer()) + .post('/user/recommended') + .send({ + modules: [{ moduleId: 1, motivation: 'Past bij je profiel' }], + }) + .expect(201); + + expect(response.body.success).toBe(true); }); }); diff --git a/test/user.intergration.spec.ts b/test/user.intergration.spec.ts new file mode 100644 index 0000000..bf54766 --- /dev/null +++ b/test/user.intergration.spec.ts @@ -0,0 +1,79 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from '@/presentation/controllers/user.controller'; +import { UserService } from '@/application/services/user.service'; +import { SessionGuard } from '@/presentation/guards/session.guard'; +import { ExecutionContext } from '@nestjs/common'; +import { LoggerService } from '@/logger.service'; +import { AuthenticatedSession } from '@/types/session.types'; + +describe('User Integration Test', () => { + let userController: UserController; + let userService: UserService; + let mockUserRepository: any; + let mockUserModulesRepository: any; + + const mockLogger = { + setContext: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const mockUser = { + id: 'user-1', + name: 'Test User', + email: 'test@example.com', + hashedPassword: 'hashed-password', + role: 'STUDENT', + createdAt: new Date(), + updatedAt: new Date(), + }; + + beforeEach(async () => { + mockUserRepository = { findById: jest.fn() }; + mockUserModulesRepository = { + findFavoritesByUserId: jest.fn(), + isFavorited: jest.fn(), + addFavorite: jest.fn(), + removeFavorite: jest.fn(), + setRecommendedModules: jest.fn(), + findRecommendedByUserId: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [ + // Service token zoals in AppModule + { provide: 'SERVICE.USER', useClass: UserService }, + { provide: 'REPO.USER', useValue: mockUserRepository }, + { provide: 'REPO.USER_MODULES', useValue: mockUserModulesRepository }, + { provide: LoggerService, useValue: mockLogger }, + ], + }) + .overrideGuard(SessionGuard) + .useValue({ + canActivate: (context: ExecutionContext) => true, + }) + .compile(); + + userController = module.get(UserController); + userService = module.get('SERVICE.USER'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return user profile', async () => { + mockUserRepository.findById.mockResolvedValue({ + _tag: 'Success', + data: mockUser, + }); + const session = { user: mockUser } as unknown as AuthenticatedSession; + + const result = await userController.getProfile(session); + expect(result.user.email).toBe(mockUser.email); + }); + + // Voeg hier je andere tests toe: favorites, recommended modules, etc. +}); diff --git a/test/user.service.spec.ts b/test/user.service.spec.ts index 1cc06bd..e70e53d 100644 --- a/test/user.service.spec.ts +++ b/test/user.service.spec.ts @@ -1,61 +1,187 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { UserService } from '../src/application/services/user.service'; -import { IUserRepository } from '../src/domain/user/user-repository.interface'; -import { IUserModulesRepository } from '../src/domain/usermodule/usermodules-repository.interface'; -import { LoggerService } from '../src/logger.service'; - -const mockUserRepository: Partial = { - findById: jest.fn(), - findByEmail: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), -}; - -const mockUserModulesRepository: Partial = { - findFavoritesByUserId: jest.fn(), - isFavorited: jest.fn(), - addFavorite: jest.fn(), - removeFavorite: jest.fn(), - findRecommendedByUserId: jest.fn(), - setRecommendedModules: jest.fn(), -}; - -const mockLoggerService = { - setContext: jest.fn(), - log: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - debug: jest.fn(), - verbose: jest.fn(), -}; - -describe('UserService', () => { +import { UserService } from '@/application/services/user.service'; +import { IUserRepository } from '@/domain/user/user-repository.interface'; +import { IUserModulesRepository } from '@/domain/usermodule/usermodules-repository.interface'; +import { LoggerService } from '@/logger.service'; +import { User } from '@/domain/user/user.model'; +import { UserFavorite } from '@/domain/usermodule/userfavorite.model'; + +describe('UserService (Unit)', () => { let service: UserService; + let userRepository: jest.Mocked; + let userModulesRepository: jest.Mocked; + + // ============================ + // Mock data + // ============================ + + const mockUser: User = { + id: 'user-1', + name: 'Test Student', + email: 'student@test.nl', + hashedPassword: 'hashed-password', + role: 'STUDENT', + createdAt: new Date('2024-01-01'), + updatedAt: new Date('2024-01-02'), + }; + + const mockFavorite: UserFavorite = { + id: 1, + userId: 'user-1', + moduleId: 1, + createdAt: new Date('2024-02-01'), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, + + // ---- Mock User Repository ---- { provide: 'REPO.USER', - useValue: mockUserRepository, + useValue: { + findById: jest + .fn() + .mockResolvedValue({ ok: true, value: mockUser }), + }, }, + + // ---- Mock UserModules Repository ---- { provide: 'REPO.USER_MODULES', - useValue: mockUserModulesRepository, + useValue: { + findFavoritesByUserId: jest.fn(), + isFavorited: jest.fn(), + addFavorite: jest.fn(), + removeFavorite: jest.fn(), + setRecommendedModules: jest.fn(), + findRecommendedByUserId: jest.fn(), + }, }, + + // ---- Mock LoggerService (FIX) ---- { provide: LoggerService, - useValue: mockLoggerService, + useValue: { + setContext: jest.fn(), + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }, }, ], }).compile(); - service = module.get(UserService); + service = module.get(UserService); + userRepository = module.get('REPO.USER'); + userModulesRepository = module.get('REPO.USER_MODULES'); + }); + + // ============================ + // User profile + // ============================ + + it('findById → retourneert een gebruiker', async () => { + (userRepository.findById as jest.Mock).mockResolvedValue({ + ok: true, + value: mockUser, + }); + + const result = await service.findById('user-1'); + + expect(result).toEqual({ ok: true, value: mockUser }); + expect(userRepository.findById).toHaveBeenCalledWith('user-1'); + }); + + // ============================ + // Favorites + // ============================ + + it('findFavorites → retourneert favoriete modules', async () => { + userModulesRepository.findFavoritesByUserId.mockResolvedValue([ + mockFavorite, + ]); + + const result = await service.findFavorites('user-1'); + + expect(result).toHaveLength(1); + expect(result[0].moduleId).toBe(1); }); - it('should be defined', () => { - expect(service).toBeDefined(); + it('isModuleFavorited → retourneert true als module favoriet is', async () => { + userModulesRepository.isFavorited.mockResolvedValue(true); + + const result = await service.isModuleFavorited('user-1', 1); + + expect(result).toBe(true); + }); + + it('favoriteModule → voegt favoriet toe als deze nog niet bestaat', async () => { + userModulesRepository.isFavorited.mockResolvedValue(false); + + await service.favoriteModule('user-1', 1); + + expect(userModulesRepository.addFavorite).toHaveBeenCalledWith('user-1', 1); + }); + + it('favoriteModule → voegt geen favoriet toe als deze al bestaat', async () => { + userModulesRepository.isFavorited.mockResolvedValue(true); + + await service.favoriteModule('user-1', 1); + + expect(userModulesRepository.addFavorite).not.toHaveBeenCalled(); + }); + + it('unfavoriteModule → verwijdert favoriet', async () => { + await service.unfavoriteModule('user-1', 1); + + expect(userModulesRepository.removeFavorite).toHaveBeenCalledWith( + 'user-1', + 1, + ); + }); + + // ============================ + // Recommended modules + // ============================ + + it('setRecommendedModules → slaat aanbevelingen op', async () => { + const recommendations = [ + { moduleId: 1, motivation: 'Past bij je profiel' }, + ]; + + await service.setRecommendedModules('user-1', recommendations); + + expect(userModulesRepository.setRecommendedModules).toHaveBeenCalledWith( + 'user-1', + recommendations, + ); + }); + + it('getRecommendedModuleIds → retourneert alleen module IDs', async () => { + userModulesRepository.findRecommendedByUserId.mockResolvedValue([ + { moduleId: 1 }, + { moduleId: 2 }, + ] as any); + + const result = await service.getRecommendedModuleIds('user-1'); + + expect(result).toEqual([1, 2]); + }); + + it('getRecommendedModules → retourneert module IDs met motivatie', async () => { + userModulesRepository.findRecommendedByUserId.mockResolvedValue([ + { + moduleId: 1, + recommendationReason: 'Interessant voor jou', + }, + ] as any); + + const result = await service.getRecommendedModules('user-1'); + + expect(result).toEqual([ + { moduleId: 1, motivation: 'Interessant voor jou' }, + ]); }); });