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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/grant/__test__/grant.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
15 changes: 5 additions & 10 deletions backend/src/notifications/__test__/notification.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
11 changes: 7 additions & 4 deletions backend/src/notifications/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,14 @@ export class NotificationService {
subject: string,
body: string
): Promise<AWS.SES.SendEmailResponse> {
// 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,
Expand Down
17 changes: 14 additions & 3 deletions backend/src/user/__test__/user.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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>(UserController);
Expand Down Expand Up @@ -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();
});

Expand Down
46 changes: 5 additions & 41 deletions backend/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!;

Expand Down Expand Up @@ -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}`
);
Expand Down Expand Up @@ -821,45 +824,6 @@ async getAllActiveUsers(): Promise<User[]> {



// 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<AWS.SES.SendEmailResponse> {
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<string> {
const tableName = process.env.DYNAMODB_USER_TABLE_NAME;
let s3Key;
Expand Down
Loading