From 9e0e660cf35ed858b13efc0afded5ddd6e994c4a Mon Sep 17 00:00:00 2001 From: J-hoplin1 Date: Sat, 6 Jan 2024 16:50:04 +0900 Subject: [PATCH] Test: Add auth, user module E2E test code and add coverage test --- .gitignore | 6 +- package.json | 6 +- src/admin-init.ts | 37 +++++ src/auth/auth.controller.e2e-spec.ts | 65 ++++++++ src/main.ts | 39 +---- src/user/user.controller.e2e-spec.ts | 223 +++++++++++++++++++++++++++ src/user/user.controller.ts | 2 +- test/jest-e2e.json | 22 +-- test/jest-unit.json | 6 +- test/test-utils.ts | 3 + 10 files changed, 353 insertions(+), 56 deletions(-) create mode 100644 src/admin-init.ts create mode 100644 src/auth/auth.controller.e2e-spec.ts create mode 100644 src/user/user.controller.e2e-spec.ts create mode 100644 test/test-utils.ts diff --git a/.gitignore b/.gitignore index 22f55ad..b550262 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,8 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# Unit, E2e coverage result +e2e-coverage/* +unit-coverage/* \ No newline at end of file diff --git a/package.json b/package.json index 42f27a3..a11fc06 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "ci:init": "dotenv -e .ci.env -- npx prisma generate", + "ci:init": "dotenv -e .ci.env -- npx prisma db push", "ci:unit": "dotenv -e .ci.env -- jest --setupFiles jest --config ./test/jest-unit.json --runInBand", "ci:e2e": "dotenv -e .ci.env -- jest --config ./test/jest-e2e.json --runInBand", "test:init": "dotenv -e .test.env -- npx prisma db push", "test:e2e": "dotenv -e .test.env -- jest --config ./test/jest-e2e.json --runInBand", - "test:unit": "dotenv -e .test.env -- jest --setupFiles jest --config ./test/jest-unit.json --runInBand" + "test:e2e:cov": "dotenv -e .test.env -- jest --config ./test/jest-e2e.json --runInBand --coverage", + "test:unit": "dotenv -e .test.env -- jest --config ./test/jest-unit.json --runInBand", + "test:unit:cov": "dotenv -e .test.env -- jest --config ./test/jest-unit.json --runInBand --coverage" }, "dependencies": { "@aws-sdk/client-s3": "^3.465.0", diff --git a/src/admin-init.ts b/src/admin-init.ts new file mode 100644 index 0000000..1cf5b4a --- /dev/null +++ b/src/admin-init.ts @@ -0,0 +1,37 @@ +import { INestApplication, LoggerService } from '@nestjs/common'; +import { PrismaService } from './prisma/prisma.service'; +import { SystemLoggerService } from './system-logger/system-logger.service'; +import * as bcrypt from 'bcryptjs'; + +export async function InitializeAdmin(app: INestApplication) { + // Generate default admin account + const prisma = app.get(PrismaService); + const logger = app.get(SystemLoggerService); + + if (!(process.env.ADMIN_EMAIL && process.env.ADMIN_PW)) { + logger.error( + 'Admin Email and Admin Pw not found. Please configure `.env` file and reboot', + ); + throw new Error('Fail to initialize server'); + } else { + const findAdmin = await prisma.user.findUnique({ + where: { + email: process.env.ADMIN_EMAIL, + }, + }); + if (!findAdmin) { + // Initialize root admin + await prisma.user.create({ + data: { + nickname: 'admin', + password: bcrypt.hashSync(process.env.ADMIN_PW, 10), + email: process.env.ADMIN_EMAIL, + type: 'Admin', + }, + }); + logger.log('Admin initialized'); + } else { + logger.log('Admin already initialized'); + } + } +} diff --git a/src/auth/auth.controller.e2e-spec.ts b/src/auth/auth.controller.e2e-spec.ts new file mode 100644 index 0000000..4f10c1c --- /dev/null +++ b/src/auth/auth.controller.e2e-spec.ts @@ -0,0 +1,65 @@ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from 'app/app.module'; +import * as request from 'supertest'; +import { userSignupGen } from 'test/mock-generator'; + +describe('/auth Auth Controller', () => { + let app: INestApplication; + + // Mock user + const user1 = userSignupGen(); + + beforeAll(async () => { + const testModule: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = testModule.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + // Signup test + describe('/singup POST', () => { + it('should sign up', () => { + return request(app.getHttpServer()) + .post('/auth/signup') + .send(user1) + .expect(200); + }); + + it('should throw if credential already taken', () => { + return request(app.getHttpServer()) + .post('/auth/signup') + .send(user1) + .expect(400); + }); + }); + + //Signin test + describe('/signin POST', () => { + it('should singin', () => { + return request(app.getHttpServer()) + .post('/auth/signin') + .send({ + password: user1.password, + email: user1.email, + }) + .expect(200); + }); + + it('should throw if credential is not correct', () => { + return request(app.getHttpServer()) + .post('/auth/signin') + .send({ + email: user1.email, + password: 'wrong-password', + }) + .expect(401); + }); + }); +}); diff --git a/src/main.ts b/src/main.ts index bc2df76..8542e24 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,8 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { Logger, LoggerService, ValidationPipe } from '@nestjs/common'; -import { PrismaService } from './prisma/prisma.service'; -import * as bcrypt from 'bcryptjs'; -import { SystemLoggerService } from './system-logger/system-logger.service'; +import { InitializeAdmin } from './admin-init'; +import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -24,36 +22,7 @@ async function bootstrap() { .addBearerAuth() .build(); - // Generate default admin account - const prisma = app.get(PrismaService); - const logger = app.get(SystemLoggerService); - - if (!(process.env.ADMIN_EMAIL && process.env.ADMIN_PW)) { - logger.error( - 'Admin Email and Admin Pw not found. Please configure `.env` file and reboot', - ); - throw new Error('Fail to initialize server'); - } else { - const findAdmin = await prisma.user.findUnique({ - where: { - email: process.env.ADMIN_EMAIL, - }, - }); - if (!findAdmin) { - // Initialize root admin - await prisma.user.create({ - data: { - nickname: 'admin', - password: bcrypt.hashSync(process.env.ADMIN_PW, 10), - email: process.env.ADMIN_EMAIL, - type: 'Admin', - }, - }); - logger.log('Admin initialized'); - } else { - logger.log('Admin already initialized'); - } - } + await InitializeAdmin(app); // Initialize Swagger Document const document = SwaggerModule.createDocument(app, config); diff --git a/src/user/user.controller.e2e-spec.ts b/src/user/user.controller.e2e-spec.ts new file mode 100644 index 0000000..2eb8164 --- /dev/null +++ b/src/user/user.controller.e2e-spec.ts @@ -0,0 +1,223 @@ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InitializeAdmin } from 'app/admin-init'; +import { AppModule } from 'app/app.module'; +import { userSignupGen } from 'test/mock-generator'; +import * as request from 'supertest'; +import { BearerTokenHeader } from 'test/test-utils'; +import { User } from '@prisma/client'; + +describe('/user User Controller', () => { + let app: INestApplication; + + // MockUser + const user1 = userSignupGen(); + let user1Token: string; + let user1Obj: User; + let adminToken: string; + let adminObj: User; + + beforeAll(async () => { + const testModule: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + // Initialize nest application + app = testModule.createNestApplication(); + await InitializeAdmin(app); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + // Pre-test + describe('/auth/signup POST', () => { + it('should create user1', async () => { + const signup = await request(app.getHttpServer()) + .post('/auth/signup') + .send(user1); + expect(signup.statusCode).toBe(200); + expect(signup.body).not.toBeUndefined(); + user1Token = signup.body['accessToken']; + }); + }); + + describe('/auth/signin POST', () => { + it('should signup admin', async () => { + const signin = await request(app.getHttpServer()) + .post('/auth/signin') + .send({ + password: process.env.ADMIN_PW, + email: process.env.ADMIN_EMAIL, + }); + + expect(signin.statusCode).toBe(200); + expect(signin.body).not.toBeUndefined(); + adminToken = signin.body['accessToken']; + }); + }); + + // Test + describe('/profile GET', () => { + it('should throw if anonymous user', () => { + return request(app.getHttpServer()).get('/user/profile').expect(401); + }); + it('should get user1 profile', async () => { + const user = await request(app.getHttpServer()) + .get('/user/profile') + .set('Authorization', BearerTokenHeader(user1Token)); + expect(user.statusCode).toBe(200); + user1Obj = user.body; + }); + it('should get admin profile', async () => { + const user = await request(app.getHttpServer()) + .get('/user/profile') + .set('Authorization', BearerTokenHeader(adminToken)); + expect(user.statusCode).toBe(200); + adminObj = user.body; + }); + }); + + describe('/profile/:uid GET', () => { + it('shold throw if anonymous user', () => { + return request(app.getHttpServer()).get(`/user/profile/id`).expect(401); + }); + + it('should throw if user not exist', () => { + return request(app.getHttpServer()) + .get(`/user/profile/0c20078f-036e-43b1-9366-7f24a58f70bc`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(400); + }); + + it('should get user profile', () => { + return request(app.getHttpServer()) + .get(`/user/profile/${adminObj.id}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/credential POST', () => { + it('should throw if not supported type of credential', () => { + return request(app.getHttpServer()) + .post(`/user/credential`) + .send({ + type: 'Invalid Type', + value: 'string', + }) + .expect(400); + }); + + it('should be proper credential', () => { + return request(app.getHttpServer()).post(`/user/credential`).send({ + type: 'EMAIL', + value: 'test__100__@example.com', + }); + }); + }); + + describe('/profile PATCH', () => { + it('should throw if anonymous user', () => { + return request(app.getHttpServer()).patch(`/user/profile`).expect(401); + }); + + it('should throw if password unmatched', () => { + return request(app.getHttpServer()) + .patch(`/user/profile`) + .set(`Authorization`, BearerTokenHeader(user1Token)) + .send({ + password: 'string', + }) + .expect(401); + }); + + it('should update user', () => { + return request(app.getHttpServer()) + .patch(`/user/profile`) + .set(`Authorization`, BearerTokenHeader(user1Token)) + .send({ + password: user1.password, + }) + .expect(200); + }); + }); + + describe('/password PATCH', () => { + it('should throw if anonymous user', () => { + return request(app.getHttpServer()).patch('/user/password').expect(401); + }); + it('should throw if previous password is incorrect', () => { + return request(app.getHttpServer()) + .patch('/user/password') + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + password: 'string', + newPassword: 'string', + }) + .expect(401); + }); + it('should change password', () => { + return request(app.getHttpServer()) + .patch('/user/password') + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + password: user1.password, + newPassword: 'string', + }) + .expect(200); + }); + + it('should signin', () => { + return request(app.getHttpServer()).post('/auth/signin').send({ + password: 'string', + email: user1.email, + }); + }); + }); + + describe('/role PATCH', () => { + it('should throw if anonymous user', () => { + return request(app.getHttpServer()).patch('/user/role').expect(401); + }); + + it('should throw if user is not admin authenticated', () => { + return request(app.getHttpServer()) + .patch('/user/role') + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(403); + }); + + it('should throw if user does not exist', () => { + return request(app.getHttpServer()) + .patch('/user/role') + .set('Authorization', BearerTokenHeader(adminToken)) + .send({ + role: 'Admin', + targetId: '0c20078f-036e-43b1-9366-7f24a58f70bc', + }) + .expect(400); + }); + + it('should change user role with admin account', () => { + return request(app.getHttpServer()) + .patch('/user/role') + .set('Authorization', BearerTokenHeader(adminToken)) + .send({ + role: 'Admin', + targetId: user1Obj.id, + }) + .expect(200); + }); + + it('should check user role changed', async () => { + const response = await request(app.getHttpServer()) + .get('/user/profile') + .set('Authorization', BearerTokenHeader(user1Token)); + expect(response.statusCode).toBe(200); + expect(response.body.type).toBe('Admin'); + }); + }); +}); diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index c49a596..078da7a 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -59,7 +59,7 @@ export class UserController { return this.userService.updatePassword(user, dto); } - @Patch('admin/role') + @Patch(['admin/role', 'role']) @Role(['Admin']) @UseGuards(RoleGuard) @UseGuards(LocalGuard) diff --git a/test/jest-e2e.json b/test/jest-e2e.json index a2c08d9..f29a1e4 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -1,29 +1,21 @@ { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": ".", + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": "../src", "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", + "testRegex": ".*\\.e2e-spec\\.ts", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "coverageDirectory": "../e2e-coverage", - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], + "collectCoverageFrom": ["**/*.(t|j)s"], "moduleNameMapper": { - "app": "/../src/$1", "app/(.*)": "/../src/$1", - "test": "/../test/$1", "test/(.*)": "/../test/$1", "domains": "/../domain/$1", "domains/(.*)": "/../domain/$1", "s3/aws-s3": "/../libs/aws-s3/src", "s3/aws-s3/(.*)": "/../libs/aws-s3/src/$1", - "aws-sqs/aws-sqs/(.*)": "/../libs/aws-sqs/src/$1", - "aws-sqs/aws-sqs": "/../libs/aws-sqs/src" + "judge/judge0": "/../libs/judge0/src", + "judge/judge0/(.*)": "/../libs/judge0/src/$1" } -} \ No newline at end of file +} diff --git a/test/jest-unit.json b/test/jest-unit.json index 4569eab..88f2e9a 100644 --- a/test/jest-unit.json +++ b/test/jest-unit.json @@ -6,7 +6,7 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "coverageDirectory": "../e2e-coverage", + "coverageDirectory": "../unit-coverage", "collectCoverageFrom": ["**/*.(t|j)s"], "moduleNameMapper": { "app/(.*)": "/../src/$1", @@ -14,6 +14,8 @@ "domains": "/../domain/$1", "domains/(.*)": "/../domain/$1", "s3/aws-s3": "/../libs/aws-s3/src", - "s3/aws-s3/(.*)": "/../libs/aws-s3/src/$1" + "s3/aws-s3/(.*)": "/../libs/aws-s3/src/$1", + "judge/judge0": "/../libs/judge0/src", + "judge/judge0/(.*)": "/../libs/judge0/src/$1" } } diff --git a/test/test-utils.ts b/test/test-utils.ts new file mode 100644 index 0000000..772c15c --- /dev/null +++ b/test/test-utils.ts @@ -0,0 +1,3 @@ +export function BearerTokenHeader(token: string) { + return `Bearer ${token}`; +}