Skip to content

Commit

Permalink
ESM setup and getting rid of Jest
Browse files Browse the repository at this point in the history
Jest works terribly with ESM, needing its own weird setup for TS,
module name remapping and so on. Native test runner looks mature enough,
and even module mocks work fairly well.
Using native `assert` instead of jest's `expect` was a pain in the ass,
so opted for using modern assertion library `earl` instead.

* Importing JSONs from root works not as good, so moved config schema
inside the sources
* Moved codegen from `postinstall` to `codegen` script, so it could be
separately from install; needed for docker build, where `yarn install`
is called before `COPY src`
* Yarn upgrade also solved some issue down the road
* Adding `.js` to all imports is a ESM requirement (even if it's
originally `.ts` files)
* mockHelpers.ts will probably be moved to `@eng-automation/testing`
  • Loading branch information
mutantcornholio committed Jan 20, 2025
1 parent 20f2614 commit d542bfe
Show file tree
Hide file tree
Showing 44 changed files with 1,656 additions and 3,027 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
data
build
test
test
src/configSchema.ts
1 change: 1 addition & 0 deletions .github/workflows/E2E.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
with:
node-version: 22
- run: yarn install --immutable
- run: yarn codegen
- name: Download Polkadot and parachain binaries
run: |
wget --no-verbose https://github.com/paritytech/polkadot-sdk/releases/download/polkadot-stable2407-1/polkadot
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Install deps in root
run: yarn install --immutable
- run: yarn generate:papi
- run: yarn codegen
- name: Install deps in client
run: yarn install --immutable
working-directory: client
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Install deps in root
run: yarn install --immutable
- run: yarn generate:papi
- run: yarn codegen
- run: yarn test
build_image:
name: Build docker image
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ e2e/containter_logs
e2e/zombienet

# Autogenerated
env.*.config.json.d.ts
src/configSchema.ts
!.env.example

/data
Expand Down
894 changes: 0 additions & 894 deletions .yarn/releases/yarn-4.3.0.cjs

This file was deleted.

934 changes: 934 additions & 0 deletions .yarn/releases/yarn-4.6.0.cjs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
yarnPath: .yarn/releases/yarn-4.3.0.cjs
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.6.0.cjs
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ COPY package.json env.faucet.config.json yarn.lock .yarnrc.yml ./
RUN yarn --immutable

COPY . .
RUN yarn papi
RUN yarn codegen
RUN yarn build

CMD yarn migrations:run && yarn start
8 changes: 0 additions & 8 deletions jest.config.js

This file was deleted.

9 changes: 0 additions & 9 deletions jest.e2e.config.js

This file was deleted.

24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@
"description": "",
"main": "build/start.js",
"scripts": {
"build": "tsc",
"build": "tsc && cp package.json build/",
"build:docker": "docker build -t polkadot-testnet-faucet .",
"dev": "concurrently \"tsc -w\" \"node --watch -r dotenv/config ./build/src/start.js\"",
"dev:db": "docker run -e 'POSTGRESQL_EXTRA_FLAGS=-c log_statement=all' -e 'POSTGRESQL_PASSWORD=postgres' -e 'POSTGRESQL_DATABASE=faucet' -v \"$(pwd)/data:/bitnami/postgresql\" -p 5432:5432 bitnami/postgresql:15",
"fix": "yarn lint:fix && yarn format:fix",
"format": "npx prettier ./src ./client/src ./client/tests --check",
"format:fix": "npx prettier ./src ./client/src ./client/tests --write",
"generate:papi": "papi generate",
"generate:types": "echo \"declare const schema: $(cat env.faucet.config.json); export default schema;\" > env.faucet.config.json.d.ts",
"generate:types": "echo \"export const schema = $(cat env.faucet.config.json) as const;\" > src/configSchema.ts",
"lint": "npx eslint ./src/ ./client/src ./client/tests --ext .js,.ts,.svelte",
"lint:fix": "npx eslint ./src/ ./client/src ./client/tests --ext .js,.ts,.svelte --fix",
"migrations:generate": "typeorm migration:generate -d build/src/db/dataSource.js",
"migrations:run": "typeorm migration:run -d build/src/db/dataSource.js",
"postinstall": "yarn generate:types && yarn generate:papi",
"codegen": "yarn generate:types && yarn generate:papi",
"start": "node ./build/src/start.js",
"e2e:zombienet": "rm -rf e2e/zombienet && npx --yes @zombienet/[email protected] --provider native --dir e2e/zombienet spawn e2e/zombienet.native.toml",
"test": "jest",
"test:e2e": "jest -c jest.e2e.config.js --runInBand --forceExit",
"test": "node --experimental-test-module-mocks --loader ts-node/esm --import ./src/test/setup.unit.ts --test ./src/**/*.test.ts",
"test:e2e": "node --test --loader ts-node/esm ./src/faucet.e2e.ts",
"typecheck": "tsc --noEmit"
},
"imports": {
Expand Down Expand Up @@ -64,7 +64,7 @@
"confmgr": "^1.0.8",
"cors": "^2.8.5",
"express": "4.19.2",
"matrix-js-sdk": "^26.1.0",
"matrix-js-sdk": "^35.1.0",
"pg": "^8.11.2",
"polkadot-api": "^1.6.5",
"prom-client": "^14.2.0",
Expand All @@ -77,24 +77,24 @@
"@eng-automation/testing": "^1.5.1",
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.13",
"@types/jest": "^29.5.12",
"@types/node": "^20",
"@types/node": "^22.10.5",
"@types/request": "^2.48.8",
"@types/supertest": "^2.0.12",
"concurrently": "^8.2.2",
"earl": "^1.3.0",
"eslint-plugin-security": "^1.5.0",
"jest": "^29.7.0",
"joi": "^17.13.1",
"lint-staged": "^12.3.8",
"rxjs": "^7.8.1",
"simple-git-hooks": "^2.7.0",
"supertest": "^6.3.3",
"testcontainers": "v10.8.1",
"ts-jest": "^29.1.4",
"testcontainers": "^10.16.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"type": "module",
"engines": {
"node": "^22"
},
"packageManager": "yarn@4.3.0"
"packageManager": "yarn@4.6.0"
}
16 changes: 11 additions & 5 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,26 @@ const bot = mSDK.createClient({
});

const sendMessage = (roomId: string, msg: string, formattedMsg?: string) => {
const msgObject: mSDK.IContent = { body: msg, msgtype: "m.text" };
const msgObject: mSDK.TimelineEvents[mSDK.EventType.RoomMessage] = { body: msg, msgtype: mSDK.MsgType.Text };

if (formattedMsg !== undefined) {
msgObject.format = "org.matrix.custom.html";
msgObject.formatted_body = formattedMsg;
}

bot.sendEvent(roomId, null, "m.room.message", msgObject, "").catch((e) => logger.error(e));
bot.sendEvent(roomId, null, mSDK.EventType.RoomMessage, msgObject, "").catch((e) => logger.error(e));
};

const sendReaction = (roomId: string, eventId: string, emoji: string) => {
const msgObject: mSDK.IContent = { "m.relates_to": { event_id: eventId, key: emoji, rel_type: "m.annotation" } };

bot.sendEvent(roomId, null, "m.reaction", msgObject, "").catch((e) => logger.error(e));
const msgObject: mSDK.TimelineEvents[mSDK.EventType.Reaction] = {
"m.relates_to": {
event_id: eventId,
key: emoji,
rel_type: mSDK.RelationType.Annotation,
},
};

bot.sendEvent(roomId, null, mSDK.EventType.Reaction, msgObject, "").catch((e) => logger.error(e));
};

const printHelpMessage = (roomId: string, message = "") =>
Expand Down
8 changes: 4 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConfigManager, ConfigObject } from "confmgr/lib";
import { ConfigManager, ConfigObject } from "confmgr/lib/index.js";

import faucetConfigSpec from "../env.faucet.config.json";
import { logger } from "./logger";
import { schema } from "./configSchema.js";
import { logger } from "./logger.js";

export type SpecType<T> = T extends { type: "string" }
? string
Expand Down Expand Up @@ -42,7 +42,7 @@ function resolveConfig(): ConfigObject {
}

const configInstance = resolveConfig();
export type ConfigSpec = (typeof faucetConfigSpec)["SMF"]["CONFIG"];
export type ConfigSpec = (typeof schema)["SMF"]["CONFIG"];

export const config = {
Get: <K extends keyof ConfigSpec>(key: K): SpecType<ConfigSpec[K]> => configInstance.Get("CONFIG", key),
Expand Down
4 changes: 2 additions & 2 deletions src/db/dataSource.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { config } from "#src/config";
import { DataSource } from "typeorm";

import { Drip } from "./entity/Drip";
import { migrations } from "./migration/migrations";
import { Drip } from "./entity/Drip.js";
import { migrations } from "./migration/migrations.js";

export const AppDataSource = new DataSource({
type: "postgres",
Expand Down
2 changes: 1 addition & 1 deletion src/db/migration/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Initial1692350473907 } from "./1692350473907-initial";
import { Initial1692350473907 } from "./1692350473907-initial.js";

export const migrations = [Initial1692350473907];
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { hasDrippedToday, saveDrip } from "./dripperStorage";
import { DripRequestHandler } from "./DripRequestHandler";
import type { PolkadotActions } from "./polkadot/PolkadotActions";
import { convertAmountToBn } from "./polkadot/utils";
import type { Recaptcha } from "./Recaptcha";
import { expectHaveBeenCalledWith, mockResolvedValueOnce } from "#src/test/mockHelpers";
import { expect } from "earl";
import { beforeEach, describe, it, mock } from "node:test";

jest.mock("./dripperStorage");
jest.mock("../config");
import * as mockedDripperStorage from "./__mocks__/dripperStorage.js";
import type { PolkadotActions } from "./polkadot/PolkadotActions.js";
import { convertAmountToBn } from "./polkadot/utils.js";
import type { Recaptcha } from "./Recaptcha.js";

mock.module("./dripperStorage.js", { namedExports: mockedDripperStorage });

const actionsMock: PolkadotActions = {
isAccountOverBalanceCap: async (addr: string) => addr === "rich",
Expand All @@ -15,16 +17,12 @@ const actionsMock: PolkadotActions = {

const recaptcha: Recaptcha = { validate: async (captcha: string) => captcha === "valid" } as any; // eslint-disable-line @typescript-eslint/no-explicit-any

function assumeMocked<R, A extends unknown[]>(f: (...args: A) => R): jest.Mock<R, A> {
return f as jest.Mock<R, A>;
}

describe("DripRequestHandler", () => {
let handler: DripRequestHandler;
const getHandler = async () => new (await import("./DripRequestHandler.js")).DripRequestHandler(actionsMock, recaptcha);

describe("DripRequestHandler", async () => {
beforeEach(async () => {
handler = new DripRequestHandler(actionsMock, recaptcha);
jest.clearAllMocks();
mockedDripperStorage.hasDrippedToday.mock.resetCalls();
mockedDripperStorage.saveDrip.mock.resetCalls();
});

/**
Expand All @@ -40,47 +38,58 @@ describe("DripRequestHandler", () => {
} as const;

it("Goes through one time", async () => {
const handler = await getHandler();
{
const result = await handler.handleRequest(defaultRequest);
expect(assumeMocked(saveDrip)).toHaveBeenCalledWith({ addr: "123", username: "someone" });

expectHaveBeenCalledWith(mockedDripperStorage.saveDrip, [{ addr: "123", username: "someone" }]);
expect(result).toEqual({ hash: "0x123" });
}
{
assumeMocked(hasDrippedToday).mockResolvedValueOnce(true);
mockResolvedValueOnce(mockedDripperStorage.hasDrippedToday, true);

const result = await handler.handleRequest(defaultRequest);
expect("error" in result).toBeTruthy();
}
});

it("Returns an error response", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, address: "unlucky" });
expect(result).toEqual({ error: "An error occurred when sending tokens" });
});

it("Doesn't allow a repeated address or username", async () => {
assumeMocked(hasDrippedToday).mockResolvedValueOnce(true);
const handler = await getHandler();
mockResolvedValueOnce(mockedDripperStorage.hasDrippedToday, true);

const result = await handler.handleRequest(defaultRequest);
expect(hasDrippedToday).toHaveBeenCalledWith({ addr: "123", username: "someone" });
expectHaveBeenCalledWith(mockedDripperStorage.hasDrippedToday, [{ addr: "123", username: "someone" }]);
expect(result).toEqual({ error: "Requester has reached their daily quota. Only request once per day." });
});

it("Doesn't allow a rich address user", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, address: "rich" });
expect(result).toEqual({ error: "Requester's balance is over the faucet's balance cap" });
});

it("Parity members are privileged in terms of repeated requests", async () => {
assumeMocked(hasDrippedToday).mockResolvedValueOnce(true);
const handler = await getHandler();
mockResolvedValueOnce(mockedDripperStorage.hasDrippedToday, true);

const result = await handler.handleRequest({ ...defaultRequest, sender: "someone:parity.io" });
expect(result).toEqual({ hash: "0x123" });
});

it("Parity members are privileged in terms of balance cap", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, sender: "someone:parity.io", address: "rich" });
expect(result).toEqual({ hash: "0x123" });
});

it("Works with empty parachain_id", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, parachain_id: "" });
expect(result).toEqual({ hash: "0x123" });
});
Expand All @@ -99,42 +108,48 @@ describe("DripRequestHandler", () => {
} as const;

it("Goes through one time", async () => {
const handler = await getHandler();
{
const result = await handler.handleRequest(defaultRequest);
expect(assumeMocked(saveDrip)).toHaveBeenCalledWith({ addr: "123" });
expectHaveBeenCalledWith(mockedDripperStorage.saveDrip, [{ addr: "123" }]);
expect(result).toEqual({ hash: "0x123" });
}
{
assumeMocked(hasDrippedToday).mockResolvedValueOnce(true);
mockResolvedValueOnce(mockedDripperStorage.hasDrippedToday, true);
const result = await handler.handleRequest(defaultRequest);
expect("error" in result).toBeTruthy();
}
});

it("Returns an error response", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, address: "unlucky" });
expect(result).toEqual({ error: "An error occurred when sending tokens" });
});

it("Doesn't allow a repeated address", async () => {
assumeMocked(hasDrippedToday).mockResolvedValueOnce(true);
const handler = await getHandler();
mockResolvedValueOnce(mockedDripperStorage.hasDrippedToday, true);
const result = await handler.handleRequest(defaultRequest);
expect(result).toEqual({ error: "Requester has reached their daily quota. Only request once per day." });
});

it("Doesn't allow a rich address user", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, address: "rich" });
expect(result).toEqual({ error: "Requester's balance is over the faucet's balance cap" });
});

it("Cannot repeat requests by (somehow) supplying Parity username", async () => {
assumeMocked(hasDrippedToday).mockResolvedValueOnce(true);
const handler = await getHandler();
mockResolvedValueOnce(mockedDripperStorage.hasDrippedToday, true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await handler.handleRequest({ ...defaultRequest, sender: "someone:parity.io" } as any);
expect(result).toEqual({ error: "Requester has reached their daily quota. Only request once per day." });
});

it("Cannot override balance cap by (somehow) supplying Parity username", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({
...defaultRequest,
sender: "someone:parity.io",
Expand All @@ -144,11 +159,13 @@ describe("DripRequestHandler", () => {
});

it("Returns an error response if captcha is invalid", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, recaptcha: "invalid" });
expect(result).toEqual({ error: "Captcha validation was unsuccessful" });
});

it("Works with empty parachain_id", async () => {
const handler = await getHandler();
const result = await handler.handleRequest({ ...defaultRequest, parachain_id: "" });
expect(result).toEqual({ hash: "0x123" });
});
Expand Down
6 changes: 3 additions & 3 deletions src/dripper/DripRequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { counters } from "#src/metrics";
import { DripRequestType, DripResponse } from "#src/types";
import { isAccountPrivileged } from "#src/utils";

import { hasDrippedToday, saveDrip } from "./dripperStorage";
import type { PolkadotActions } from "./polkadot/PolkadotActions";
import { Recaptcha } from "./Recaptcha";
import { hasDrippedToday, saveDrip } from "./dripperStorage.js";
import type { PolkadotActions } from "./polkadot/PolkadotActions.js";
import { Recaptcha } from "./Recaptcha.js";

const validateParachainId = (parachain: string): number | null => {
const id = Number.parseInt(parachain);
Expand Down
Loading

0 comments on commit d542bfe

Please sign in to comment.