Skip to content

Commit

Permalink
Merge pull request #144 from amosproj/93-backend-email-notification-r…
Browse files Browse the repository at this point in the history
…eciever-list

93 backend email notification reciever list
  • Loading branch information
chrisklg authored Dec 8, 2024
2 parents 19a98a6 + c78b8c8 commit 6498ff2
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 13 deletions.
3 changes: 1 addition & 2 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ MAIL_HOST=smtp.example.com
MAIL_PORT=465
MAIL_USER=[email protected]
MAIL_PASSWORD=topsecret
MAIL_FROM=[email protected]
MAILING_LIST=[email protected],[email protected]
MAIL_FROM=[email protected]
3 changes: 3 additions & 0 deletions apps/backend/src/app/alerting/alerting.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CreateSizeAlertDto } from './dto/alerts/createSizeAlert.dto';
import { CREATION_DATE_ALERT, SIZE_ALERT } from '../utils/constants';
import { CreateCreationDateAlertDto } from './dto/alerts/createCreationDateAlert.dto';
import { CreationDateAlertEntity } from './entity/alerts/creationDateAlert.entity';
import { MailReceiverEntity } from '../utils/mail/entity/MailReceiver.entity';

const mockedBackupDataEntity: BackupDataEntity = {
id: 'backup-id',
Expand Down Expand Up @@ -104,6 +105,8 @@ describe('AlertingController (e2e)', () => {
.useValue(mockCreationDateAlertRepository)
.overrideProvider(getRepositoryToken(AlertTypeEntity))
.useValue(mockAlertTypeRepository)
.overrideProvider(getRepositoryToken(MailReceiverEntity))
.useValue({})
.compile();

repository = module.get(getRepositoryToken(SizeAlertEntity));
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/app/db-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { CreationDateAlertEntity } from './alerting/entity/alerts/creationDateAl
import { CreationDateAlert1733070019992 } from './migrations/1733070019992-CreationDateAlert';
import { TaskEntity } from './tasks/entity/task.entity';
import { Tasks1733397652480 } from './migrations/1733397652480-Tasks';
import { MailReceiverEntity } from './utils/mail/entity/MailReceiver.entity';
import { MailReceiver1733580333590 } from './migrations/1733580333590-MailReceiver';

/**
* Used by NestJS to reach database.
Expand All @@ -46,6 +48,7 @@ export class DbConfigService implements TypeOrmOptionsFactory {
SizeAlertEntity,
CreationDateAlertEntity,
TaskEntity,
MailReceiverEntity,
],
migrationsRun: true,
migrations: [
Expand All @@ -61,6 +64,7 @@ export class DbConfigService implements TypeOrmOptionsFactory {
NewAlertStructure1732887680122,
CreationDateAlert1733070019992,
Tasks1733397652480,
MailReceiver1733580333590,
],
logging: true,
};
Expand Down
14 changes: 14 additions & 0 deletions apps/backend/src/app/migrations/1733580333590-MailReceiver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class MailReceiver1733580333590 implements MigrationInterface {
name = 'MailReceiver1733580333590'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "MailReceiver" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "mail" character varying NOT NULL, CONSTRAINT "UQ_9b0a9d71b5bb67a4b0bb2499b4c" UNIQUE ("mail"), CONSTRAINT "PK_71d9ee85e0f42a3a278d0ed19cf" PRIMARY KEY ("id"))`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "MailReceiver"`);
}

}
12 changes: 12 additions & 0 deletions apps/backend/src/app/utils/mail/dto/createMailReceiver.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';

export class CreateMailReceiverDto {
@ApiProperty({
description: 'Mail Address',
required: true,
uniqueItems: true,
})
@IsEmail()
mail!: string;
}
20 changes: 20 additions & 0 deletions apps/backend/src/app/utils/mail/entity/MailReceiver.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';

@Entity('MailReceiver')
export class MailReceiverEntity {
@ApiProperty({
description: 'Auto-generated UUID of the Mail Receiver',
required: true,
})
@PrimaryGeneratedColumn('uuid')
id!: string;

@ApiProperty({
description: 'Mail Address',
required: true,
uniqueItems: true,
})
@Column({ nullable: false, unique: true })
mail!: string;
}
98 changes: 98 additions & 0 deletions apps/backend/src/app/utils/mail/mail.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { MailController } from './mail.controller';
import { MailService } from './mail.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MailReceiverEntity } from './entity/MailReceiver.entity';
import { DeleteResult, Repository } from 'typeorm';
import { MailerService } from '@nestjs-modules/mailer';
import { ConfigService } from '@nestjs/config';

describe('MailController (e2e)', () => {
let app: INestApplication;
let mailReceiverRepository: Repository<MailReceiverEntity>;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
controllers: [MailController],
providers: [
MailService,
{
provide: MailerService,
useValue: {
sendMail: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('true'),
},
},
{
provide: getRepositoryToken(MailReceiverEntity),
useValue: {
find: jest.fn(),
save: jest.fn(),
findOneBy: jest.fn(),
delete: jest.fn(),
},
},
],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();

mailReceiverRepository = moduleFixture.get<Repository<MailReceiverEntity>>(
getRepositoryToken(MailReceiverEntity)
);
});

it('/GET mail receivers', async () => {
const receivers = [{ id: '1', mail: '[email protected]' }];
jest.spyOn(mailReceiverRepository, 'find').mockResolvedValue(receivers);

const response = await request(app.getHttpServer()).get('/mail');
expect(response.status).toBe(200);
expect(response.body).toEqual(receivers);
});

it('/POST mail receiver', async () => {
const createMailReceiverDto = { mail: '[email protected]' };
const savedReceiver = { id: '2', mail: '[email protected]' };
jest.spyOn(mailReceiverRepository, 'save').mockResolvedValue(savedReceiver);

const response = await request(app.getHttpServer())
.post('/mail')
.send(createMailReceiverDto);
expect(response.status).toBe(201);
expect(response.body).toEqual(savedReceiver);
});

it('/DELETE mail receiver', async () => {
const id = 'ea1a2f52-5cf4-44a6-b266-175ee396a18c';
jest
.spyOn(mailReceiverRepository, 'findOneBy')
.mockResolvedValue({ id, mail: '[email protected]' });
jest
.spyOn(mailReceiverRepository, 'delete')
.mockResolvedValue(new DeleteResult());

const response = await request(app.getHttpServer()).delete(`/mail/${id}`);
expect(response.status).toBe(200);
});

it('should throw NotFoundException if mail receiver not found', async () => {
const id = 'ea1a2f52-5cf4-44a6-b266-175ee396a18e';
jest.spyOn(mailReceiverRepository, 'findOneBy').mockResolvedValue(null);

const response = await request(app.getHttpServer()).delete(`/mail/${id}`);
expect(response.status).toBe(404);
});

afterAll(async () => {
await app.close();
});
});
52 changes: 52 additions & 0 deletions apps/backend/src/app/utils/mail/mail.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
Body,
Controller,
Delete,
Get,
Logger,
Param,
ParseUUIDPipe,
Post,
} from '@nestjs/common';
import {
ApiCreatedResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { MailService } from './mail.service';
import { CreateMailReceiverDto } from './dto/createMailReceiver.dto';
import { MailReceiverEntity } from './entity/MailReceiver.entity';

@ApiTags('Mail')
@Controller('mail')
export class MailController {
readonly logger = new Logger(MailController.name);

constructor(private readonly mailService: MailService) {}

@Get()
@ApiOperation({ summary: 'Returns all Mail Receiver.' })
@ApiOkResponse()
async findAll(): Promise<MailReceiverEntity[]> {
return this.mailService.getAllMailReceiver();
}

@Delete(':id')
@ApiOperation({ summary: 'Removes the mail receiver with the given id.' })
@ApiOkResponse()
@ApiNotFoundResponse()
async findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.mailService.removeMailReceiver(id);
}

@Post()
@ApiOperation({ summary: 'Adds a new Mail Receiver.' })
@ApiCreatedResponse()
async create(
@Body() createMailReceiverDto: CreateMailReceiverDto
): Promise<MailReceiverEntity> {
return this.mailService.addMailReceiver(createMailReceiverDto);
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/app/utils/mail/mail.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { MailService } from './mail.service';
import { join } from 'path';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MailReceiverEntity } from './entity/MailReceiver.entity';
import { MailController } from './mail.controller';

@Global()
@Module({
Expand Down Expand Up @@ -35,8 +38,10 @@ import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handleba
}),
inject: [ConfigService],
}),
TypeOrmModule.forFeature([MailReceiverEntity]),
],
providers: [MailService],
exports: [MailService],
controllers: [MailController],
})
export class MailModule {}
69 changes: 66 additions & 3 deletions apps/backend/src/app/utils/mail/mail.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,39 @@ import { BackupType } from '../../backupData/dto/backupType';
import { SizeAlertEntity } from '../../alerting/entity/alerts/sizeAlert.entity';
import { SeverityType } from '../../alerting/dto/severityType';
import { SIZE_ALERT } from '../constants';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MailReceiverEntity } from './entity/MailReceiver.entity';
import { NotFoundException } from '@nestjs/common';

jest.mock('path', () => ({
resolve: jest.fn().mockReturnValue('mocked/path/to/logo.png'),
dirname: jest.fn(),
}));

const mockMailReceiverRepository = {
find: jest.fn().mockResolvedValue([
{
id: '1',
mail: '[email protected]',
},
]),
save: jest.fn().mockImplementation((receiver) => Promise.resolve(receiver)),
findOneBy: jest.fn().mockImplementation(({ id }) => {
if (id === '1') {
return Promise.resolve({
id: '1',
mail: '[email protected]',
});
} else {
return Promise.resolve(null);
}
}),
delete: jest.fn().mockResolvedValue({}),
};

describe('MailService', () => {
let service: MailService;
let mailerService: MailerService;
let configService: ConfigService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -29,16 +53,18 @@ describe('MailService', () => {
{
provide: ConfigService,
useValue: {
getOrThrow: jest.fn().mockReturnValue('[email protected]'),
get: jest.fn().mockReturnValue('true'),
},
},
{
provide: getRepositoryToken(MailReceiverEntity),
useValue: mockMailReceiverRepository,
},
],
}).compile();

service = module.get(MailService);
mailerService = module.get(MailerService);
configService = module.get(ConfigService);
});

it('should be defined', () => {
Expand Down Expand Up @@ -112,4 +138,41 @@ describe('MailService', () => {
attachments,
});
});

it('should get all mail receivers', async () => {
const receivers = [{ id: '1', mail: '[email protected]' }];

expect(await service.getAllMailReceiver()).toStrictEqual(receivers);
});

it('should add a mail receiver', async () => {
const createMailReceiverDto = { mail: '[email protected]' };
const savedReceiver = { mail: '[email protected]' };

await service.addMailReceiver(createMailReceiverDto);

expect(mockMailReceiverRepository.save).toBeCalledWith({
mail: createMailReceiverDto.mail,
});

expect(await service.addMailReceiver(createMailReceiverDto)).toStrictEqual(
savedReceiver
);
});

it('should remove a mail receiver', async () => {
const id = '1';
await service.removeMailReceiver(id);

expect(mockMailReceiverRepository.findOneBy).toHaveBeenCalledWith({ id });
expect(mockMailReceiverRepository.delete).toHaveBeenCalledWith({ id });
});

it('should throw NotFoundException if mail receiver not found', async () => {
const id = 'non-existent-id';

await expect(service.removeMailReceiver(id)).rejects.toThrow(
NotFoundException
);
});
});
Loading

0 comments on commit 6498ff2

Please sign in to comment.