diff --git a/apps/api/src/api-key/api-key.e2e.spec.ts b/apps/api/src/api-key/api-key.e2e.spec.ts index f74393ef..6429ebea 100644 --- a/apps/api/src/api-key/api-key.e2e.spec.ts +++ b/apps/api/src/api-key/api-key.e2e.spec.ts @@ -81,7 +81,32 @@ describe('Api Key Role Controller Tests', () => { apiKeyValue = response.json().value }) - it('should be able to update the api key', async () => { + it('should not have any authorities if none are provided', async () => { + const response = await app.inject({ + method: 'POST', + url: '/api-key', + payload: { + name: 'Test Key 2', + expiresAfter: '24' + }, + headers: { + 'x-e2e-user-email': user.email + } + }) + + expect(response.statusCode).toBe(201) + expect(response.json()).toEqual({ + id: expect.any(String), + name: 'Test Key 2', + value: expect.stringMatching(/^ks_*/), + authorities: [], + expiresAt: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String) + }) + }) + + it('should be able to update the api key without without changing the authorities', async () => { const response = await app.inject({ method: 'PUT', url: `/api-key/${apiKey.id}`, @@ -107,6 +132,31 @@ describe('Api Key Role Controller Tests', () => { apiKey = response.json() }) + it('should be able to update the api key with changing the expiry', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/api-key/${apiKey.id}`, + payload: { + name: 'Updated Test Key', + expiresAfter: '24', + authorities: ['READ_API_KEY', 'CREATE_ENVIRONMENT'] + }, + headers: { + 'x-e2e-user-email': user.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json()).toEqual({ + id: apiKey.id, + name: 'Updated Test Key', + authorities: ['READ_API_KEY', 'CREATE_ENVIRONMENT'], + expiresAt: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String) + }) + }) + it('should be able to get the api key', async () => { const response = await app.inject({ method: 'GET', @@ -120,13 +170,25 @@ describe('Api Key Role Controller Tests', () => { expect(response.json()).toEqual({ id: apiKey.id, name: 'Updated Test Key', - authorities: ['READ_API_KEY'], + authorities: ['READ_API_KEY', 'CREATE_ENVIRONMENT'], expiresAt: expect.any(String), createdAt: expect.any(String), updatedAt: expect.any(String) }) }) + it('should not be able to get the API key if not exists', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api-key/ks_1234567890`, + headers: { + 'x-e2e-user-email': user.email + } + }) + + expect(response.statusCode).toBe(404) + }) + it('should be able to get all the api keys of the user', async () => { const response = await app.inject({ method: 'GET', @@ -137,16 +199,18 @@ describe('Api Key Role Controller Tests', () => { }) expect(response.statusCode).toBe(200) - expect(response.json()).toEqual([ - { - id: apiKey.id, - name: 'Updated Test Key', - authorities: ['READ_API_KEY'], - expiresAt: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String) - } - ]) + expect(response.json()).toEqual( + expect.arrayContaining([ + { + id: apiKey.id, + name: 'Updated Test Key', + authorities: ['READ_API_KEY', 'CREATE_ENVIRONMENT'], + expiresAt: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String) + } + ]) + ) }) it('should be able to get all api keys using the API key', async () => { @@ -159,16 +223,18 @@ describe('Api Key Role Controller Tests', () => { }) expect(response.statusCode).toBe(200) - expect(response.json()).toEqual([ - { - id: apiKey.id, - name: 'Updated Test Key', - authorities: ['READ_API_KEY'], - expiresAt: expect.any(String), - createdAt: expect.any(String), - updatedAt: expect.any(String) - } - ]) + expect(response.json()).toEqual( + expect.arrayContaining([ + { + id: apiKey.id, + name: 'Updated Test Key', + authorities: ['READ_API_KEY', 'CREATE_ENVIRONMENT'], + expiresAt: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String) + } + ]) + ) }) it('should not be able to create api key with invalid authorities of API key', async () => { diff --git a/apps/api/src/api-key/service/api-key.service.ts b/apps/api/src/api-key/service/api-key.service.ts index a2fd5866..99ed9371 100644 --- a/apps/api/src/api-key/service/api-key.service.ts +++ b/apps/api/src/api-key/service/api-key.service.ts @@ -152,9 +152,7 @@ export class ApiKeyService { }) if (!apiKey) { - throw new NotFoundException( - `User ${user.id} is not authorized to access API key ${apiKeyId}` - ) + throw new NotFoundException(`API key with id ${apiKeyId} not found`) } return apiKey diff --git a/apps/api/src/auth/auth.e2e.spec.ts b/apps/api/src/auth/auth.e2e.spec.ts new file mode 100644 index 00000000..473289fb --- /dev/null +++ b/apps/api/src/auth/auth.e2e.spec.ts @@ -0,0 +1,125 @@ +import { + FastifyAdapter, + NestFastifyApplication +} from '@nestjs/platform-fastify' +import { PrismaService } from '../prisma/prisma.service' +import { Test } from '@nestjs/testing' +import { AuthModule } from './auth.module' +import { MAIL_SERVICE } from '../mail/services/interface.service' +import { MockMailService } from '../mail/services/mock.service' +import { AppModule } from '../app/app.module' +import { Otp } from '@prisma/client' + +describe('Auth Controller Tests', () => { + let app: NestFastifyApplication + let prisma: PrismaService + + let otp: Otp + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule, AuthModule] + }) + .overrideProvider(MAIL_SERVICE) + .useClass(MockMailService) + .compile() + + app = moduleRef.createNestApplication( + new FastifyAdapter() + ) + prisma = moduleRef.get(PrismaService) + + await app.init() + await app.getHttpAdapter().getInstance().ready() + }) + + it('should be defined', async () => { + expect(app).toBeDefined() + expect(prisma).toBeDefined() + }) + + it('should not send otp if email is blank', async () => { + const response = await app.inject({ + method: 'POST', + url: '/auth/send-otp/' + }) + + expect(response.statusCode).toBe(400) + expect(response.json().message).toBe('Please enter a valid email address') + }) + + it('should not send otp if email is invalid', async () => { + const response = await app.inject({ + method: 'POST', + url: '/auth/send-otp/abcdef' + }) + + expect(response.statusCode).toBe(400) + expect(response.json().message).toBe('Please enter a valid email address') + }) + + it('should send otp if email is valid', async () => { + const response = await app.inject({ + method: 'POST', + url: '/auth/send-otp/johndoe@keyshade.xyz' + }) + + expect(response.statusCode).toBe(201) + }) + + it('should have generated an otp', async () => { + otp = await prisma.otp.findFirst({ + where: { + user: { + email: 'johndoe@keyshade.xyz' + } + } + }) + + expect(otp).toBeDefined() + expect(otp.code).toBeDefined() + expect(otp.expiresAt).toBeDefined() + expect(otp.code.length).toBe(6) + }) + + it('should upsert otp if regenerated', async () => { + await app.inject({ + method: 'POST', + url: '/auth/send-otp/johndoe@keyshade.xyz' + }) + + const regenerated = await prisma.otp.findFirst({ + where: { + user: { + email: 'johndoe@keyshade.xyz' + } + } + }) + + expect(regenerated).toBeDefined() + expect(regenerated.code).toBeDefined() + expect(regenerated.expiresAt).toBeDefined() + expect(regenerated.code.length).toBe(6) + expect(regenerated.code).not.toBe(otp.code) + + otp = regenerated + }) + + it('should not be able to validate otp with invalid email', async () => { + const response = await app.inject({ + method: 'POST', + url: '/auth/validate-otp?email=abcdef&otp=123456' + }) + + expect(response.statusCode).toBe(404) + }) + + it('should not be able to validate otp with invalid otp', async () => { + const response = await app.inject({ + method: 'POST', + url: '/auth/validate-otp?email=johndoe@keyshade.xyz&otp=123456' + }) + + expect(response.statusCode).toBe(401) + }) +}) diff --git a/apps/api/src/auth/controller/auth.controller.ts b/apps/api/src/auth/controller/auth.controller.ts index eddf35cd..fe71f6ef 100644 --- a/apps/api/src/auth/controller/auth.controller.ts +++ b/apps/api/src/auth/controller/auth.controller.ts @@ -52,6 +52,7 @@ export class AuthController { await this.authService.sendOtp(email) } + /* istanbul ignore next */ @Public() @Post('validate-otp') @ApiOperation({ @@ -88,6 +89,7 @@ export class AuthController { return await this.authService.validateOtp(email, otp) } + /* istanbul ignore next */ @Public() @Get('github') @ApiOperation({ @@ -106,6 +108,7 @@ export class AuthController { res.status(302).redirect('/api/auth/github/callback') } + /* istanbul ignore next */ @Public() @Get('github/callback') @UseGuards(AuthGuard('github')) diff --git a/apps/api/src/auth/service/auth.service.ts b/apps/api/src/auth/service/auth.service.ts index ee60c473..a90a9dff 100644 --- a/apps/api/src/auth/service/auth.service.ts +++ b/apps/api/src/auth/service/auth.service.ts @@ -1,10 +1,11 @@ import { - HttpException, - HttpStatus, + BadRequestException, Inject, Injectable, Logger, - LoggerService + LoggerService, + NotFoundException, + UnauthorizedException } from '@nestjs/common' import { randomUUID } from 'crypto' import { JwtService } from '@nestjs/jwt' @@ -33,10 +34,7 @@ export class AuthService { async sendOtp(email: string): Promise { if (!email || !email.includes('@')) { this.logger.error(`Invalid email address: ${email}`) - throw new HttpException( - 'Please enter a valid email address', - HttpStatus.BAD_REQUEST - ) + throw new BadRequestException('Please enter a valid email address') } const user = await this.createUserIfNotExists(email) @@ -64,6 +62,7 @@ export class AuthService { this.logger.log(`Login code sent to ${email}`) } + /* istanbul ignore next */ async validateOtp( email: string, otp: string @@ -71,7 +70,7 @@ export class AuthService { const user = await this.findUserByEmail(email) if (!user) { this.logger.error(`User not found: ${email}`) - throw new HttpException('User not found', HttpStatus.NOT_FOUND) + throw new NotFoundException('User not found') } const isOtpValid = @@ -89,7 +88,7 @@ export class AuthService { if (!isOtpValid) { this.logger.error(`Invalid login code for ${email}: ${otp}`) - throw new HttpException('Invalid login code', HttpStatus.UNAUTHORIZED) + throw new UnauthorizedException('Invalid login code') } await this.prisma.otp.delete({ @@ -111,6 +110,7 @@ export class AuthService { } } + /* istanbul ignore next */ async handleGithubOAuth( email: string, name: string, @@ -131,6 +131,7 @@ export class AuthService { } } + /* istanbul ignore next */ @Cron(CronExpression.EVERY_HOUR) async cleanUpExpiredOtps() { try {