diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 1af37fa..81f446f 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,4 +1,4 @@ -import { type HttpException, ConflictException } from "@nestjs/common"; +import { ConflictException } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { JwtModule } from "@nestjs/jwt"; import { PassportModule } from "@nestjs/passport"; @@ -57,55 +57,55 @@ describe("AuthController", () => { }); describe("create", () => { + const createUserDto: CreateUserDto = { + account: "account", + email: "jhon@gmail.com", + name: "displayname", + password: "Password@123", + }; + let mockedAuthService: jest.SpyInstance; + + beforeEach(async () => { + mockedAuthService = jest.spyOn(authService, "register"); + }); + it("應該會創建一個使用者,並返回 201 狀態碼", async () => { - const createUserDto: CreateUserDto = { - account: "account", - email: "jhon@gmail.com", - name: "displayname", - password: "Password@123", - }; const expectedResponse: CreateUserResponse = { message: "創建成功", statusCode: 201, }; - jest.spyOn(authService, "register").mockResolvedValue(expectedResponse); + mockedAuthService.mockResolvedValue(expectedResponse); + const result = await authController.register(createUserDto); expect(result).toEqual(expectedResponse); }); it("應該會發生資料使用者重覆,並返回 409 狀態碼", async () => { - const createUserDto1: CreateUserDto = { - account: "account1", - email: "jhon1@gmail.com", - name: "displayname", - password: "Password@123", - }; - - await authService.register(createUserDto1); - await authService - .register(createUserDto1) - .catch((error: HttpException) => { - expect(error).toBeInstanceOf(ConflictException); - expect(error.getResponse()).toEqual({ - error: "Conflict", - message: ["email 已被註冊。", "account 已被註冊。"], - statusCode: 409, - }); + await authService.register(createUserDto); + await authService.register(createUserDto).catch(error => { + expect(error).toBeInstanceOf(ConflictException); + expect((error as ConflictException).getResponse()).toEqual({ + error: "Conflict", + message: ["email 已被註冊。", "account 已被註冊。"], + statusCode: 409, }); + }); }); + }); - it("should return access, refresh token and 201 http code when account information is correct.", async () => { - const request: Request = { - user: { - id: 1, - } as JwtUser, - } as unknown as Request; - - const fakeAccessToken = "mocked_access_token"; - const fakeRefreshToken = "mocked_refresh_token"; - + describe("login and refresh", () => { + const request: Request = { + user: { + id: 1, + } as JwtUser, + } as unknown as Request; + const fakeAccessToken = "mocked_access_token"; + const fakeRefreshToken = "mocked_refresh_token"; + let mockedAuthService: jest.SpyInstance; + + beforeEach(async () => { jest .spyOn(authService, "generateAccessToken") .mockReturnValue(Promise.resolve(fakeAccessToken)); @@ -114,12 +114,13 @@ describe("AuthController", () => { .spyOn(authService, "generateRefreshToken") .mockReturnValue(Promise.resolve(fakeRefreshToken)); - const mockAuthService = jest.spyOn(authService, "sign"); + mockedAuthService = jest.spyOn(authService, "sign"); + }); + it("should return access, refresh token and 201 http code when account information is correct.", async () => { const result = await authController.login(request); - expect(mockAuthService).toHaveBeenCalledWith(request.user); - + expect(mockedAuthService).toHaveBeenCalledWith(request.user); const expectedResponse: GenerateTokenResponse = { accessToken: fakeAccessToken, refreshToken: fakeRefreshToken, @@ -130,29 +131,9 @@ describe("AuthController", () => { }); it("should return access, refresh token and 201 http code when refresh token is correct.", async () => { - const request: Request = { - user: { - id: 1, - } as JwtUser, - } as unknown as Request; - - const fakeAccessToken = "mocked_access_token"; - const fakeRefreshToken = "mocked_refresh_token"; - - jest - .spyOn(authService, "generateAccessToken") - .mockReturnValue(Promise.resolve(fakeAccessToken)); - - jest - .spyOn(authService, "generateRefreshToken") - .mockReturnValue(Promise.resolve(fakeRefreshToken)); - - const mockAuthService = jest.spyOn(authService, "sign"); - const result = await authController.refresh(request); - expect(mockAuthService).toHaveBeenCalledWith(request.user); - + expect(mockedAuthService).toHaveBeenCalledWith(request.user); const expectedResponse: GenerateTokenResponse = { accessToken: fakeAccessToken, refreshToken: fakeRefreshToken, diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index e38bc91..48318dc 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -1,8 +1,4 @@ -import { - type HttpException, - ConflictException, - HttpStatus, -} from "@nestjs/common"; +import { ConflictException, HttpStatus } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { JwtModule } from "@nestjs/jwt"; import { PassportModule } from "@nestjs/passport"; @@ -57,13 +53,27 @@ describe("AuthService", () => { }); describe("createUser - Data", () => { + const rawUser: CreateUserDto = { + account: "account1", + email: "jhon@gmail.com", + name: "displayname", + password: "Password@123", + }; + const rawUserConflictEmail: CreateUserDto = { + account: "account2", + email: "jhon@gmail.com", + name: "displayname", + password: "Password@123", + }; + + const rawUserConflictAccount: CreateUserDto = { + account: "account", + email: "jhon2@gmail.com", + name: "displayname", + password: "Password@123", + }; + it("應該會創建 一個使用者", async () => { - const rawUser: CreateUserDto = { - account: "account1", - email: "jhon@gmail.com", - name: "displayname", - password: "Password@123", - }; const user = await authService.register(rawUser); expect(user).toBeDefined(); @@ -72,46 +82,25 @@ describe("AuthService", () => { }); it("應該會發生 email、account 已被註冊衝突", async () => { - const createUserDto1: CreateUserDto = { - account: "account", - email: "jhon@gmail.com", - name: "displayname", - password: "Password@123", - }; - - await authService.register(createUserDto1); - await authService - .register(createUserDto1) - .catch((error: HttpException) => { - expect(error).toBeInstanceOf(ConflictException); - expect(error.getResponse()).toEqual({ - error: "Conflict", - message: ["email 已被註冊。", "account 已被註冊。"], - statusCode: 409, - }); + await authService.register(rawUser); + await authService.register(rawUser).catch(error => { + expect(error).toBeInstanceOf(ConflictException); + expect((error as ConflictException).getResponse()).toEqual({ + error: "Conflict", + message: ["email 已被註冊。", "account 已被註冊。"], + statusCode: 409, }); + }); }); it("應該會發生 email 已被註冊衝突", async () => { - const rawUser1: CreateUserDto = { - account: "account1", - email: "jhon@gmail.com", - name: "displayname", - password: "Password@123", - }; - const rawUser2: CreateUserDto = { - account: "account2", - email: "jhon@gmail.com", - name: "displayname", - password: "Password@123", - }; - const errors = await validate(rawUser1); + const errors = await validate(rawUser); expect(errors.length).toBe(0); - await authService.register(rawUser1); - await authService.register(rawUser2).catch((error: HttpException) => { + await authService.register(rawUser); + await authService.register(rawUserConflictEmail).catch(error => { expect(error).toBeInstanceOf(ConflictException); - expect(error.getResponse()).toEqual({ + expect((error as ConflictException).getResponse()).toEqual({ error: "Conflict", message: ["email 已被註冊。"], statusCode: 409, @@ -120,25 +109,13 @@ describe("AuthService", () => { }); it("應該會發生 account 已被註冊衝突", async () => { - const rawUser1: CreateUserDto = { - account: "account", - email: "jhon@gmail.com", - name: "displayname", - password: "Password@123", - }; - const rawUser2: CreateUserDto = { - account: "account", - email: "jhon2@gmail.com", - name: "displayname", - password: "Password@123", - }; - const errors = await validate(rawUser1); + const errors = await validate(rawUser); expect(errors.length).toBe(0); - await authService.register(rawUser1); - await authService.register(rawUser2).catch((error: HttpException) => { + await authService.register(rawUser); + await authService.register(rawUserConflictAccount).catch(error => { expect(error).toBeInstanceOf(ConflictException); - expect(error.getResponse()).toEqual({ + expect((error as ConflictException).getResponse()).toEqual({ error: "Conflict", message: ["account 已被註冊。"], statusCode: 409, @@ -148,15 +125,20 @@ describe("AuthService", () => { }); describe("user local login", () => { - it("should be login successfully.", async () => { - const mockUser = { - email: "test@example.com", - id: 1, - }; - const fakeAccessToken = "mocked_access_token"; - const fakeRefreshToken = "mocked_refresh_token"; - const expectedStatusCode = HttpStatus.CREATED; - + const fakeAccessToken = "mocked_access_token"; + const fakeRefreshToken = "mocked_refresh_token"; + const mockUser: Partial = { + account: "test", + email: "test@example.com", + id: 1, + name: "test", + password: "$2b$05$zc4SaUDmE68OgrabgSoLX.CDMHZ8SD/aDeuJc7rxKmtqjP5WpH.Me", + }; + const mockJwtUser: JwtUser = { + id: 1, + }; + + beforeEach(async () => { jest .spyOn(authService, "generateAccessToken") .mockReturnValue(Promise.resolve(fakeAccessToken)); @@ -164,8 +146,12 @@ describe("AuthService", () => { jest .spyOn(authService, "generateRefreshToken") .mockReturnValue(Promise.resolve(fakeRefreshToken)); + }); + + it("should be login successfully.", async () => { + const expectedStatusCode = HttpStatus.CREATED; - const result = await authService.sign(mockUser); + const result = await authService.sign(mockJwtUser); expect(result).toEqual({ accessToken: fakeAccessToken, @@ -177,14 +163,6 @@ describe("AuthService", () => { it("should be validate successfully.", async () => { const mockAccount = "test"; const mockPassword = "Password@123"; - const mockUser: Partial = { - account: "test", - email: "test@example.com", - id: 1, - name: "test", - password: - "$2b$05$zc4SaUDmE68OgrabgSoLX.CDMHZ8SD/aDeuJc7rxKmtqjP5WpH.Me", - }; jest .spyOn(userService, "findOne") @@ -213,14 +191,6 @@ describe("AuthService", () => { it("when the account exist but password not correct should be validate failure.", async () => { const mockAccount = "test"; const mockPassword = "Password@1234"; - const mockUser: Partial = { - account: "test", - email: "test@example.com", - id: 1, - name: "test", - password: - "$2b$05$zc4SaUDmE68OgrabgSoLX.CDMHZ8SD/aDeuJc7rxKmtqjP5WpH.Me", - }; jest .spyOn(userService, "findOne") @@ -234,21 +204,18 @@ describe("AuthService", () => { }); describe("generate Token", () => { + const userId = 1; + const payload: JwtUser = { + id: userId, + }; + it("should generate access token", async () => { - const userId = 1; - const payload: JwtUser = { - id: userId, - }; const result = await authService.generateAccessToken(payload); expect(result).toBeDefined(); }); it("should generate refresh token", async () => { - const userId = 1; - const payload: JwtUser = { - id: userId, - }; const result = await authService.generateRefreshToken(payload); expect(result).toBeDefined(); diff --git a/src/auth/jwt/jwt-access.guard.spec.ts b/src/auth/jwt/jwt-access.guard.spec.ts index 95b76e1..e38e3e5 100644 --- a/src/auth/jwt/jwt-access.guard.spec.ts +++ b/src/auth/jwt/jwt-access.guard.spec.ts @@ -12,6 +12,8 @@ describe("JwtAccessGuard", () => { let jwtAccessGuard: JwtAccessGuard; let jwtService: JwtService; let configService: ConfigService; + let secret: string | undefined; + let token: string; beforeEach(async () => { jest.useFakeTimers(); @@ -29,20 +31,23 @@ describe("JwtAccessGuard", () => { jwtAccessGuard = moduleRef.get(JwtAccessGuard); jwtService = moduleRef.get(JwtService); configService = moduleRef.get(ConfigService); + secret = configService.get("jwtSecret.access"); + token = jwtService.sign( + { + id: 1, + }, + { + expiresIn: "1h", + secret, + }, + ); }); it("should be defined", () => { expect(jwtAccessGuard).toBeDefined(); }); - it("should return true for a valid JWT", async () => { - const payload = { id: 1 }; - const secret: string | undefined = configService.get("jwtSecret.access"); - const token = jwtService.sign(payload, { - expiresIn: "1h", - secret, - }); - + describe("valid JWT", () => { const response = {}; const context: ExecutionContext = { getRequest: () => ({ @@ -54,37 +59,24 @@ describe("JwtAccessGuard", () => { switchToHttp: () => context, } as unknown as ExecutionContext; - const canActivate = await jwtAccessGuard.canActivate(context); + it("should return true for a valid JWT", async () => { + const canActivate = await jwtAccessGuard.canActivate(context); - expect(canActivate).toBe(true); - }); - - it("should throw an error for an expired JWT", async () => { - const payload = { id: 1 }; - const secret: string | undefined = configService.get("jwtSecret.access"); - const token = jwtService.sign(payload, { expiresIn: "1h", secret }); + expect(canActivate).toBe(true); + }); - jest.advanceTimersByTime(60 * 60 * 1000); + it("should throw an error for an expired JWT", async () => { + jest.advanceTimersByTime(60 * 60 * 1000); - const response = {}; - const context: ExecutionContext = { - getRequest: () => ({ - headers: { - authorization: `bearer ${token}`, - }, - }), - getResponse: () => response, - switchToHttp: () => context, - } as unknown as ExecutionContext; - - try { - await jwtAccessGuard.canActivate(context); - } catch (error) { - expect(error).toBeInstanceOf(UnauthorizedException); - } - }); + try { + await jwtAccessGuard.canActivate(context); + } catch (error) { + expect(error).toBeInstanceOf(UnauthorizedException); + } + }); - afterEach(async () => { - jest.clearAllTimers(); + afterEach(async () => { + jest.clearAllTimers(); + }); }); }); diff --git a/src/auth/jwt/jwt-refresh.guard.spec.ts b/src/auth/jwt/jwt-refresh.guard.spec.ts index c024506..ca22aac 100644 --- a/src/auth/jwt/jwt-refresh.guard.spec.ts +++ b/src/auth/jwt/jwt-refresh.guard.spec.ts @@ -12,6 +12,8 @@ describe("JwtRefreshGuard", () => { let jwtRefreshGuard: JwtRefreshGuard; let jwtService: JwtService; let configService: ConfigService; + let secret: string | undefined; + let token: string; beforeEach(async () => { jest.useFakeTimers(); @@ -29,39 +31,8 @@ describe("JwtRefreshGuard", () => { jwtRefreshGuard = moduleRef.get(JwtRefreshGuard); jwtService = moduleRef.get(JwtService); configService = moduleRef.get(ConfigService); - }); - - it("should be defined", () => { - expect(jwtRefreshGuard).toBeDefined(); - }); - - it("should return true for a valid JWT", async () => { - const payload = { id: 1 }; - const secret: string | undefined = configService.get("jwtSecret.refresh"); - const token = jwtService.sign(payload, { - expiresIn: "7d", - secret, - }); - - const response = {}; - const context: ExecutionContext = { - getRequest: () => ({ - headers: { - authorization: `bearer ${token}`, - }, - }), - getResponse: () => response, - switchToHttp: () => context, - } as unknown as ExecutionContext; - - const canActivate = await jwtRefreshGuard.canActivate(context); - - expect(canActivate).toBe(true); - }); - - it("should throw an error for an expired JWT", async () => { - const secret: string | undefined = configService.get("jwtSecret.refresh"); - const token = jwtService.sign( + secret = configService.get("jwtSecret.refresh"); + token = jwtService.sign( { id: 1, }, @@ -70,9 +41,13 @@ describe("JwtRefreshGuard", () => { secret, }, ); + }); - jest.advanceTimersByTime(8 * 24 * 60 * 60 * 1000); + it("should be defined", () => { + expect(jwtRefreshGuard).toBeDefined(); + }); + describe("valid JWT", () => { const response = {}; const context: ExecutionContext = { getRequest: () => ({ @@ -84,14 +59,24 @@ describe("JwtRefreshGuard", () => { switchToHttp: () => context, } as unknown as ExecutionContext; - try { - await jwtRefreshGuard.canActivate(context); - } catch (error) { - expect(error).toBeInstanceOf(UnauthorizedException); - } - }); + it("should return true for a valid JWT", async () => { + const canActivate = await jwtRefreshGuard.canActivate(context); + + expect(canActivate).toBe(true); + }); - afterEach(async () => { - jest.clearAllTimers(); + it("should throw an error for an expired JWT", async () => { + jest.advanceTimersByTime(8 * 24 * 60 * 60 * 1000); + + try { + await jwtRefreshGuard.canActivate(context); + } catch (error) { + expect(error).toBeInstanceOf(UnauthorizedException); + } + }); + + afterEach(async () => { + jest.clearAllTimers(); + }); }); }); diff --git a/src/auth/local/local-auth.guard.spec.ts b/src/auth/local/local-auth.guard.spec.ts index 56deeab..5e94280 100644 --- a/src/auth/local/local-auth.guard.spec.ts +++ b/src/auth/local/local-auth.guard.spec.ts @@ -1,11 +1,8 @@ -import { - type ExecutionContext, - ForbiddenException, - HttpException, -} from "@nestjs/common"; +import { type ExecutionContext, ForbiddenException } from "@nestjs/common"; import { BadRequestException } from "@nestjs/common/exceptions"; import { PassportModule } from "@nestjs/passport"; import { Test } from "@nestjs/testing"; +import { type LoginUserDto } from "src/user/dto/login-user.dto"; import { AuthService } from "../auth.service"; import { LocalStrategy } from "./local.strategy"; @@ -34,6 +31,19 @@ describe("LocalAuthGuard", () => { authService = moduleRef.get(AuthService); }); + function createMockExecutionContext(requestBody: LoginUserDto) { + const mockResponse = {}; + + return { + switchToHttp: () => ({ + getRequest: () => ({ + body: requestBody, + }), + getResponse: () => mockResponse, + }), + } as ExecutionContext; + } + it("should be defined", () => { expect(localAuthGuard).toBeDefined(); }); @@ -45,88 +55,56 @@ describe("LocalAuthGuard", () => { }; const mockRequest = { - body: { - account: "test", - password: "password", - }, - }; - - const mockResponse = {}; - - const mockExecutionContext = { - switchToHttp: () => ({ - getRequest: () => mockRequest, - getResponse: () => mockResponse, - }), - } as ExecutionContext; + account: "test", + password: "password", + } as LoginUserDto; jest .spyOn(authService, "validateUser") .mockImplementation(async () => mockUser); - const result = await localAuthGuard.canActivate(mockExecutionContext); + const result = await localAuthGuard.canActivate( + createMockExecutionContext(mockRequest), + ); expect(result).toBe(true); }); it("should return Forbidden and 403 http code when account information is wrong.", async () => { - const mockResponse = {}; - const mockExecutionContext = { - switchToHttp: () => ({ - getRequest: () => ({ - body: { - account: "test", - password: "password", - }, - }), - getResponse: () => mockResponse, - }), - } as ExecutionContext; + const mockRequest = { + account: "test", + password: "password", + } as LoginUserDto; jest .spyOn(authService, "validateUser") .mockImplementation(async () => null); try { - await localAuthGuard.canActivate(mockExecutionContext); + await localAuthGuard.canActivate(createMockExecutionContext(mockRequest)); } catch (error) { - if (error instanceof HttpException) { - expect(error).toBeInstanceOf(ForbiddenException); - expect(error.getResponse()).toEqual({ - message: ["Account or password is wrong."], - statusCode: 403, - }); - } + expect(error).toBeInstanceOf(ForbiddenException); + expect((error as BadRequestException).getResponse()).toEqual({ + message: ["Account or password is wrong."], + statusCode: 403, + }); } }); it("should return BadRequest and 400 http code when field format validation failed.", async () => { - const mockResponse = {}; - const mockExecutionContext = { - switchToHttp: () => ({ - getRequest: () => ({ - body: { - account: "test", - }, - }), - getResponse: () => mockResponse, - }), - } as ExecutionContext; + const mockRequest = { + account: "test", + } as LoginUserDto; try { - await localAuthGuard.canActivate(mockExecutionContext); + await localAuthGuard.canActivate(createMockExecutionContext(mockRequest)); } catch (error) { - if (error instanceof HttpException) { - expect(error).toBeInstanceOf(BadRequestException); - expect(error.getResponse()).toEqual({ - error: "Bad Request", - message: [ - "password 必須長度大於等於8個字。", - "password 為必填欄位。", - ], - statusCode: 400, - }); - } + expect(error).toBeInstanceOf(BadRequestException); + expect((error as BadRequestException).getResponse()).toEqual({ + error: "Bad Request", + message: ["password 必須長度大於等於8個字。", "password 為必填欄位。"], + statusCode: 400, + }); } }); }); diff --git a/src/auth/local/local.strategy.spec.ts b/src/auth/local/local.strategy.spec.ts index b54cd84..be5efe3 100644 --- a/src/auth/local/local.strategy.spec.ts +++ b/src/auth/local/local.strategy.spec.ts @@ -29,37 +29,36 @@ describe("LocalStrategy", () => { expect(localStrategy).toBeDefined(); }); - it("should return user payload if user is valid", async () => { + describe("validate user", () => { const mockAccount = "test"; const mockPassword = "password"; - const mockUser = { - id: 1, - }; + it("should return user payload if user is valid", async () => { + const mockUser = { + id: 1, + }; - jest - .spyOn(authService, "validateUser") - .mockImplementation(async () => mockUser); + jest + .spyOn(authService, "validateUser") + .mockImplementation(async () => mockUser); - const result = await localStrategy.validate(mockAccount, mockPassword); + const result = await localStrategy.validate(mockAccount, mockPassword); - expect(result).toEqual({ - id: mockUser.id, + expect(result).toEqual({ + id: mockUser.id, + }); }); - }); - it("should throw ForbiddenException if user is invalid", async () => { - const mockAccount = "test"; - const mockPassword = "password"; + it("should throw ForbiddenException if user is invalid", async () => { + jest + .spyOn(authService, "validateUser") + .mockImplementation(async () => null); - jest - .spyOn(authService, "validateUser") - .mockImplementation(async () => null); - - try { - await localStrategy.validate(mockAccount, mockPassword); - } catch (error) { - expect(error).toBeInstanceOf(ForbiddenException); - } + try { + await localStrategy.validate(mockAccount, mockPassword); + } catch (error) { + expect(error).toBeInstanceOf(ForbiddenException); + } + }); }); });