Skip to content

Commit

Permalink
Merge pull request #1 from usherlabs/test/setup
Browse files Browse the repository at this point in the history
Test/setup
  • Loading branch information
rsoury authored Aug 22, 2024
2 parents 2bc25c9 + f3a21f1 commit 4ea388f
Show file tree
Hide file tree
Showing 12 changed files with 3,622 additions and 600 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
# env:
# # ROOCH_ORACLE_ADDRESS= ${{ secrets.ROOCH_ORACLE_ADDRESS }}
# # ROOCH_PRIVATE_KEY= ${{ secrets.ROOCH_PRIVATE_KEY }}

strategy:
matrix:
node-version: [20.x]

steps:
- name: Use Node.js ${{ matrix.node-version }} 🛎️
uses: actions/checkout@v3
with:
node-version: ${{ matrix.node-version }}
persist-credentials: false

- name: Install 🔧 dependencies
run: npx yarn install

- name: build
run: npx yarn build
2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
npx lint-staged
# npm test
3 changes: 3 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

# Check if test cases pass
npm run test
5 changes: 3 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"recommended": true,
"suspicious": {
"noExplicitAny": "off",
"noConfusingVoidType": "off"
"noConfusingVoidType": "off",
"noThenProperty": "off"
},
"style": {
"noUselessElse": "off",
Expand All @@ -27,4 +28,4 @@
}
}
}
}
}
40 changes: 40 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// TODO: Improve coverage to 90%
const coverageToNumber = 40; // [0..100]

/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/

export default {
testTimeout: 40000,

verbose: true,
rootDir: "./",
transform: {
"^.+\\.ts?$": "ts-jest",
},
moduleFileExtensions: ["ts", "js", "json", "node"],
clearMocks: true, // clear mocks before every test
resetMocks: false, // reset mock state before every test
testMatch: [
"<rootDir>/**/*.spec.ts", // Commenting cache test for github actions
"<rootDir>/**/*.test.ts",
"<rootDir>/**/*.test.js",
], // match only tests inside /tests folder
testPathIgnorePatterns: ["<rootDir>/node_modules/"], // exclude unnecessary folders

// following lines are about coverage
collectCoverage: true,
collectCoverageFrom: ["<rootDir>/orchestrator/src/**/*.ts"],
coverageDirectory: "<rootDir>/coverage",
coverageReporters: ["lcov"],
coverageThreshold: {
global: {
// branches: coverageToNumber,
// functions: coverageToNumber,
lines: coverageToNumber,
statements: coverageToNumber,
},
},
};
67 changes: 62 additions & 5 deletions orchestrator/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,68 @@
import type { RoochNetwork } from "@/types";
import Joi from "joi";
import { ChainList, type RoochNetwork, RoochNetworkList, SupportedChain } from "./types";
import { addressValidator, isRequiredWhenPreferredChainIs, privateKeyValidator } from "./validator";

const baseConfig = {
preferredChain: process.env.PREFERRED_CHAIN ?? ChainList[0],
roochChainId: process.env.ROOCH_CHAIN_ID,
roochPrivateKey: process.env.ROOCH_PRIVATE_KEY ?? "",
roochOracleAddress: process.env.ROOCH_ORACLE_ADDRESS ?? "",
sentryDSN: process.env.SENTRY_DSN ?? "",
ecdsaPrivateKey: process.env.SENTRY_DSN ?? "",
};
interface IEnvVars {
preferredChain: SupportedChain;
roochChainId: RoochNetwork;
roochOracleAddress: string;
roochPrivateKey: string;
roochIndexerCron: string;
sentryDSN?: string;
ecdsaPrivateKey?: string;
}

const envVarsSchema = Joi.object({
preferredChain: Joi.string()
.valid(...ChainList)
.insensitive()
.default(ChainList[0]),
roochChainId: Joi.string()
.valid(...RoochNetworkList)
.insensitive()
.default(RoochNetworkList[0]),
roochOracleAddress: isRequiredWhenPreferredChainIs(
Joi.string().custom((value, helper) => addressValidator(value, helper)),
SupportedChain.ROOCH,
),
roochPrivateKey: isRequiredWhenPreferredChainIs(
Joi.string().custom((value, helper) => {
return privateKeyValidator(value, helper);
}),
SupportedChain.ROOCH,
),
roochIndexerCron: Joi.string().default("*/5 * * * * *"),
sentryDSN: Joi.string().allow("", null),
ecdsaPrivateKey: Joi.string().allow("", null),
});

const { value, error } = envVarsSchema.validate({
...baseConfig,
});

if (error) {
throw new Error(error.message);
}
const envVars = value as IEnvVars;

export default {
chain: envVars.preferredChain,
ecdsaPrivateKey: envVars.ecdsaPrivateKey,
sentryDSN: envVars.sentryDSN,
rooch: {
chainId: process.env.ROOCH_CHAIN_ID || ("testnet" as RoochNetwork),
oracleAddress: process.env.ROOCH_ORACLE_ADDRESS || "",
indexerCron: process.env.ROOCH_INDEXER_CRON || "*/5 * * * * *",
privateKey: process.env.ROOCH_PRIVATE_KEY || "",
chainId: envVars.roochChainId,
oracleAddress: envVars.roochOracleAddress,
// Ideally promote indexerCron, it shouldn't necessary be tided to a chain
indexerCron: envVars.roochIndexerCron,
privateKey: envVars.roochPrivateKey,
},
// aptos: {
// chainId: process.env.APTOS_CHAIN_ID,
Expand Down
117 changes: 117 additions & 0 deletions orchestrator/src/test/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import * as path from "node:path";
import { config } from "dotenv";

// Convert object to .env format
const convertToEnvFormat = (obj: Record<string, string | undefined>): string => {
return Object.entries(obj)
.map(([key, value]) => `${key}=${value ?? ""}`)
.join("\n");
};

const envFilePath = path.resolve(__dirname, ".env.");

// Function to create .env file
const createEnvFile = (config: Record<string, string | undefined>, envFilePath: string) => {
fs.writeFileSync(envFilePath, convertToEnvFormat(config), "utf8");
};

// Function to delete .env file
const deleteEnvFile = (envFilePath: string) => {
if (fs.existsSync(envFilePath)) {
fs.unlinkSync(envFilePath);
}
};

describe(".env Check", () => {
let testFilePath: string;
beforeEach(() => {
testFilePath = envFilePath + randomUUID();
});

afterEach(() => {
deleteEnvFile(testFilePath);
});

test("test dynamic Env", async () => {
const data = {
test: "tedded",
};
createEnvFile(data, testFilePath);
config({ path: testFilePath });
expect(data.test).toBe(process.env.test);
});

const tests = [
{
name: "Empty .env file",
data: {},
wantErr: true,
errorMessage: '"roochOracleAddress" is not allowed to be empty',
},
{
name: "test invalid roochOracleAddress",
data: {
ROOCH_ORACLE_ADDRESS: "0xf81628c3bf85c3fc628f29a3739365d4428101fbbecca0dcc7e3851f34faea6V",
},
wantErr: true,
errorMessage: '"roochOracleAddress" contains an invalid value',
},
{
name: "Invalid Chain Check ",
data: {
PREFERRED_CHAIN: "ROOCHd",
ROOCH_ORACLE_ADDRESS: "0xf81628c3bf85c3fc628f29a3739365d4428101fbbecca0dcc7e3851f34faea6a",
},
wantErr: true,
errorMessage: '"preferredChain" must be one of [ROOCH, APTOS]',
},
{
name: "valid ROOCH_ORACLE_ADDRESS but missing",
data: {
PREFERRED_CHAIN: "ROOCH",
ROOCH_ORACLE_ADDRESS: "0xf81628c3bf85c3fc628f29a3739365d4428101fbbecca0dcc7e3851f34faea6c",
},
wantErr: true,
errorMessage: '"roochPrivateKey" is not allowed to be empty',
},
{
name: "valid data",
data: {
PREFERRED_CHAIN: "ROOCH",
ROOCH_ORACLE_ADDRESS: "0xf81628c3bf85c3fc628f29a3739365d4428101fbbecca0dcc7e3851f34faea6c",
ROOCH_PRIVATE_KEY: "0xf81628c3bf85c3fc628f29a3739365d4428101fbbecca0dcc7e3851f34faea6c",
},
wantErr: false,
errorMessage: '"roochPrivateKey" is not allowed to be empty',
},
{
name: "rooch variables not required when preferred chain is set to APTOS ",
data: {
PREFERRED_CHAIN: "APTOS",
},
wantErr: false,
errorMessage: "",
},
];

tests.forEach(({ name, data, wantErr, errorMessage }) => {
it(name, async () => {
createEnvFile(data, testFilePath);
config({ path: testFilePath, override: true });
if (wantErr) {
try {
const { default: envVars } = await import("../env");
expect(envVars).toBeNull();
} catch (err: any) {
expect(err?.message).toBe(errorMessage);
expect(err).toBeInstanceOf(Error);
}
} else {
const { default: envVars } = await import("../env");
expect(envFilePath).not.toBeNull();
}
});
});
});
16 changes: 15 additions & 1 deletion orchestrator/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,18 @@ export type RoochEnv = {
indexerCron?: string;
};

export type RoochNetwork = "testnet" | "devnet" | "localnet";
export const RoochNetworkList = ["testnet", "devnet", "localnet"] as const;

export const ChainList = ["ROOCH", "APTOS"] as const;

export type RoochNetwork = (typeof RoochNetworkList)[number];

export type SupportedChain = (typeof ChainList)[number];

export const SupportedChain = ChainList.reduce(
(acc, value) => {
acc[value] = value;
return acc;
},
{} as Record<(typeof ChainList)[number], string>,
);
37 changes: 37 additions & 0 deletions orchestrator/src/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Joi from "joi";

export const addressValidator = (value: string, helpers: Joi.CustomHelpers<any>) => {
if (/^0x[a-fA-F0-9]{64}$/.test(value)) {
return value;
}
return helpers.error("any.invalid");
};

export const privateKeyValidator = (value: string, helpers: Joi.CustomHelpers<any>) => {
// TODO: Add proper validation for Private key
return value;
};

// Define a regex pattern for basic cron expressions (seconds not included)
const cronPattern =
/^(\*|([0-5]?[0-9])) (\*|([01]?[0-9]|2[0-3])) (\*|([0-2]?[0-9]|3[0-1])) (\*|([0-1]?[0-9]|1[0-2])) (\*|[0-6])$/;

// Define a regex pattern for cron expressions including seconds
const cronPatternWithSeconds =
/^(\*|([0-5]?[0-9])) (\*|([0-5]?[0-9])) (\*|([0-5]?[0-9])) (\*|([0-1]?[0-9]|2[0-3])) (\*|([0-2]?[0-9]|3[0-1])) (\*|([0-1]?[0-9]|1[0-2])) (\*|[0-6])$/;

// Check for standard cron (5 fields) or extended cron (6 fields including seconds)
export const cronValidator = (value: string, helpers: Joi.CustomHelpers<any>) => {
if (cronPattern.test(value) || cronPatternWithSeconds.test(value)) {
return value;
}
return helpers.error("any.invalid");
};

/* eslint-disable lint/suspicious/noThenProperty */
export const isRequiredWhenPreferredChainIs = (schema: Joi.StringSchema<string>, value: string) =>
Joi.string().when("preferredChain", {
is: value,
then: schema.required(), // 'details' is required if 'status' is 'active'
otherwise: Joi.string().optional().allow("", null).default(""), // 'details' is optional otherwise
});
Loading

0 comments on commit 4ea388f

Please sign in to comment.