Skip to content

Commit

Permalink
Merge pull request #618 from King-AJr/feat/create-email-templates
Browse files Browse the repository at this point in the history
feat: store hng email templates and update create email endpoint
  • Loading branch information
Cobidex authored Aug 8, 2024
2 parents a61da57 + 0ebfb7a commit 54f9e4d
Show file tree
Hide file tree
Showing 36 changed files with 7,256 additions and 301 deletions.
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
import * as fs from 'fs';
import { promisify } from 'util';

/** Check if a file exists at a given path.*/
export const checkIfFileOrDirectoryExists = (path: string): boolean => {
return fs.existsSync(path);
};

/** Gets file data from a given path via a promise interface.*/
export const getFile = async (path: string, encoding?: BufferEncoding): Promise<string | Buffer> => {
const readFile = promisify(fs.readFile);

return encoding ? readFile(path, { encoding }) : readFile(path);
};

/** Writes a file at a given path via a promise interface */
export const createFile = async (path: string, fileName: string, data: string): Promise<void> => {
if (!checkIfFileOrDirectoryExists(path)) {
fs.mkdirSync(path);
fs.mkdirSync(path, { recursive: true });
}

console.log(`Creating file at ${path}/${fileName}`);

const writeFile = promisify(fs.writeFile);

return await writeFile(`${path}/${fileName}`, data, 'utf8');
await writeFile(`${path}/${fileName}`, data, 'utf8');
};

/**Delete file at the given path via a promise interface*/
export const deleteFile = async (path: string): Promise<void> => {
const unlink = promisify(fs.unlink);

return await unlink(path);
await unlink(path);
};
1 change: 1 addition & 0 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default class AuthenticationService {

const token = (await this.otpService.createOtp(user.id)).token;
await this.emailService.sendForgotPasswordMail(user.email, `${process.env.BASE_URL}/auth/reset-password`, token);

return {
message: SYS_MSG.EMAIL_SENT,
};
Expand Down
7 changes: 3 additions & 4 deletions src/modules/auth/tests/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,10 @@ describe('AuthenticationService', () => {
const emailData = {
to: email,
subject: 'Reset Password',
template: 'reset-password',
template: 'Password-Reset-Complete-Template',
context: {
link: 'http://example.com/auth/reset-password',
email: email,
token: '123456',
otp: '123456',
name: email,
},
};

Expand Down
19 changes: 12 additions & 7 deletions src/modules/email/email.controller.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
import { Controller, Post, Get, Body, Res } from '@nestjs/common';
import { Controller, Post, Get, Body, Res, UseGuards, Query, Delete } from '@nestjs/common';
import { Response } from 'express';
import { EmailService } from './email.service';
import { skipAuth } from '../../helpers/skipAuth';
import { SuperAdminGuard } from '../../guards/super-admin.guard';
import { SendEmailDto, createTemplateDto, getTemplateDto } from './dto/email.dto';
import { skip } from 'node:test';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';


@ApiTags('Emails')
@ApiBearerAuth()
@Controller('email')
export class EmailController {
constructor(private emailService: EmailService) {}

@UseGuards(SuperAdminGuard)
@Post('store-template')
async storeTemplate(@Body() body: createTemplateDto, @Res() res: Response): Promise<any> {
const response = await this.emailService.createTemplate(body);
res.status(response.status_code).send(response);
}

@Post('get-template')
async getTemplate(@Body() body: getTemplateDto, @Res() res: Response): Promise<any> {
const response = await this.emailService.getTemplate(body);
@UseGuards(SuperAdminGuard)
@Get('get-template')
async getTemplate(@Query() query: getTemplateDto, @Res() res: Response): Promise<any> {
const response = await this.emailService.getTemplate(query);
res.status(response.status_code).send(response);
}

@Post('delete-template')
@UseGuards(SuperAdminGuard)
@Delete('delete-template')
async deleteTemplate(@Body() body: getTemplateDto, @Res() res: Response): Promise<any> {
const response = await this.emailService.getTemplate(body);
const response = await this.emailService.deleteTemplate(body);
res.status(response.status_code).send(response);
}

@UseGuards(SuperAdminGuard)
@Get('get-all-templates')
async getAllTemplates(@Res() res: Response): Promise<any> {
const response = await this.emailService.getAllTemplates();
Expand Down
5 changes: 4 additions & 1 deletion src/modules/email/email.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import { MailerModule } from '@nestjs-modules/mailer';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { EmailController } from './email.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../user/entities/user.entity';

@Module({
providers: [EmailService, QueueService, EmailQueueConsumer],
exports: [EmailService, QueueService],
imports: [
TypeOrmModule.forFeature([User]),
BullModule.registerQueueAsync({
name: 'emailSending',
}),
Expand All @@ -31,7 +34,7 @@ import { EmailController } from './email.controller';
from: `"Team Remote Bingo" <${configService.get<string>('SMTP_USER')}>`,
},
template: {
dir: process.cwd() + '/src/modules/email/templates',
dir: process.cwd() + '/src/modules/email/hng-templates',
adapter: new HandlebarsAdapter(),
options: {
strict: true,
Expand Down
57 changes: 51 additions & 6 deletions src/modules/email/email.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import * as Handlebars from 'handlebars';
import * as htmlValidator from 'html-validator';
import * as fs from 'fs';
import { HttpStatus } from '@nestjs/common';
import { createFile, deleteFile, getFile } from '../../helpers/fileHelpers';
import { MailInterface } from './interfaces/MailInterface';

// Mock module-level functions
jest.mock('./email_storage.service', () => ({
jest.mock('../../helpers/fileHelpers', () => ({
createFile: jest.fn(),
deleteFile: jest.fn(),
getFile: jest.fn(),
Expand Down Expand Up @@ -87,13 +88,57 @@ describe('EmailService', () => {
const result = await queueService.sendMail(mailSender);

expect(mockQueue.add).toHaveBeenCalledWith(mailSender.variant, { mail: mailSender.mail });
expect(result).toEqual({ jobId: jobMock.id });
expect(result).toEqual({jobId: jobMock.id});
});


describe('createTemplate', () => {
it('should create a template if HTML is valid', async () => {
const templateInfo: createTemplateDto = { templateName: 'test', template: '<div></div>' };
(htmlValidator as jest.Mock).mockResolvedValue({ messages: [] });
(createFile as jest.Mock).mockResolvedValue(Promise.resolve());

const result = await service.createTemplate(templateInfo);

expect(result).toEqual({
status_code: HttpStatus.CREATED,
message: 'Template created successfully',
validation_errors: [],
});
expect(createFile).toHaveBeenCalledWith('./src/modules/email/hng-templates', 'test.hbs', '<div></div>');
});

it('should return validation errors if HTML is invalid', async () => {
const templateInfo: createTemplateDto = { templateName: 'test', template: '<div></div>' };
(htmlValidator as jest.Mock).mockResolvedValue({ messages: [{ message: 'Invalid HTML', type: 'error' }] });

const result = await service.createTemplate(templateInfo);

expect(result).toEqual({
status_code: HttpStatus.BAD_REQUEST,
message: 'Invalid HTML format',
validation_errors: ['Invalid HTML'],
});
});

it('should handle errors during template creation', async () => {
const templateInfo: createTemplateDto = { templateName: 'test', template: '<div></div>' };
(htmlValidator as jest.Mock).mockRejectedValue(new Error('Validation error'));

const result = await service.createTemplate(templateInfo);

expect(result).toEqual({
status_code: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'Something went wrong, please try again',
});
});
});


describe('getTemplate', () => {
it('should return the content of a template', async () => {
const templateInfo: getTemplateDto = { templateName: 'test' };
(require('./email_storage.service').getFile as jest.Mock).mockResolvedValue('template content');
(getFile as jest.Mock).mockResolvedValue('template content');

const result = await service.getTemplate(templateInfo);

Expand All @@ -106,7 +151,7 @@ describe('EmailService', () => {

it('should return NOT_FOUND if template does not exist', async () => {
const templateInfo: getTemplateDto = { templateName: 'test' };
(require('./email_storage.service').getFile as jest.Mock).mockRejectedValue(new Error('File not found'));
(getFile as jest.Mock).mockRejectedValue(new Error('File not found'));

const result = await service.getTemplate(templateInfo);

Expand All @@ -120,7 +165,7 @@ describe('EmailService', () => {
describe('deleteTemplate', () => {
it('should delete a template', async () => {
const templateInfo: getTemplateDto = { templateName: 'test' };
(require('./email_storage.service').deleteFile as jest.Mock).mockResolvedValue(Promise.resolve());
(deleteFile as jest.Mock).mockResolvedValue(Promise.resolve());

const result = await service.deleteTemplate(templateInfo);

Expand All @@ -132,7 +177,7 @@ describe('EmailService', () => {

it('should return NOT_FOUND if template does not exist', async () => {
const templateInfo: getTemplateDto = { templateName: 'test' };
(require('./email_storage.service').deleteFile as jest.Mock).mockRejectedValue(new Error('File not found'));
(deleteFile as jest.Mock).mockRejectedValue(new Error('File not found'));

const result = await service.deleteTemplate(templateInfo);

Expand Down
34 changes: 19 additions & 15 deletions src/modules/email/email.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { HttpStatus, Injectable } from '@nestjs/common';
import { SendEmailDto, createTemplateDto, getTemplateDto } from './dto/email.dto';
import { validateOrReject } from 'class-validator';
import * as Handlebars from 'handlebars';
import * as htmlValidator from 'html-validator';
import * as fs from 'fs';
import { promisify } from 'util';
import * as path from 'path';
import { MailerService } from '@nestjs-modules/mailer';
import { MailInterface } from './interfaces/MailInterface';
import QueueService from './queue.service';
import { ArticleInterface } from './interface/article.interface';
import { createTemplateDto, getTemplateDto } from './dto/email.dto';
import { IMessageInterface } from './interface/message.interface';
import { promisify } from 'util';
import path from 'path';
import { createFile, deleteFile, getFile } from './email_storage.service';
import fs from 'fs';
import * as htmlValidator from 'html-validator';
import { getFile, createFile, deleteFile } from '../../helpers/fileHelpers';


@Injectable()
export class EmailService {
Expand Down Expand Up @@ -105,9 +109,7 @@ export class EmailService {

async createTemplate(templateInfo: createTemplateDto) {
try {
const html = Handlebars.compile(templateInfo.template)({});

const validationResult = await htmlValidator({ data: html });
const validationResult = await htmlValidator({ data: templateInfo.template });

const filteredMessages = validationResult.messages.filter(
message =>
Expand All @@ -130,7 +132,11 @@ export class EmailService {
}

if (response.status_code === HttpStatus.CREATED) {
await createFile('./src/modules/email/templates', `${templateInfo.templateName}.hbs`, html);
await createFile(
'./src/modules/email/hng-templates',
`${templateInfo.templateName}.hbs`,
templateInfo.template
);
}

return response;
Expand All @@ -144,8 +150,7 @@ export class EmailService {

async getTemplate(templateInfo: getTemplateDto) {
try {
const template = await getFile(`./src/modules/email/templates/${templateInfo.templateName}.hbs`, 'utf-8');
console.log(template);
const template = await getFile(`./src/modules/email/hng-templates/${templateInfo.templateName}.hbs`, 'utf-8');

return {
status_code: HttpStatus.OK,
Expand All @@ -162,7 +167,7 @@ export class EmailService {

async deleteTemplate(templateInfo: getTemplateDto) {
try {
await deleteFile(`./src/modules/email/templates/${templateInfo.templateName}.hbs`);
await deleteFile(`./src/modules/email/hng-templates/${templateInfo.templateName}.hbs`);
return {
status_code: HttpStatus.OK,
message: 'Template deleted successfully',
Expand All @@ -177,7 +182,7 @@ export class EmailService {

async getAllTemplates() {
try {
const templatesDirectory = './src/modules/email/templates';
const templatesDirectory = './src/modules/email/hng-templates';
const files = await promisify(fs.readdir)(templatesDirectory);

const templates = await Promise.all(
Expand All @@ -201,7 +206,6 @@ export class EmailService {
templates: validTemplates,
};
} catch (error) {
console.log(error);
return {
status_code: HttpStatus.NOT_FOUND,
message: 'Template not found',
Expand Down
218 changes: 218 additions & 0 deletions src/modules/email/hng-templates/Account-Deactivation.hbs

Large diffs are not rendered by default.

223 changes: 223 additions & 0 deletions src/modules/email/hng-templates/Account-Inactivity-Deactivation.hbs

Large diffs are not rendered by default.

Loading

0 comments on commit 54f9e4d

Please sign in to comment.