-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(identity): unit tests for services (#369)
* build(package/): installed jest-mock-axios * build(package): installed ts-jest and removed jest config in package.json * test(jest.config,.js): added jest config this resolves ts files so that jest can handle them and ts-jest can provide proper typings. the module name mapping is relocated here * test(authservice): add spec * refactor(mailclient): changed mailclient so that initialization fails if api key is empty * test(mailclient): add tests and axios mock * refactor(constants): add constants for tests * refactor(smsclient): change api key retrieval to be done at constructor to allow for tesing * test(smsclient): add test * test(tokenstore): added tests * build(paths): add mocks to path * chore(tokenstore): update naming for clarity * chore(totpgenerator): changed access values * test(totpgenerator): add tests * refactor(mailclient): changed mailclient initialization for better readability
- Loading branch information
Showing
17 changed files
with
1,315 additions
and
722 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// This is a manual module mock for the AWS client. | ||
// This is done to prevent our internal services from pinging the actual AWS ones | ||
// and to ensure our tests are 1. consistent 2. reliable. | ||
|
||
export const mockSend = jest.fn() | ||
|
||
export const secretsManagerClient = { | ||
send: mockSend, | ||
} | ||
|
||
export const SecretsManagerClient: jest.Mock< | ||
typeof secretsManagerClient | ||
> = jest.fn().mockImplementation(() => secretsManagerClient) | ||
|
||
export const GetSecretValueCommand = jest.fn((secret) => ({ SecretId: secret })) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import mockAxios from "jest-mock-axios" | ||
|
||
export default mockAxios |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// eslint-disable-next-line import/prefer-default-export | ||
export const totp = { | ||
clone: jest.fn().mockReturnThis(), | ||
generate: jest.fn(), | ||
verify: jest.fn(), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ | ||
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
moduleNameMapper: { | ||
"^@root/(.*)": "<rootDir>/$1", | ||
"^@classes/(.*)": "<rootDir>/classes/$1", | ||
"^@errors/(.*)": "<rootDir>/errors/$1", | ||
"^@logger/(.*)": "<rootDir>/logger/$1", | ||
"^@middleware/(.*)": "<rootDir>/middleware/$1", | ||
"^@routes/(.*)": "<rootDir>/routes/$1", | ||
"^@utils/(.*)": "<rootDir>/utils/$1", | ||
"^@loaders/(.*)": "<rootDir>/loaders/$1", | ||
"^@database/(.*)": "<rootDir>/database/$1", | ||
"^@services/(.*)": "<rootDir>/services/$1", | ||
"^@validators/(.*)": "<rootDir>/validators/$1", | ||
"^@fixtures/(.*)": "<rootDir>/fixtures/$1", | ||
"^@mocks/(.*)": "<rootDir>/__mocks__/$1", | ||
}, | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import mockAxios from "jest-mock-axios" | ||
|
||
import { BadRequestError } from "@root/errors/BadRequestError" | ||
|
||
import _AuthService from "../AuthService" | ||
|
||
import { | ||
mockAccessToken, | ||
mockHeaders, | ||
mockSiteName, | ||
mockUserId, | ||
} from "./constants" | ||
|
||
const AuthService = new _AuthService({ axiosClient: mockAxios }) | ||
const mockEndpoint = `/${mockSiteName}/collaborators/${mockUserId}` | ||
|
||
describe("Auth Service", () => { | ||
afterEach(() => mockAxios.reset()) | ||
|
||
it("should call axios successfully and return true when the call is successful", async () => { | ||
// Arrange | ||
const expected = true | ||
mockAxios.get.mockResolvedValueOnce({ | ||
response: { status: 200 }, | ||
}) | ||
|
||
// Act | ||
const actual = await AuthService.hasAccessToSite( | ||
mockSiteName, | ||
mockUserId, | ||
mockAccessToken | ||
) | ||
|
||
// Assert | ||
expect(actual).toBe(expected) | ||
expect(mockAxios.get).toHaveBeenCalledWith(mockEndpoint, mockHeaders) | ||
}) | ||
|
||
it("should call axios successfully and return false when the call fails with 403", async () => { | ||
// Arrange | ||
const expected = false | ||
mockAxios.get.mockRejectedValueOnce({ | ||
response: { status: 403 }, | ||
// NOTE: Axios uses this property to determine if it's an axios error | ||
isAxiosError: true, | ||
}) | ||
|
||
// Act | ||
const actual = await AuthService.hasAccessToSite( | ||
mockSiteName, | ||
mockUserId, | ||
mockAccessToken | ||
) | ||
|
||
// Assert | ||
expect(actual).toBe(expected) | ||
expect(mockAxios.get).toHaveBeenCalledWith(mockEndpoint, mockHeaders) | ||
}) | ||
|
||
it("should call axios successfully and return false when the call fails with 404", async () => { | ||
// Arrange | ||
const expected = false | ||
mockAxios.get.mockRejectedValueOnce({ | ||
response: { status: 404 }, | ||
// NOTE: Axios uses this property to determine if it's an axios error | ||
isAxiosError: true, | ||
}) | ||
|
||
// Act | ||
const actual = await AuthService.hasAccessToSite( | ||
mockSiteName, | ||
mockUserId, | ||
mockAccessToken | ||
) | ||
|
||
// Assert | ||
expect(actual).toBe(expected) | ||
expect(mockAxios.get).toHaveBeenCalledWith(mockEndpoint, mockHeaders) | ||
}) | ||
|
||
it("should call axios successfully and bubble the error when the status is not 403 or 404", async () => { | ||
// Arrange | ||
const expected = { | ||
response: { status: 400 }, | ||
} | ||
mockAxios.get.mockRejectedValueOnce(new BadRequestError(expected)) | ||
|
||
// Act | ||
const actual = AuthService.hasAccessToSite( | ||
mockSiteName, | ||
mockUserId, | ||
mockAccessToken | ||
) | ||
|
||
// Assert | ||
expect(actual).rejects.toThrowError(new BadRequestError(expected)) | ||
expect(mockAxios.get).toHaveBeenCalledWith(mockEndpoint, mockHeaders) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import mockAxios from "jest-mock-axios" | ||
|
||
import _MailClient from "../MailClient" | ||
|
||
import { mockRecipient, mockBody, mockHeaders } from "./constants" | ||
|
||
const mockEndpoint = "https://api.postman.gov.sg/v1/transactional/email/send" | ||
|
||
const MailClient = new _MailClient() | ||
|
||
const generateEmail = (recipient: string, body: string) => ({ | ||
subject: "One-Time Password (OTP) for IsomerCMS", | ||
from: "IsomerCMS <[email protected]>", | ||
body, | ||
recipient, | ||
}) | ||
|
||
describe("Mail Client", () => { | ||
const OLD_ENV = process.env | ||
|
||
beforeEach(() => { | ||
// Clears the cache so imports in tests uses a fresh copy | ||
jest.resetModules() | ||
// Make a copy of existing environment | ||
process.env = { ...OLD_ENV } | ||
}) | ||
|
||
afterAll(() => { | ||
// Restore old environment | ||
process.env = OLD_ENV | ||
}) | ||
|
||
afterEach(() => mockAxios.reset()) | ||
|
||
it("should return the result successfully when all parameters are valid", async () => { | ||
// Arrange | ||
mockAxios.post.mockResolvedValueOnce(200) | ||
|
||
// Act | ||
const actual = await MailClient.sendMail(mockRecipient, mockBody) | ||
|
||
// Assert | ||
expect(actual).toBeUndefined() | ||
expect(mockAxios.post).toHaveBeenCalledWith( | ||
mockEndpoint, | ||
generateEmail(mockRecipient, mockBody), | ||
mockHeaders | ||
) | ||
}) | ||
|
||
it("should throw an error on initialization when the env var is not set", async () => { | ||
// Arrange | ||
// Store the API key and set it later so that other tests are not affected | ||
const curApiKey = process.env.POSTMAN_API_KEY | ||
process.env.POSTMAN_API_KEY = "" | ||
// NOTE: This is because of typescript transpiling down to raw js | ||
// Export default compiles down to module.exports.default, which is also | ||
// done by babel. | ||
// Read more here: https://www.typescriptlang.org/tsconfig#allowSyntheticDefaultImports | ||
const _MailClientWithoutKey = (await import("../MailClient")).default | ||
|
||
// Act | ||
// NOTE: We require a new instance because the old one would already have the API key bound | ||
const actual = () => new _MailClientWithoutKey() | ||
|
||
// Assert | ||
expect(actual).toThrowError("Postman.gov.sg API key cannot be empty") | ||
process.env.POSTMAN_API_KEY = curApiKey | ||
expect(process.env.POSTMAN_API_KEY).toBe(curApiKey) | ||
}) | ||
|
||
it("should return an error when a network error occurs", async () => { | ||
// Arrange | ||
const generatedEmail = generateEmail(mockRecipient, mockBody) | ||
mockAxios.post.mockRejectedValueOnce("some error") | ||
|
||
// Act | ||
const actual = MailClient.sendMail(mockRecipient, mockBody) | ||
|
||
// Assert | ||
expect(actual).rejects.toThrowError("Failed to send email") | ||
expect(mockAxios.post).toHaveBeenCalledWith( | ||
mockEndpoint, | ||
generatedEmail, | ||
mockHeaders | ||
) | ||
}) | ||
}) |
Oops, something went wrong.