diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 9481e6f5..de93339a 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -23,6 +23,7 @@ import { ProviderModule } from '../provider/provider.module' import { ScheduleModule } from '@nestjs/schedule' import { EnvSchema } from '../common/env/env.schema' import { IntegrationModule } from '../integration/integration.module' +import { FeedbackModule } from '../feedback/feedback.module' @Module({ controllers: [AppController], @@ -54,7 +55,8 @@ import { IntegrationModule } from '../integration/integration.module' ApprovalModule, SocketModule, ProviderModule, - IntegrationModule + IntegrationModule, + FeedbackModule ], providers: [ { diff --git a/apps/api/src/feedback/controller/feedback.controller.spec.ts b/apps/api/src/feedback/controller/feedback.controller.spec.ts new file mode 100644 index 00000000..d4d73f3f --- /dev/null +++ b/apps/api/src/feedback/controller/feedback.controller.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { FeedbackController } from './feedback.controller' +import { FeedbackService } from '../service/feedback.service' +import { MAIL_SERVICE } from '../../mail/services/interface.service' +import { MockMailService } from '../../mail/services/mock.service' + +describe('FeedbackController', () => { + let controller: FeedbackController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FeedbackController], + providers: [ + FeedbackService, + { provide: MAIL_SERVICE, useValue: MockMailService } + ] + }).compile() + + controller = module.get(FeedbackController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/apps/api/src/feedback/controller/feedback.controller.ts b/apps/api/src/feedback/controller/feedback.controller.ts new file mode 100644 index 00000000..ed448e96 --- /dev/null +++ b/apps/api/src/feedback/controller/feedback.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Post, Body, HttpStatus } from '@nestjs/common' +import { Public } from '../../decorators/public.decorator' +import { FeedbackService } from '../service/feedback.service' +import { ApiTags, ApiBody, ApiResponse, ApiOperation } from '@nestjs/swagger' + +@ApiTags('Feedback Controller') +@Controller('feedback') +export class FeedbackController { + constructor(private readonly feedbackService: FeedbackService) {} + + @Public() + @Post() + @ApiOperation({ + summary: 'Send Feedback message to Admin', + description: 'This endpoint sends a feedback message to the Admin email.' + }) + @ApiBody({ + schema: { + type: 'object', + properties: { + feedback: { + type: 'string', + example: 'Your feedback message here' + } + } + } + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Feedback registered successfully' + }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Bad Request' }) + async registerFeedback( + @Body() feedbackData: { feedback: string } + ): Promise { + await this.feedbackService.registerFeedback(feedbackData.feedback) + } +} diff --git a/apps/api/src/feedback/feedback.e2e.spec.ts b/apps/api/src/feedback/feedback.e2e.spec.ts new file mode 100644 index 00000000..a5cfecff --- /dev/null +++ b/apps/api/src/feedback/feedback.e2e.spec.ts @@ -0,0 +1,109 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { AppModule } from '../app/app.module' +import { FeedbackService } from '../feedback/service/feedback.service' +import { MockMailService } from '../mail/services/mock.service' +import { + FastifyAdapter, + NestFastifyApplication +} from '@nestjs/platform-fastify' +import { MAIL_SERVICE } from '../mail/services/interface.service' +import { FeedbackModule } from './feedback.module' +import { MailModule } from '../mail/mail.module' +import { PrismaService } from '../prisma/prisma.service' +import { User } from '@prisma/client' +import cleanUp from '../common/cleanup' + +describe('Feedback Controller (E2E)', () => { + let app: NestFastifyApplication + let feedbackService: FeedbackService + let mockMailService: MockMailService + let prisma: PrismaService + let user: User + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [AppModule, FeedbackModule, MailModule] + }) + .overrideProvider(MAIL_SERVICE) + .useClass(MockMailService) + .compile() + + app = moduleRef.createNestApplication( + new FastifyAdapter() + ) + feedbackService = moduleRef.get(FeedbackService) + mockMailService = moduleRef.get(MAIL_SERVICE) + + prisma = moduleRef.get(PrismaService) + + await app.init() + await app.getHttpAdapter().getInstance().ready() + + await cleanUp(prisma) + }) + + beforeEach(async () => { + user = await prisma.user.create({ + data: { + email: 'john@keyshade.xyz', + name: 'John', + isActive: true, + isAdmin: false, + isOnboardingFinished: false + } + }) + }) + + afterEach(async () => { + if (user) { + await prisma.user.delete({ + where: { id: user.id } + }) + } + }) + + afterAll(async () => { + await prisma.$disconnect() + await app.close() + }) + + it('should be defined', async () => { + expect(app).toBeDefined() + expect(feedbackService).toBeDefined() + expect(mockMailService).toBeDefined() + expect(prisma).toBeDefined() + }) + + it('should register feedback successfully', async () => { + const feedbackMessage = 'Test feedback message' + + const { statusCode } = await app.inject({ + method: 'POST', + url: '/feedback', + payload: { feedback: feedbackMessage }, + headers: { + 'x-e2e-user-email': user.email + } + }) + + expect(statusCode).toBe(201) + }) + + it('should handle empty feedback', async () => { + const { statusCode, payload } = await app.inject({ + method: 'POST', + url: '/feedback', + payload: { feedback: '' }, + headers: { + 'x-e2e-user-email': user.email + } + }) + + expect(statusCode).toBe(400) + expect(JSON.parse(payload)).toEqual({ + error: 'Bad Request', + message: 'Feedback cannot be null or empty', + statusCode: 400 + }) + }) +}) diff --git a/apps/api/src/feedback/feedback.module.ts b/apps/api/src/feedback/feedback.module.ts new file mode 100644 index 00000000..4a49482a --- /dev/null +++ b/apps/api/src/feedback/feedback.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { FeedbackService } from './service/feedback.service' +import { FeedbackController } from './controller/feedback.controller' + +@Module({ + providers: [FeedbackService], + controllers: [FeedbackController] +}) +export class FeedbackModule {} diff --git a/apps/api/src/feedback/service/feedback.service.spec.ts b/apps/api/src/feedback/service/feedback.service.spec.ts new file mode 100644 index 00000000..e650f963 --- /dev/null +++ b/apps/api/src/feedback/service/feedback.service.spec.ts @@ -0,0 +1,23 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { FeedbackService } from './feedback.service' +import { MAIL_SERVICE } from '../../mail/services/interface.service' +import { MockMailService } from '../../mail/services/mock.service' + +describe('FeedbackService', () => { + let service: FeedbackService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FeedbackService, + { provide: MAIL_SERVICE, useClass: MockMailService } + ] + }).compile() + + service = module.get(FeedbackService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/feedback/service/feedback.service.ts b/apps/api/src/feedback/service/feedback.service.ts new file mode 100644 index 00000000..d8ccae5d --- /dev/null +++ b/apps/api/src/feedback/service/feedback.service.ts @@ -0,0 +1,21 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common' +import { + IMailService, + MAIL_SERVICE +} from '../../mail/services/interface.service' + +@Injectable() +export class FeedbackService { + constructor( + @Inject(MAIL_SERVICE) private readonly mailService: IMailService + ) {} + + async registerFeedback(feedback: string): Promise { + if (!feedback || feedback.trim().length === 0) { + throw new BadRequestException('Feedback cannot be null or empty') + } + const adminEmail = 'admin@keyshade.xyz' + + await this.mailService.feedbackEmail(adminEmail, feedback.trim()) + } +} diff --git a/apps/api/src/mail/services/interface.service.ts b/apps/api/src/mail/services/interface.service.ts index 11414444..983d420e 100644 --- a/apps/api/src/mail/services/interface.service.ts +++ b/apps/api/src/mail/services/interface.service.ts @@ -14,4 +14,6 @@ export interface IMailService { accountLoginEmail(email: string): Promise adminUserCreateEmail(email: string): Promise + + feedbackEmail(email: string, feedback: string): Promise } diff --git a/apps/api/src/mail/services/mail.service.ts b/apps/api/src/mail/services/mail.service.ts index b3664062..f2c0dd85 100644 --- a/apps/api/src/mail/services/mail.service.ts +++ b/apps/api/src/mail/services/mail.service.ts @@ -111,6 +111,28 @@ export class MailService implements IMailService { await this.sendEmail(process.env.ADMIN_EMAIL, subject, body) } + async feedbackEmail(email: string, feedback: string): Promise { + const subject = 'New Feedback Received !' + const body = ` + + + New Feedback Received ! + + +

New Feedback Received

+

Hello,

+

We have received new feedback from a user:

+
${feedback}
+

Please review this feedback as soon as possible.

+

Thank you.

+

Best Regards,

+

Keyshade Team

+ + + ` + await this.sendEmail(email, subject, body) + } + private async sendEmail( email: string, subject: string, diff --git a/apps/api/src/mail/services/mock.service.ts b/apps/api/src/mail/services/mock.service.ts index c2967958..0bb962b3 100644 --- a/apps/api/src/mail/services/mock.service.ts +++ b/apps/api/src/mail/services/mock.service.ts @@ -30,4 +30,8 @@ export class MockMailService implements IMailService { async accountLoginEmail(email: string): Promise { this.log.log(`Account Login Email for ${email}`) } + + async feedbackEmail(email: string, feedback: string): Promise { + this.log.log(`Feedback is : ${feedback}, for email : ${email}`) + } }