diff --git a/runtime/backend/tests/unit/AppConfiguration.spec.ts b/runtime/backend/tests/unit/AppConfiguration.spec.ts index 74b1de9e..ead7b068 100644 --- a/runtime/backend/tests/unit/AppConfiguration.spec.ts +++ b/runtime/backend/tests/unit/AppConfiguration.spec.ts @@ -39,14 +39,21 @@ jest.mock("@nestjs/event-emitter", () => { }); // force-mock the mailer module `forRootAsync` call -const mailerForRootAsyncCall = jest.fn(() => MailerModuleMock); +const configService = { + get: jest.fn().mockReturnValue({ + mailConfig: mockTransportConfigLoaderCall().mailer, + }), +} +const mailerForRootAsyncCall: any = jest.fn((params: any) => { + params.useFactory(configService); + return MailerModuleMock +}); const MailerModuleMock: any = { forRootAsync: mailerForRootAsyncCall }; jest.mock("@nestjs-modules/mailer", () => { return { MailerModule: MailerModuleMock }; }); // external dependencies - import { Account, Address, PublicAccount } from "@dhealth/sdk"; // internal dependencies @@ -111,6 +118,10 @@ describe("AppConfiguration", () => { (AppConfiguration as any).MAILER = undefined; }); + afterEach(() => { + jest.clearAllMocks(); + }); + it("should be defined", () => { // act service = new AppConfiguration(); @@ -438,10 +449,11 @@ describe("AppConfiguration", () => { const mailer3 = AppConfiguration.getMailerModule(); // assert - expect(mailer1).toBeDefined(); - expect(mailer2).toBeDefined(); - expect(mailer3).toBeDefined(); + expect(mailer1).toBe(mailer2); + expect(mailer1).toBe(mailer3); + expect(mailer2).toBe(mailer3); expect(mailerForRootAsyncCall).toHaveBeenCalledTimes(1); // once! + expect(configService.get).toHaveBeenCalledTimes(1); // once! }); }); @@ -905,14 +917,14 @@ describe("AppConfiguration", () => { it("should check the configuration of activated scopes", () => { // prepare - const mockCheckDiscoverySettings = jest.fn(); - const mockCheckProcessorSettings = jest.fn(); - const mockCheckPayoutSettings = jest.fn(); - const mockCheckOAuthSettings = jest.fn(); - (AppConfiguration as any).checkDiscoverySettings = mockCheckDiscoverySettings; - (AppConfiguration as any).checkProcessorSettings = mockCheckProcessorSettings; - (AppConfiguration as any).checkPayoutSettings = mockCheckPayoutSettings; - (AppConfiguration as any).checkOAuthSettings = mockCheckOAuthSettings; + const mockCheckDiscoverySettings = jest + .spyOn((AppConfiguration as any), "checkDiscoverySettings"); + const mockCheckProcessorSettings = jest + .spyOn((AppConfiguration as any), "checkProcessorSettings"); + const mockCheckPayoutSettings = jest + .spyOn((AppConfiguration as any), "checkPayoutSettings"); + const mockCheckOAuthSettings = jest + .spyOn((AppConfiguration as any), "checkOAuthSettings"); // act AppConfiguration.checkApplicationScopes(service); @@ -930,14 +942,14 @@ describe("AppConfiguration", () => { ...(service as any).dapp, scopes: ["discovery", "database"], // payout + processor disabled } - const mockCheckDiscoverySettings = jest.fn(); - const mockCheckProcessorSettings = jest.fn(); - const mockCheckPayoutSettings = jest.fn(); - const mockCheckOAuthSettings = jest.fn(); - (AppConfiguration as any).checkDiscoverySettings = mockCheckDiscoverySettings; - (AppConfiguration as any).checkProcessorSettings = mockCheckProcessorSettings; - (AppConfiguration as any).checkPayoutSettings = mockCheckPayoutSettings; - (AppConfiguration as any).checkOAuthSettings = mockCheckOAuthSettings; + const mockCheckDiscoverySettings = jest + .spyOn((AppConfiguration as any), "checkDiscoverySettings"); + const mockCheckProcessorSettings = jest + .spyOn((AppConfiguration as any), "checkProcessorSettings"); + const mockCheckPayoutSettings = jest + .spyOn((AppConfiguration as any), "checkPayoutSettings"); + const mockCheckOAuthSettings = jest + .spyOn((AppConfiguration as any), "checkOAuthSettings"); // act AppConfiguration.checkApplicationScopes(service); @@ -958,4 +970,238 @@ describe("AppConfiguration", () => { expect(actual).toBe(true); // <-- mocked config is valid }); }); + + describe("checkDiscoverySettings()", () => { + it("should run correcty and return true", () => { + // prepare + PublicAccount.createFromPublicKey = jest.fn().mockReturnValue(true); + + // act + const result = (AppConfiguration as any).checkDiscoverySettings({ + dapp: { + dappPublicKey: "test-dappPublicKey", + discovery: { sources: ["0123456789012345678901234567890123456789012345678901234567890123"] } + }, + network: { network: { networkIdentifier: "test-networkIdentifier" } }, + }); + + // assert + expect(result).toBe(true); + expect(PublicAccount.createFromPublicKey).toHaveBeenNthCalledWith( + 1, + "test-dappPublicKey", + "test-networkIdentifier", + ); + expect(PublicAccount.createFromPublicKey).toHaveBeenNthCalledWith( + 2, + "0123456789012345678901234567890123456789012345678901234567890123", + "test-networkIdentifier", + ); + }); + + it("should throw ConfigurationError if error was caught while creating account from public key", () => { + // prepare + const expectedError = new ConfigurationError( + `The configuration field "dappPublicKey" must contain ` + + `a valid 32-bytes public key in hexadecimal format.` + ); + PublicAccount.createFromPublicKey = jest.fn(() => { throw new Error("error") }); + + // act + const result = () => (AppConfiguration as any).checkDiscoverySettings({ + dapp: { + dappPublicKey: "test-dappPublicKey", + discovery: { sources: ["test-source"] } + }, + network: { network: { networkIdentifier: "test-networkIdentifier" } }, + }); + + // assert + expect(result).toThrow(expectedError); + expect(PublicAccount.createFromPublicKey).toHaveBeenNthCalledWith( + 1, + "test-dappPublicKey", + "test-networkIdentifier", + ); + }); + + it("should throw ConfigurationError if error was caught while creating address from raw value", () => { + // prepare + const expectedError = new ConfigurationError( + `The configuration field "discovery.sources" must contain ` + + `an array of valid 32-bytes public keys in hexadecimal format ` + + `or valid addresses that are compatible with dHealth Network.`, + ); + PublicAccount.createFromPublicKey = jest.fn().mockReturnValue(true); + Address.createFromRawAddress = jest.fn(() => { throw new Error("error") }); + + // act + const result = () => (AppConfiguration as any).checkDiscoverySettings({ + dapp: { + dappPublicKey: "test-dappPublicKey", + discovery: { sources: ["test-source"] } + }, + network: { network: { networkIdentifier: "test-networkIdentifier" } }, + }); + + // assert + expect(result).toThrow(expectedError); + expect(PublicAccount.createFromPublicKey).toHaveBeenNthCalledWith( + 1, + "test-dappPublicKey", + "test-networkIdentifier", + ); + expect(Address.createFromRawAddress).toHaveBeenNthCalledWith( + 1, + "test-source", + ) + }); + }); + + describe("checkPayoutSettings()", () => { + beforeEach(() => { + service = new AppConfiguration(); + }); + + it("should throw ConfigurationError if payouts information is empty or invalid", () => { + // prepare + [undefined, {}, {issuerPrivateKey: ""}].forEach((payouts: any) => { + (service as any).payout.payouts = payouts; + const expectedError = new ConfigurationError( + `The configuration field "payouts.issuerPrivateKey" cannot be empty.`, + ); + + // act + const result = () => (AppConfiguration as any).checkPayoutSettings(service); + + // assert + expect(result).toThrow(expectedError); + }); + }); + + it("should throw ConfigurationError if error was caught while creating account from private key", () => { + // prepare + (service as any).payout.payouts = { + issuerPrivateKey: "test-issuerPrivateKey", + }; + const expectedError = new ConfigurationError( + `The configuration field "payouts.issuerPrivateKey" must ` + + `contain a valid 32-bytes private key in hexadecimal format.`, + ); + Account.createFromPrivateKey = jest.fn(() => { throw new Error("error") }); + + // // act + const result = () => (AppConfiguration as any).checkPayoutSettings(service); + + // // assert + expect(result).toThrow(expectedError); + expect(Account.createFromPrivateKey).toHaveBeenNthCalledWith( + 1, + "test-issuerPrivateKey", + 104, + ); + }); + }); + + describe("checkProcessorSettings()", () => { + beforeEach(() => { + service = new AppConfiguration(); + }); + + it("shoud run correctly and return true", () => { + // prepare + (service as any).processor.contracts = ["test-contract"]; + (service as any).processor.operations = ["test-operation"]; + + // act + const result = (AppConfiguration as any).checkProcessorSettings(service); + + // assert + expect(result).toBe(true); + }); + + it("should throw ConfigurationError if contracts is undefined/empty", () => { + // prepare + [undefined, []].forEach((contracts: any) => { + (service as any).processor.contracts = contracts; + const expectedError = new ConfigurationError( + `The configuration field "contracts" must be a non-empty array.`, + ); + + // act + const result = () => (AppConfiguration as any).checkProcessorSettings(service); + + // assert + expect(result).toThrow(expectedError); + }); + }); + + it("should throw ConfigurationError if operations is undefined/empty", () => { + // prepare + [undefined, []].forEach((operations: any) => { + (service as any).processor.contracts = ["test-contract"]; + (service as any).processor.operations = operations; + const expectedError = new ConfigurationError( + `The configuration field "operations" must be a non-empty array.`, + ); + + // act + const result = () => (AppConfiguration as any).checkProcessorSettings(service); + + // assert + expect(result).toThrow(expectedError); + }); + }); + }); + + describe("checkOAuthSettings()", () => { + beforeEach(() => { + service = new AppConfiguration(); + }); + + it("should throw ConfigurationError if providers is undefined", () => { + // prepare + (service as any).oauth.providers = undefined; + const expectedError = new ConfigurationError( + `The configuration field "providers" cannot be empty.`, + ); + + // act + const result = () => (AppConfiguration as any).checkOAuthSettings(service); + + // assert + expect(result).toThrow(expectedError); + }); + + it("should throw ConfigurationError if strava is undefined or is not in providers", () => { + // prepare + [ {}, { strava: undefined }].forEach((providers) => { + (service as any).oauth.providers = providers; + const expectedError = new ConfigurationError( + `The configuration field "providers.strava" cannot be empty.`, + ); + + // act + const result = () => (AppConfiguration as any).checkOAuthSettings(service); + + // assert + expect(result).toThrow(expectedError); + }); + }); + + it("should throw ConfigurationError if scopes don't include users", () => { + // prepare + (service as any).oauth.providers = { strava: "test-strava" }; + (service as any).dapp.scopes = ["test-scope"]; + const expectedError = new ConfigurationError( + `The application scope "users" cannot be disabled.`, + ); + + // act + const result = () => (AppConfiguration as any).checkOAuthSettings(service); + + // assert + expect(result).toThrow(expectedError); + }); + }); }); diff --git a/runtime/backend/tests/unit/common/events/OnAuthClosed.spec.ts b/runtime/backend/tests/unit/common/events/OnAuthClosed.spec.ts new file mode 100644 index 00000000..d1a5980d --- /dev/null +++ b/runtime/backend/tests/unit/common/events/OnAuthClosed.spec.ts @@ -0,0 +1,27 @@ +/** + * 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 + */ +// internal dependencies +import { OnAuthClosed } from "../../../../src/common/events/OnAuthClosed"; + +describe("common/OnAuthClosed", () => { + describe("create()", () => { + it("should run correctly & return correct instance", () => { + // prepare + [null, "test-challenge"].forEach((challenge: string) => { + // act + const result = OnAuthClosed.create(challenge); + + // assert + expect(result).toBeDefined(); + expect(result.challenge).toBe(challenge); + }); + }); + }); +}); \ No newline at end of file diff --git a/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts b/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts index 12553c85..09c0f0c6 100644 --- a/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts +++ b/runtime/backend/tests/unit/common/gateways/AuthGateway.spec.ts @@ -61,6 +61,10 @@ describe("common/AuthGateway", () => { }).compile(); authGateway = module.get(AuthGateway); + (authGateway as any).options.debug = true; + (authGateway as any).logger = { + debug: jest.fn(), + } }); it("should be defined", () => { @@ -88,6 +92,9 @@ describe("common/AuthGateway", () => { authGateway.onAuthOpened({ challenge: "fakeChallenge" }); expect(validateMethodMock).toBeCalledTimes(1); + expect((authGateway as any).logger.debug).toHaveBeenNthCalledWith( + 1, `Received event "auth.open" with challenge "fakeChallenge"` + ); }); }); @@ -106,6 +113,9 @@ describe("common/AuthGateway", () => { // assert expect(sendMock).toBeCalledTimes(1); + expect((authGateway as any).logger.debug).toHaveBeenNthCalledWith( + 1, `Received event "auth.complete" with challenge "fakeChallenge"` + ); }); }); @@ -122,6 +132,9 @@ describe("common/AuthGateway", () => { // assert expect("fakeChallenge" in clients).toBe(false); + expect((authGateway as any).logger.debug).toHaveBeenNthCalledWith( + 1, `Received event "auth.close" with challenge "fakeChallenge"` + ); }); }); }); diff --git a/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts b/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts index a1694809..7e8b8a01 100644 --- a/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts +++ b/runtime/backend/tests/unit/common/gateways/BaseGateway.spec.ts @@ -7,6 +7,21 @@ * @author dHealth Network * @license LGPL-3.0 */ +// mock config dapp backend https value +import { mockDappConfigLoaderCall } from "../../../mocks/config"; +const dappConfig = mockDappConfigLoaderCall(); +dappConfig.backendApp.https = true; + +jest.mock("cookie", () => ({ + parse: jest.fn((cookie: string) => { + if (cookie.length > 0) return { challenge: "test-challenge" }; + return { challenge: "" }; + }) +})); + +jest.mock("cookie-parser", () => ({ + signedCookie: jest.fn((challenge: string) => challenge), +})); // external dependencies import { EventEmitter2 } from "@nestjs/event-emitter"; @@ -14,7 +29,7 @@ import { TestingModule, Test } from "@nestjs/testing"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import { getModelToken } from "@nestjs/mongoose"; -import { Socket } from "dgram"; +import { HttpException, HttpStatus } from "@nestjs/common"; // internal dependencies import { @@ -37,8 +52,9 @@ describe("common/BaseGateway", () => { let testGateway: TestGateway; let authService: AuthService; + let module: TestingModule; beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ + module = await Test.createTestingModule({ providers: [ EventEmitter2, LogService, @@ -87,20 +103,28 @@ describe("common/BaseGateway", () => { it("should initialize logger", () => { expect("logger" in testGateway).toBe(true); }); + + it("should use 'wss' if config enables https", () => { + expect((testGateway as any).websocketUrl) + .toBe(`wss://${dappConfig.backendApp.host}:${dappConfig.backendApp.wsPort}/ws`); + }); }); 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", - }, - }; + let request: any; + beforeEach(() => { + request = { + // 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 @@ -118,13 +142,25 @@ describe("common/BaseGateway", () => { expect((testGateway as any).ws).not.toBe(null); }); - it("should emit event in case of success", () => { + it("should throw HttpException if challenge is not present from cookie", () => { + // prepare + request.headers.cookie = ""; + const expectedError = new HttpException("Unauthorized", HttpStatus.UNAUTHORIZED); + + // act + const result = testGateway.handleConnection({}, request); + + // assert + expect(result).rejects.toThrow(expectedError); + }); + + it("should emit event in case of success", async () => { const emitMock = jest.fn(); (testGateway as any).emitter = { emit: emitMock, }; - testGateway.handleConnection({}, request); + await testGateway.handleConnection({}, request); expect(emitMock).toBeCalledTimes(1); }); @@ -150,6 +186,38 @@ describe("common/BaseGateway", () => { expect(Object.keys((testGateway as any).clients)).toHaveLength(1); expect("fakeChallenge" in (testGateway as any).clients).toBe(true); }); + + it("should return if challenge is not present or is empty", () => { + // prepare + [{}, { challenge: "" }].forEach((ws: object) => { + const clients = { + "test-challenge": {}, + }; + (testGateway as any).clients = clients; + + // act + testGateway.handleDisconnect(ws); + + // assert + expect((testGateway as any).clients).toBe(clients); + }); + }); + + it("should return if challenge is not from a connected client", () => { + // prepare + const clients = { + "test-challenge": {}, + }; + (testGateway as any).clients = clients; + + // act + testGateway.handleDisconnect({ + challenge: "test-challenge2", + }); + + // assert + expect((testGateway as any).clients).toBe(clients); + }); }); describe("afterInit", () => { @@ -163,5 +231,25 @@ describe("common/BaseGateway", () => { expect(debugMock).toBeCalledTimes(1); }); + + it("should log message if connection was established", () => { + const debugMock = jest.fn(); + (testGateway as any).logger = { + debug: debugMock, + }; + + testGateway.afterInit({ + connections: "test-connection" + } as any); + + expect(debugMock).toHaveBeenNthCalledWith( + 1, + "Now listening for websocket connections on wss://fake.example.com:4321/ws" + ); + expect(debugMock).toHaveBeenNthCalledWith( + 2, + "test-connection websocket clients connected" + ); + }); }); }); diff --git a/runtime/backend/tests/unit/common/models/AccountSchema.spec.ts b/runtime/backend/tests/unit/common/models/AccountSchema.spec.ts index ab83677e..ba7dfef9 100644 --- a/runtime/backend/tests/unit/common/models/AccountSchema.spec.ts +++ b/runtime/backend/tests/unit/common/models/AccountSchema.spec.ts @@ -29,4 +29,19 @@ describe("common/AccountSchema", () => { expect(accountToQuery).toEqual({ address, referredBy, referralCode }); }); }); + + describe("get slug()", () => { + it("should return correct result", () => { + // prepare + const address = "test-address"; + const account: Account = new Account(); + (account as any).address = address; + + // act + const result = account.slug; + + // assert + expect(result).toBe(address); + }); + }); }); \ No newline at end of file diff --git a/runtime/backend/tests/unit/common/routes/AuthController.spec.ts b/runtime/backend/tests/unit/common/routes/AuthController.spec.ts index e3db24ee..1922613b 100644 --- a/runtime/backend/tests/unit/common/routes/AuthController.spec.ts +++ b/runtime/backend/tests/unit/common/routes/AuthController.spec.ts @@ -111,11 +111,15 @@ describe("common/AuthController", () => { address: "testAddress", }); + const token = new AccessTokenDTO(); const tokens = { - accessToken: "testAccessToken", - refreshToken: "testRefreshToken", - expiresAt: 1, - } as AccessTokenDTO; + ...token, + ...{ + accessToken: "testAccessToken", + refreshToken: "testRefreshToken", + expiresAt: 1, + } + }; const authServiceGetAccessTokenCall = jest .spyOn(authService, "getAccessToken") .mockResolvedValue(tokens); diff --git a/runtime/backend/tests/unit/common/schedulers/ValidateChallengeScheduler.spec.ts b/runtime/backend/tests/unit/common/schedulers/ValidateChallengeScheduler.spec.ts index e66aa057..c889a6ef 100644 --- a/runtime/backend/tests/unit/common/schedulers/ValidateChallengeScheduler.spec.ts +++ b/runtime/backend/tests/unit/common/schedulers/ValidateChallengeScheduler.spec.ts @@ -49,6 +49,7 @@ describe("common/ValidateChallengeScheduler", () => { log: jest.fn(), debug: jest.fn(), error: jest.fn(), + warn: jest.fn(), }; beforeEach(async () => { @@ -136,15 +137,16 @@ describe("common/ValidateChallengeScheduler", () => { expect(mockFn).toBeCalledTimes(1); }); - it("should assign argument to challenge prop", () => { - validateChallengeScheduler.startCronJob("someFakeChallenge"); - - expect((validateChallengeScheduler as any).challenge).toBe( - "someFakeChallenge", - ); - }); - it("should start stop timeout", () => { + // prepare + const setTimeOutCall = jest + .spyOn(global, 'setTimeout') + .mockImplementation((paramFn: Function) => { + paramFn(); + return {} as any; + }); + + // act validateChallengeScheduler.startCronJob("someFakeChallenge"); expect((validateChallengeScheduler as any).stopCronJobTimeout).not.toBe( @@ -153,6 +155,7 @@ describe("common/ValidateChallengeScheduler", () => { expect((validateChallengeScheduler as any).stopCronJobTimeout).not.toBe( undefined, ); + expect(setTimeOutCall).toHaveBeenCalledTimes(1); }); }); diff --git a/runtime/backend/tests/unit/common/services/AccountsService.spec.ts b/runtime/backend/tests/unit/common/services/AccountsService.spec.ts index 3dd34748..f6a7b796 100644 --- a/runtime/backend/tests/unit/common/services/AccountsService.spec.ts +++ b/runtime/backend/tests/unit/common/services/AccountsService.spec.ts @@ -17,6 +17,7 @@ import { QueryService } from "../../../../src/common/services/QueryService"; import { AccountsService } from "../../../../src/common/services/AccountsService"; import { AccountDocument, AccountModel, AccountQuery } from "../../../../src/common/models/AccountSchema"; import { PaginatedResultDTO } from "../../../../src/common/models/PaginatedResultDTO"; +import { AuthenticationPayload } from "../../../../src/common/services/AuthService"; describe("common/AccountsService", () => { let service: AccountsService; @@ -150,6 +151,77 @@ describe("common/AccountsService", () => { }); }); + describe("getOrCreateForAuth()", () => { + it("should call findOne() with address and return result if account exists in db", async () => { + // prepare + const expectedAccountQuery = new AccountQuery({ + address: "test-address", + } as AccountDocument); + const expectedResult = {}; + const existsCall = jest + .spyOn(service, "exists") + .mockResolvedValue(true); + const findOneCall = jest + .spyOn(service, "findOne") + .mockResolvedValue(expectedResult as AccountDocument); + + // act + const result = await service.getOrCreateForAuth({ + address: "test-address", + } as AuthenticationPayload); + + // assert + expect(existsCall).toHaveBeenNthCalledWith(1, expectedAccountQuery); + expect(findOneCall).toHaveBeenNthCalledWith(1, expectedAccountQuery); + expect(result).toEqual(expectedResult); + }); + + it("should call findOne() with referrerCode if referralCode exists in payload", async () => { + // prepare + const payload = { + address: "test-address", + referralCode: "test-referralCode", + } as AuthenticationPayload; + const expectedAccountQuery = new AccountQuery({ + address: "test-address", + } as AccountDocument); + const expectedReferrerAccountQuery = new AccountQuery({ + referralCode: "test-referralCode", + } as AccountDocument); + const expectedReferrerResult = { address: "test-address" }; + const expectedResult = { address: "test-addressResult" }; + const existsCall = jest + .spyOn(service, "exists") + .mockResolvedValue(false); + const findOneCall = jest + .spyOn(service, "findOne") + .mockResolvedValue(expectedReferrerResult as AccountDocument); + const getRandomReferralCodeCall = jest + .spyOn(AccountsService, "getRandomReferralCode") + .mockReturnValue("test-randomReferralCode"); + const createOrUpdateCall = jest + .spyOn(service, "createOrUpdate") + .mockResolvedValue(expectedResult as AccountDocument); + + // act + const result = await service.getOrCreateForAuth(payload); + + // assert + expect(existsCall).toHaveBeenNthCalledWith(1, expectedAccountQuery); + expect(findOneCall).toHaveBeenNthCalledWith(1, expectedReferrerAccountQuery); + expect(getRandomReferralCodeCall).toHaveBeenCalledTimes(1); + expect(createOrUpdateCall).toHaveBeenNthCalledWith( + 1, + expectedAccountQuery, + { + referralCode: "test-randomReferralCode", + referredBy: expectedReferrerResult.address, + } + ); + expect(result).toEqual(expectedResult); + }); + }); + describe("updateBatch()", () => { // for each updateBatch() test we overwrite the diff --git a/runtime/backend/tests/unit/common/services/AuthService.spec.ts b/runtime/backend/tests/unit/common/services/AuthService.spec.ts index b4212740..32771fae 100644 --- a/runtime/backend/tests/unit/common/services/AuthService.spec.ts +++ b/runtime/backend/tests/unit/common/services/AuthService.spec.ts @@ -64,11 +64,15 @@ import { AuthenticationPayload, AuthService, CookiePayload } from "../../../../s import { ChallengesService } from "../../../../src/common/services/ChallengesService"; import { Factory } from "@dhealth/contracts"; import { AccountSessionDocument, AccountSessionQuery } from "../../../../src/common/models/AccountSessionSchema"; +import { LogService } from "../../../../src/common/services/LogService"; +import { AccountDocument } from "../../../../src/common/models/AccountSchema"; describe("common/AuthService", () => { let authService: AuthService; + let accountsService: AccountsService; const httpUnauthorizedError = new HttpException("Unauthorized", HttpStatus.UNAUTHORIZED); + const logCall = jest.fn(); let module: TestingModule; beforeEach(async () => { module = await Test.createTestingModule({ @@ -97,6 +101,12 @@ describe("common/AuthService", () => { }).compile(); authService = module.get(AuthService); + accountsService = module.get(AccountsService); + createFromJSONCall().inputs = { + dappIdentifier: "fake-dapp", + challenge: "12345678", + refCode: "123456ab", + }; }); afterEach(() => { @@ -618,20 +628,50 @@ describe("common/AuthService", () => { it("should respond with error if the challenge could not be found", () => { // prepare + (authService as any).cookie = { + name: "fake-dapp", + domain: "fake-dapp-host" + }; (authService as any).challengesService = { exists: jest.fn().mockResolvedValue(false), + createOrUpdate: jest.fn().mockResolvedValue({}), }; (authService as any).findRecentChallenge = - jest.fn().mockResolvedValue(undefined); // <-- force not found + jest.fn().mockResolvedValue({ + signer: { + address: { + plain: () => "NDAPPH6ZGD4D6LBWFLGFZUT2KQ5OLBLU32K3HNY", + }, + }, + transactionInfo: { + hash: "fakeHash1", + }, + // transaction must contain correct JSON + message: { + payload: JSON.stringify({ + contract: "elevate:auth", + version: 1, + challenge: "fakeChallenge", + }), + } + }); + Factory.createFromJSON( + JSON.stringify({ + contract: "elevate:auth", + version: 1, + challenge: "fakeChallenge", + }) + ).inputs = undefined; // act - const validateChallenge = () => (authService as any).validateChallenge({ + const result = (authService as any).validateChallenge({ challenge: "test-challenge", sub: "test-sub", }); // assert - expect(validateChallenge).rejects.toEqual(httpUnauthorizedError); + expect(result).rejects.toThrow(httpUnauthorizedError); + expect(createFromJSONCall).toHaveBeenCalled(); }); it("should respond with error given incorrect contract", () => { @@ -763,6 +803,55 @@ describe("common/AuthService", () => { expect(createFromJSONCall).toHaveBeenCalled(); expect(result).toStrictEqual(expectedResult); }); + + it("should create the `accounts` document for socket authentication", async () => { + // prepare + (authService as any).cookie = { + name: "fake-dapp", + domain: "fake-dapp-host" + }; + (authService as any).challengesService = { + exists: jest.fn().mockResolvedValue(false), + createOrUpdate: jest.fn().mockResolvedValue({}), + }; + (authService as any).findRecentChallenge = + jest.fn().mockResolvedValue({ + signer: { + address: { + plain: () => "NDAPPH6ZGD4D6LBWFLGFZUT2KQ5OLBLU32K3HNY", + }, + }, + transactionInfo: { + hash: "fakeHash1", + }, + // transaction must contain correct JSON + message: { + payload: JSON.stringify({ + contract: "elevate:auth", + version: 1, + challenge: "fakeChallenge", + }), + } + }); + const accountsServiceGetOrCreateForAuthCall = jest + .spyOn(accountsService, "getOrCreateForAuth") + .mockResolvedValue({} as AccountDocument); + const expectedResult = { + address: "NDAPPH6ZGD4D6LBWFLGFZUT2KQ5OLBLU32K3HNY", + referralCode: "123456ab", + sub: "test-sub", + }; + + // act + const result = await (authService as any).validateChallenge({ + challenge: "test-challenge", + sub: "test-sub", + }, false); + + // assert + expect(accountsServiceGetOrCreateForAuthCall).toHaveBeenCalled(); + expect(result).toStrictEqual(expectedResult); + }); }); describe("getAccessToken()", () => { diff --git a/runtime/backend/tests/unit/common/services/NetworkService.spec.ts b/runtime/backend/tests/unit/common/services/NetworkService.spec.ts index a00b63aa..73e19c1a 100644 --- a/runtime/backend/tests/unit/common/services/NetworkService.spec.ts +++ b/runtime/backend/tests/unit/common/services/NetworkService.spec.ts @@ -190,6 +190,30 @@ describe("common/NetworkService", () => { }); }); + describe("getChainInfo()", () => { + it("should call delegatePromises() and return first result set", async () => { + // prepare + const chainRepositoryGetChainInfoCall = jest.fn() + .mockReturnValue({ toPromise: jest.fn() } as any); + service.chainRepository = { + getChainInfo: chainRepositoryGetChainInfoCall, + }; + const delegatePromisesCall = jest + .spyOn(service, "delegatePromises") + .mockResolvedValue({ + shift: () => ({}) + } as any); + + // act + const result = await service.getChainInfo(); + + // assert + expect(chainRepositoryGetChainInfoCall).toHaveBeenCalledTimes(1); + expect(delegatePromisesCall).toHaveBeenCalledTimes(1); + expect(result).toEqual({}); + }); + }); + describe("getNextAvailableNode()", () => { it("return node if node is healthy", async () => { // prepare diff --git a/runtime/backend/tests/unit/oauth/services/OAuthService.spec.ts b/runtime/backend/tests/unit/oauth/services/OAuthService.spec.ts index 95632e54..1b982e74 100644 --- a/runtime/backend/tests/unit/oauth/services/OAuthService.spec.ts +++ b/runtime/backend/tests/unit/oauth/services/OAuthService.spec.ts @@ -40,6 +40,7 @@ import { } from "../../../../src/common/models/AccountIntegrationSchema"; import { OAuthEntityType } from "../../../../src/oauth/drivers/OAuthEntity"; import { HttpMethod } from "../../../../src/common/drivers/HttpRequestHandler"; +import { AccessTokenDTO } from "../../../../src/common/models/AccessTokenDTO"; describe("common/OAuthService", () => { let mockDate: Date; @@ -609,7 +610,7 @@ describe("common/OAuthService", () => { it("should update integration entry with encrypted tokens", async () => { // prepare - const expectedAccessTokenDTO: any = {}; + const expectedAccessTokenDTO: any = new AccessTokenDTO(); expectedAccessTokenDTO.remoteIdentifier = "fake-identifier"; expectedAccessTokenDTO.encAccessToken = "fake-encrypted-payload"; expectedAccessTokenDTO.encRefreshToken = "fake-encrypted-payload"; diff --git a/runtime/backend/tests/unit/payout/schedulers/ActivityPayouts/PrepareActivityPayouts.spec.ts b/runtime/backend/tests/unit/payout/schedulers/ActivityPayouts/PrepareActivityPayouts.spec.ts index 0dab5756..82af7208 100644 --- a/runtime/backend/tests/unit/payout/schedulers/ActivityPayouts/PrepareActivityPayouts.spec.ts +++ b/runtime/backend/tests/unit/payout/schedulers/ActivityPayouts/PrepareActivityPayouts.spec.ts @@ -555,7 +555,7 @@ describe("payout/PrepareActivityPayouts", () => { { count: 100, expectedPc: 1.15 }, { count: 150, expectedPc: 1.15 }, { count: -1, expectedPc: 1 }, - { count: 0, expectedPc: 1 }, + { count: null, expectedPc: 1 }, ]; fakeReferralCounts.map( diff --git a/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/BroadcastBoosterPayouts.spec.ts b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/BroadcastBoosterPayouts.spec.ts new file mode 100644 index 00000000..82b0aede --- /dev/null +++ b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/BroadcastBoosterPayouts.spec.ts @@ -0,0 +1,179 @@ +/** + * 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 { Test, TestingModule } from "@nestjs/testing"; +import { getModelToken } from "@nestjs/mongoose"; +import { ConfigService } from "@nestjs/config"; + +// internal dependencies +import { BroadcastBoosterPayouts } from "../../../../../src/payout/schedulers/BoosterPayouts/BroadcastBoosterPayouts"; +import { StateService } from "../../../../../src/common/services/StateService"; +import { QueryService } from "../../../../../src/common/services/QueryService"; +import { PayoutsService } from "../../../../../src/payout/services/PayoutsService"; +import { SignerService } from "../../../../../src/payout/services/SignerService"; +import { NetworkService } from "../../../../../src/common/services/NetworkService"; +import { ActivitiesService } from "../../../../../src/users/services/ActivitiesService"; +import { LogService } from "../../../../../src/common/services/LogService"; +import { AssetsService } from "../../../../../src/discovery/services/AssetsService"; +import { MockModel } from "../../../../mocks/global"; +import { PayoutDocument, PayoutQuery } from "../../../../../src/payout/models/PayoutSchema"; +import { PayoutState } from "../../../../../src/payout/models/PayoutStatusDTO"; +import { PaginatedResultDTO } from "../../../../../src/common/models/PaginatedResultDTO"; + +describe("payout/BroadcastBoosterPayouts", () => { + let command: BroadcastBoosterPayouts; + let payoutsService: PayoutsService; + let logService: LogService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BroadcastBoosterPayouts, + ConfigService, + StateService, + QueryService, + PayoutsService, + SignerService, + NetworkService, + ActivitiesService, + LogService, + AssetsService, + { + provide: getModelToken("Account"), + useValue: MockModel, + }, + { + provide: getModelToken("State"), + useValue: MockModel, + }, + { + provide: getModelToken("Payout"), + useValue: MockModel, + }, + { + provide: getModelToken("Activity"), + useValue: MockModel, + }, + { + provide: getModelToken("Asset"), + useValue: MockModel, + }, + ] + }).compile(); + + command = module.get(BroadcastBoosterPayouts); + payoutsService = module.get(PayoutsService); + logService = module.get(LogService); + }); + + it("should be defined", () => { + expect(command).toBeDefined(); + }); + + describe("get signature()", () => { + it("should return correct result", () => { + // act + const result = (command as any).signature; + + // assert + expect(result).toBe("BroadcastBoosterPayouts"); + }); + }); + + describe("get collection()", () => { + it("should return correct result", () => { + // act + const result = (command as any).collection; + + // assert + expect(result).toBe("accounts"); + }); + }); + + describe("countSubjects()", () => { + it("should call and return result of count() from PayoutsService", async () => { + // prepare + const payoutsServiceCountCall = jest + .spyOn(payoutsService, "count") + .mockResolvedValue(1); + + // act + const result = await (command as any).countSubjects(); + + // assert + expect(result).toBe(1); + expect(payoutsServiceCountCall).toHaveBeenNthCalledWith( + 1, + new PayoutQuery({ + payoutState: PayoutState.Prepared, + subjectCollection: "accounts", + } as PayoutDocument), + ); + }); + }); + + describe("fetchSubjects()", () => { + it("should call and return result of find() from PayoutsService", async () => { + // prepare + const expectedResult = { data: {} } as PaginatedResultDTO; + const payoutsServiceFindCall = jest + .spyOn(payoutsService, "find") + .mockResolvedValue(expectedResult); + + // act + const result = await (command as any).fetchSubjects(10); + + // assert + expect(payoutsServiceFindCall).toHaveBeenNthCalledWith( + 1, + new PayoutQuery( + { + payoutState: PayoutState.Prepared, + subjectCollection: "accounts", + } as PayoutDocument, + { + pageNumber: 1, + pageSize: 10, + sort: "createdAt", + order: "asc", + }, + ), + ) + expect(result).toEqual(expectedResult.data); + }); + }); + + describe("runAsScheduler()", () => { + it("shoud run correctly and call correct methods", async () => { + // prepare + const logServiceSetModuleCall = jest + .spyOn(logService, "setModule"); + const debugLogCall = jest + .spyOn((command as any), "debugLog"); + const runCall = jest + .spyOn(command, "run").mockResolvedValue(); + + // act + await command.runAsScheduler(); + + // assert + expect(logServiceSetModuleCall).toHaveBeenNthCalledWith(1, "payout/BroadcastBoosterPayouts"); + expect(debugLogCall).toHaveBeenNthCalledWith(1, "Starting payout broadcast for boosters"); + expect(runCall).toHaveBeenNthCalledWith( + 1, + ["accounts"], + { + maxCount: 3, + debug: true, + } + ) + }); + }); +}); \ No newline at end of file diff --git a/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost10Payouts.spec.ts b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost10Payouts.spec.ts new file mode 100644 index 00000000..ed9cdb3d --- /dev/null +++ b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost10Payouts.spec.ts @@ -0,0 +1,147 @@ +/** + * 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 { Test, TestingModule } from "@nestjs/testing"; +import { getModelToken } from "@nestjs/mongoose"; +import { ConfigService } from "@nestjs/config"; +import { EventEmitter2 } from "@nestjs/event-emitter"; + +// internal dependencies +import { MockModel } from "../../../../mocks/global"; + +// common scope +import { StateService } from "../../../../../src/common/services/StateService"; +import { QueryService } from "../../../../../src/common/services/QueryService"; +import { LogService } from "../../../../../src/common/services/LogService"; +import { AccountDocument, AccountModel, AccountQuery } from "../../../../../src/common/models/AccountSchema"; + +// discovery scope +import { AssetsService } from "../../../../../src/discovery/services/AssetsService"; + +// payout scope +import { PayoutsService } from "../../../../../src/payout/services/PayoutsService"; +import { SignerService } from "../../../../../src/payout/services/SignerService"; +import { MathService } from "../../../../../src/payout/services/MathService"; +import { AccountSessionsService } from "../../../../../src/common/services/AccountSessionsService"; +import { PayoutCommandOptions } from "../../../../../src/payout/schedulers/PayoutCommand"; + +import { PrepareBoost10Payouts } from "../../../../../src/payout/schedulers/BoosterPayouts/PrepareBoost10Payouts"; + +describe("payout/PrepareBoost5Payouts", () => { + let command: PrepareBoost10Payouts; + let logger: LogService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PrepareBoost10Payouts, + ConfigService, + StateService, + QueryService, + PayoutsService, + SignerService, + AssetsService, + MathService, + EventEmitter2, + AccountSessionsService, + { + provide: getModelToken("Payout"), + useValue: MockModel, + }, + { + provide: getModelToken("Asset"), + useValue: MockModel, + }, + { + provide: getModelToken("Account"), + useValue: MockModel, + }, + { + provide: getModelToken("State"), + useValue: MockModel, + }, + { + provide: LogService, + useValue: { + setContext: jest.fn(), + setModule: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, + }, + { + provide: getModelToken("AccountSession"), + useValue: MockModel, + }, + ] + }).compile(); + + command = module.get(PrepareBoost10Payouts); + logger = module.get(LogService); + }); + + it("should be defined", () => { + expect(command).toBeDefined(); + }); + + describe("get command()", () => { + it("should return correct result", () => { + // act + const result = (command as any).command; + + // assert + expect(result).toBe("PrepareBoost10Payouts"); + }); + }); + + describe("get minReferred()", () => { + it("should return correct result", () => { + // prepare + const expectedResult = 50; + + // act + const result = (command as any).minReferred; + + // assert + expect(result).toBe(expectedResult); + }); + }); + + describe("runAsScheduler()", () => { + it("should call correct methods and run correctly", async () => { + // prepare + const loggerSetModuleCall = jest + .spyOn(logger, "setModule") + .mockReturnValue(logger); + const debugLogCall = jest + .spyOn((command as any), "debugLog") + .mockReturnValue(true); + const runCall = jest + .spyOn(command, "run") + .mockResolvedValue(); + + // act + await command.runAsScheduler(); + + // assert + expect(loggerSetModuleCall).toHaveBeenNthCalledWith(1, "payout/PrepareBoost10Payouts"); + expect(debugLogCall).toHaveBeenNthCalledWith(1, `Starting payout preparation for booster type: boost10`); + expect(debugLogCall).toHaveBeenNthCalledWith(2, `Total number of boost10 payouts prepared: "0"`); + expect(runCall).toHaveBeenNthCalledWith( + 1, + ["accounts"], + { + debug: false, + } as PayoutCommandOptions + ); + }); + }); +}); \ No newline at end of file diff --git a/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost15Payouts.spec.ts b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost15Payouts.spec.ts new file mode 100644 index 00000000..7d69884b --- /dev/null +++ b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost15Payouts.spec.ts @@ -0,0 +1,147 @@ +/** + * 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 { Test, TestingModule } from "@nestjs/testing"; +import { getModelToken } from "@nestjs/mongoose"; +import { ConfigService } from "@nestjs/config"; +import { EventEmitter2 } from "@nestjs/event-emitter"; + +// internal dependencies +import { MockModel } from "../../../../mocks/global"; + +// common scope +import { StateService } from "../../../../../src/common/services/StateService"; +import { QueryService } from "../../../../../src/common/services/QueryService"; +import { LogService } from "../../../../../src/common/services/LogService"; +import { AccountDocument, AccountModel, AccountQuery } from "../../../../../src/common/models/AccountSchema"; + +// discovery scope +import { AssetsService } from "../../../../../src/discovery/services/AssetsService"; + +// payout scope +import { PayoutsService } from "../../../../../src/payout/services/PayoutsService"; +import { SignerService } from "../../../../../src/payout/services/SignerService"; +import { MathService } from "../../../../../src/payout/services/MathService"; +import { AccountSessionsService } from "../../../../../src/common/services/AccountSessionsService"; +import { PayoutCommandOptions } from "../../../../../src/payout/schedulers/PayoutCommand"; + +import { PrepareBoost15Payouts } from "../../../../../src/payout/schedulers/BoosterPayouts/PrepareBoost15Payouts"; + +describe("payout/PrepareBoost5Payouts", () => { + let command: PrepareBoost15Payouts; + let logger: LogService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PrepareBoost15Payouts, + ConfigService, + StateService, + QueryService, + PayoutsService, + SignerService, + AssetsService, + MathService, + EventEmitter2, + AccountSessionsService, + { + provide: getModelToken("Payout"), + useValue: MockModel, + }, + { + provide: getModelToken("Asset"), + useValue: MockModel, + }, + { + provide: getModelToken("Account"), + useValue: MockModel, + }, + { + provide: getModelToken("State"), + useValue: MockModel, + }, + { + provide: LogService, + useValue: { + setContext: jest.fn(), + setModule: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, + }, + { + provide: getModelToken("AccountSession"), + useValue: MockModel, + }, + ] + }).compile(); + + command = module.get(PrepareBoost15Payouts); + logger = module.get(LogService); + }); + + it("should be defined", () => { + expect(command).toBeDefined(); + }); + + describe("get command()", () => { + it("should return correct result", () => { + // act + const result = (command as any).command; + + // assert + expect(result).toBe("PrepareBoost15Payouts"); + }); + }); + + describe("get minReferred()", () => { + it("should return correct result", () => { + // prepare + const expectedResult = 100; + + // act + const result = (command as any).minReferred; + + // assert + expect(result).toBe(expectedResult); + }); + }); + + describe("runAsScheduler()", () => { + it("should call correct methods and run correctly", async () => { + // prepare + const loggerSetModuleCall = jest + .spyOn(logger, "setModule") + .mockReturnValue(logger); + const debugLogCall = jest + .spyOn((command as any), "debugLog") + .mockReturnValue(true); + const runCall = jest + .spyOn(command, "run") + .mockResolvedValue(); + + // act + await command.runAsScheduler(); + + // assert + expect(loggerSetModuleCall).toHaveBeenNthCalledWith(1, "payout/PrepareBoost15Payouts"); + expect(debugLogCall).toHaveBeenNthCalledWith(1, `Starting payout preparation for booster type: boost15`); + expect(debugLogCall).toHaveBeenNthCalledWith(2, `Total number of boost15 payouts prepared: "0"`); + expect(runCall).toHaveBeenNthCalledWith( + 1, + ["accounts"], + { + debug: false, + } as PayoutCommandOptions + ); + }); + }); +}); \ No newline at end of file diff --git a/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost5Payouts.spec.ts b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost5Payouts.spec.ts index 7b11993b..03f59f93 100644 --- a/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost5Payouts.spec.ts +++ b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoost5Payouts.spec.ts @@ -175,6 +175,19 @@ describe("payout/PrepareBoost5Payouts", () => { }); }); + describe("get minReferred()", () => { + it("should return correct result", () => { + // prepare + const expectedResult = 10; + + // act + const result = (command as any).minReferred; + + // assert + expect(result).toBe(expectedResult); + }); + }); + describe("get signature()", () => { it("should return correct result", () => { // act diff --git a/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoosterPayouts.spec.ts b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoosterPayouts.spec.ts new file mode 100644 index 00000000..8a9fbea6 --- /dev/null +++ b/runtime/backend/tests/unit/payout/schedulers/BoosterPayouts/PrepareBoosterPayouts.spec.ts @@ -0,0 +1,325 @@ +/** + * 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 { Test, TestingModule } from "@nestjs/testing"; +import { getModelToken } from "@nestjs/mongoose"; +import { ConfigService } from "@nestjs/config"; +import { EventEmitter2 } from "@nestjs/event-emitter"; + +// internal dependencies +import { MockModel } from "../../../../mocks/global"; + +// common scope +import { StateService } from "../../../../../src/common/services/StateService"; +import { QueryService } from "../../../../../src/common/services/QueryService"; +import { LogService } from "../../../../../src/common/services/LogService"; +import { AccountDocument, AccountModel, AccountQuery } from "../../../../../src/common/models/AccountSchema"; + +// discovery scope +import { AssetsService } from "../../../../../src/discovery/services/AssetsService"; + +// payout scope +import { PayoutsService } from "../../../../../src/payout/services/PayoutsService"; +import { SignerService } from "../../../../../src/payout/services/SignerService"; +import { MathService } from "../../../../../src/payout/services/MathService"; +import { AccountSessionsService } from "../../../../../src/common/services/AccountSessionsService"; + +import { PrepareBoosterPayouts } from "../../../../../src/payout/schedulers/BoosterPayouts/PrepareBoosterPayouts"; +import { AssetDocument, AssetQuery } from "../../../../../src/discovery/models/AssetSchema"; + +class MockPrepareBoosterPayout extends PrepareBoosterPayouts { + protected get minReferred(): number { + return 10; + } + protected get command(): string { + return "MockPrepareBossterPayout"; + } + public runAsScheduler = jest.fn(); +} + +describe("payout/PrepareBoost5Payouts", () => { + let command: MockPrepareBoosterPayout; + let assetsService: AssetsService; + let queryService: QueryService< + AccountDocument, + AccountModel + >; + + let logger: LogService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MockPrepareBoosterPayout, + ConfigService, + StateService, + QueryService, + PayoutsService, + SignerService, + AssetsService, + MathService, + EventEmitter2, + AccountSessionsService, + { + provide: getModelToken("Payout"), + useValue: MockModel, + }, + { + provide: getModelToken("Asset"), + useValue: MockModel, + }, + { + provide: getModelToken("Account"), + useValue: MockModel, + }, + { + provide: getModelToken("State"), + useValue: MockModel, + }, + { + provide: LogService, + useValue: { + setContext: jest.fn(), + setModule: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, + }, + { + provide: getModelToken("AccountSession"), + useValue: MockModel, + }, + ] + }).compile(); + + command = module.get(MockPrepareBoosterPayout); + assetsService = module.get(AssetsService); + queryService = module.get>(QueryService); + + logger = module.get(LogService); + }); + + it("should be defined", () => { + expect(command).toBeDefined(); + }); + + describe("verifyAttributionAllowance()", () => { + it("should return false if the booster asset already exists for user address", async () => { + // prepare + const assetsServiceExistsCall = jest + .spyOn(assetsService, "exists") + .mockResolvedValue(true); + (command as any).boosterAsset = { + mosaicId: "test-mosaicId", + }; + + // act + const result = await (command as any).verifyAttributionAllowance({ + address: "test-address", + }); + + // assert + expect(result).toBe(false); + expect(assetsServiceExistsCall).toHaveBeenNthCalledWith( + 1, + new AssetQuery({ + userAddress: "test-address", + mosaicId: "test-mosaicId", + } as AssetDocument), + ); + }); + + it("should return true if the booster asset doesn't exist for user address", async () => { + // prepare + const assetsServiceExistsCall = jest + .spyOn(assetsService, "exists") + .mockResolvedValue(false); + (command as any).boosterAsset = { + mosaicId: "test-mosaicId", + }; + + // act + const result = await (command as any).verifyAttributionAllowance({ + address: "test-address", + }); + + // assert + expect(result).toBe(true); + expect(assetsServiceExistsCall).toHaveBeenNthCalledWith( + 1, + new AssetQuery({ + userAddress: "test-address", + mosaicId: "test-mosaicId", + } as AssetDocument), + ); + }); + }); + + describe("fetchSubjects()", () => { + it("should run correctly and return correct result", async () => { + // prepare + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([ + { _id: { referredBy: "test-referrer" }, count: 10 }, + ] as any); + const queryServiceFindOneCall = jest + .spyOn(queryService, "findOne") + .mockResolvedValue({ address: "test-referrer" } as AccountDocument); + const verifyAttributionAllowanceCall = jest + .spyOn((command as any), "verifyAttributionAllowance") + .mockResolvedValue(true); + const expectedResult = [{ address: "test-referrer" }]; + + // act + const result = await (command as any).fetchSubjects(); + + // assert + expect(result).toEqual(expectedResult); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith( + 1, + [ + { + $match: { + referredBy: { $exists: true }, + }, + }, + { + $group: { + _id: { + referredBy: "$referredBy", + }, + count: { $sum: 1 }, + }, + }, + ], + MockModel, + ); + expect(queryServiceFindOneCall).toHaveBeenNthCalledWith( + 1, + new AccountQuery({ address: "test-referrer" } as AccountDocument), + MockModel, + ); + expect(verifyAttributionAllowanceCall).toHaveBeenNthCalledWith( + 1, + { address: "test-referrer" }, + ); + }); + + it("should disregard if address is null", async () => { + // prepare + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([ + { _id: { referredBy: null }, count: 10 }, + ] as any); + const queryServiceFindOneCall = jest + .spyOn(queryService, "findOne") + .mockResolvedValue({ address: "test-referrer" } as AccountDocument); + const verifyAttributionAllowanceCall = jest + .spyOn((command as any), "verifyAttributionAllowance") + .mockResolvedValue(true); + const expectedResult: AccountDocument[] = []; + + // act + const result = await (command as any).fetchSubjects(); + + // assert + expect(result).toEqual(expectedResult); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith( + 1, + [ + { + $match: { + referredBy: { $exists: true }, + }, + }, + { + $group: { + _id: { + referredBy: "$referredBy", + }, + count: { $sum: 1 }, + }, + }, + ], + MockModel, + ); + expect(queryServiceFindOneCall).toHaveBeenCalledTimes(0); + expect(verifyAttributionAllowanceCall).toHaveBeenCalledTimes(0); + }); + + it("should disregard if referrals count doesn't equal minReferred", async () => { + // prepare + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([ + { _id: { referredBy: "test-referrer" }, count: 9 }, + ] as any); + const queryServiceFindOneCall = jest + .spyOn(queryService, "findOne") + .mockResolvedValue({ address: "test-referrer" } as AccountDocument); + const verifyAttributionAllowanceCall = jest + .spyOn((command as any), "verifyAttributionAllowance") + .mockResolvedValue(true); + const expectedResult: AccountDocument[] = []; + + // act + const result = await (command as any).fetchSubjects(); + + // assert + expect(result).toEqual(expectedResult); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith( + 1, + [ + { + $match: { + referredBy: { $exists: true }, + }, + }, + { + $group: { + _id: { + referredBy: "$referredBy", + }, + count: { $sum: 1 }, + }, + }, + ], + MockModel, + ); + expect(queryServiceFindOneCall).toHaveBeenCalledTimes(0); + expect(verifyAttributionAllowanceCall).toHaveBeenCalledTimes(0); + }); + + it("should throw error if error was caught", () => { + // prepare + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([ + { _id: { referredBy: "test-referrer" }, count: 10 }, + ] as any); + const queryServiceFindOneCall = jest + .spyOn(queryService, "findOne") + .mockResolvedValue({ address: "test-referrer" } as AccountDocument); + const expectedError = new Error("error"); + const verifyAttributionAllowanceCall = jest + .spyOn((command as any), "verifyAttributionAllowance") + .mockRejectedValue(expectedError); + + // act + const result = (command as any).fetchSubjects(); + + // assert + expect(result).rejects.toThrow(expectedError); + }); + }); +}); \ No newline at end of file diff --git a/runtime/backend/tests/unit/payout/services/MathService.spec.ts b/runtime/backend/tests/unit/payout/services/MathService.spec.ts index fbe36d68..9ddb695a 100644 --- a/runtime/backend/tests/unit/payout/services/MathService.spec.ts +++ b/runtime/backend/tests/unit/payout/services/MathService.spec.ts @@ -88,4 +88,36 @@ describe("payout/MathService", () => { }); }); }); + + describe("getRandomVariates()", () => { + it("should run correctly and return correct result", () => { + // prepare + const mathServiceRNG = jest + .spyOn((MathService as any), "RNG") + .mockReturnValue(1); + const mathLogCall = jest + .spyOn(Math, "log") + .mockReturnValue(1); + const mathSqrtCall = jest + .spyOn(Math, "sqrt") + .mockReturnValue(1); + const mathCosCall = jest + .spyOn(Math, "cos") + .mockReturnValue(1); + const mathSinCall = jest + .spyOn(Math, "sin") + .mockReturnValue(0); + + // act + const result = (mathService as any).getRandomVariates(); + + // assert + expect(result).toEqual([1, 0]); + expect(mathServiceRNG).toHaveBeenCalledTimes(2); + expect(mathSqrtCall).toHaveBeenNthCalledWith(1, -2.0 * 1); + expect(mathLogCall).toHaveBeenNthCalledWith(1, 1); + expect(mathCosCall).toHaveBeenNthCalledWith(1, 2.0 * Math.PI * 1); + expect(mathSinCall).toHaveBeenNthCalledWith(1, 2.0 * Math.PI * 1); + }); + }); }); \ No newline at end of file diff --git a/runtime/backend/tests/unit/statistics/schedulers/UserAggregation.spec.ts b/runtime/backend/tests/unit/statistics/schedulers/UserAggregation.spec.ts index 5edac397..147b9f49 100644 --- a/runtime/backend/tests/unit/statistics/schedulers/UserAggregation.spec.ts +++ b/runtime/backend/tests/unit/statistics/schedulers/UserAggregation.spec.ts @@ -298,7 +298,7 @@ describe("statistics/UserAggregation", () => { await service.aggregate( { periodFormat: "D", - debug: true + debug: false, } ); @@ -340,6 +340,71 @@ describe("statistics/UserAggregation", () => { }, ); }); + + it("should not merge with previous entry if it's not available", async () => { + // prepare + const aggregateMocks = [ + { + _id: "test-address1", + totalAssetsAmount: 123, + totalSecondsPracticed: 456, + }, + ]; + const serviceCreateAggregationQueryCall = jest + .spyOn((service as any), "createAggregationQuery") + .mockReturnValue({}); + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue(aggregateMocks as any); + const serviceGeneratePeriodCall = jest + .spyOn((service as any), "getNextPeriod") + .mockReturnValue("test-period-string"); + const expectedDate = new Date(); + const statisticsServiceFindOneCall = jest + .spyOn(statisticsService, "findOne") + .mockResolvedValue(null); + + // act + await service.aggregate( + { + periodFormat: "D", + debug: false, + } + ); + + // assert + expect(serviceCreateAggregationQueryCall).toHaveBeenCalledTimes(1); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith(1, {}, MockModel); + expect(serviceGeneratePeriodCall).toHaveBeenNthCalledWith(1, expectedDate); + expect(statisticsServiceFindOneCall).toHaveBeenCalledTimes(1); + expect(statisticsServiceFindOneCall).toHaveBeenNthCalledWith( + 1, + new StatisticsQuery({ + type: "user", + period: "test-period-string", + periodFormat: "D", + address: aggregateMocks[0]._id, // <-- uses correct address + } as StatisticsDocument), + ) + expect(statisticsCreateOrUpdateMock).toHaveBeenCalledTimes(1); + expect(statisticsCreateOrUpdateMock).toHaveBeenNthCalledWith( + 1, + new StatisticsQuery({ + type: "user", + period: "test-period-string", + address: aggregateMocks[0]._id, // <-- uses correct address + } as StatisticsDocument), + { + periodFormat: "D", + amount: aggregateMocks[0].totalAssetsAmount, + data: { + totalEarned: aggregateMocks[0].totalAssetsAmount, + totalPracticedMinutes: Math.ceil(aggregateMocks[0].totalSecondsPracticed/60), + ...{}, + }, + }, + ); + }); }); describe("getNextPeriod()", () => { diff --git a/runtime/backend/tests/unit/statistics/schedulers/UserTopActivities.spec.ts b/runtime/backend/tests/unit/statistics/schedulers/UserTopActivities.spec.ts index 763265d2..653958b5 100644 --- a/runtime/backend/tests/unit/statistics/schedulers/UserTopActivities.spec.ts +++ b/runtime/backend/tests/unit/statistics/schedulers/UserTopActivities.spec.ts @@ -21,6 +21,7 @@ import { StatisticsDocument, StatisticsModel, StatisticsQuery } from "../../../. import { UserTopActivities } from "../../../../src/statistics/schedulers/UserTopActivities/UserTopActivities"; import { StatisticsService } from "../../../../src/statistics/services/StatisticsService"; import { LogService } from "../../../../src/common/services/LogService"; +import { StatisticsCommandOptions } from "../../../../src/statistics/schedulers/StatisticsCommand"; describe("statistics/UserTopActivities", () => { let service: UserTopActivities; @@ -89,4 +90,229 @@ describe("statistics/UserTopActivities", () => { expect(result).toBe("UserTopActivities"); }); }); + + describe("get signature()", () => { + it("should return correct result", () => { + // prepare + const expectedResult = "UserTopActivities"; + + // act + const result = (service as any).signature; + + // assert + expect(result).toBe(expectedResult); + }); + }); + + describe("aggregate()", () => { + it("should run correctly and return correct result", async () => { + // prepare + const createAggregationQueryCall = jest + .spyOn((service as any), "createAggregationQuery") + .mockResolvedValue({}); + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([ + { _id: { address: "test-address", sportType: "test-sport" } } + ] as any); + const debugLogCall = jest + .spyOn((service as any), "debugLog"); + const getNextPeriodCall = jest + .spyOn((service as any), "getNextPeriod") + .mockReturnValue("test-getNextPeriod"); + const statisticsServiceFindOneCall = jest + .spyOn(statisticsService, "findOne") + .mockResolvedValue({} as StatisticsDocument); + const statisticsServiceCreateOrUpdateCall = jest + .spyOn(statisticsService, "createOrUpdate") + .mockResolvedValue({} as StatisticsDocument); + + // act + await service.aggregate({ debug: true } as StatisticsCommandOptions); + + // assert + expect(createAggregationQueryCall).toHaveBeenCalledTimes(1); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith(1, {}, MockModel); + expect(debugLogCall).toHaveBeenNthCalledWith(1, "Found 1 aggregation subjects"); + expect(getNextPeriodCall).toHaveBeenNthCalledWith(1, new Date()); + expect(statisticsServiceFindOneCall).toHaveBeenNthCalledWith( + 1, + new StatisticsQuery({ + address: "test-address", + period: "test-getNextPeriod", + periodFormat: "D", + type: "user", + } as StatisticsDocument) + ); + expect(statisticsServiceCreateOrUpdateCall).toHaveBeenNthCalledWith( + 1, + new StatisticsQuery({ + address: "test-address", + period: "test-getNextPeriod", + type: "user", + } as StatisticsDocument), + { + periodFormat: "D", + data: { + // merge with previous entry if available + ...{}, + topActivities: ["test-sport"], + }, + }, + ) + }); + + it("should print message if no aggregation subject found", async () => { + // prepare + const createAggregationQueryCall = jest + .spyOn((service as any), "createAggregationQuery") + .mockResolvedValue({}); + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([] as any); + const debugLogCall = jest + .spyOn((service as any), "debugLog"); + const getNextPeriodCall = jest + .spyOn((service as any), "getNextPeriod") + .mockReturnValue("test-getNextPeriod"); + const statisticsServiceFindOneCall = jest + .spyOn(statisticsService, "findOne") + .mockResolvedValue({} as StatisticsDocument); + const statisticsServiceCreateOrUpdateCall = jest + .spyOn(statisticsService, "createOrUpdate") + .mockResolvedValue({} as StatisticsDocument); + + // act + await service.aggregate({ debug: true } as StatisticsCommandOptions); + + // assert + expect(createAggregationQueryCall).toHaveBeenCalledTimes(1); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith(1, {}, MockModel); + expect(debugLogCall).toHaveBeenNthCalledWith(1, "No aggregation subjects found"); + expect(getNextPeriodCall).toHaveBeenNthCalledWith(1, new Date()); + expect(statisticsServiceFindOneCall).toHaveBeenCalledTimes(0); + expect(statisticsServiceCreateOrUpdateCall).toHaveBeenCalledTimes(0); + }); + + it("should not merge with previous entry if it doesn't exist", async () => { + // prepare + const createAggregationQueryCall = jest + .spyOn((service as any), "createAggregationQuery") + .mockResolvedValue({}); + const queryServiceAggregateCall = jest + .spyOn(queryService, "aggregate") + .mockResolvedValue([ + { _id: { address: "test-address", sportType: "test-sport" } } + ] as any); + const debugLogCall = jest + .spyOn((service as any), "debugLog"); + const getNextPeriodCall = jest + .spyOn((service as any), "getNextPeriod") + .mockReturnValue("test-getNextPeriod"); + const statisticsServiceFindOneCall = jest + .spyOn(statisticsService, "findOne") + .mockResolvedValue(null); + const statisticsServiceCreateOrUpdateCall = jest + .spyOn(statisticsService, "createOrUpdate") + .mockResolvedValue({} as StatisticsDocument); + + // act + await service.aggregate({ debug: true } as StatisticsCommandOptions); + + // assert + expect(createAggregationQueryCall).toHaveBeenCalledTimes(1); + expect(queryServiceAggregateCall).toHaveBeenNthCalledWith(1, {}, MockModel); + expect(debugLogCall).toHaveBeenNthCalledWith(1, "Found 1 aggregation subjects"); + expect(getNextPeriodCall).toHaveBeenNthCalledWith(1, new Date()); + expect(statisticsServiceFindOneCall).toHaveBeenNthCalledWith( + 1, + new StatisticsQuery({ + address: "test-address", + period: "test-getNextPeriod", + periodFormat: "D", + type: "user", + } as StatisticsDocument) + ); + expect(statisticsServiceCreateOrUpdateCall).toHaveBeenNthCalledWith( + 1, + new StatisticsQuery({ + address: "test-address", + period: "test-getNextPeriod", + type: "user", + } as StatisticsDocument), + { + periodFormat: "D", + data: { + // merge with previous entry if available + ...{}, + topActivities: ["test-sport"], + }, + }, + ) + }); + }); + + describe("runAsScheduler()", () => { + it("should run correctly", async () => { + // prepare + const debugLogCall = jest + .spyOn((service as any), "debugLog"); + const runCall = jest + .spyOn(service, "run") + .mockResolvedValue(); + + // act + await service.runAsScheduler(); + + // assert + expect(logger.setModule).toHaveBeenNthCalledWith(1, "statistics/UserTopActivities"); + expect(debugLogCall).toHaveBeenNthCalledWith(1, `Starting user aggregation type: D`); + expect(runCall).toHaveBeenNthCalledWith(1, ["user"], { debug: false }); + }); + }); + + describe("createAggregationQuery()", () => { + it("should return correct result", async () => { + // prepare + const expectedResult = [ + { + $match: { address: { $exists: true } }, + }, + { + $group: { + _id: { + address: "$address", + sportType: "$activityData.sport", + }, + count: { $sum: 1 }, + }, + }, + { + // sort by count DESC + $sort: { + count: -1, + }, + }, + ]; + + // act + const result = await (service as any).createAggregationQuery(); + + // assert + expect(result).toEqual(expectedResult); + }); + }); + + describe("getNextPeriod()", () => { + it("should return correct result", () => { + // prepare + const expectedResult = "20220201"; + + // act + const result = (service as any).getNextPeriod(new Date()); + + // assert + expect(result).toBe(expectedResult); + }); + }); }); diff --git a/runtime/backend/tests/unit/statistics/services/StatisticsService.spec.ts b/runtime/backend/tests/unit/statistics/services/StatisticsService.spec.ts index 5268c2bd..73c1c9d2 100644 --- a/runtime/backend/tests/unit/statistics/services/StatisticsService.spec.ts +++ b/runtime/backend/tests/unit/statistics/services/StatisticsService.spec.ts @@ -123,6 +123,56 @@ describe('statistics/StatisticsService', () => { }); }); + describe("findOrFill()", () => { + it("should return paginated result if it exists", async () => { + // prepare + const expectedResult = { data: [{}] } as PaginatedResultDTO; + const statisticsQuery = new StatisticsQuery(); + const queryServiceFindCall = jest + .spyOn(queryService, "find") + .mockResolvedValue(expectedResult); + + // act + const result = await service.findOrFill(statisticsQuery); + + // assert + expect(result).toEqual(expectedResult); + expect(queryServiceFindCall).toHaveBeenNthCalledWith(1, statisticsQuery, MockModel); + }); + + it("should return correct result if paginated result doesn't exist", async () => { + // prepare + const expectedResult = { data: [{}] } as PaginatedResultDTO; + const statisticsQuery = new StatisticsQuery(); + const queryServiceFindCall = jest + .spyOn(queryService, "find") + .mockResolvedValueOnce({} as PaginatedResultDTO) + .mockResolvedValue(expectedResult); + + // act + const result = await service.findOrFill(statisticsQuery); + + // assert + expect(result).toEqual(expectedResult); + expect(queryServiceFindCall).toHaveBeenNthCalledWith(1, statisticsQuery, MockModel); + expect(queryServiceFindCall).toHaveBeenNthCalledWith( + 2, + new StatisticsQuery( + { + type: "leaderboard", + } as StatisticsDocument, + { + pageSize: 3, + pageNumber: 1, + sort: "position", + order: "asc", + }, + ), + MockModel, + ); + }); + }); + describe("createOrUpdate()", () => { it("should call createOrUpdate() from queryService", async () => { // prepare diff --git a/runtime/backend/tests/unit/users/models/ActivitySchema.spec.ts b/runtime/backend/tests/unit/users/models/ActivitySchema.spec.ts index c5934a3b..b17e834e 100644 --- a/runtime/backend/tests/unit/users/models/ActivitySchema.spec.ts +++ b/runtime/backend/tests/unit/users/models/ActivitySchema.spec.ts @@ -52,10 +52,31 @@ describe("users/ActivitySchema", () => { // prepare const address = "test-address"; const slug = "test-slug"; + const activityData = { + key: "value", + distance: 1, + sport: "test-sport", + elevation: 1, + elapsedTime: 123, + }; + const activityAssets = [{ asset: "test-activityAssets" }]; + const provider = "test-provider"; const activity: Activity = new Activity(); (activity as any).address = address; (activity as any).slug = slug; - const expectedResult = { address, slug }; + (activity as any).activityData = activityData; + (activity as any).activityAssets = activityAssets; + (activity as any).provider = provider; + const expectedResult = { + address, + slug, + assets: activityAssets, + distance: activityData.distance, + sport: activityData.sport, + elevationGain: activityData.elevation, + elapsedTime: activityData.elapsedTime, + provider, + }; // act const result = Activity.fillDTO(