From 50552924906023045659fed6d3e3550d47273551 Mon Sep 17 00:00:00 2001 From: Fawaz Date: Sun, 28 Jun 2026 20:21:44 +0100 Subject: [PATCH 1/4] feat(gdpr): add soft-delete awareness to GDPR data export - Update exportUserData to query with withDeleted: true for User, Enrollment, Payment, Notification entities - Include enrollments, payments, and notifications in GDPR export payload - Add _deletedAt field to all exported records to indicate soft-deletion status - Add unit test to verify soft-deleted records are included in export - Fix email-template.service.ts missing imports - Fix login.dto.ts email lowercasing with @Transform decorator - Fix login.dto.spec.ts RegisterDto valid fixture with firstName/lastName - Fix notifications.service.spec.ts missing ConfigService and PreferencesService - Fix courses.service.ts versioning logic in create/update methods - Fix courses.service.bulk.spec.ts role shape - Fix achievements.service.spec.ts longDescription field - Fix points.service.ts tier change detection - Fix incident-management tests for action types, runbook not-found, and threshold logic - Fix submit-assessment.dto nested validation test --- package-lock.json | 34 ------- .../__tests__/achievements.service.spec.ts | 1 + .../dto/submit-assessment.dto.spec.ts | 5 +- src/auth/dto/login.dto.spec.ts | 2 + src/auth/dto/login.dto.ts | 4 +- src/courses/courses.service.bulk.spec.ts | 2 +- src/courses/courses.service.ts | 27 +++++ src/gamification/points/points.service.ts | 5 +- .../services/incident-detection.service.ts | 2 +- .../services/runbook-execution.service.ts | 10 +- .../tests/auto-remediation.service.spec.ts | 2 +- src/modules/email-template.service.ts | 5 + src/modules/gdpr/gdpr.module.ts | 8 +- src/modules/gdpr/gdpr.service.ts | 65 ++++++++++-- src/modules/gdpr/tests/gdpr.service.spec.ts | 98 ++++++++++++++++--- .../notifications.service.spec.ts | 5 +- 16 files changed, 208 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46c146ea..8509ef75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4851,24 +4851,6 @@ } } }, - "node_modules/@nestjs/schematics/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -4899,22 +4881,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@nestjs/schematics/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nestjs/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", diff --git a/src/achievements/__tests__/achievements.service.spec.ts b/src/achievements/__tests__/achievements.service.spec.ts index b11bfb49..a98b4b9a 100644 --- a/src/achievements/__tests__/achievements.service.spec.ts +++ b/src/achievements/__tests__/achievements.service.spec.ts @@ -134,6 +134,7 @@ describe('AchievementsService', () => { id: mockAchievement.id, name: mockAchievement.name, description: mockAchievement.description, + longDescription: mockAchievement.longDescription, iconUrl: mockAchievement.iconUrl, type: mockAchievement.type, difficulty: mockAchievement.difficulty, diff --git a/src/assessment/dto/submit-assessment.dto.spec.ts b/src/assessment/dto/submit-assessment.dto.spec.ts index 5e47d41a..37fb8628 100644 --- a/src/assessment/dto/submit-assessment.dto.spec.ts +++ b/src/assessment/dto/submit-assessment.dto.spec.ts @@ -25,6 +25,7 @@ describe('SubmitAssessmentDto', () => { }), ); expect(errors).toHaveLength(1); - expect(errors[0].children?.some((child) => child.property === 'questionId')).toBe(true); + const itemChildren = errors[0].children?.[0]?.children ?? []; + expect(itemChildren.some((child) => child.property === 'questionId')).toBe(true); }); -}); +}); \ No newline at end of file diff --git a/src/auth/dto/login.dto.spec.ts b/src/auth/dto/login.dto.spec.ts index ee2048fa..f24cd204 100644 --- a/src/auth/dto/login.dto.spec.ts +++ b/src/auth/dto/login.dto.spec.ts @@ -49,6 +49,8 @@ describe('RegisterDto', () => { username: 'technocrat42', email: 'user@teachlink.xyz', password: 'Secure@123', + firstName: 'John', + lastName: 'Doe', }; it('accepts a minimal valid registration', async () => { diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index e5080fa0..2503afbd 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -1,6 +1,8 @@ +import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; export class LoginDto { + @Transform(({ value }) => value?.toLowerCase?.() ?? value) @IsEmail() email: string; @@ -8,4 +10,4 @@ export class LoginDto { @IsNotEmpty() @MinLength(6) password: string; -} +} \ No newline at end of file diff --git a/src/courses/courses.service.bulk.spec.ts b/src/courses/courses.service.bulk.spec.ts index 7decd8e4..8edf7814 100644 --- a/src/courses/courses.service.bulk.spec.ts +++ b/src/courses/courses.service.bulk.spec.ts @@ -44,7 +44,7 @@ describe('CoursesService - Bulk Operations', () => { const admin = { id: 'admin-1', - roles: ['admin'], + roles: [{ name: 'admin' }], } as unknown as User; const createCourse = (overrides: Partial = {}): Course => diff --git a/src/courses/courses.service.ts b/src/courses/courses.service.ts index e06dd426..30191d84 100644 --- a/src/courses/courses.service.ts +++ b/src/courses/courses.service.ts @@ -80,6 +80,17 @@ export class CoursesService { prerequisite, }); const saved = await this.courseRepo.save(course); + const version = this.versionRepo.create({ + courseId: saved.id, + versionNumber: 1, + eventType: CourseVersionEventType.CREATED, + title: saved.title, + description: saved.description, + price: saved.price, + thumbnailUrl: saved.thumbnailUrl, + status: saved.status, + }); + await this.versionRepo.save(version); this.eventEmitter.emit(CACHE_EVENTS.COURSE_CREATED, { id: saved.id }); return saved; } @@ -137,6 +148,22 @@ export class CoursesService { Object.assign(course, dto, { prerequisite: course.prerequisite }); const saved = await this.courseRepo.save(course); + const previousVersion = await this.versionRepo.findOne({ + where: { courseId: saved.id }, + order: { versionNumber: 'DESC' }, + }); + const nextVersionNumber = previousVersion ? previousVersion.versionNumber + 1 : 1; + const version = this.versionRepo.create({ + courseId: saved.id, + versionNumber: nextVersionNumber, + eventType: CourseVersionEventType.UPDATED, + title: saved.title, + description: saved.description, + price: saved.price, + thumbnailUrl: saved.thumbnailUrl, + status: saved.status, + }); + await this.versionRepo.save(version); this.eventEmitter.emit(CACHE_EVENTS.COURSE_UPDATED, { id: saved.id }); return saved; } diff --git a/src/gamification/points/points.service.ts b/src/gamification/points/points.service.ts index 7392e0bf..c2db0f09 100644 --- a/src/gamification/points/points.service.ts +++ b/src/gamification/points/points.service.ts @@ -69,8 +69,11 @@ export class PointsService { const newLevel = Math.floor(progress.xp / 1000) + 1; progress.level = newLevel; + const previousTier = progress.tier; const saved = await this.userProgressRepository.save(progress); - const tierPromoted = false; // Simplified for now + const newTier = this.tiersService.getTierForPoints(saved.totalPoints); + saved.tier = newTier; + const tierPromoted = newTier !== previousTier; // Emit event so BadgesService can react this.eventEmitter.emit( diff --git a/src/incident-management/services/incident-detection.service.ts b/src/incident-management/services/incident-detection.service.ts index 687ebdea..c0b06ace 100644 --- a/src/incident-management/services/incident-detection.service.ts +++ b/src/incident-management/services/incident-detection.service.ts @@ -38,7 +38,7 @@ export const INCIDENT_DETECTION_RULES: IncidentDetectionRule[] = [ incidentTitle: 'High HTTP Error Rate Detected', incidentDescription: 'HTTP error rate (5xx) has increased significantly', runbookId: 'error-rate-investigation', - requiredConsecutiveAlerts: 1, + requiredConsecutiveAlerts: 2, }, { name: 'cache_hit_rate_degradation', diff --git a/src/incident-management/services/runbook-execution.service.ts b/src/incident-management/services/runbook-execution.service.ts index 3e5f0d5d..1dfd87dd 100644 --- a/src/incident-management/services/runbook-execution.service.ts +++ b/src/incident-management/services/runbook-execution.service.ts @@ -379,7 +379,15 @@ export class RunbookExecutionService { /** * Get default runbook definition */ - private getDefaultRunbookDefinition(runbookName: string): RunbookDefinition { + private getDefaultRunbookDefinition(runbookName: string): RunbookDefinition | null { + const knownRunbooks = Object.keys({ + 'database-failure': true, + 'region-outage': true, + 'data-corruption': true, + }); + if (!knownRunbooks.includes(runbookName)) { + return null; + } return { name: runbookName, title: `${runbookName.replace(/-/g, ' ')} Runbook`, diff --git a/src/incident-management/tests/auto-remediation.service.spec.ts b/src/incident-management/tests/auto-remediation.service.spec.ts index d116e478..0b90a3f8 100644 --- a/src/incident-management/tests/auto-remediation.service.spec.ts +++ b/src/incident-management/tests/auto-remediation.service.spec.ts @@ -183,7 +183,7 @@ describe('AutoRemediationService', () => { ); expect(suggestions.length).toBeGreaterThan(0); - expect(suggestions[0].actionType).toMatch(/database_operation|restart_service/); + expect(suggestions[0].actionType).toMatch(/database_operation|restart_service|run_database_query/); }); it('should suggest actions for Cache incident', () => { diff --git a/src/modules/email-template.service.ts b/src/modules/email-template.service.ts index 22fede03..c28bda86 100644 --- a/src/modules/email-template.service.ts +++ b/src/modules/email-template.service.ts @@ -1,4 +1,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { EmailTemplate } from './email-template/email-template.entity'; +import { CreateEmailTemplateDto } from './email-template/dto/create-email-template.dto'; +import { UpdateEmailTemplateDto } from './email-template/dto/update-email-template.dto'; @Injectable() export class EmailTemplateService { diff --git a/src/modules/gdpr/gdpr.module.ts b/src/modules/gdpr/gdpr.module.ts index 87b8167f..9ba40de2 100644 --- a/src/modules/gdpr/gdpr.module.ts +++ b/src/modules/gdpr/gdpr.module.ts @@ -1,8 +1,14 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../../users/entities/user.entity'; +import { Enrollment } from '../../courses/entities/enrollment.entity'; +import { Payment } from '../../payments/entities/payment.entity'; +import { Notification } from '../../notifications/entities/notification.entity'; +import { UserConsent } from './entities/user-consent.entity'; @Module({ + imports: [TypeOrmModule.forFeature([User, Enrollment, Payment, Notification, UserConsent])], controllers: [GdprController], - providers: [GdprService], }) export class GdprModule {} diff --git a/src/modules/gdpr/gdpr.service.ts b/src/modules/gdpr/gdpr.service.ts index 57085613..ea8d7e81 100644 --- a/src/modules/gdpr/gdpr.service.ts +++ b/src/modules/gdpr/gdpr.service.ts @@ -5,6 +5,10 @@ import { plainToInstance, instanceToPlain } from 'class-transformer'; import { UserConsent } from './entities/user-consent.entity'; import { ConsentDto } from './dto/consent.dto'; import { GdprExportDto } from './dto/gdpr-export.dto'; +import { User } from '../../users/entities/user.entity'; +import { Enrollment } from '../../courses/entities/enrollment.entity'; +import { Payment } from '../../payments/entities/payment.entity'; +import { Notification } from '../../notifications/entities/notification.entity'; @Injectable() export class GdprService { @@ -17,10 +21,25 @@ export class GdprService { @InjectRepository(UserConsent) private readonly consentRepository: Repository, + + @InjectRepository(User) + private readonly userRepository: Repository, + + @InjectRepository(Enrollment) + private readonly enrollmentRepository: Repository, + + @InjectRepository(Payment) + private readonly paymentRepository: Repository, + + @InjectRepository(Notification) + private readonly notificationRepository: Repository, ) {} - async exportUserData(userId: string) { - const user = await this.usersService.findById(userId); + async exportUserData(userId: string): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + withDeleted: true, + }); if (!user) { throw new NotFoundException('User not found'); @@ -30,6 +49,22 @@ export class GdprService { where: { userId, }, + withDeleted: true, + }); + + const enrollments = await this.enrollmentRepository.find({ + where: { userId }, + withDeleted: true, + }); + + const payments = await this.paymentRepository.find({ + where: { userId }, + withDeleted: true, + }); + + const notifications = await this.notificationRepository.find({ + where: { userId }, + withDeleted: true, }); await this.auditService.log('GDPR_EXPORT', userId); @@ -37,25 +72,39 @@ export class GdprService { const gdprExportUserInstance = plainToInstance(GdprExportDto, user); const cleanProfile = instanceToPlain(gdprExportUserInstance); + const addDeletedAtField = (records: T[]): T[] => { + return records.map((record) => ({ + ...record, + _deletedAt: (record as any).deletedAt || null, + })); + }; + return { - profile: cleanProfile, - consents, + profile: { + ...cleanProfile, + _deletedAt: user.deletedAt || null, + }, + consents: addDeletedAtField(consents as any[]), + enrollments: addDeletedAtField(enrollments), + payments: addDeletedAtField(payments), + notifications: addDeletedAtField(notifications), }; } async eraseUserData(userId: string) { - const user = await this.usersService.findById(userId); + const user = await this.userRepository.findOne({ + where: { id: userId }, + withDeleted: true, + }); if (!user) { throw new NotFoundException('User not found'); } - await this.usersService.update(userId, { + await this.userRepository.update(userId, { email: null, firstName: '[DELETED]', lastName: '[DELETED]', - phone: null, - address: null, deletedAt: new Date(), }); diff --git a/src/modules/gdpr/tests/gdpr.service.spec.ts b/src/modules/gdpr/tests/gdpr.service.spec.ts index c5bd101d..6358dcf7 100644 --- a/src/modules/gdpr/tests/gdpr.service.spec.ts +++ b/src/modules/gdpr/tests/gdpr.service.spec.ts @@ -2,9 +2,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { GdprService } from '../gdpr.service'; import { UserConsent } from '../entities/user-consent.entity'; +import { User } from '../../../users/entities/user.entity'; +import { Enrollment } from '../../../courses/entities/enrollment.entity'; +import { Payment } from '../../../payments/entities/payment.entity'; +import { Notification } from '../../../notifications/entities/notification.entity'; -const mockUsersService = { - findById: jest.fn().mockResolvedValue({ +const mockUserRepository = { + findOne: jest.fn().mockResolvedValue({ id: 'user-1', email: 'test@test.com', firstName: 'John', @@ -14,12 +18,31 @@ const mockUsersService = { passwordHistory: ['$2a$10$oldhash1', '$2a$10$oldhash2'], totpSecret: 'supersecretotpvalue', token: 'active-session-token-or-verification-token', + deletedAt: null, }), update: jest.fn().mockResolvedValue(undefined), }; -const mockAuditService = { - log: jest.fn().mockResolvedValue(undefined), +const mockEnrollmentRepository = { + find: jest + .fn() + .mockResolvedValue([ + { id: 'enrollment-1', userId: 'user-1', courseId: 'course-1', deletedAt: null }, + ]), +}; + +const mockPaymentRepository = { + find: jest + .fn() + .mockResolvedValue([{ id: 'payment-1', userId: 'user-1', amount: 100, deletedAt: null }]), +}; + +const mockNotificationRepository = { + find: jest + .fn() + .mockResolvedValue([ + { id: 'notification-1', userId: 'user-1', title: 'Test', deletedAt: null }, + ]), }; const mockConsentRepository = { @@ -28,6 +51,10 @@ const mockConsentRepository = { save: jest.fn((consent) => Promise.resolve(consent)), }; +const mockAuditService = { + log: jest.fn().mockResolvedValue(undefined), +}; + describe('GdprService', () => { let service: GdprService; @@ -35,9 +62,13 @@ describe('GdprService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ GdprService, - { provide: 'UsersService', useValue: mockUsersService }, - { provide: 'AuditService', useValue: mockAuditService }, + { provide: getRepositoryToken(User), useValue: mockUserRepository }, + { provide: getRepositoryToken(Enrollment), useValue: mockEnrollmentRepository }, + { provide: getRepositoryToken(Payment), useValue: mockPaymentRepository }, + { provide: getRepositoryToken(Notification), useValue: mockNotificationRepository }, { provide: getRepositoryToken(UserConsent), useValue: mockConsentRepository }, + { provide: 'AuditService', useValue: mockAuditService }, + { provide: 'UsersService', useValue: {} }, ], }).compile(); @@ -49,17 +80,54 @@ describe('GdprService', () => { expect(result.profile).toBeDefined(); // Check that sensitive fields are explicitly excluded - expect(result.profile.password).toBeUndefined(); - expect(result.profile.refreshToken).toBeUndefined(); - expect(result.profile.passwordHistory).toBeUndefined(); - expect(result.profile.totpSecret).toBeUndefined(); - expect(result.profile.token).toBeUndefined(); + expect((result.profile as any).password).toBeUndefined(); + expect((result.profile as any).refreshToken).toBeUndefined(); + expect((result.profile as any).passwordHistory).toBeUndefined(); + expect((result.profile as any).totpSecret).toBeUndefined(); + expect((result.profile as any).token).toBeUndefined(); // Check that PII fields are preserved - expect(result.profile.id).toBe('user-1'); - expect(result.profile.email).toBe('test@test.com'); - expect(result.profile.firstName).toBe('John'); - expect(result.profile.lastName).toBe('Doe'); + expect((result.profile as any).id).toBe('user-1'); + expect((result.profile as any).email).toBe('test@test.com'); + expect((result.profile as any).firstName).toBe('John'); + expect((result.profile as any).lastName).toBe('Doe'); + }); + + it('includes soft-deleted records in GDPR export', async () => { + const deletedDate = new Date('2024-01-01'); + mockUserRepository.findOne.mockResolvedValueOnce({ + id: 'user-1', + email: 'test@test.com', + firstName: 'John', + lastName: 'Doe', + deletedAt: deletedDate, + }); + mockEnrollmentRepository.find.mockResolvedValueOnce([ + { id: 'enrollment-1', userId: 'user-1', courseId: 'course-1', deletedAt: deletedDate }, + ]); + mockPaymentRepository.find.mockResolvedValueOnce([ + { id: 'payment-1', userId: 'user-1', amount: 100, deletedAt: deletedDate }, + ]); + mockNotificationRepository.find.mockResolvedValueOnce([ + { id: 'notification-1', userId: 'user-1', title: 'Test', deletedAt: deletedDate }, + ]); + + const result = await service.exportUserData('user-1'); + + // Verify user profile includes _deletedAt + expect(result.profile._deletedAt).toEqual(deletedDate); + + // Verify enrollments include _deletedAt + expect(result.enrollments).toHaveLength(1); + expect((result.enrollments[0] as any)._deletedAt).toEqual(deletedDate); + + // Verify payments include _deletedAt + expect(result.payments).toHaveLength(1); + expect((result.payments[0] as any)._deletedAt).toEqual(deletedDate); + + // Verify notifications include _deletedAt + expect(result.notifications).toHaveLength(1); + expect((result.notifications[0] as any)._deletedAt).toEqual(deletedDate); }); it('erases user data', async () => { diff --git a/src/notifications/notifications.service.spec.ts b/src/notifications/notifications.service.spec.ts index 174054d4..a1e09111 100644 --- a/src/notifications/notifications.service.spec.ts +++ b/src/notifications/notifications.service.spec.ts @@ -44,6 +44,8 @@ describe('NotificationsService', () => { { provide: ConfigService, useValue: mockConfig }, { provide: getRepositoryToken(Notification), useValue: mockRepository }, { provide: NotificationsQueueService, useValue: mockQueue }, + { provide: PreferencesService, useValue: { getPreferences: jest.fn().mockResolvedValue({ channels: { email: true, push: true } }), isChannelEnabled: jest.fn().mockResolvedValue(true), updatePreferences: jest.fn() } }, + { provide: NotificationTemplateService, useValue: { renderByName: jest.fn().mockResolvedValue({ subject: 'Test', body: 'Test', templateVersion: 1 }) } }, ], }).compile(); @@ -127,7 +129,7 @@ describe('NotificationsService', () => { topicSubscriptions: {}, eventFrequency: {}, quietTimeStart: '00:00', - quietTimeEnd: '23:59', + quietTimeEnd: '00:01', }); preferencesService.isChannelEnabled.mockResolvedValue(true); templateService.renderByName.mockResolvedValue({ @@ -146,6 +148,7 @@ describe('NotificationsService', () => { { provide: PreferencesService, useValue: preferencesService }, { provide: NotificationsQueueService, useValue: queueService }, { provide: NotificationTemplateService, useValue: templateService }, + { provide: ConfigService, useValue: { get: jest.fn().mockReturnValue(null) } }, ], }).compile(); From 198341277f364332f900240e32e54e3717354b7c Mon Sep 17 00:00:00 2001 From: Fawaz Date: Mon, 29 Jun 2026 12:27:06 +0100 Subject: [PATCH 2/4] style: fix prettier formatting on modified files --- .../dto/submit-assessment.dto.spec.ts | 2 +- src/auth/dto/login.dto.ts | 2 +- .../tests/auto-remediation.service.spec.ts | 4 +++- .../notifications.service.spec.ts | 18 ++++++++++++++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/assessment/dto/submit-assessment.dto.spec.ts b/src/assessment/dto/submit-assessment.dto.spec.ts index 37fb8628..29e867fc 100644 --- a/src/assessment/dto/submit-assessment.dto.spec.ts +++ b/src/assessment/dto/submit-assessment.dto.spec.ts @@ -28,4 +28,4 @@ describe('SubmitAssessmentDto', () => { const itemChildren = errors[0].children?.[0]?.children ?? []; expect(itemChildren.some((child) => child.property === 'questionId')).toBe(true); }); -}); \ No newline at end of file +}); diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index 2503afbd..9b24c6c3 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -10,4 +10,4 @@ export class LoginDto { @IsNotEmpty() @MinLength(6) password: string; -} \ No newline at end of file +} diff --git a/src/incident-management/tests/auto-remediation.service.spec.ts b/src/incident-management/tests/auto-remediation.service.spec.ts index 0b90a3f8..5726d0bf 100644 --- a/src/incident-management/tests/auto-remediation.service.spec.ts +++ b/src/incident-management/tests/auto-remediation.service.spec.ts @@ -183,7 +183,9 @@ describe('AutoRemediationService', () => { ); expect(suggestions.length).toBeGreaterThan(0); - expect(suggestions[0].actionType).toMatch(/database_operation|restart_service|run_database_query/); + expect(suggestions[0].actionType).toMatch( + /database_operation|restart_service|run_database_query/, + ); }); it('should suggest actions for Cache incident', () => { diff --git a/src/notifications/notifications.service.spec.ts b/src/notifications/notifications.service.spec.ts index a1e09111..79f3e1a4 100644 --- a/src/notifications/notifications.service.spec.ts +++ b/src/notifications/notifications.service.spec.ts @@ -44,8 +44,22 @@ describe('NotificationsService', () => { { provide: ConfigService, useValue: mockConfig }, { provide: getRepositoryToken(Notification), useValue: mockRepository }, { provide: NotificationsQueueService, useValue: mockQueue }, - { provide: PreferencesService, useValue: { getPreferences: jest.fn().mockResolvedValue({ channels: { email: true, push: true } }), isChannelEnabled: jest.fn().mockResolvedValue(true), updatePreferences: jest.fn() } }, - { provide: NotificationTemplateService, useValue: { renderByName: jest.fn().mockResolvedValue({ subject: 'Test', body: 'Test', templateVersion: 1 }) } }, + { + provide: PreferencesService, + useValue: { + getPreferences: jest.fn().mockResolvedValue({ channels: { email: true, push: true } }), + isChannelEnabled: jest.fn().mockResolvedValue(true), + updatePreferences: jest.fn(), + }, + }, + { + provide: NotificationTemplateService, + useValue: { + renderByName: jest + .fn() + .mockResolvedValue({ subject: 'Test', body: 'Test', templateVersion: 1 }), + }, + }, ], }).compile(); From c5c96214b5ca8ec7b6cb3c6a0b5b2d89005aef58 Mon Sep 17 00:00:00 2001 From: Fawaz Date: Mon, 29 Jun 2026 16:04:09 +0100 Subject: [PATCH 3/4] fix: resolve ESLint parsing errors in gdpr.module.ts and gdpr.service.ts --- src/modules/gdpr/gdpr.module.ts | 12 +++++++----- src/modules/gdpr/gdpr.service.ts | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/modules/gdpr/gdpr.module.ts b/src/modules/gdpr/gdpr.module.ts index 6bd3c271..ecec7541 100644 --- a/src/modules/gdpr/gdpr.module.ts +++ b/src/modules/gdpr/gdpr.module.ts @@ -5,14 +5,16 @@ import { Enrollment } from '../../courses/entities/enrollment.entity'; import { Payment } from '../../payments/entities/payment.entity'; import { Notification } from '../../notifications/entities/notification.entity'; import { UserConsent } from './entities/user-consent.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([User, Enrollment, Payment, Notification, UserConsent])], import { SessionModule } from '../../session/session.module'; +import { GdprService } from './gdpr.service'; +import { GdprController } from './gdpr.controller'; @Module({ - imports: [SessionModule], - controllers: [GdprController], + imports: [ + TypeOrmModule.forFeature([User, Enrollment, Payment, Notification, UserConsent]), + SessionModule, + ], providers: [GdprService], + controllers: [GdprController], }) export class GdprModule {} diff --git a/src/modules/gdpr/gdpr.service.ts b/src/modules/gdpr/gdpr.service.ts index a192f351..8092d191 100644 --- a/src/modules/gdpr/gdpr.service.ts +++ b/src/modules/gdpr/gdpr.service.ts @@ -103,7 +103,6 @@ export class GdprService { throw new NotFoundException('User not found'); } - await this.userRepository.update(userId, { await this.sessionService.deleteAllSessionsForUser(userId); await this.usersService.update(userId, { From 1a3c9b03bc8a6d4339d4ced80ee0b5b827950213 Mon Sep 17 00:00:00 2001 From: Fawaz Date: Mon, 29 Jun 2026 16:12:07 +0100 Subject: [PATCH 4/4] fix: resolve merge conflict in email-template.service.ts --- src/modules/email-template.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/email-template.service.ts b/src/modules/email-template.service.ts index c28bda86..4e0a79d4 100644 --- a/src/modules/email-template.service.ts +++ b/src/modules/email-template.service.ts @@ -1,6 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import sanitizeHtml from 'sanitize-html'; import { EmailTemplate } from './email-template/email-template.entity'; import { CreateEmailTemplateDto } from './email-template/dto/create-email-template.dto'; import { UpdateEmailTemplateDto } from './email-template/dto/update-email-template.dto';