diff --git a/backend/src/grant/__test__/grant.service.spec.ts b/backend/src/grant/__test__/grant.service.spec.ts index 5b966312..bbfecd23 100644 --- a/backend/src/grant/__test__/grant.service.spec.ts +++ b/backend/src/grant/__test__/grant.service.spec.ts @@ -511,7 +511,7 @@ describe("GrantService", () => { const data = await grantService.addGrant(mockCreateGrantDto); - expect(data).closeTo(now, 1); + expect(data).closeTo(now, 10); expect(mockDocumentClient.put).toHaveBeenCalledWith({ TableName: "Grants", Item: { diff --git a/backend/src/notifications/__test__/notification.service.test.ts b/backend/src/notifications/__test__/notification.service.test.ts index 8edfd815..17914876 100644 --- a/backend/src/notifications/__test__/notification.service.test.ts +++ b/backend/src/notifications/__test__/notification.service.test.ts @@ -189,19 +189,14 @@ describe('NotificationController', () => { expect(mockPromise).toHaveBeenCalled(); }); - it('should use fallback email when NOTIFICATION_EMAIL_SENDER is not set', async () => { + it('should throw when NOTIFICATION_EMAIL_SENDER is not set', async () => { delete process.env.NOTIFICATION_EMAIL_SENDER; - await notificationService.sendEmailNotification('user@example.com', 'Test Subject', 'Test Body'); + await expect( + notificationService.sendEmailNotification('user@example.com', 'Test Subject', 'Test Body') + ).rejects.toThrow(InternalServerErrorException); - expect(mockSendEmail).toHaveBeenCalledWith({ - Source: 'u&@nveR1ified-failure@dont-send.com', - Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Subject: { Charset: 'UTF-8', Data: 'Test Subject' }, - Body: { Text: { Charset: 'UTF-8', Data: 'Test Body' } }, - }, - }); + expect(mockSendEmail).not.toHaveBeenCalled(); }); it('should handle special characters in email content', async () => { diff --git a/backend/src/notifications/notification.service.ts b/backend/src/notifications/notification.service.ts index 46cfa486..af2d98ee 100644 --- a/backend/src/notifications/notification.service.ts +++ b/backend/src/notifications/notification.service.ts @@ -163,11 +163,14 @@ export class NotificationService { subject: string, body: string ): Promise { - // Default to an invalid email to prevent non-verified sender mails - // if BCAN's is not defined in the environment + + if (!process.env.NOTIFICATION_EMAIL_SENDER) { + this.logger.error('NOTIFICATION_EMAIL_SENDER is not defined in environment variables'); + throw new InternalServerErrorException("Internal Server Error") + } + this.logger.log(`Sending email notification to: ${to}, subject: ${subject}`); - const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER || - 'u&@nveR1ified-failure@dont-send.com'; + const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER; const params: AWS.SES.SendEmailRequest = { Source: fromEmail, diff --git a/backend/src/user/__test__/user.service.spec.ts b/backend/src/user/__test__/user.service.spec.ts index dd73b17e..608eb0ba 100644 --- a/backend/src/user/__test__/user.service.spec.ts +++ b/backend/src/user/__test__/user.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { UserController } from '../user.controller'; import { UserService } from '../user.service'; +import { NotificationService } from '../../notifications/notification.service'; import { User } from '../../../../middle-layer/types/User'; import { UserStatus } from '../../../../middle-layer/types/UserStatus'; import { VerifyUserGuard, VerifyAdminRoleGuard, VerifyAdminOrEmployeeRoleGuard } from '../../guards/auth.guard'; @@ -60,6 +61,8 @@ vi.mock('../../guards/auth.guard', () => ({ VerifyAdminOrEmployeeRoleGuard: vi.fn(class { canActivate = vi.fn().mockResolvedValue(true); }), })); + + // ─── Mock database (email is now the partition key) ─────────────────────────── const mockDatabase = { users: [ @@ -114,6 +117,7 @@ describe('UserController', () => { process.env.DYNAMODB_USER_TABLE_NAME = 'test-users-table'; process.env.COGNITO_USER_POOL_ID = 'test-pool-id'; process.env.PROFILE_PICTURE_BUCKET = 'test-profile-pics-bucket'; + process.env.NOTIFICATION_EMAIL_SENDER = 'noreply@c4cneu.com'; }); beforeEach(async () => { @@ -132,9 +136,17 @@ describe('UserController', () => { mockPromise.mockResolvedValue({}); + const mockNotificationService = { + sendEmailNotification: vi.fn().mockResolvedValue({ MessageId: 'test-id' }), + deleteNotificationsByUserEmail: vi.fn().mockResolvedValue(undefined), + }; + const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], - providers: [UserService], + providers: [ + UserService, + { provide: NotificationService, useValue: mockNotificationService }, + ], }).compile(); controller = module.get(UserController); @@ -388,13 +400,12 @@ describe('UserController', () => { .mockResolvedValueOnce({ Item: user }) .mockResolvedValueOnce({}) .mockResolvedValueOnce({}) - .mockResolvedValueOnce({ MessageId: 'test-id' }) .mockResolvedValueOnce({ Attributes: { ...user, position: UserStatus.Employee } }); const result = await userService.addUserToGroup(user, UserStatus.Employee, admin); expect(result.position).toBe(UserStatus.Employee); expect(mockAdminAddUserToGroup).toHaveBeenCalledWith({ GroupName: 'Employee', UserPoolId: 'test-pool-id', Username: 'inactive1@example.com' }); - expect(mockSendEmail).toHaveBeenCalled(); + expect(mockSendEmail).not.toHaveBeenCalled(); expect(mockUpdate).toHaveBeenCalled(); }); diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index bb100347..68191a2b 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -22,7 +22,6 @@ export class UserService { private readonly logger = new Logger(UserService.name); private dynamoDb = new AWS.DynamoDB.DocumentClient(); - private ses = new AWS.SES({ region: process.env.AWS_REGION }); private s3 = new AWS.S3(); private profilePicBucket : string = process.env.PROFILE_PICTURE_BUCKET!; @@ -514,7 +513,11 @@ async addUserToGroup( groupName === UserStatus.Employee ) { try { - await this.sendVerificationEmail(user.email); + await this.notificationService.sendEmailNotification( + user.email, + "BCAN Account Approval", + "Your account has been approved; Try using your login credentials now!" + ); this.logger.log( `✓ Verification email sent to ${user.email} upon group change to ${groupName}` ); @@ -821,45 +824,6 @@ async getAllActiveUsers(): Promise { - // purpose statement: sends email to user once account is approved, used in method above when a user - // is added to the Employee or Admin group from Inactive - async sendVerificationEmail(userEmail: string): Promise { - this.logger.log(`Starting sendVerificationEmail for email: ${userEmail}`); - - if (!userEmail || !this.isValidEmail(userEmail)) { - this.logger.error(`Invalid email address provided: ${userEmail}`); - throw new BadRequestException("Valid email address is required"); - } - - // remove actual email and add to env later!! - const fromEmail = process.env.NOTIFICATION_EMAIL_SENDER || 'c4cneu.bcan@gmail.com'; - - const params: AWS.SES.SendEmailRequest = { - Source: fromEmail, - Destination: { - ToAddresses: [userEmail], - }, - Message: { - // UTF-8 is a top reliable way to define special characters and symbols in emails - Subject: { Charset: 'UTF-8', Data: "BCAN Account Approval" }, - Body: { - Text: { Charset: 'UTF-8', Data: "Your account has been approved; Try using your login credentials now!" }, - }, - }, - }; - - try { - this.logger.log(`Calling AWS SES to send email to ${userEmail}...`); - const result = await this.ses.sendEmail(params).promise(); - this.logger.log(`✅ Verification email sent successfully to ${userEmail}. MessageId: ${result.MessageId}`); - return result; - } catch (err: unknown) { - this.logger.error('Error sending email: ', err); - const errMessage = (err instanceof Error) ? err.message : 'Unknown error'; - throw new InternalServerErrorException(`Failed to send email: ${errMessage}`); - } - } - async removeProfilePicture(email: string): Promise { const tableName = process.env.DYNAMODB_USER_TABLE_NAME; let s3Key;