diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..2eea525d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..44b54153 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:22-slim + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY package.json package-lock.json ./ +RUN npm ci + +# Environment Variables +ENV NETWORK=local \ + RUNNING_IN_DOCKER=true \ + TEST="ALL" + +# Copy the rest of the application +COPY . . + +# Use the runner script +CMD ["npx", "ts-node", "--files", "/app/src/services/RunTestsInContainer.ts"] \ No newline at end of file diff --git a/README.md b/README.md index f7974dc5..2a7bc3fc 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,87 @@ task generate-mirror-node-models This command uses `openapi-typescript-codegen` to parse the `mirror-node.yaml` file and generate corresponding TypeScript models in `src/utils/models/mirror-node-models` +## Docker + +The TCK is also available as a Docker image, providing an easy way to run tests in an isolated environment. + +### Pull the Docker Image + +You can pull the pre-built Docker image from DockerHub: + +```bash +docker pull ivaylogarnev/tck-client +``` + +### Running Tests + +The Docker image supports running tests against both local and testnet environments. + +#### Local Network (Default) +To run tests against a local network: +```bash +# Run specific test +docker run --network host -e TEST=AccountCreate ivaylogarnev/tck-client + +# Run all tests +docker run --network host ivaylogarnev/tck-client +``` + +#### Testnet +To run tests against Hedera Testnet: +```bash +docker run --network host \ + -e NETWORK=testnet \ + -e OPERATOR_ACCOUNT_ID=your-account-id \ + -e OPERATOR_ACCOUNT_PRIVATE_KEY=your-private-key \ + # Run specific test + -e TEST=AccountCreate \ + ivaylogarnev/tck-client +``` + +### Available Tests +Some of the available test options include: +- AccountCreate +- AccountUpdate +- AccountDelete +- AccountAllowanceDelete +- AccountAllowanceApprove +- TokenCreate +- TokenUpdate +- TokenDelete +- TokenBurn +- TokenMint +- TokenAssociate +- TokenDissociate +- TokenFeeScheduleUpdate +- TokenGrantKyc +- TokenRevokeKyc +- TokenPause +- TokenUnpause +- TokenFreeze +- TokenUnfreeze +- ALL (runs all tests) + +Running an invalid test name will display the complete list of available tests. + +### Building the Docker Image Locally + +If you want to build the image locally: +```bash +docker build -t tck-client . +``` + +Then run it using the same commands as above, replacing `ivaylogarnev/tck-client` with `tck-client`. + +### Docker additional notes + +```bash +# entry point for the Docker image. It sets the network environment, maps the ports, and runs the tests. + +RunTestsInContainer.ts +``` + +**Note:** This file is specifically used for running tests within the Docker environment and does not affect how tests are run locally. For local test execution, please refer to the instructions provided in the "Install and run" section above. ## Contributing @@ -125,4 +206,4 @@ Hiero uses the Linux Foundation Decentralised Trust [Code of Conduct](https://ww ## License -[Apache License 2.0](LICENSE) +[Apache License 2.0](LICENSE) \ No newline at end of file diff --git a/src/services/Client.ts b/src/services/Client.ts index ee3bcf7a..1e405776 100644 --- a/src/services/Client.ts +++ b/src/services/Client.ts @@ -5,20 +5,26 @@ import "dotenv/config"; let nextID = 0; const createID: CreateID = () => nextID++; +// Determine the host based on environment +const getHost = () => { + const url = process.env.JSON_RPC_SERVER_URL ?? "http://localhost:8544"; + + if (process.env.RUNNING_IN_DOCKER) { + return url.replace("localhost", "host.docker.internal"); + } + return url; +}; + // JSONRPCClient needs to know how to send a JSON-RPC request. // Tell it by passing a function to its constructor. The function must take a JSON-RPC request and send it. const JSONRPClient = new JSONRPCClient( async (jsonRPCRequest): Promise => { try { - const response = await axios.post( - process.env.JSON_RPC_SERVER_URL ?? "http://localhost:8544", - jsonRPCRequest, - { - headers: { - "Content-Type": "application/json", - }, + const response = await axios.post(getHost(), jsonRPCRequest, { + headers: { + "Content-Type": "application/json", }, - ); + }); if (response.status === 200) { return JSONRPClient.receive(response.data); diff --git a/src/services/RunTestsInContainer.ts b/src/services/RunTestsInContainer.ts new file mode 100644 index 00000000..a4da43d0 --- /dev/null +++ b/src/services/RunTestsInContainer.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-console */ +import { TEST_CONFIGURATIONS } from "../utils/constants/test-paths"; +import { setNetworkEnvironment } from "../utils/helpers/network-config"; + +const runTests = async (testName: string, network: string): Promise => { + try { + // Test parameter validation + const testConfig = TEST_CONFIGURATIONS[testName]; + if (!testConfig) { + console.error(`Unknown test: ${testName}`); + console.log("\nAvailable tests:"); + Object.entries(TEST_CONFIGURATIONS).forEach(([name]) => { + console.log(` - ${name}`); + }); + process.exit(1); + } + + setNetworkEnvironment(network); + + // Run the test + const { execSync } = require("child_process"); + + const command = [ + "npx mocha", + "--require ts-node/register", + "--require tsconfig-paths/register", + `--recursive "${testConfig}"`, + "--reporter mochawesome", + "--exit", + ].join(" "); + + execSync(command, { + stdio: "inherit", + env: process.env, + shell: true, + }); + } catch (error: any) { + console.error("Unhandled error:", error); + process.exit(1); + } +}; + +// Get arguments from process.argv or environment variables +const testName = process.env.TEST || "ALL"; +const network = process.env.NETWORK || "local"; + +runTests(testName, network).catch((error) => { + console.error("Unhandled error:", error); + process.exit(1); +}); diff --git a/src/utils/constants/test-paths.ts b/src/utils/constants/test-paths.ts new file mode 100644 index 00000000..9643b857 --- /dev/null +++ b/src/utils/constants/test-paths.ts @@ -0,0 +1,29 @@ +export const TEST_CONFIGURATIONS: Record = { + // Crypto Service Tests + AccountCreate: "src/tests/crypto-service/test-account-create-transaction.ts", + AccountUpdate: "src/tests/crypto-service/test-account-update-transaction.ts", + AccountDelete: "src/tests/crypto-service/test-account-delete-transaction.ts", + AccountAllowanceDelete: + "src/tests/crypto-service/test-account-allowance-delete-transaction.ts", + AccountAllowanceApprove: + "src/tests/crypto-service/test-account-allowance-approve-transaction.ts", + // Token Service Tests + TokenCreate: "src/tests/token-service/test-token-create-transaction.ts", + TokenUpdate: "src/tests/token-service/test-token-update-transaction.ts", + TokenDelete: "src/tests/token-service/test-token-delete-transaction.ts", + TokenBurn: "src/tests/token-service/test-token-burn-transaction.ts", + TokenMint: "src/tests/token-service/test-token-mint-transaction.ts", + TokenAssociate: "src/tests/token-service/test-token-associate-transaction.ts", + TokenDissociate: + "src/tests/token-service/test-token-dissociate-transaction.ts", + TokenFeeScheduleUpdate: + "src/tests/token-service/test-token-fee-schedule-update-transaction.ts", + TokenGrantKyc: "src/tests/token-service/test-token-grant-kyc-transaction.ts", + TokenRevokeKyc: + "src/tests/token-service/test-token-revoke-kyc-transaction.ts", + TokenPause: "src/tests/token-service/test-token-pause-transaction.ts", + TokenUnpause: "src/tests/token-service/test-token-unpause-transaction.ts", + TokenFreeze: "src/tests/token-service/test-token-freeze-transaction.ts", + TokenUnfreeze: "src/tests/token-service/test-token-unfreeze-transaction.ts", + ALL: "src/tests/**/*.ts", +}; diff --git a/src/utils/helpers/network-config.ts b/src/utils/helpers/network-config.ts new file mode 100644 index 00000000..a915a99c --- /dev/null +++ b/src/utils/helpers/network-config.ts @@ -0,0 +1,66 @@ +import dotenv from "dotenv"; + +// Helper function to convert camelCase to SNAKE_CASE +const toEnvFormat = (str: string): string => { + return str + .split(/(?=[A-Z])/) + .join("_") + .toUpperCase(); +}; + +// Set the network config based on the network name +export const getNetworkConfig = ( + network: string, +): Record => { + if (network === "testnet") { + dotenv.config({ path: ".env.testnet" }); + + if ( + process.env.OPERATOR_ACCOUNT_ID === "***" || + process.env.OPERATOR_ACCOUNT_PRIVATE_KEY === "***" + ) { + console.log( + "\n" + + "TESTNET_OPERATOR_ACCOUNT_ID and TESTNET_OPERATOR_ACCOUNT_PRIVATE_KEY must be set for testnet!", + ); + + process.exit(1); + } + + return { + nodeType: "testnet", + nodeTimeout: process.env.NODE_TIMEOUT, + mirrorNodeRestUrl: process.env.MIRROR_NODE_REST_URL, + operatorAccountId: process.env.OPERATOR_ACCOUNT_ID, + operatorAccountPrivateKey: process.env.OPERATOR_ACCOUNT_PRIVATE_KEY, + jsonRpcServerUrl: process.env.JSON_RPC_SERVER_URL, + }; + } + + dotenv.config({ path: ".env.custom_node" }); + + return { + nodeType: "local", + nodeTimeout: process.env.NODE_TIMEOUT, + nodeIp: process.env.NODE_IP, + nodeAccountId: process.env.NODE_ACCOUNT_ID, + mirrorNetwork: process.env.MIRROR_NETWORK, + mirrorNodeRestUrl: process.env.MIRROR_NODE_REST_URL, + mirrorNodeRestJavaUrl: process.env.MIRROR_NODE_REST_JAVA_URL, + operatorAccountId: process.env.OPERATOR_ACCOUNT_ID, + operatorAccountPrivateKey: process.env.OPERATOR_ACCOUNT_PRIVATE_KEY, + jsonRpcServerUrl: process.env.JSON_RPC_SERVER_URL, + }; +}; + +export const setNetworkEnvironment = (network: string): void => { + const config = getNetworkConfig(network); + + // Set environment variables with correct naming convention + Object.entries(config).forEach(([key, value]) => { + if (value) { + const envKey = toEnvFormat(key); + process.env[envKey] = value.toString(); + } + }); +};