From 15f042eca9a43da09057e64fc1fb1c1a98dd5887 Mon Sep 17 00:00:00 2001 From: kravchenkodhealth <106426895+kravchenkodhealth@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:04:10 +0200 Subject: [PATCH] [@dhealthdapps/frontend] feat(test): improve base gateway tests, add auth gateway tests --- .../src/common/gateways/AuthGateway.ts | 2 +- .../src/common/services/AuthService.ts | 5 +- .../tests/unit/common/ScopeFactory.spec.ts | 17 ++- .../backend/tests/unit/common/Scopes.spec.ts | 25 +++- .../unit/common/gateways/AuthGateway.spec.ts | 116 ++++++++++++++++ .../unit/common/gateways/BaseGateway.spec.ts | 124 +++++++++++++++++- .../unit/common/routes/AuthController.spec.ts | 74 ++++++----- 7 files changed, 310 insertions(+), 53 deletions(-) create mode 100644 runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts diff --git a/runtime/backend/src/common/gateways/AuthGateway.ts b/runtime/backend/src/common/gateways/AuthGateway.ts index 23e7bd5a..a4bb840b 100644 --- a/runtime/backend/src/common/gateways/AuthGateway.ts +++ b/runtime/backend/src/common/gateways/AuthGateway.ts @@ -53,7 +53,7 @@ export class AuthGateway extends BaseGateway { * @returns {void} Emits "auth.open" event which triggers validating of the received challenge */ @OnEvent("auth.open") - handleEvent(payload: any) { + handleAuthOpen(payload: any) { this.validateChallengeScheduler.startCronJob(payload.challenge); } diff --git a/runtime/backend/src/common/services/AuthService.ts b/runtime/backend/src/common/services/AuthService.ts index ec64f478..49e09a88 100644 --- a/runtime/backend/src/common/services/AuthService.ts +++ b/runtime/backend/src/common/services/AuthService.ts @@ -67,6 +67,7 @@ const conf = dappConfigLoader(); * name: "ELEVATE", * domain: "elevate.dhealth.com", * secret: "AuthSecretUsedToSignCookies", + * challenge: "fakeChallenge" * } as CookiePayload; * ``` * @@ -76,6 +77,7 @@ export interface CookiePayload { name: string; domain: string; secret?: string; + challenge?: string; } /** @@ -223,9 +225,10 @@ export class AuthService { const name = this.configService.get("dappName"); const domain = this.configService.get("frontendApp.host"); const secret = this.configService.get("auth.secret"); + const challenge = this.configService.get("challenge"); // configures cookie(s) creation - this.cookie = { name, domain, secret } as CookiePayload; + this.cookie = { name, domain, secret, challenge } as CookiePayload; this.challengeSize = this.configService.get("auth.challengeSize"); this.authSecret = secret; } diff --git a/runtime/backend/tests/unit/common/ScopeFactory.spec.ts b/runtime/backend/tests/unit/common/ScopeFactory.spec.ts index 84a4d19b..39ff3c2b 100644 --- a/runtime/backend/tests/unit/common/ScopeFactory.spec.ts +++ b/runtime/backend/tests/unit/common/ScopeFactory.spec.ts @@ -161,6 +161,11 @@ jest.mock("../../../src/users/UsersModule", () => { }); // schedulers +const ValidateChallengeSchedulerMock: any = jest.fn(); +jest.mock("../../../src/common/schedulers/ValidateChallengeScheduler", () => { + return { ValidateChallengeScheduler: ValidateChallengeSchedulerMock }; +}); + const DiscoverAccountsCommandMock: any = jest.fn(); jest.mock( "../../../src/discovery/schedulers/DiscoverAccounts/DiscoverAccountsCommand", @@ -205,7 +210,9 @@ const LeaderboardsAggregationCommandMock: any = jest.fn(); jest.mock( "../../../src/statistics/schedulers/LeaderboardAggregation/LeaderboardsAggregationCommand", () => { - return { LeaderboardsAggregationCommand: LeaderboardsAggregationCommandMock }; + return { + LeaderboardsAggregationCommand: LeaderboardsAggregationCommandMock, + }; }, ); @@ -230,7 +237,7 @@ jest.mock( "../../../src/payout/schedulers/ActivityPayouts/ActivityPayoutsCommand", () => { return { ActivityPayoutsCommand: ActivityPayoutsCommandMock }; - } + }, ); const ReportNotifierCommandMock: any = { register: jest.fn() }; @@ -250,14 +257,14 @@ const mockDappConfig: DappConfig = { url: "test-url", host: "test-host", port: "test-port", - https: false + https: false, }, backendApp: { url: "test-url", host: "test-host", port: "test-port", - https: false - } + https: false, + }, }; // internal dependencies diff --git a/runtime/backend/tests/unit/common/Scopes.spec.ts b/runtime/backend/tests/unit/common/Scopes.spec.ts index 1c9c6d8a..f878e5a2 100644 --- a/runtime/backend/tests/unit/common/Scopes.spec.ts +++ b/runtime/backend/tests/unit/common/Scopes.spec.ts @@ -155,6 +155,11 @@ jest.mock("../../../src/users/UsersModule", () => { }); // schedulers +const ValidateChallengeSchedulerMock: any = jest.fn(); +jest.mock("../../../src/common/schedulers/ValidateChallengeScheduler", () => { + return { ValidateChallengeScheduler: ValidateChallengeSchedulerMock }; +}); + const DiscoverAccountsCommandMock: any = jest.fn(); jest.mock( "../../../src/discovery/schedulers/DiscoverAccounts/DiscoverAccountsCommand", @@ -199,7 +204,9 @@ const LeaderboardsAggregationCommandMock: any = jest.fn(); jest.mock( "../../../src/statistics/schedulers/LeaderboardAggregation/LeaderboardsAggregationCommand", () => { - return { LeaderboardsAggregationCommand: LeaderboardsAggregationCommandMock }; + return { + LeaderboardsAggregationCommand: LeaderboardsAggregationCommandMock, + }; }, ); @@ -224,7 +231,7 @@ jest.mock( "../../../src/payout/schedulers/ActivityPayouts/ActivityPayoutsCommand", () => { return { ActivityPayoutsCommand: ActivityPayoutsCommandMock }; - } + }, ); const ReportNotifierCommandMock: any = { register: jest.fn() }; @@ -251,8 +258,7 @@ class MockFactory extends ScopeFactory { } describe("common/Scopes", () => { - let dappConfig: any, - actualModules: any[]; + let dappConfig: any, actualModules: any[]; beforeEach(() => { // prepare for all (includes database scope) dappConfig = { @@ -260,13 +266,20 @@ describe("common/Scopes", () => { dappPublicKey: "FakePublicKeyOfAdApp", authAuthority: "NonExistingAuthority", scopes: ["database"], - database: { host: "fake-host", port: 1234, name: "fake-db-name", user: "fake-user" }, + database: { + host: "fake-host", + port: 1234, + name: "fake-db-name", + user: "fake-user", + }, }; }); it("should use environment variables from mocks", () => { // assert - expect(process.env.ANOTHER_DB_NAME_THROUGH_ENV).toEqual("this-exists-only-in-mock"); + expect(process.env.ANOTHER_DB_NAME_THROUGH_ENV).toEqual( + "this-exists-only-in-mock", + ); }); it("should include database scope in enabled modules", () => { diff --git a/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts b/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts new file mode 100644 index 00000000..05f06397 --- /dev/null +++ b/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts @@ -0,0 +1,116 @@ +/** + * This file is part of dHealth dApps Framework shared under LGPL-3.0 + * Copyright (C) 2022-present dHealth Network, All rights reserved. + * + * @package dHealth dApps Framework + * @subpackage Backend + * @author dHealth Network + * @license LGPL-3.0 + */ + +// external dependencies +import { TestingModule, Test } from "@nestjs/testing"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { SchedulerRegistry } from "@nestjs/schedule"; +import { ConfigService } from "@nestjs/config"; +import { JwtService } from "@nestjs/jwt"; + +// internal dependencies +import { ValidateChallengeScheduler } from "../../../../src/common/schedulers/ValidateChallengeScheduler"; +import { AuthGateway } from "../../../../src/common/gateways/AuthGateway"; +import { AuthService } from "../../../../src/common/services/AuthService"; +import { NetworkService } from "../../../../src/common/services/NetworkService"; +import { AccountsService } from "../../../../src/common/services/AccountsService"; +import { ChallengesService } from "../../../../src/common/services/ChallengesService"; +import { QueryService } from "../../../../src/common/services/QueryService"; +import { MockModel } from "../../../mocks/global"; +import { getModelToken } from "@nestjs/mongoose"; + +describe("common/AuthGateway", () => { + let authGateway: AuthGateway; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + EventEmitter2, + ValidateChallengeScheduler, + AuthGateway, + SchedulerRegistry, + AuthService, + ConfigService, + NetworkService, + AccountsService, + ChallengesService, + QueryService, + JwtService, + { + provide: getModelToken("Account"), + useValue: MockModel, + }, // requirement from AccountsService + { + provide: getModelToken("AuthChallenge"), + useValue: MockModel, + }, + ], + }).compile(); + + authGateway = module.get(AuthGateway); + }); + + it("should be defined", () => { + expect(authGateway).toBeDefined(); + }); + + describe("constructor()", () => { + it("should initialize emitter", () => { + expect("emitter" in authGateway).toBe(true); + }); + + it("should initialize validateChallengeScheduler", () => { + expect("validateChallengeScheduler" in authGateway).toBe(true); + }); + }); + + describe("handleAuthOpen()", () => { + it("should start validation of challenge", () => { + const validateMethodMock = jest.fn(); + + (authGateway as any).validateChallengeScheduler = { + startCronJob: validateMethodMock, + }; + + authGateway.handleAuthOpen({ challenge: "fakeChallenge" }); + + expect(validateMethodMock).toBeCalledTimes(1); + }); + }); + + describe("complete()", () => { + it("should send complete message to client and log message", () => { + const mockedMethod = jest.fn(); + + (authGateway as any).ws = { + send: mockedMethod, + }; + (authGateway as any).logger = { + log: mockedMethod, + }; + + authGateway.complete(); + expect(mockedMethod).toBeCalledTimes(2); + }); + }); + + describe("close", () => { + it("should log message on close", () => { + const mockedMethod = jest.fn(); + + (authGateway as any).logger = { + log: mockedMethod, + }; + + authGateway.close(); + expect(mockedMethod).toBeCalledTimes(1); + }); + }); +}); diff --git a/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts b/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts index 8d9373a1..16df1ff9 100644 --- a/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts +++ b/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts @@ -11,30 +11,146 @@ // external dependencies import { EventEmitter2 } from "@nestjs/event-emitter"; import { TestingModule, Test } from "@nestjs/testing"; +import { ConfigService } from "@nestjs/config"; +import { JwtService } from "@nestjs/jwt"; +import { getModelToken } from "@nestjs/mongoose"; // internal dependencies -import { AuthService } from "../../../../src/common/services/AuthService"; +import { + AuthService, + CookiePayload, +} from "../../../../src/common/services/AuthService"; import { LogService } from "../../../../src/common/services/LogService"; import { BaseGateway } from "../../../../src/common/gateways/BaseGateway"; +import { NetworkService } from "../../../../src/common/services/NetworkService"; +import { AccountsService } from "../../../../src/common/services/AccountsService"; +import { ChallengesService } from "../../../../src/common/services/ChallengesService"; +import { QueryService } from "../../../../src/common/services/QueryService"; +import { MockModel } from "../../../mocks/global"; class TestGateway extends BaseGateway {} describe("common/BaseGateway", () => { - let authService: AuthService; let logger: LogService; let testGateway: TestGateway; + let authService: AuthService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [EventEmitter2, LogService, TestGateway, AuthService], + providers: [ + EventEmitter2, + LogService, + TestGateway, + AuthService, + ConfigService, + NetworkService, + AccountsService, + ChallengesService, + QueryService, + JwtService, + { + provide: getModelToken("Account"), + useValue: MockModel, + }, // requirement from AccountsService + { + provide: getModelToken("AuthChallenge"), + useValue: MockModel, + }, + ], }).compile(); logger = module.get(LogService); - authService = module.get(AuthService); testGateway = module.get(TestGateway); + authService = module.get(AuthService); }); it("should be defined", () => { expect(testGateway).toBeDefined(); }); + + describe("constructor()", () => { + it("should initialize clients properly", () => { + expect("clients" in testGateway).toBe(true); + }); + + it("should initialize emitter", () => { + expect("emitter" in testGateway).toBe(true); + }); + + it("should initialize logger", () => { + expect("logger" in testGateway).toBe(true); + }); + }); + + describe("handleConnection()", () => { + let request: any = { + // empty signed cookies + signedCookies: undefined, + // then load from request cookies + cookies: { + "fake-cookie-name": "expectedToken", + }, + headers: { + cookie: "s%3Aszbi8fzi.JqeTesglrltbou3CNrkYcHo8iTqAx%2BHVYMiDNGwQwnU", + }, + }; + + it("should receive challenge from cookie", () => { + // act + const cookie: CookiePayload = (authService as any).cookie; + + // assert + expect("challenge" in cookie).toBe(true); + }); + + it("should store current websocket in ws prop", () => { + (testGateway as any).ws = { + testData: "test value", + }; + + expect((testGateway as any).ws).not.toBe(null); + }); + + it("should emit event in case of success", () => { + const emitMock = jest.fn(); + (testGateway as any).emitter = { + emit: emitMock, + }; + + testGateway.handleConnection({}, request); + + expect(emitMock).toBeCalledTimes(1); + }); + + it("should push value to clients", () => { + testGateway.handleConnection({}, request); + expect((testGateway as any).clients).toHaveLength(1); + }); + }); + + describe("handleDisconnect()", () => { + const mockedWsWithChallenge = { + challenge: "fakeChallengeHello", + }; + + it("should remove correct challenge from clients", () => { + (testGateway as any).clients = ["fakeChallengeHello"]; + testGateway.handleDisconnect(mockedWsWithChallenge); + + expect((testGateway as any).clients).toHaveLength(0); + }); + }); + + describe("afterInit", () => { + it("should log info after gateway initialized", () => { + const mockedLog = jest.fn(); + (testGateway as any).logger = { + log: mockedLog, + }; + + testGateway.afterInit({} as any); + + expect(mockedLog).toBeCalledTimes(1); + }); + }); }); diff --git a/runtime/backend/tests/unit/common/routes/AuthController.spec.ts b/runtime/backend/tests/unit/common/routes/AuthController.spec.ts index fec1b4d4..e3db24ee 100644 --- a/runtime/backend/tests/unit/common/routes/AuthController.spec.ts +++ b/runtime/backend/tests/unit/common/routes/AuthController.spec.ts @@ -82,9 +82,12 @@ describe("common/AuthController", () => { .spyOn(authService, "getChallenge") .mockReturnValue(challenge); const expectedResult = { challenge }; + const responseCookieCall = jest.fn(); // act - const result = await (controller as any).getAuthCode(); + const result = await (controller as any).getAuthCode({ + cookie: responseCookieCall, + }); // assert expect(authServiceGetChallengeCall).toHaveBeenCalledTimes(1); @@ -107,11 +110,12 @@ describe("common/AuthController", () => { sub: "testSub", address: "testAddress", }); - const tokens = new AccessTokenDTO(); - tokens.accessToken = "testAccessToken"; - tokens.refreshToken = "testRefreshToken"; - tokens.expiresAt = 1; - + + const tokens = { + accessToken: "testAccessToken", + refreshToken: "testRefreshToken", + expiresAt: 1, + } as AccessTokenDTO; const authServiceGetAccessTokenCall = jest .spyOn(authService, "getAccessToken") .mockResolvedValue(tokens); @@ -120,7 +124,7 @@ describe("common/AuthController", () => { // act const result = await (controller as any).getAccessToken( { challenge: "testChallenge" }, - { cookie: responseCookieCall } + { cookie: responseCookieCall }, ); // assert @@ -144,9 +148,9 @@ describe("common/AuthController", () => { .mockResolvedValue(null); // act - const result = await (controller as any).getAccessToken( - { challenge: "testChallenge" } - ); + const result = await (controller as any).getAccessToken({ + challenge: "testChallenge", + }); // assert expect(authServiceGetCookieCall).toHaveBeenCalledTimes(1); @@ -183,9 +187,9 @@ describe("common/AuthController", () => { }); // act - const result = (controller as any).getAccessToken( - { challenge: "testChallenge" } - ); + const result = (controller as any).getAccessToken({ + challenge: "testChallenge", + }); // assert expect(authServiceGetCookieCall).toHaveBeenCalledTimes(1); @@ -197,11 +201,11 @@ describe("common/AuthController", () => { it("should return correct result", async () => { // prepare const authServiceGetCookieCall = jest - .spyOn(authService, "getCookie") - .mockReturnValue({ - name: "testCookie", - domain: "testDomain", - }); + .spyOn(authService, "getCookie") + .mockReturnValue({ + name: "testCookie", + domain: "testDomain", + }); const authServiceExtractTokenCall = jest .spyOn(AuthService, "extractToken") .mockReturnValue("testToken"); @@ -219,10 +223,9 @@ describe("common/AuthController", () => { const responseCookieCall = jest.fn(); // act - const result = await (controller as any).refreshTokens( - jest.fn(), - { cookie: responseCookieCall } - ); + const result = await (controller as any).refreshTokens(jest.fn(), { + cookie: responseCookieCall, + }); // assert expect(authServiceGetCookieCall).toHaveBeenCalledTimes(1); @@ -244,10 +247,9 @@ describe("common/AuthController", () => { const responseCookieCall = jest.fn(); // act - const result = (controller as any).refreshTokens( - jest.fn(), - { cookie: responseCookieCall } - ); + const result = (controller as any).refreshTokens(jest.fn(), { + cookie: responseCookieCall, + }); // assert expect(authServiceGetCookieCall).toHaveBeenCalledTimes(1); @@ -277,16 +279,16 @@ describe("common/AuthController", () => { expect(result).rejects.toThrowError(expectedError); }); }); - + describe("logout()", () => { it("should return correct result", async () => { // prepare const authServiceGetCookieCall = jest - .spyOn(authService, "getCookie") - .mockReturnValue({ - name: "testCookie", - domain: "testDomain", - }); + .spyOn(authService, "getCookie") + .mockReturnValue({ + name: "testCookie", + domain: "testDomain", + }); const responseCookieCall = jest.fn(); const expectedResult = { code: 200, @@ -294,9 +296,9 @@ describe("common/AuthController", () => { }; // act - const result = await (controller as any).logout( - { cookie: responseCookieCall } - ); + const result = await (controller as any).logout({ + cookie: responseCookieCall, + }); // assert expect(authServiceGetCookieCall).toHaveBeenCalledTimes(1); @@ -304,4 +306,4 @@ describe("common/AuthController", () => { expect(result).toEqual(expectedResult); }); }); -}); \ No newline at end of file +});