From 48ceac949dec595a6ad7fbab2e7e1dd8261d5a82 Mon Sep 17 00:00:00 2001 From: 0xmad <0xmad@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:14:21 -0600 Subject: [PATCH] test: add tests for providers package --- packages/providers/jest.config.js | 19 +++ packages/providers/package.json | 5 +- .../src/sdk/CryptKeeperInjectedProvider.ts | 4 +- .../CryptKeeperInjectedProvider.test.ts | 78 +++++++++++ .../initializeInjectedProvider.test.ts | 65 +++++++++ .../src/services/event/EventEmitter.ts | 12 +- .../event/__tests__/EventEmitter.test.ts | 41 ++++++ .../handler/__tests__/Handler.test.ts | 132 ++++++++++++++++++ .../providers/src/services/handler/index.ts | 39 +++--- .../providers/src/services/handler/types.ts | 12 ++ packages/providers/src/services/index.ts | 1 + packages/providers/src/setupTests.ts | 10 ++ 12 files changed, 390 insertions(+), 28 deletions(-) create mode 100644 packages/providers/jest.config.js create mode 100644 packages/providers/src/sdk/__tests__/CryptKeeperInjectedProvider.test.ts create mode 100644 packages/providers/src/sdk/__tests__/initializeInjectedProvider.test.ts create mode 100644 packages/providers/src/services/event/__tests__/EventEmitter.test.ts create mode 100644 packages/providers/src/services/handler/__tests__/Handler.test.ts create mode 100644 packages/providers/src/setupTests.ts diff --git a/packages/providers/jest.config.js b/packages/providers/jest.config.js new file mode 100644 index 000000000..3bec2ddfc --- /dev/null +++ b/packages/providers/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + preset: "../../jest-preset.js", + displayName: "@cryptkeeperzk/providers", + setupFilesAfterEnv: ["/src/setupTests.ts"], + moduleNameMapper: { + "@src/(.*)$": "/src/$1", + }, + moduleFileExtensions: ["ts", "js"], + collectCoverageFrom: ["src/**/*.{ts,js}"], + coveragePathIgnorePatterns: ["/node_modules/", "/test/", "/__tests__/", "./src/index.ts"], + coverageThreshold: { + global: { + statements: 95, + branches: 95, + functions: 95, + lines: 95, + }, + }, +}; diff --git a/packages/providers/package.json b/packages/providers/package.json index fbabcc331..572bb216f 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -30,10 +30,13 @@ "publish:package": "pnpm publish --access=public --no-git-checks", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "pnpm run lint --fix", + "test": "jest", + "test:coverage": "pnpm run test --coverage", "prettier": "prettier -c . --ignore-path ../../.prettierignore", "prettier:fix": "prettier -w . --ignore-path ../../.prettierignore", "types": "tsc -p tsconfig.json --noEmit", - "githook:precommit": "lint-staged && pnpm run types" + "githook:precommit": "lint-staged && pnpm run types", + "githook:prepush": "pnpm run test:coverage" }, "dependencies": { "@cryptkeeperzk/types": "workspace:^", diff --git a/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts b/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts index 4abbcace7..2018f1770 100644 --- a/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts +++ b/packages/providers/src/sdk/CryptKeeperInjectedProvider.ts @@ -2,7 +2,7 @@ import type { ICryptKeeperInjectedProvider } from "./interface"; import type { IInjectedMessageData, IInjectedProviderRequest } from "@cryptkeeperzk/types"; import { RPCExternalAction } from "../constants"; -import { type EventHandler, type EventName, Handler } from "../services"; +import { type EventHandler, type EventName, type IHandler, Handler } from "../services"; /** * Represents the CryptKeeper provider that is injected into the application. @@ -14,7 +14,7 @@ export class CryptKeeperInjectedProvider implements ICryptKeeperInjectedProvider /** * Handler service */ - private readonly handler: Handler; + private readonly handler: IHandler; /** * Indicates whether the provider is CryptKeeper. diff --git a/packages/providers/src/sdk/__tests__/CryptKeeperInjectedProvider.test.ts b/packages/providers/src/sdk/__tests__/CryptKeeperInjectedProvider.test.ts new file mode 100644 index 000000000..8c67042fd --- /dev/null +++ b/packages/providers/src/sdk/__tests__/CryptKeeperInjectedProvider.test.ts @@ -0,0 +1,78 @@ +/** + * @jest-environment jsdom + */ +import { RPCExternalAction } from "@src/constants"; + +import type { IInjectedMessageData } from "@cryptkeeperzk/types"; + +import { CryptKeeperInjectedProvider } from ".."; +import { EventName, Handler } from "../../services"; + +jest.mock("nanoevents", (): unknown => ({ + createNanoEvents: jest.fn(), +})); + +jest.mock("../../services", (): unknown => ({ + ...jest.requireActual("../../services"), + Handler: jest.fn(), +})); + +describe("sdk/CryptKeeperInjectedProvider", () => { + const defaultHandler = { + request: jest.fn(), + eventResponser: jest.fn(), + on: jest.fn(), + emit: jest.fn(), + cleanListeners: jest.fn(), + getConnectedOrigin: jest.fn(), + }; + + beforeEach(() => { + (Handler as jest.Mock).mockReturnValue(defaultHandler); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should connect properly", async () => { + const provider = new CryptKeeperInjectedProvider(); + + await provider.connect(); + + expect(defaultHandler.request).toHaveBeenCalledTimes(1); + expect(defaultHandler.request).toHaveBeenCalledWith({ + method: RPCExternalAction.CONNECT, + payload: { + isChangeIdentity: false, + urlOrigin: undefined, + }, + }); + }); + + test("should request rpc properly", async () => { + const provider = new CryptKeeperInjectedProvider(); + + await provider.request({ method: RPCExternalAction.GET_CONNECTED_IDENTITY_DATA }); + + expect(defaultHandler.request).toHaveBeenCalledTimes(1); + expect(defaultHandler.request).toHaveBeenCalledWith({ + method: RPCExternalAction.GET_CONNECTED_IDENTITY_DATA, + }); + }); + + test("should handle events properly", () => { + const provider = new CryptKeeperInjectedProvider(); + + provider.eventResponser({} as MessageEvent); + provider.on(EventName.CONNECT, jest.fn()); + provider.emit(EventName.CONNECT, { data: true }); + provider.cleanListeners(); + + expect(defaultHandler.eventResponser).toHaveBeenCalledTimes(1); + expect(defaultHandler.on).toHaveBeenCalledTimes(1); + expect(defaultHandler.emit).toHaveBeenCalledTimes(1); + expect(defaultHandler.emit).toHaveBeenCalledWith(EventName.CONNECT, { data: true }); + expect(defaultHandler.cleanListeners).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/providers/src/sdk/__tests__/initializeInjectedProvider.test.ts b/packages/providers/src/sdk/__tests__/initializeInjectedProvider.test.ts new file mode 100644 index 000000000..3a8ac2fac --- /dev/null +++ b/packages/providers/src/sdk/__tests__/initializeInjectedProvider.test.ts @@ -0,0 +1,65 @@ +/** + * @jest-environment jsdom + */ + +import { CryptKeeperInjectedProvider, initializeCryptKeeper, initializeCryptKeeperProvider } from ".."; + +jest.mock("nanoevents", (): unknown => ({ + createNanoEvents: jest.fn(), +})); + +jest.mock("../CryptKeeperInjectedProvider", (): unknown => ({ + ...jest.requireActual("../CryptKeeperInjectedProvider"), + CryptKeeperInjectedProvider: jest.fn(), +})); + +describe("sdk/initializeInjectedProvider", () => { + const defaultProvider = { + request: jest.fn(), + eventResponser: jest.fn(), + on: jest.fn(), + emit: jest.fn(), + cleanListeners: jest.fn(), + getConnectedOrigin: jest.fn(), + }; + + beforeEach(() => { + (CryptKeeperInjectedProvider as jest.Mock).mockReturnValue(defaultProvider); + }); + + afterEach(() => { + jest.clearAllMocks(); + window.isCryptkeeperInjected = true; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + window.cryptkeeper = undefined; + }); + + test("should initialize cryptkeeper properly", () => { + const provider = initializeCryptKeeper(); + + expect(provider).toStrictEqual(defaultProvider); + expect(window.cryptkeeper).toStrictEqual(provider); + expect(window.dispatchEvent).toHaveBeenCalledTimes(1); + expect(window.addEventListener).toHaveBeenCalledTimes(1); + }); + + test("should initialize cryptkeeper for extension properly", () => { + const provider = initializeCryptKeeperProvider(); + + expect(provider).toStrictEqual(defaultProvider); + expect(window.cryptkeeper).toStrictEqual(provider); + expect(window.dispatchEvent).toHaveBeenCalledTimes(1); + expect(window.addEventListener).toHaveBeenCalledTimes(1); + }); + + test("should not initialize if cryptkeeper is not injected", () => { + window.isCryptkeeperInjected = false; + const provider = initializeCryptKeeper(); + + expect(provider).toBeUndefined(); + expect(window.cryptkeeper).toBeUndefined(); + expect(window.dispatchEvent).toHaveBeenCalledTimes(0); + expect(window.addEventListener).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/providers/src/services/event/EventEmitter.ts b/packages/providers/src/services/event/EventEmitter.ts index b704354d5..7a4f2e6d3 100644 --- a/packages/providers/src/services/event/EventEmitter.ts +++ b/packages/providers/src/services/event/EventEmitter.ts @@ -1,6 +1,6 @@ -import { Emitter, createNanoEvents } from "nanoevents"; +import { type Emitter, createNanoEvents } from "nanoevents"; -import { Events, EventHandler, EventName } from "./types"; +import type { Events, EventHandler, EventName } from "./types"; /** * Event emitter class that allows subscribing to and emitting events. @@ -29,9 +29,9 @@ export class EventEmitter { * @param {EventHandler} cb - The event handler callback function. * @returns {void} */ - on(eventName: EventName, cb: EventHandler): void { + on = (eventName: EventName, cb: EventHandler): void => { this.emitter.on(eventName, cb); - } + }; /** * Emits an event. @@ -49,7 +49,7 @@ export class EventEmitter { * * @returns {void} */ - cleanListeners(): void { + cleanListeners = (): void => { this.emitter.events = {}; - } + }; } diff --git a/packages/providers/src/services/event/__tests__/EventEmitter.test.ts b/packages/providers/src/services/event/__tests__/EventEmitter.test.ts new file mode 100644 index 000000000..c71b30ba5 --- /dev/null +++ b/packages/providers/src/services/event/__tests__/EventEmitter.test.ts @@ -0,0 +1,41 @@ +/** + * @jest-environment jsdom + */ + +/* eslint-disable @typescript-eslint/unbound-method */ +import { type Emitter, createNanoEvents } from "nanoevents"; + +import { EventEmitter, EventName } from ".."; + +jest.mock("nanoevents", (): unknown => ({ + createNanoEvents: jest.fn(), +})); + +describe("services/event", () => { + const defaultEventEmitter: Emitter = { + events: {}, + on: jest.fn(), + emit: jest.fn(), + }; + + const defaultHandler = jest.fn(); + + beforeEach(() => { + (createNanoEvents as jest.Mock).mockReturnValue(defaultEventEmitter); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should handle events properly", () => { + const eventEmitter = new EventEmitter(); + + eventEmitter.on(EventName.CONNECT, defaultHandler); + eventEmitter.emit(EventName.CONNECT, { data: true }); + eventEmitter.cleanListeners(); + + expect(defaultEventEmitter.on).toHaveBeenCalledTimes(1); + expect(defaultEventEmitter.emit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/providers/src/services/handler/__tests__/Handler.test.ts b/packages/providers/src/services/handler/__tests__/Handler.test.ts new file mode 100644 index 000000000..f629a8d74 --- /dev/null +++ b/packages/providers/src/services/handler/__tests__/Handler.test.ts @@ -0,0 +1,132 @@ +/** + * @jest-environment jsdom + */ +import type { IInjectedMessageData, IInjectedProviderRequest } from "@cryptkeeperzk/types"; + +import { Handler, EventEmitter, EventName } from "../.."; + +jest.mock("nanoevents", (): unknown => ({ + createNanoEvents: jest.fn(), +})); + +jest.mock("../../event", (): unknown => ({ + ...jest.requireActual("../../event"), + EventEmitter: jest.fn(), +})); + +describe("services/handler", () => { + const defaultEventEmitter = { + on: jest.fn(), + emit: jest.fn(), + cleanListeners: jest.fn(), + }; + + const defaultMessage: IInjectedProviderRequest = { + method: EventName.CONNECT, + }; + + const defaultEvent = { + data: { + target: "injected-injectedscript", + nonce: "0", + payload: ["", { data: true }], + }, + } as MessageEvent; + + beforeEach(() => { + (EventEmitter as jest.Mock).mockReturnValue(defaultEventEmitter); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should handle events properly", () => { + const handler = new Handler(); + + handler.on(EventName.CONNECT, jest.fn()); + handler.emit(EventName.CONNECT, { data: true }); + handler.cleanListeners(); + + expect(defaultEventEmitter.on).toHaveBeenCalledTimes(1); + expect(defaultEventEmitter.emit).toHaveBeenCalledTimes(1); + expect(defaultEventEmitter.cleanListeners).toHaveBeenCalledTimes(1); + }); + + test("should get connected origin properly", () => { + const handler = new Handler("url"); + + const urlOrigin = handler.getConnectedOrigin(); + + expect(urlOrigin).toBe("url"); + }); + + test("should request rpc method properly", async () => { + const handler = new Handler(); + + const promise = handler.request(defaultMessage); + handler.eventResponser(defaultEvent); + + await expect(promise).resolves.toStrictEqual(defaultEvent.data.payload[1]); + expect(window.postMessage).toHaveBeenCalledTimes(1); + expect(window.postMessage).toHaveBeenCalledWith( + { + target: "injected-contentscript", + message: { + ...defaultMessage, + meta: { + ...defaultMessage.meta, + urlOrigin: undefined, + }, + type: defaultMessage.method, + }, + nonce: 0, + }, + "*", + ); + }); + + test("should not handle unknown requests", () => { + const handler = new Handler(); + + handler.eventResponser({ ...defaultEvent, data: { ...defaultEvent.data, nonce: "unknown" } }); + handler.eventResponser({ ...defaultEvent, data: { ...defaultEvent.data, target: "unknown" } }); + + expect(defaultEventEmitter.emit).toHaveBeenCalledTimes(0); + }); + + test("should handle event request properly", () => { + const handler = new Handler(); + + handler.eventResponser({ ...defaultEvent, data: { ...defaultEvent.data, nonce: EventName.CONNECT } }); + + expect(defaultEventEmitter.emit).toHaveBeenCalledTimes(1); + expect(defaultEventEmitter.emit).toHaveBeenCalledWith(EventName.CONNECT, defaultEvent.data.payload[1]); + }); + + test("should reject request properly", async () => { + const handler = new Handler(); + + const error = new Error("error"); + const promise = handler.request(defaultMessage); + handler.eventResponser({ ...defaultEvent, data: { ...defaultEvent.data, payload: [error.message, null] } }); + + await expect(promise).rejects.toStrictEqual(error); + expect(window.postMessage).toHaveBeenCalledTimes(1); + expect(window.postMessage).toHaveBeenCalledWith( + { + target: "injected-contentscript", + message: { + ...defaultMessage, + meta: { + ...defaultMessage.meta, + urlOrigin: undefined, + }, + type: defaultMessage.method, + }, + nonce: 0, + }, + "*", + ); + }); +}); diff --git a/packages/providers/src/services/handler/index.ts b/packages/providers/src/services/handler/index.ts index b99c91503..741706a36 100644 --- a/packages/providers/src/services/handler/index.ts +++ b/packages/providers/src/services/handler/index.ts @@ -1,11 +1,11 @@ -import type { RequestsPromisesHandlers } from "./types"; +import type { RequestsPromisesHandlers, IHandler } from "./types"; import type { IInjectedMessageData, IInjectedProviderRequest } from "@cryptkeeperzk/types"; import { type EventHandler, EventEmitter, EventName } from "../event"; const EVENTS = Object.values(EventName); -export class Handler { +export class Handler implements IHandler { /** * Nonce used for message communication. */ @@ -95,28 +95,29 @@ export class Handler { eventResponser(event: MessageEvent): unknown { const { data } = event; - if (data.target === "injected-injectedscript") { - if (EVENTS.includes(data.nonce as EventName)) { - const [, res] = data.payload; - this.emit(data.nonce as EventName, res); - return; - } + if (data.target !== "injected-injectedscript") { + return; + } - if (!this.requestsPromises.has(data.nonce.toString())) { - return; - } + if (EVENTS.includes(data.nonce as EventName)) { + const [, res] = data.payload; + this.emit(data.nonce as EventName, res); + return; + } - const [err, res] = data.payload; - const { reject, resolve } = this.requestsPromises.get(data.nonce.toString())!; + if (!this.requestsPromises.has(data.nonce.toString())) { + return; + } - if (err) { - reject(new Error(err)); - return; - } + const [err, res] = data.payload; + const { reject, resolve } = this.requestsPromises.get(data.nonce.toString())!; + if (err) { + reject(new Error(err)); + } else { resolve(res); - - this.requestsPromises.delete(data.nonce.toString()); } + + this.requestsPromises.delete(data.nonce.toString()); } } diff --git a/packages/providers/src/services/handler/types.ts b/packages/providers/src/services/handler/types.ts index b945a2d62..0fb9fcffb 100644 --- a/packages/providers/src/services/handler/types.ts +++ b/packages/providers/src/services/handler/types.ts @@ -1,4 +1,16 @@ +import type { EventHandler, EventName } from "../event"; +import type { IInjectedMessageData, IInjectedProviderRequest } from "@cryptkeeperzk/types"; + export interface RequestsPromisesHandlers { resolve: (res?: unknown) => void; reject: (reason?: unknown) => void; } + +export interface IHandler { + request: (message: IInjectedProviderRequest) => Promise; + eventResponser: (event: MessageEvent) => unknown; + on: (eventName: EventName, cb: EventHandler) => void; + emit: (eventName: EventName, payload?: unknown) => void; + cleanListeners: () => void; + getConnectedOrigin: () => string | undefined; +} diff --git a/packages/providers/src/services/index.ts b/packages/providers/src/services/index.ts index a76c51d59..8fa8fb6d8 100644 --- a/packages/providers/src/services/index.ts +++ b/packages/providers/src/services/index.ts @@ -1,3 +1,4 @@ export { EventEmitter, EventName } from "./event"; export type { EventHandler, Events } from "./event"; export { Handler } from "./handler"; +export type { IHandler } from "./handler/types"; diff --git a/packages/providers/src/setupTests.ts b/packages/providers/src/setupTests.ts new file mode 100644 index 000000000..05a4358cb --- /dev/null +++ b/packages/providers/src/setupTests.ts @@ -0,0 +1,10 @@ +/** + * @jest-environment jsdom + */ + +jest.retryTimes(1, { logErrorsBeforeRetry: true }); + +window.postMessage = jest.fn(); +window.dispatchEvent = jest.fn(); +window.addEventListener = jest.fn(); +window.isCryptkeeperInjected = true;