From 58fa94e194f7716934e717a0e3075773ebd31b4c Mon Sep 17 00:00:00 2001 From: Fabricius Zatti Date: Mon, 20 May 2024 09:50:56 -0300 Subject: [PATCH] feat(test-tooling): add Stellar test ledger - Add a Stellar test ledger class that can be used in integration tests to start and stop a Stellar test network based on the Stellar quickstart docker image: https://github.com/stellar/quickstart - Inclues the following services for fetching ledger state, executing classic transactions and also soroban smart contracts transactions. - Stellar Core - Horizon API - Soroban RPC - Friendbot Fixes #3239 Signed-off-by: Peter Somogyvari Signed-off-by: Fabricius Zatti --- .cspell.json | 405 ++++++++--------- packages/cactus-test-tooling/README.md | 28 ++ packages/cactus-test-tooling/package.json | 5 + .../src/main/typescript/public-api.ts | 5 + .../src/main/typescript/stellar/network.ts | 9 + .../typescript/stellar/resource-limits.ts | 12 + .../typescript/stellar/stellar-test-ledger.ts | 421 ++++++++++++++++++ .../stellar/supported-image-versions.ts | 6 + .../constructor-validates-options.test.ts | 48 ++ 9 files changed, 737 insertions(+), 202 deletions(-) create mode 100644 packages/cactus-test-tooling/src/main/typescript/stellar/network.ts create mode 100644 packages/cactus-test-tooling/src/main/typescript/stellar/resource-limits.ts create mode 100644 packages/cactus-test-tooling/src/main/typescript/stellar/stellar-test-ledger.ts create mode 100644 packages/cactus-test-tooling/src/main/typescript/stellar/supported-image-versions.ts create mode 100644 packages/cactus-test-tooling/src/test/typescript/integration/stellar/stellar-test-ledger/constructor-validates-options.test.ts diff --git a/.cspell.json b/.cspell.json index 6d8fb626e0..22aecbaec8 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,204 +1,205 @@ { - "version": "0.1", - "language": "en", - "minWordLength": 4, - "allowCompoundWords": true, - "words": [ - "adminpw", - "Albertirsa", - "ALLFORTX", - "anoncreds", - "Anoncreds", - "ANYFORTX", - "Apim", - "APIV", - "approveformyorg", - "askar", - "Askar", - "Authz", - "authzn", - "AWSSM", - "benchmarkjs", - "Besu", - "Bools", - "brioux", - "cact", - "cactusf", - "cafile", - "caio", - "cbdc", - "Cbdc", - "cbor", - "cccg", - "cccs", - "ccep", - "ccid", - "celo", - "cids", - "clsx", - "commenceack", - "configtx", - "connectrpc", - "Corda", - "Cordapp", - "couchdb", - "COUCHDBADDRESS", - "COUCHDBCONFIG", - "cplcs", - "Creds", - "Crpc", - "data", - "davecgh", - "dclm", - "DHTAPI", - "dids", - "Dids", - "DockerOde", - "dokka", - "ealen", - "ecparams", - "embeddable", - "Errorf", - "escc", - "execa", - "faio", - "fastify", - "fidm", - "flowdb", - "fsouza", - "genproto", - "GETHKEYCHAINPASSWORD", - "ghcr", - "gobuffalo", - "gopath", - "gopkg", - "goquorum", - "grpc", - "grpcs", - "grpcwebtext", - "guks", - "hada", - "hashicorp", - "Healthcheck", - "htlc", - "Htlc", - "HTLC", - "Hursley", - "HyperLedger", - "immalleable", - "ipaddress", - "ipfs", - "IPFSHTTP", - "ipld", - "IPLD", - "Iroha", - "Irohad", - "isready", - "jboss", - "joho", - "JORDI", - "jsrsa", - "jsrsasign", - "keccak", - "Keychain", - "Keycloak", - "KEYUTIL", - "KJUR", - "Knetic", - "kubo", - "LEDGERBLOCKACK", - "leveldb", - "lmify", - "LOCALMSPID", - "mailru", - "Merkle", - "merkletreejs", - "miekg", - "mitchellh", - "MSPCONFIGPATH", - "MSPID", - "Mspids", - "MSPIDSCOPEALLFORTX", - "MSPIDSCOPEANYFORTX", - "Mtls", - "myapp", - "mychannel", - "myroot", - "mysecretpassword", - "myvolume", - "Nerc", - "NETWORKSCOPEALLFORTX", - "NETWORKSCOPEANYFORTX", - "NODETXPOOLACK", - "notok", - "Odap", - "Oidc", - "oneofs", - "onsi", - "OpenAPI", - "openethereum", - "organisation", - "Orgs", - "ossp", - "outsh", - "Panicf", - "parameterizable", - "pmezard", - "Postgres", - "proto", - "protobuf", - "protoc", - "protos", - "qscc", - "recoverupdateackmessage", - "rogpeppe", - "RUSTC", - "Rwset", - "satp", - "Satp", - "sbjpubkey", - "Secp", - "shrn", - "Smonitor", - "socketio", - "SPDX", - "Splug", - "Sprintf", - "stretchr", - "supervisorctl", - "supervisord", - "svcs", - "sykesm", - "tezos", - "TEZOS", - "thream", - "tlsca", - "tlscacerts", - "Trivy", - "txid", - "txqueue", - "Uisrs", - "undici", - "unixfs", - "Unmarshal", - "uuidv", - "vscc", - "vuln", - "wasm", - "WSPROVIDER", - "Xdai", - "xeipuuv" - ], - "dictionaries": [ - "typescript,node,npm,go,rust" - ], - "ignorePaths": [ - "**/node_modules/**", - "**/build/**", - "**/src/main/typescript/generated/**", - "packages/cactus-plugin-verifier-cc/**", - "packages/cactus-cmd-socketio-server/**", - "packages/cactus-plugin-ledger-connector-go-ethereum-socketio/**", - "packages/cactus-plugin-ledger-connector-*-socketio/**" - ] + "version": "0.1", + "language": "en", + "minWordLength": 4, + "allowCompoundWords": true, + "words": [ + "adminpw", + "Albertirsa", + "ALLFORTX", + "anoncreds", + "Anoncreds", + "ANYFORTX", + "Apim", + "APIV", + "approveformyorg", + "askar", + "Askar", + "Authz", + "authzn", + "AWSSM", + "benchmarkjs", + "Besu", + "Bools", + "brioux", + "cact", + "cactusf", + "cafile", + "caio", + "cbdc", + "Cbdc", + "cbor", + "cccg", + "cccs", + "ccep", + "ccid", + "celo", + "cids", + "clsx", + "cmds", + "commenceack", + "configtx", + "connectrpc", + "Corda", + "Cordapp", + "couchdb", + "COUCHDBADDRESS", + "COUCHDBCONFIG", + "cplcs", + "Creds", + "Crpc", + "data", + "davecgh", + "dclm", + "DHTAPI", + "dids", + "Dids", + "DockerOde", + "dokka", + "ealen", + "ecparams", + "embeddable", + "Errorf", + "escc", + "execa", + "faio", + "fastify", + "fidm", + "flowdb", + "fsouza", + "genproto", + "GETHKEYCHAINPASSWORD", + "ghcr", + "gobuffalo", + "gopath", + "gopkg", + "goquorum", + "grpc", + "grpcs", + "grpcwebtext", + "guks", + "hada", + "hashicorp", + "Healthcheck", + "htlc", + "Htlc", + "HTLC", + "Hursley", + "HyperLedger", + "immalleable", + "ipaddress", + "ipfs", + "IPFSHTTP", + "ipld", + "IPLD", + "Iroha", + "Irohad", + "isready", + "jboss", + "joho", + "JORDI", + "jsrsa", + "jsrsasign", + "keccak", + "Keychain", + "Keycloak", + "KEYUTIL", + "KJUR", + "Knetic", + "kubo", + "LEDGERBLOCKACK", + "leveldb", + "lmify", + "LOCALMSPID", + "mailru", + "Merkle", + "merkletreejs", + "miekg", + "mitchellh", + "MSPCONFIGPATH", + "MSPID", + "Mspids", + "MSPIDSCOPEALLFORTX", + "MSPIDSCOPEANYFORTX", + "Mtls", + "myapp", + "mychannel", + "myroot", + "mysecretpassword", + "myvolume", + "Nerc", + "NETWORKSCOPEALLFORTX", + "NETWORKSCOPEANYFORTX", + "NODETXPOOLACK", + "notok", + "Odap", + "Oidc", + "oneofs", + "onsi", + "OpenAPI", + "openethereum", + "organisation", + "Orgs", + "ossp", + "outsh", + "Panicf", + "parameterizable", + "pmezard", + "Postgres", + "proto", + "protobuf", + "protoc", + "protos", + "qscc", + "recoverupdateackmessage", + "rogpeppe", + "RUSTC", + "Rwset", + "satp", + "Satp", + "sbjpubkey", + "Secp", + "shrn", + "Smonitor", + "socketio", + "Soroban", + "soroban", + "SPDX", + "Splug", + "Sprintf", + "stretchr", + "supervisorctl", + "supervisord", + "svcs", + "sykesm", + "tezos", + "TEZOS", + "thream", + "tlsca", + "tlscacerts", + "Trivy", + "txid", + "txqueue", + "Uisrs", + "undici", + "unixfs", + "Unmarshal", + "uuidv", + "vscc", + "vuln", + "wasm", + "WSPROVIDER", + "Xdai", + "xeipuuv" + ], + "dictionaries": ["typescript,node,npm,go,rust"], + "ignorePaths": [ + "**/node_modules/**", + "**/build/**", + "**/src/main/typescript/generated/**", + "packages/cactus-plugin-verifier-cc/**", + "packages/cactus-cmd-socketio-server/**", + "packages/cactus-plugin-ledger-connector-go-ethereum-socketio/**", + "packages/cactus-plugin-ledger-connector-*-socketio/**" + ] } diff --git a/packages/cactus-test-tooling/README.md b/packages/cactus-test-tooling/README.md index 4d59df6304..3eb995365f 100644 --- a/packages/cactus-test-tooling/README.md +++ b/packages/cactus-test-tooling/README.md @@ -7,15 +7,43 @@ ``` // TODO: DEMONSTRATE API ``` + ## Docker image for the ws-identity server A docker image of the [ws-identity server](https://hub.docker.com/repository/docker/brioux/ws-identity) is used to test integration of WS-X.509 credential type in the fabric connector plugin. [ws-identity](https://github.com/brioux/ws-identity) includes A Docker file to build the image: clone the repo, install packages, build src and the image + ``` npm install npm run build docker build . -t [image-name] ``` +## Stellar Test Ledger Usage + +The Stellar test ledger follows the same structure present in the test ledger tools for other networks within the Cacti project. It pulls up and manages the [Stellar Quickstart Docker Image](https://github.com/stellar/quickstart) and can be used by importing the class `StellarTestLedger`, then instantiating it with some key optional arguments to define how the image should be configure. + +- `network`: Defines if the image should pull up a pristine local ledger or alternatively connect to an existing public test ledger. Defaults to `local`. It is important to note that connecting to an existing network can take up to several minutes to synchronize the ledger state. + +- `limits`: Defines the resource limits for soroban smart contract transactions. A valid transaction and only be included in a ledger block if enough resources are available for that operation. Defaults to `testnet`, which mimics the actual resource limits applied to the mainnet based on its test environment. + +Once the class is successfully instantiated, one can start the environment by triggering + +```typescript +await stellarTestLedger.start(); +``` + +The image will be pulled up and wait until the healthcheck ensures all of its services have started successfully and are accessible, then returns the container object. + +When integrating to a Stellar environment, it is common to use a few key services provided at different ports and paths. Once the class has been started, one can use the method `getNetworkConfiguration()` to get an object containing the required information to connect to this services. + +This object is already formatted to be used with the [stellar-plus](https://github.com/CheesecakeLabs/stellar-plus) open source js library to create a custom network configuration object that integrates with its provided tools, ensuring a frictionless development flow for this test ledger. + +Once the image have been fully utilized, one can fully stop and remove the environment by triggering + +```typescript +await stellarTestLedger.stop(); +await stellarTestLedger.destroy(); +``` diff --git a/packages/cactus-test-tooling/package.json b/packages/cactus-test-tooling/package.json index fe61f7944b..02ab508d1c 100644 --- a/packages/cactus-test-tooling/package.json +++ b/packages/cactus-test-tooling/package.json @@ -43,6 +43,11 @@ "name": "Peter Somogyvari", "email": "peter.somogyvari@accenture.com", "url": "https://accenture.com" + }, + { + "name": "Fabricius Zatti", + "email": "fazzatti@gmail.com", + "url": "https://oififo.com" } ], "main": "dist/lib/main/typescript/index.js", diff --git a/packages/cactus-test-tooling/src/main/typescript/public-api.ts b/packages/cactus-test-tooling/src/main/typescript/public-api.ts index 1380f8fb59..fb13f15d19 100755 --- a/packages/cactus-test-tooling/src/main/typescript/public-api.ts +++ b/packages/cactus-test-tooling/src/main/typescript/public-api.ts @@ -165,6 +165,11 @@ export { SawtoothTestLedger, } from "./sawtooth/sawtooth-test-ledger"; +export { + IStellarTestLedgerOptions, + StellarTestLedger, +} from "./stellar/stellar-test-ledger"; + export { ISubstrateTestLedgerOptions, SubstrateTestLedger, diff --git a/packages/cactus-test-tooling/src/main/typescript/stellar/network.ts b/packages/cactus-test-tooling/src/main/typescript/stellar/network.ts new file mode 100644 index 0000000000..7a3229ec63 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/stellar/network.ts @@ -0,0 +1,9 @@ +// +// List of supported networks to connect +// when the test ledger image is pulled up. +// +export enum Network { + LOCAL = "local", // (Default) pull up a new pristine network image locally. + FUTURENET = "futurenet", // pull up an image to connect to futurenet. Can take several minutes to sync the ledger state. + TESTNET = "testnet", // pull up an image to connect to testnet Can take several minutes to sync the ledger state. +} diff --git a/packages/cactus-test-tooling/src/main/typescript/stellar/resource-limits.ts b/packages/cactus-test-tooling/src/main/typescript/stellar/resource-limits.ts new file mode 100644 index 0000000000..984f8caecd --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/stellar/resource-limits.ts @@ -0,0 +1,12 @@ +// +// Define the resource limits set to Soroban transactions +// when pulling up a local network. This defines how smart contract +// transactions are limited in terms of resources during execution. +// +// Transactions that exceed these limits will be rejected. +// +export enum ResourceLimits { + TESTNET = "testnet", // (Default) sets the limits to match those used on testnet. + DEFAULT = "default", // leaves resource limits set extremely low as per Stellar's core default configuration + UNLIMITED = "unlimited", // set limits to maximum resources that can be configured +} diff --git a/packages/cactus-test-tooling/src/main/typescript/stellar/stellar-test-ledger.ts b/packages/cactus-test-tooling/src/main/typescript/stellar/stellar-test-ledger.ts new file mode 100644 index 0000000000..ed723bf9bd --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/stellar/stellar-test-ledger.ts @@ -0,0 +1,421 @@ +import Docker, { + Container, + ContainerCreateOptions, + ContainerInfo, +} from "dockerode"; +import { ITestLedger } from "../i-test-ledger"; +import { + Bools, + LogLevelDesc, + Logger, + LoggerProvider, +} from "@hyperledger/cactus-common"; +import { Containers } from "../public-api"; +import EventEmitter from "events"; +import { SupportedImageVersions } from "./supported-image-versions"; +import { Network } from "./network"; +import { ResourceLimits } from "./resource-limits"; + +export interface IStellarTestLedger extends ITestLedger { + getNetworkConfiguration(): Promise; + getContainer(): Container; + getContainerIpAddress(): Promise; +} + +// This interface defines the network configuration data for the test stellar ledger. +// This is used to manage the connections to different services necessary to interact with the ledger. +export interface INetworkConfigData { + networkPassphrase: string; + rpcUrl?: string; + horizonUrl?: string; + friendbotUrl?: string; + allowHttp?: boolean; +} + +export interface IStellarTestLedgerOptions { + // Defines which type of network will the image will be configured to run. + network?: Network; + + // Defines the resource limits for soroban transactions. A valid transaction and only be included in a ledger + // block if enough resources are available for that operation. + limits?: ResourceLimits; + + // For test development, attach to ledger that is already running, don't spin up new one + useRunningLedger?: boolean; + + readonly logLevel?: LogLevelDesc; + readonly containerImageName?: string; + readonly containerImageVersion?: SupportedImageVersions; + readonly emitContainerLogs?: boolean; +} + +const DEFAULTS = Object.freeze({ + imageName: "stellar/quickstart", + imageVersion: SupportedImageVersions.V425_LATEST, + network: Network.LOCAL, + limits: ResourceLimits.TESTNET, + useRunningLedger: false, + logLevel: "info" as LogLevelDesc, + emitContainerLogs: false, +}); + +/** + * This class provides the functionality to start and stop a test stellar ledger. + * The ledger is started as a docker container and can be configured to run on different networks. + * + * @param {IStellarTestLedgerOptions} options - Options for the test stellar ledger. + * @param {Network} options.network - Defines which type of network will the image will be configured to run. + * @param {ResourceLimits} options.limits - Defines the resource limits for soroban transactions. + * @param {boolean} options.useRunningLedger - For test development, attach to ledger that is already running, don't spin up new one. + * @param {LogLevelDesc} options.logLevel - The log level for the test stellar ledger. + * @param {string} options.containerImageName - The name of the container image to use for the test stellar ledger. + * @param {SupportedImageVersions} options.containerImageVersion - The version of the container image to use for the test stellar ledger. + * @param {boolean} options.emitContainerLogs - If true, the container logs will be emitted. + * + */ +export class StellarTestLedger implements IStellarTestLedger { + public readonly containerImageName: string; + public readonly containerImageVersion: SupportedImageVersions; + + private readonly network: string; + private readonly limits: string; + private readonly useRunningLedger: boolean; + + private readonly emitContainerLogs: boolean; + private readonly log: Logger; + private readonly logLevel: LogLevelDesc; + public container: Container | undefined; + public containerId: string | undefined; + + constructor(options?: IStellarTestLedgerOptions) { + this.network = options?.network || DEFAULTS.network; + this.limits = options?.limits || DEFAULTS.limits; + + if (this.network != Network.LOCAL) { + throw new Error( + `StellarTestLedger#constructor() network ${this.network} not supported yet.`, + ); + } + if (this.limits != Network.TESTNET) { + throw new Error( + `StellarTestLedger#constructor() limits ${this.limits} not supported yet.`, + ); + } + + this.containerImageVersion = + options?.containerImageVersion || DEFAULTS.imageVersion; + + // if image name is not a supported version + if ( + !Object.values(SupportedImageVersions).includes( + this.containerImageVersion, + ) + ) { + throw new Error( + `StellarTestLedger#constructor() containerImageVersion ${options?.containerImageVersion} not supported.`, + ); + } + + this.containerImageName = options?.containerImageName || DEFAULTS.imageName; + + this.useRunningLedger = Bools.isBooleanStrict(options?.useRunningLedger) + ? (options?.useRunningLedger as boolean) + : DEFAULTS.useRunningLedger; + + this.logLevel = options?.logLevel || DEFAULTS.logLevel; + this.emitContainerLogs = Bools.isBooleanStrict(options?.emitContainerLogs) + ? (options?.emitContainerLogs as boolean) + : DEFAULTS.emitContainerLogs; + + this.log = LoggerProvider.getOrCreate({ + level: this.logLevel, + label: "stellar-test-ledger", + }); + } + + /** + * Get the full container image name. + * + * @returns {string} Full container image name + */ + public get fullContainerImageName(): string { + return [this.containerImageName, this.containerImageVersion].join(":"); + } + + public getContainer(): Container { + if (!this.container) { + throw new Error( + `StellarTestLedger#getContainer() Container not started yet by this instance.`, + ); + } else { + return this.container; + } + } + + /** + * + * Get the container information for the test stellar ledger. + * + * @returns {ContainerInfo} Container information + */ + protected async getContainerInfo(): Promise { + const fnTag = "StellarTestLedger#getContainerInfo()"; + const docker = new Docker(); + const image = this.containerImageName; + const containerInfos = await docker.listContainers({}); + + let aContainerInfo; + if (this.containerId !== undefined) { + aContainerInfo = containerInfos.find((ci) => ci.Id === this.containerId); + } + + if (aContainerInfo) { + return aContainerInfo; + } else { + throw new Error(`${fnTag} no image "${image}"`); + } + } + + /** + * + * Get the IP address of the container. + * + * @returns {string} IP address of the container + */ + public async getContainerIpAddress(): Promise { + const fnTag = "StellarTestLedger#getContainerIpAddress()"; + const aContainerInfo = await this.getContainerInfo(); + + if (aContainerInfo) { + const { NetworkSettings } = aContainerInfo; + const networkNames: string[] = Object.keys(NetworkSettings.Networks); + if (networkNames.length < 1) { + throw new Error(`${fnTag} container not connected to any networks`); + } else { + // return IP address of container on the first network that we found + return NetworkSettings.Networks[networkNames[0]].IPAddress; + } + } else { + throw new Error(`${fnTag} cannot find image: ${this.containerImageName}`); + } + } + + /** + * + * Get the commands to pass to the docker container. + * + * @returns {string[]} Array of commands to pass to the docker container + */ + private getImageCommands(): string[] { + const cmds = []; + + switch (this.network) { + case Network.FUTURENET: + cmds.push("--futurenet"); + break; + case Network.TESTNET: + cmds.push("--testnet"); + break; + case Network.LOCAL: + default: + cmds.push("--local"); + break; + } + + switch (this.limits) { + case ResourceLimits.DEFAULT: + cmds.push("--limits", "default"); + break; + case ResourceLimits.UNLIMITED: + cmds.push("--limits", "unlimited"); + break; + case ResourceLimits.TESTNET: + default: + cmds.push("--limits", "testnet"); + break; + } + + return cmds; + } + + /** + * + * Get the network configuration data for the test stellar ledger. + * This includes the network passphrase, rpcUrl, horizonUrl, + * friendbotUrl, and the allowHttp flag. + * + * @returns {INetworkConfigData} Network configuration data + */ + public async getNetworkConfiguration(): Promise { + if (this.network != "local") { + throw new Error( + `StellarTestLedger#getNetworkConfiguration() network ${this.network} not supported yet.`, + ); + } + const cInfo = await this.getContainerInfo(); + const publicPort = await Containers.getPublicPort(8000, cInfo); + + // Default docker internal domain. Redirects to the local host where docker is running. + const domain = "127.0.0.1"; + + return Promise.resolve({ + networkPassphrase: "Standalone Network ; February 2017", + rpcUrl: `http://${domain}:${publicPort}/rpc`, + horizonUrl: `http://${domain}:${publicPort}`, + friendbotUrl: `http://${domain}:${publicPort}/friendbot`, + allowHttp: true, + }); + } + + /** + * Start a test stellar ledger. + * + * @param {boolean} omitPull - If true, the image will not be pulled from the registry. + * @returns {Container} The container object. + */ + public async start(omitPull = false): Promise { + if (this.useRunningLedger) { + this.log.info( + "Search for already running Stellar Test Ledger because 'useRunningLedger' flag is enabled.", + ); + this.log.info( + "Search criteria - image name: ", + this.fullContainerImageName, + ", state: running", + ); + const containerInfo = await Containers.getByPredicate( + (ci) => + ci.Image === this.fullContainerImageName && ci.State === "running", + ); + const docker = new Docker(); + this.containerId = containerInfo.Id; + this.container = docker.getContainer(this.containerId); + return this.container; + } + + if (this.container) { + await this.container.stop(); + await this.container.remove(); + this.container = undefined; + this.containerId = undefined; + } + + if (!omitPull) { + await Containers.pullImage( + this.fullContainerImageName, + {}, + this.logLevel, + ); + } + + const createOptions: ContainerCreateOptions = { + ExposedPorts: { + "8000/tcp": {}, // Stellar services will use this port (Horizon, RPC and Friendbot) + }, + HostConfig: { + PublishAllPorts: true, + Privileged: true, + }, + }; + + const Healthcheck = { + Test: [ + "CMD-SHELL", + "curl -s -o /dev/null -w '%{http_code}' http://localhost:8000 | grep -q '200' && curl -s -X POST -H 'Content-Type: application/json' -d '{\"jsonrpc\": \"2.0\", \"id\": 8675309, \"method\": \"getHealth\"}' http://localhost:8000/rpc | grep -q 'healthy' && curl -s http://localhost:8000/friendbot | grep -q '\"status\": 400' || exit 1", + ], + Interval: 1000000000, // 1 second + Timeout: 3000000000, // 3 seconds + Retries: 120, // 120 retries over 2 min should be enough for a big variety of systems + StartPeriod: 1000000000, // 1 second + }; + + return new Promise((resolve, reject) => { + const docker = new Docker(); + const eventEmitter: EventEmitter = docker.run( + this.fullContainerImageName, + [...this.getImageCommands()], + [], + { ...createOptions, Healthcheck: Healthcheck }, + {}, + (err: unknown) => { + if (err) { + reject(err); + } + }, + ); + + eventEmitter.once("start", async (container: Container) => { + this.container = container; + this.containerId = container.id; + + if (this.emitContainerLogs) { + const fnTag = `[${this.fullContainerImageName}]`; + await Containers.streamLogs({ + container: this.container, + tag: fnTag, + log: this.log, + }); + } + + try { + this.log.debug("Waiting for services to fully start."); + await Containers.waitForHealthCheck(this.containerId); + this.log.debug("Stellar Test Ledger is ready."); + resolve(container); + } catch (ex) { + this.log.error(ex); + reject(ex); + } + }); + }); + } + + /** + * Stop the test stellar ledger. + * + * @returns {Promise} A promise that resolves when the ledger is stopped. + */ + public async stop(): Promise { + if (this.useRunningLedger) { + this.log.info("Ignore stop request because useRunningLedger is enabled."); + return Promise.resolve(); + } else { + return Containers.stop(this.getContainer()); + } + } + + /** + * Destroy the test stellar ledger. + * + * @returns {Promise} A promise that resolves when the ledger is destroyed. + */ + public async destroy(): Promise { + if (this.useRunningLedger) { + this.log.info( + "Ignore destroy request because useRunningLedger is enabled.", + ); + return Promise.resolve(); + } else if (this.container) { + const docker = new Docker(); + const containerInfo = await this.container.inspect(); + const volumes = containerInfo.Mounts; + await this.container.remove(); + volumes.forEach(async (volume) => { + this.log.info(`Removing volume ${volume}`); + if (volume.Name) { + const volumeToDelete = docker.getVolume(volume.Name); + await volumeToDelete.remove(); + } else { + this.log.warn(`Volume ${volume} could not be removed!`); + } + }); + return Promise.resolve(); + } else { + return Promise.reject( + new Error( + `StellarTestLedger#destroy() Container was never created, nothing to destroy.`, + ), + ); + } + } +} diff --git a/packages/cactus-test-tooling/src/main/typescript/stellar/supported-image-versions.ts b/packages/cactus-test-tooling/src/main/typescript/stellar/supported-image-versions.ts new file mode 100644 index 0000000000..c02715bdc2 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/stellar/supported-image-versions.ts @@ -0,0 +1,6 @@ +// For now, only the latest version of the image is supported. +// This enum can be expanded to support more versions in the future. +export enum SupportedImageVersions { + LASTEST = "latest", + V425_LATEST = "v425-latest", +} diff --git a/packages/cactus-test-tooling/src/test/typescript/integration/stellar/stellar-test-ledger/constructor-validates-options.test.ts b/packages/cactus-test-tooling/src/test/typescript/integration/stellar/stellar-test-ledger/constructor-validates-options.test.ts new file mode 100644 index 0000000000..69596067dd --- /dev/null +++ b/packages/cactus-test-tooling/src/test/typescript/integration/stellar/stellar-test-ledger/constructor-validates-options.test.ts @@ -0,0 +1,48 @@ +import "jest-extended"; +import { Container } from "dockerode"; +import { StellarTestLedger } from "../../../../../main/typescript/public-api"; +import axios from "axios"; +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { SupportedImageVersions } from "../../../../../main/typescript/stellar/supported-image-versions"; + +describe("StellarTestLEdger", () => { + const logLevel: LogLevelDesc = "TRACE"; + const stellarTestLedger = new StellarTestLedger({ logLevel }); + + afterAll(async () => { + await stellarTestLedger.stop(); + await stellarTestLedger.destroy(); + }); + + test("constructor throws if invalid input is provided", () => { + expect(StellarTestLedger).toBeDefined(); + + expect(() => { + new StellarTestLedger({ + containerImageVersion: "nope" as unknown as SupportedImageVersions, + }); + }).toThrow(); + }); + + test("constructor does not throw if valid input is provided", async () => { + expect(StellarTestLedger).toBeDefined(); + expect(() => { + new StellarTestLedger(); + }).not.toThrow(); + }); + + test("starts/stops/destroys a valid docker container", async () => { + const container: Container = await stellarTestLedger.start(); + expect(container).toBeDefined(); + + const networkConfig = await stellarTestLedger.getNetworkConfiguration(); + expect(networkConfig).toBeDefined(); + expect(networkConfig.horizonUrl).toBeDefined(); + expect(networkConfig.networkPassphrase).toBeDefined(); + expect(networkConfig.rpcUrl).toBeDefined(); + expect(networkConfig.friendbotUrl).toBeDefined(); + + const horizonResponse = await axios.get(networkConfig.horizonUrl as string); + expect(horizonResponse.status).toBe(200); + }); +});