From e3ab39c0dab3a3e04fc1d8223fea58f799601c22 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Thu, 15 Feb 2024 15:56:56 +0100 Subject: [PATCH] refactor(cardano-services): rework cli argument parsing and relative tests --- .../continuous-integration-unit-tests.yaml | 1 + packages/cardano-services/jest.config.js | 10 +- packages/cardano-services/package.json | 3 +- .../src/Program/options/common.ts | 25 +- .../src/Program/options/ogmios.ts | 3 +- .../src/Program/options/postgres.ts | 4 +- .../src/Program/options/stakePoolMetadata.ts | 16 +- .../src/Program/programs/blockfrostWorker.ts | 2 - .../src/Program/programs/types.ts | 2 + packages/cardano-services/src/cli.ts | 96 +- .../cardano-services/src/util/validators.ts | 50 +- packages/cardano-services/test/cli.test.ts | 3648 +++++------------ 12 files changed, 1178 insertions(+), 2682 deletions(-) diff --git a/.github/workflows/continuous-integration-unit-tests.yaml b/.github/workflows/continuous-integration-unit-tests.yaml index 1c4d820b48b..0c8f31ca354 100644 --- a/.github/workflows/continuous-integration-unit-tests.yaml +++ b/.github/workflows/continuous-integration-unit-tests.yaml @@ -48,5 +48,6 @@ jobs: run: | yarn test:build:verify yarn test --forceExit + yarn workspace @cardano-sdk/cardano-services test:cli env: NODE_OPTIONS: '--max_old_space_size=8192' diff --git a/packages/cardano-services/jest.config.js b/packages/cardano-services/jest.config.js index bbd7c0e1f5a..b56bf66fb88 100644 --- a/packages/cardano-services/jest.config.js +++ b/packages/cardano-services/jest.config.js @@ -1,6 +1,14 @@ -module.exports = { +const common = { ...require('../../test/jest.config'), globalSetup: './test/jest-setup/jest-setup.ts', globalTeardown: './test/jest-setup/jest-teardown.ts', setupFilesAfterEnv: ['./test/jest-setup/matchers.ts'] }; + +module.exports = { + ...common, + projects: [ + { ...common, displayName: 'cli', testMatch: ['/test/cli.test.ts'] }, + { ...common, displayName: 'unit', testPathIgnorePatterns: ['cli.test'] } + ] +}; diff --git a/packages/cardano-services/package.json b/packages/cardano-services/package.json index 7339e700a64..29c3d428ff2 100644 --- a/packages/cardano-services/package.json +++ b/packages/cardano-services/package.json @@ -47,8 +47,9 @@ "preview:dev": "FILES='-f ../../compose/dev.yml -f dev.yml' yarn preview:up", "preview:up": "NETWORK=preview SUBMIT_API_ARGS='--testnet-magic 2' yarn compose:up", "preview:down": "NETWORK=preview yarn compose:down", - "test": "jest --runInBand -c ./jest.config.js", + "test": "jest --runInBand -c ./jest.config.js --selectProjects unit", "test:build:verify": "tsc --build ./test", + "test:cli": "jest --runInBand -c ./jest.config.js --selectProjects cli", "test:debug": "DEBUG=true yarn test", "test:e2e": "echo 'test:e2e' command not implemented yet", "test:load": "jest -c ./load.jest.config.js --runInBand", diff --git a/packages/cardano-services/src/Program/options/common.ts b/packages/cardano-services/src/Program/options/common.ts index 5b292e2ef78..06f6ba81eb9 100644 --- a/packages/cardano-services/src/Program/options/common.ts +++ b/packages/cardano-services/src/Program/options/common.ts @@ -11,7 +11,7 @@ import { import { Seconds } from '@cardano-sdk/core'; import { BuildInfo as ServiceBuildInfo } from '../../Http'; import { addOptions, newOption } from './util'; -import { buildInfoValidator, cacheTtlValidator } from '../../util/validators'; +import { buildInfoValidator, floatValidator, integerValidator, urlValidator } from '../../util/validators'; import { loggerMethodNames } from '@cardano-sdk/util'; export const ENABLE_METRICS_DEFAULT = false; @@ -21,10 +21,10 @@ export const LAST_ROS_EPOCHS_DEFAULT = 10; enum Descriptions { ApiUrl = 'API URL', BuildInfo = 'Service build info', + DumpOnly = 'Dumps the input arguments and exits. Used for tests', + EnableMetrics = 'Enable Prometheus Metrics', LastRosEpochs = 'Number of epochs over which lastRos is computed', LoggerMinSeverity = 'Log level', - HealthCheckCacheTtl = 'Health check cache TTL in seconds between 1 and 10', - EnableMetrics = 'Enable Prometheus Metrics', ServiceDiscoveryBackoffFactor = 'Exponential backoff factor for service discovery', ServiceDiscoveryTimeout = 'Timeout for service discovery attempts' } @@ -32,6 +32,7 @@ enum Descriptions { export type CommonProgramOptions = { apiUrl: URL; buildInfo?: ServiceBuildInfo; + dumpOnly?: boolean; enableMetrics?: boolean; lastRosEpochs?: number; loggerMinSeverity?: LogLevel; @@ -41,8 +42,11 @@ export type CommonProgramOptions = { export const withCommonOptions = (command: Command, apiUrl: URL) => { addOptions(command, [ - newOption('--api-url ', Descriptions.ApiUrl, 'API_URL', (url) => new URL(url), apiUrl), + newOption('--api-url ', Descriptions.ApiUrl, 'API_URL', urlValidator(Descriptions.ApiUrl), apiUrl), newOption('--build-info ', Descriptions.BuildInfo, 'BUILD_INFO', buildInfoValidator), + newOption('--dump-only ', Descriptions.DumpOnly, 'DUMP_ONLY', (dumpOnly) => + stringOptionToBoolean(dumpOnly, Programs.ProviderServer, Descriptions.DumpOnly) + ), newOption( '--enable-metrics ', Descriptions.EnableMetrics, @@ -50,18 +54,11 @@ export const withCommonOptions = (command: Command, apiUrl: URL) => { (enableMetrics) => stringOptionToBoolean(enableMetrics, Programs.ProviderServer, Descriptions.EnableMetrics), ENABLE_METRICS_DEFAULT ), - newOption( - '--health-check-cache-ttl ', - Descriptions.HealthCheckCacheTtl, - 'HEALTH_CHECK_CACHE_TTL', - (ttl: string) => cacheTtlValidator(ttl, { lowerBound: 1, upperBound: 120 }, Descriptions.HealthCheckCacheTtl), - DEFAULT_HEALTH_CHECK_CACHE_TTL - ), newOption( '--last-ros-epochs ', Descriptions.LastRosEpochs, 'LAST_ROS_EPOCHS', - (lastRosEpochs) => Number.parseInt(lastRosEpochs, 10), + integerValidator(Descriptions.LastRosEpochs), LAST_ROS_EPOCHS_DEFAULT ), newOption( @@ -78,14 +75,14 @@ export const withCommonOptions = (command: Command, apiUrl: URL) => { '--service-discovery-backoff-factor ', Descriptions.ServiceDiscoveryBackoffFactor, 'SERVICE_DISCOVERY_BACKOFF_FACTOR', - (factor) => Number.parseFloat(factor), + floatValidator(Descriptions.ServiceDiscoveryBackoffFactor), SERVICE_DISCOVERY_BACKOFF_FACTOR_DEFAULT ), newOption( '--service-discovery-timeout ', Descriptions.ServiceDiscoveryTimeout, 'SERVICE_DISCOVERY_TIMEOUT', - (interval) => Number.parseInt(interval, 10), + integerValidator(Descriptions.ServiceDiscoveryTimeout), SERVICE_DISCOVERY_TIMEOUT_DEFAULT ) ]); diff --git a/packages/cardano-services/src/Program/options/ogmios.ts b/packages/cardano-services/src/Program/options/ogmios.ts index 62cfd9e4403..c48bdebfd8d 100644 --- a/packages/cardano-services/src/Program/options/ogmios.ts +++ b/packages/cardano-services/src/Program/options/ogmios.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { Ogmios } from '@cardano-sdk/ogmios'; import { addOptions, newOption } from './util'; +import { urlValidator } from '../../util/validators'; const OGMIOS_URL_DEFAULT = (() => { const connection = Ogmios.createConnectionObject(); @@ -28,7 +29,7 @@ export const withOgmiosOptions = (command: Command) => '--ogmios-url ', OgmiosOptionDescriptions.Url, 'OGMIOS_URL', - (url) => new URL(url), + urlValidator(OgmiosOptionDescriptions.Url), new URL(OGMIOS_URL_DEFAULT) ).conflicts('ogmiosSrvServiceName') ]); diff --git a/packages/cardano-services/src/Program/options/postgres.ts b/packages/cardano-services/src/Program/options/postgres.ts index 785a22f499e..b731493c8c3 100644 --- a/packages/cardano-services/src/Program/options/postgres.ts +++ b/packages/cardano-services/src/Program/options/postgres.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { addOptions, newOption } from './util'; -import { existingFileValidator } from '../../util/validators'; +import { existingFileValidator, integerValidator } from '../../util/validators'; export enum PostgresOptionDescriptions { ConnectionString = 'PostgreSQL Connection string', @@ -120,7 +120,7 @@ export const withPostgresOptions = (command: Command, suffixes: ConnectionNames[ `--postgres-pool-max${cliSuffix} `, PostgresOptionDescriptions.PoolMax + descSuffix, `POSTGRES_POOL_MAX${envSuffix}`, - (max) => Number.parseInt(max, 10) + integerValidator(PostgresOptionDescriptions.PoolMax + descSuffix) ), newOption( `--postgres-port${cliSuffix} `, diff --git a/packages/cardano-services/src/Program/options/stakePoolMetadata.ts b/packages/cardano-services/src/Program/options/stakePoolMetadata.ts index 63b06baa7ae..bbdf5751388 100644 --- a/packages/cardano-services/src/Program/options/stakePoolMetadata.ts +++ b/packages/cardano-services/src/Program/options/stakePoolMetadata.ts @@ -1,8 +1,8 @@ import { Command } from 'commander'; import { MissingProgramOption } from '../errors'; import { STAKE_POOL_METADATA_QUEUE } from '@cardano-sdk/projection-typeorm'; -import { URL } from 'url'; import { addOptions, newOption } from './util'; +import { urlValidator } from '../../util/validators'; export enum StakePoolMetadataOptionDescriptions { Mode = 'This mode governs where the stake pool metadata is fetched from', @@ -32,8 +32,11 @@ export const withStakePoolMetadataOptions = (command: Command) => { (mode: string) => StakePoolMetadataFetchMode[mode.toUpperCase() as keyof typeof StakePoolMetadataFetchMode], 'direct' ).choices(['direct', 'smash']), - newOption('--smash-url ', StakePoolMetadataOptionDescriptions.Url, 'SMASH_URL', (url) => - new URL(url).toString() + newOption( + '--smash-url ', + StakePoolMetadataOptionDescriptions.Url, + 'SMASH_URL', + urlValidator(StakePoolMetadataOptionDescriptions.Url, true) ) ]); @@ -41,11 +44,6 @@ export const withStakePoolMetadataOptions = (command: Command) => { }; export const checkProgramOptions = (metadataFetchMode: StakePoolMetadataFetchMode, smashUrl: string | undefined) => { - if (!metadataFetchMode) throw new MissingProgramOption(STAKE_POOL_METADATA_QUEUE, 'medata-fetch-mode'); - if (metadataFetchMode === StakePoolMetadataFetchMode.SMASH && !smashUrl) - throw new MissingProgramOption( - STAKE_POOL_METADATA_QUEUE, - `smash-url to be set when medata-fetch-mode is smash ${smashUrl}` - ); + throw new MissingProgramOption(STAKE_POOL_METADATA_QUEUE, 'smash-url to be set when metadata-fetch-mode is smash'); }; diff --git a/packages/cardano-services/src/Program/programs/blockfrostWorker.ts b/packages/cardano-services/src/Program/programs/blockfrostWorker.ts index 6da0d71de4f..62559f89d1a 100644 --- a/packages/cardano-services/src/Program/programs/blockfrostWorker.ts +++ b/packages/cardano-services/src/Program/programs/blockfrostWorker.ts @@ -67,8 +67,6 @@ export const loadBlockfrostWorker = async (args: BlockfrostWorkerArgs, deps: Loa BlockfrostWorkerOptionDescriptions.BlockfrostApiKey ]); - if (!args.network) throw new MissingProgramOption(blockfrostWorker, BlockfrostWorkerOptionDescriptions.Network); - if (!db) throw new MissingProgramOption(blockfrostWorker, [ PostgresOptionDescriptions.ConnectionString, diff --git a/packages/cardano-services/src/Program/programs/types.ts b/packages/cardano-services/src/Program/programs/types.ts index 71b276cedce..25c9227b703 100644 --- a/packages/cardano-services/src/Program/programs/types.ts +++ b/packages/cardano-services/src/Program/programs/types.ts @@ -51,9 +51,11 @@ export enum ProviderServerOptionDescriptions { DisableStakePoolMetricApy = 'Omit this metric for improved query performance', EpochPollInterval = 'Epoch poll interval', HandleProviderServerUrl = 'URL for the Handle provider server', + HealthCheckCacheTtl = 'Health check cache TTL in seconds between 1 and 10', PaginationPageSizeLimit = 'Pagination page size limit shared across all providers', SubmitApiUrl = 'cardano-submit-api URL', TokenMetadataCacheTtl = 'Token Metadata API cache TTL in seconds', + TokenMetadataRequestTimeout = 'Token Metadata request timeout in milliseconds', TokenMetadataServerUrl = 'Token Metadata API server URL', UseTypeOrmStakePoolProvider = 'Enables the TypeORM Stake Pool Provider', UseBlockfrost = 'Enables Blockfrost cached data DB', diff --git a/packages/cardano-services/src/cli.ts b/packages/cardano-services/src/cli.ts index f3590731f8f..90f4dd55be6 100755 --- a/packages/cardano-services/src/cli.ts +++ b/packages/cardano-services/src/cli.ts @@ -9,6 +9,7 @@ import { BlockfrostWorkerOptionDescriptions, CACHE_TTL_DEFAULT, CREATE_SCHEMA_DEFAULT, + DEFAULT_HEALTH_CHECK_CACHE_TTL, DISABLE_DB_CACHE_DEFAULT, DISABLE_STAKE_POOL_METRIC_APY_DEFAULT, DROP_SCHEMA_DEFAULT, @@ -60,7 +61,7 @@ import { EPOCH_POLL_INTERVAL_DEFAULT } from './util'; import { HttpServer } from './Http'; import { PgBossQueue, isValidQueue } from './PgBoss'; import { ProjectionName } from './Projection'; -import { dbCacheValidator } from './util/validators'; +import { cacheTtlValidator, dbCacheValidator, integerValidator, urlValidator } from './util/validators'; import { readScheduleConfig } from './util/schedule'; import fs from 'fs'; import onDeath from 'death'; @@ -72,7 +73,22 @@ const packageJsonPath = fs.existsSync(copiedPackageJsonPath) ? copiedPackageJsonPath : path.join(__dirname, '../package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); -const projectionNameParser = (names: string) => names.split(',') as ProjectionName[]; + +const projections = Object.values(ProjectionName); +const projectionNameParser = (names: string) => + (names.split(',') as ProjectionName[]).map((name) => { + if (!projections.includes(name)) throw new Error(`Unknown projection name "${name}"`); + + return name; + }); + +const services = Object.values(ServiceNames); +const serviceNameParser = (names: string) => + (names.split(',') as ServiceNames[]).map((name) => { + if (!services.includes(name)) throw new Error(`Unknown service name "${name}"`); + + return name; + }); process.on('unhandledRejection', (reason) => { // To be handled by 'onDeath' @@ -85,7 +101,17 @@ const program = new Command('cardano-services'); program.version(packageJson.version); -const runServer = async (message: string, loadServer: () => Promise) => { +const runServer = async ( + message: string, + args: { + args: BlockfrostWorkerArgs | PgBossWorkerArgs | ProjectorArgs | ProviderServerArgs; + projectionNames?: ProjectionName[]; + serviceNames?: ServiceNames[]; + }, + loadServer: () => Promise +) => { + if (args.args.dumpOnly) return process.stdout.write(`${JSON.stringify(args)}\n`) as unknown as void; + try { process.stdout.write(`${message}\n`); const server = await loadServer(); @@ -119,7 +145,7 @@ addOptions(withCommonOptions(projectorWithArgs, PROJECTOR_API_URL_DEFAULT), [ '--blocks-buffer-length ', ProjectorOptionDescriptions.BlocksBufferLength, 'BLOCKS_BUFFER_LENGTH', - (blocksBufferLength) => Number.parseInt(blocksBufferLength, 10), + integerValidator(ProjectorOptionDescriptions.BlocksBufferLength), BLOCKS_BUFFER_LENGTH_DEFAULT ), newOption( @@ -140,28 +166,28 @@ addOptions(withCommonOptions(projectorWithArgs, PROJECTOR_API_URL_DEFAULT), [ '--exit-at-block-no ', ProjectorOptionDescriptions.ExitAtBlockNo, 'EXIT_AT_BLOCK_NO', - (exitAtBlockNo) => (exitAtBlockNo ? Number.parseInt(exitAtBlockNo, 10) : 0), - '' + integerValidator(ProjectorOptionDescriptions.ExitAtBlockNo), + 0 ), newOption( '--metadata-job-retry-delay ', ProjectorOptionDescriptions.MetadataJobRetryDelay, 'METADATA_JOB_RETRY_DELAY', - (metadataJobRetryDelay) => Number.parseInt(metadataJobRetryDelay, 10), + integerValidator(ProjectorOptionDescriptions.MetadataJobRetryDelay), METADATA_JOB_RETRY_DELAY_DEFAULT ), newOption( '--pools-metrics-interval ', ProjectorOptionDescriptions.PoolsMetricsInterval, 'POOLS_METRICS_INTERVAL', - (interval) => Number.parseInt(interval, 10), + integerValidator(ProjectorOptionDescriptions.PoolsMetricsInterval), POOLS_METRICS_INTERVAL_DEFAULT ), newOption( '--pools-metrics-outdated-interval ', ProjectorOptionDescriptions.PoolsMetricsOutdatedInterval, 'POOLS_METRICS_OUTDATED_INTERVAL', - (interval) => Number.parseInt(interval, 10), + integerValidator(ProjectorOptionDescriptions.PoolsMetricsOutdatedInterval), POOLS_METRICS_OUTDATED_INTERVAL_DEFAULT ), newOption( @@ -177,8 +203,8 @@ addOptions(withCommonOptions(projectorWithArgs, PROJECTOR_API_URL_DEFAULT), [ (synchronize) => stringOptionToBoolean(synchronize, Programs.Projector, ProjectorOptionDescriptions.Synchronize), false ) -]).action(async (projectionNames: ProjectionName[], args: { apiUrl: URL } & ProjectorArgs) => - runServer('projector', () => +]).action(async (projectionNames: ProjectionName[], args: ProjectorArgs) => + runServer('projector', { args, projectionNames }, () => loadProjector({ ...args, postgresConnectionString: connectionStringFromArgs(args, ''), @@ -191,7 +217,11 @@ addOptions(withCommonOptions(projectorWithArgs, PROJECTOR_API_URL_DEFAULT), [ const providerServer = program .command('start-provider-server') .description('Start the Provider Server') - .argument('[serviceNames...]', `List of services to attach: ${Object.values(ServiceNames).toString()}`); + .argument( + '[serviceNames...]', + `List of services to attach: ${Object.values(ServiceNames).toString()}`, + serviceNameParser + ); const providerServerWithPostgres = withPostgresOptions(providerServer, ['Asset', 'DbSync', 'Handle', 'StakePool']); const providerServerWithCommon = withCommonOptions(providerServerWithPostgres, PROVIDER_SERVER_API_URL_DEFAULT); @@ -200,7 +230,7 @@ addOptions(withOgmiosOptions(withHandlePolicyIdsOptions(providerServerWithCommon '--service-names ', `List of services to attach: ${Object.values(ServiceNames).toString()}`, 'SERVICE_NAMES', - (names) => names.split(',') as ServiceNames[] + serviceNameParser ), newOption( '--allowed-origins ', @@ -218,7 +248,7 @@ addOptions(withOgmiosOptions(withHandlePolicyIdsOptions(providerServerWithCommon '--db-cache-ttl ', ProviderServerOptionDescriptions.DbCacheTtl, 'DB_CACHE_TTL', - dbCacheValidator, + dbCacheValidator(ProviderServerOptionDescriptions.DbCacheTtl), DB_CACHE_TTL_DEFAULT ), newOption( @@ -245,41 +275,49 @@ addOptions(withOgmiosOptions(withHandlePolicyIdsOptions(providerServerWithCommon '--epoch-poll-interval ', ProviderServerOptionDescriptions.EpochPollInterval, 'EPOCH_POLL_INTERVAL', - (interval) => Number.parseInt(interval, 10), + integerValidator(ProviderServerOptionDescriptions.EpochPollInterval), EPOCH_POLL_INTERVAL_DEFAULT ), + newOption( + '--health-check-cache-ttl ', + ProviderServerOptionDescriptions.HealthCheckCacheTtl, + 'HEALTH_CHECK_CACHE_TTL', + (ttl: string) => + cacheTtlValidator(ttl, { lowerBound: 1, upperBound: 120 }, ProviderServerOptionDescriptions.HealthCheckCacheTtl), + DEFAULT_HEALTH_CHECK_CACHE_TTL + ), newOption( '--submit-api-url ', ProviderServerOptionDescriptions.SubmitApiUrl, 'SUBMIT_API_URL', - (url) => new URL(url) + urlValidator(ProviderServerOptionDescriptions.SubmitApiUrl) ), newOption( '--token-metadata-server-url ', ProviderServerOptionDescriptions.TokenMetadataServerUrl, 'TOKEN_METADATA_SERVER_URL', - (url) => new URL(url).toString(), + urlValidator(ProviderServerOptionDescriptions.TokenMetadataServerUrl, true), DEFAULT_TOKEN_METADATA_SERVER_URL ), newOption( '--token-metadata-cache-ttl ', ProviderServerOptionDescriptions.TokenMetadataCacheTtl, 'TOKEN_METADATA_CACHE_TTL', - dbCacheValidator, + dbCacheValidator(ProviderServerOptionDescriptions.TokenMetadataCacheTtl), DEFAULT_TOKEN_METADATA_CACHE_TTL ), newOption( '--asset-cache-ttl ', ProviderServerOptionDescriptions.AssetCacheTtl, 'ASSET_CACHE_TTL', - dbCacheValidator, + dbCacheValidator(ProviderServerOptionDescriptions.AssetCacheTtl), DB_CACHE_TTL_DEFAULT ), newOption( '--token-metadata-request-timeout ', - ProviderServerOptionDescriptions.PaginationPageSizeLimit, + ProviderServerOptionDescriptions.TokenMetadataRequestTimeout, 'TOKEN_METADATA_REQUEST_TIMEOUT', - (interval) => Number.parseInt(interval, 10), + integerValidator(ProviderServerOptionDescriptions.TokenMetadataRequestTimeout), DEFAULT_TOKEN_METADATA_REQUEST_TIMEOUT ), newOption( @@ -326,7 +364,7 @@ addOptions(withOgmiosOptions(withHandlePolicyIdsOptions(providerServerWithCommon '--pagination-page-size-limit ', ProviderServerOptionDescriptions.PaginationPageSizeLimit, 'PAGINATION_PAGE_SIZE_LIMIT', - (interval) => Number.parseInt(interval, 10), + integerValidator(ProviderServerOptionDescriptions.PaginationPageSizeLimit), PAGINATION_PAGE_SIZE_LIMIT_DEFAULT ), newOption( @@ -356,7 +394,7 @@ addOptions(withOgmiosOptions(withHandlePolicyIdsOptions(providerServerWithCommon USE_TYPEORM_ASSET_PROVIDER_DEFAULT ) ]).action(async (serviceNames: ServiceNames[], args: ProviderServerArgs) => - runServer('Provider server', () => + runServer('Provider server', { args, serviceNames }, () => loadProviderServer({ ...args, postgresConnectionStringAsset: connectionStringFromArgs(args, 'Asset'), @@ -385,7 +423,7 @@ addOptions(withCommonOptions(withPostgresOptions(blockfrost, ['DbSync']), BLOCKF '--cache-ttl ', BlockfrostWorkerOptionDescriptions.CacheTTL, 'CACHE_TTL', - (interval) => Number.parseInt(interval, 10), + integerValidator(BlockfrostWorkerOptionDescriptions.CacheTTL), CACHE_TTL_DEFAULT ), newOption( @@ -415,16 +453,16 @@ addOptions(withCommonOptions(withPostgresOptions(blockfrost, ['DbSync']), BLOCKF if (availableNetworks.includes(network as AvailableNetworks)) return network; throw new Error(`Unknown network: ${network}`); - }), + }).makeOptionMandatory(), newOption( '--scan-interval ', BlockfrostWorkerOptionDescriptions.ScanInterval, 'SCAN_INTERVAL', - (interval) => Number.parseInt(interval, 10), + integerValidator(BlockfrostWorkerOptionDescriptions.ScanInterval), SCAN_INTERVAL_DEFAULT ) ]).action(async (args: BlockfrostWorkerArgs) => - runServer('Blockfrost worker', () => + runServer('Blockfrost worker', { args }, () => loadBlockfrostWorker({ ...args, postgresConnectionStringDbSync: connectionStringFromArgs(args, 'DbSync') }) ) ); @@ -441,7 +479,7 @@ addOptions( '--parallel-jobs ', PgBossWorkerOptionDescriptions.ParallelJobs, 'PARALLEL_JOBS', - (parallelJobs) => Number.parseInt(parallelJobs, 10), + integerValidator(PgBossWorkerOptionDescriptions.ParallelJobs), PARALLEL_JOBS_DEFAULT ), newOption('--queues ', PgBossWorkerOptionDescriptions.Queues, 'QUEUES', (queues) => { @@ -463,7 +501,7 @@ addOptions( ) ] ).action(async (args: PgBossWorkerArgs) => - runServer('pg-boss worker', () => + runServer('pg-boss worker', { args }, () => loadPgBossWorker({ ...args, postgresConnectionStringDbSync: connectionStringFromArgs(args, 'DbSync'), diff --git a/packages/cardano-services/src/util/validators.ts b/packages/cardano-services/src/util/validators.ts index 7ef92b44be2..fa458ad5e8a 100644 --- a/packages/cardano-services/src/util/validators.ts +++ b/packages/cardano-services/src/util/validators.ts @@ -1,6 +1,5 @@ import { BuildInfo } from '../Http'; import { CACHE_TTL_LOWER_LIMIT, CACHE_TTL_UPPER_LIMIT } from '../InMemoryCache'; -import { ProviderServerOptionDescriptions } from '../Program/programs/types'; import { Range, throwIfOutsideRange } from '@cardano-sdk/util'; import { validate } from 'jsonschema'; import fs from 'fs'; @@ -22,7 +21,7 @@ export const buildInfoValidator = (buildInfo: string): BuildInfo => { try { result = JSON.parse(buildInfo || '{}'); } catch (error) { - throw new Error(`Invalid JSON format of process.env.BUILD_INFO: ${error}`); + throw new Error(`Invalid JSON format of build-info: ${error}`); } validate(result, buildInfoSchema, { throwError: true }); return result; @@ -35,20 +34,39 @@ export const existingFileValidator = (filePath: string) => { throw new Error(`No file exists at ${filePath}`); }; -export const cacheTtlValidator = ( - ttlInSecs: string, - range: Required>, - description: Description -) => { - const cacheTtlInSecs = Number.parseInt(ttlInSecs, 10); - if (Number.isNaN(cacheTtlInSecs)) throw new TypeError(`${description} - ${ttlInSecs} is not a number`); - throwIfOutsideRange(cacheTtlInSecs, range, description as string); +export const floatValidator = (description: string) => (float: string) => { + const parsed = Number.parseFloat(float); + + if (parsed.toString() === float || (float.startsWith('.') && parsed.toString() === `0${float}`)) return parsed; + + throw new TypeError(`${description} - "${float}" is not a number`); +}; + +export const integerValidator = (description: string) => (integer: string) => { + const parsed = Number.parseInt(integer, 10); + + if (parsed.toString() === integer) return parsed; + + throw new TypeError(`${description} - "${integer}" is not an integer`); +}; + +export const cacheTtlValidator = (ttlInSecs: string, range: Required>, description: string) => { + const cacheTtlInSecs = integerValidator(description)(ttlInSecs); + throwIfOutsideRange(cacheTtlInSecs, range, description); return cacheTtlInSecs; }; -export const dbCacheValidator = (ttl: string) => - cacheTtlValidator( - ttl, - { lowerBound: CACHE_TTL_LOWER_LIMIT, upperBound: CACHE_TTL_UPPER_LIMIT }, - ProviderServerOptionDescriptions.DbCacheTtl - ); +export const dbCacheValidator = (description: string) => (ttl: string) => + cacheTtlValidator(ttl, { lowerBound: CACHE_TTL_LOWER_LIMIT, upperBound: CACHE_TTL_UPPER_LIMIT }, description); + +export const urlValidator = + (description: string, toString = false) => + (url: string) => { + try { + const parsed = new URL(url); + + return toString ? parsed.toString() : parsed; + } catch { + throw new Error(`${description} - "${url}" is not an URL`); + } + }; diff --git a/packages/cardano-services/test/cli.test.ts b/packages/cardano-services/test/cli.test.ts index 34a0ce27771..cdda7b6c0f3 100644 --- a/packages/cardano-services/test/cli.test.ts +++ b/packages/cardano-services/test/cli.test.ts @@ -1,2660 +1,1094 @@ -/* eslint-disable unicorn/consistent-function-scoping */ /* eslint-disable sonarjs/cognitive-complexity */ -/* eslint-disable sonarjs/no-identical-functions */ /* eslint-disable sonarjs/no-duplicate-string */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Asset } from '@cardano-sdk/core'; -import { AssetData, AssetFixtureBuilder, AssetWith } from './Asset/fixtures/FixtureBuilder'; -import { ChildProcess, fork } from 'child_process'; -import { LedgerTipModel, findLedgerTip } from '../src/util/DbSyncProvider'; -import { Ogmios } from '@cardano-sdk/ogmios'; -import { Pool } from 'pg'; -import { ProjectionName, ServerMetadata, ServiceNames } from '../src'; -import { - baseVersionPath, - createHealthyMockOgmiosServer, - createUnhealthyMockOgmiosServer, - ogmiosServerReady, - serverStarted, - servicesWithVersionPath as services -} from './util'; import { createLogger } from '@cardano-sdk/util-dev'; -import { fromSerializableObject } from '@cardano-sdk/util'; -import { getRandomPort } from 'get-port-please'; -import { healthCheckResponseMock } from '../../core/test/CardanoNode/mocks'; -import { listenPromise, serverClosePromise } from '../src/util'; -import { mockTokenRegistry } from './Asset/fixtures/mocks'; -import axios, { AxiosError } from 'axios'; -import connString from 'pg-connection-string'; -import http from 'http'; +import { fork } from 'child_process'; import path from 'path'; -jest.setTimeout(90_000); - -const DNS_SERVER_NOT_REACHABLE_ERROR = 'querySrv ENOTFOUND'; -const CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE = 'cannot be used with option'; -const CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE = 'cannot be used with environment variable'; -const METRICS_ENDPOINT_LABEL_RESPONSE = - 'http_request_duration_seconds duration histogram of http responses labeled with: status_code, method, path'; -const REQUIRES_PG_CONNECTION = 'requires the PostgreSQL Connection string or Postgres SRV service name'; - const exePath = path.join(__dirname, '..', 'dist', 'cjs', 'cli.js'); const logger = createLogger({ env: process.env.TL_LEVEL ? process.env : { ...process.env, TL_LEVEL: 'error' } }); +const queues = ['pool-metadata', 'pool-metrics', 'pool-rewards']; -const assertServiceHealthy = async ( - apiUrl: string, - service: { - name: ServiceNames; - versionPath: string; - }, - lastBlock: LedgerTipModel, - options?: { - unhealthy?: boolean; - usedQueue?: boolean; - withTip?: boolean; - } -) => { - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - const res = await axios.post(`${apiUrl}${service.versionPath}/${service.name}/health`, { headers }); - const { unhealthy, usedQueue, withTip } = { withTip: true, ...options }; - - const healthCheckResponse = usedQueue - ? { ok: true } - : healthCheckResponseMock({ - projectedTip: { - blockNo: lastBlock!.block_no, - hash: lastBlock!.hash.toString('hex'), - slot: Number(lastBlock!.slot_no) - }, - withTip - }); - - expect(res.status).toBe(200); - if (unhealthy) expect(res.data.ok).toBeFalsy(); - else expect(res.data).toEqual(healthCheckResponse); +const HANDLE_POLICY_IDS = 'f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a'; +const { POSTGRES_CONNECTION_STRING_DB_SYNC, POSTGRES_CONNECTION_STRING_STAKE_POOL } = process.env; +const QUEUES = queues.join(','); + +const programs = { + blockfrost: 'start-blockfrost-worker', + pgboss: 'start-pg-boss-worker', + projector: 'start-projector', + provider: 'start-provider-server' }; -const assertServerWithCORSHeaders = async (apiUrl: string, origin: string) => { - expect.assertions(2); - const headers = { 'Content-Type': 'application/json', Origin: origin }; - await serverStarted(apiUrl, 404, headers); - try { - const res = await axios.get(`${apiUrl}${baseVersionPath}/health`, { headers }); - expect(res.headers['access-control-allow-origin']).toEqual(origin); - expect(res.data).toBeDefined(); - } catch (error) { - expect((error as AxiosError).response!.status).toBe(403); - } -}; +type Program = keyof typeof programs; -const assertMetricsEndpoint = async (apiUrl: string, assertFound: boolean) => { - expect.assertions(1); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - try { - const res = await axios.get(`${apiUrl}${baseVersionPath}/metrics`, { headers }); - expect(res.data.toString().includes(METRICS_ENDPOINT_LABEL_RESPONSE)).toEqual(assertFound); - } catch (error) { - expect((error as AxiosError).response?.status).toBe(404); - } +type RunCli = { + args?: string[]; + env?: NodeJS.ProcessEnv; + expectedArgs?: unknown; + expectedError?: string; + expectedOutput?: string; + notDump?: boolean; }; -const assertMetaEndpoint = async (apiUrl: string, dataMatch: any) => { - expect.assertions(1); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - try { - const res = await axios.get(`${apiUrl}${baseVersionPath}/meta`, { headers }); - expect(res.data).toMatchShapeOf(dataMatch); - } catch (error) { - expect((error as AxiosError).response?.status).toBe(404); - } -}; +const runCli = ( + program: Program, + { args = [], env = {}, expectedArgs, expectedError, expectedOutput, notDump }: RunCli +) => + new Promise((resolve, reject) => { + const chunks = { stderr: [] as string[], stdout: [] as string[] }; + const method = { stderr: expectedError ? 'debug' : 'error', stdout: 'info' } as const; + const argv = [programs[program], ...(notDump ? [] : ['--dump-only', 'true']), ...args]; + const proc = fork(exePath, argv, { env, stdio: 'pipe' }); + + const assert = (assertions: () => void) => { + try { + assertions(); + } catch (error) { + reject(error); + } + }; -const assertStakePoolApyInResponse = async (apiUrl: string, assertFound: boolean) => { - expect.assertions(1); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - const res = await axios.post(`${apiUrl}${services.stakePool.versionPath}/stake-pool/search`, { - headers, - pagination: { limit: 1, startAt: 0 } + for (const stream of ['stderr', 'stdout'] as const) + proc[stream]!.on('data', (data) => { + const str = data.toString(); + + logger[method[stream]](str); + chunks[stream].push(str); + }); + + proc.on('error', (error) => assert(() => expect(error).toBeUndefined())); + + proc.on('close', (code) => { + assert(() => { + const stderr = chunks.stderr.join(''); + const stdout = chunks.stdout.join(''); + + if (expectedArgs) { + const [, dump] = stdout.split('\n'); + + expect(stderr).toBe(''); + expect(JSON.parse(dump)).toMatchObject(expectedArgs); + } + + if (expectedError) expect(stderr).toContain(expectedError); + if (expectedOutput) expect(stdout).toContain(expectedOutput); + expect(code).toBe(expectedError || expectedOutput ? 1 : 0); + }); + resolve(); + }); }); - const apy = res.data.pageResults[0].metrics.apy; - if (assertFound) { - expect(typeof apy).toBe('number'); - } else { - expect(apy.__type).toBe('undefined'); - } -}; -type CallCliAndAssertExitArgs = { - args?: string[]; - dataMatchOnError: string; - env?: NodeJS.ProcessEnv; -}; +const testCli = (name: string, program: Program, args: RunCli) => { + test(`cmd - ${name}`, async () => { + const { env: _, ...rest } = args; -const baseArgs = ['start-provider-server', '--logger-min-severity', 'error']; + await runCli(program, rest); + }); -const withLogging = (proc: ChildProcess, expectingExceptions = false): ChildProcess => { - const methodOnFailure = expectingExceptions ? 'debug' : 'error'; - proc.on('error', (error) => logger[methodOnFailure](error)); - proc.stderr!.on('data', (data) => logger[methodOnFailure](data.toString())); - proc.stdout!.on('data', (data) => logger.info(data.toString())); - return proc; -}; + test(`env - ${name}`, async () => { + const { args: _, ...rest } = args; -const callCliAndAssertExit = ( - { args = [], dataMatchOnError, env = {} }: CallCliAndAssertExitArgs, - done: jest.DoneCallback -) => { - const spy = jest.fn(); - expect.assertions(dataMatchOnError ? 3 : 2); - const proc = withLogging( - fork(exePath, [...baseArgs, ...args], { - env, - stdio: 'pipe' - }), - true - ); - const chunks: string[] = []; - proc.stderr!.on('data', (data) => { - spy(); - chunks.push(data.toString()); + await runCli(program, rest); }); - proc.on('exit', (code) => { - try { - expect(code).toBe(1); - expect(spy).toHaveBeenCalled(); - if (dataMatchOnError) expect(chunks.join('')).toContain(dataMatchOnError); - done(); - } catch (error) { - done(error); - } +}; + +const testBlockfrost = (name: string, program: Program, _args: RunCli) => { + const { args, env, expectedArgs, expectedError } = { args: [], env: {}, ..._args }; + + return testCli(name, program, { + args: ['--network', 'mainnet', ...args], + env: { NETWORK: 'mainnet', ...env }, + expectedArgs, + expectedError }); }; -const HANDLE_POLICY_IDS = 'f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a'; -const HANDLE_PROVIDER_SERVER_URL = 'http://localhost:3000'; +const testPgBoss = (name: string, program: Program, _args: RunCli) => { + const { args, env, expectedArgs, expectedError } = { args: [], env: {}, ..._args }; + + return testCli(name, program, { + args: ['--queues', QUEUES, ...args], + env: { QUEUES, ...env }, + expectedArgs, + expectedError + }); +}; describe('CLI', () => { - let db: Pool; - let fixtureBuilder: AssetFixtureBuilder; - let lastBlock: LedgerTipModel; - let postgresConnectionString: string; - let postgresConnectionStringHandle: string; - let postgresConnectionStringProjection: string; - let postgresConnectionStringStakePool: string; - let postgresConnectionStringAsset: string; - - beforeAll(() => { - postgresConnectionString = process.env.POSTGRES_CONNECTION_STRING_DB_SYNC!; - postgresConnectionStringHandle = process.env.POSTGRES_CONNECTION_STRING_HANDLE!; - postgresConnectionStringProjection = process.env.POSTGRES_CONNECTION_STRING_PROJECTION!; - postgresConnectionStringStakePool = process.env.POSTGRES_CONNECTION_STRING_STAKE_POOL!; - postgresConnectionStringAsset = process.env.POSTGRES_CONNECTION_STRING_ASSET!; + const notDump = true; + + describe('withCommonOptions', () => { + describe('apiUrl', () => { + const apiUrl = 'http://test.url/'; + + testCli('accepts a valid URL', 'provider', { + args: ['--api-url', apiUrl], + env: { API_URL: apiUrl }, + expectedArgs: { args: { apiUrl } } + }); + + testCli('expects a valid URL', 'provider', { + args: ['--api-url', 'test'], + env: { API_URL: 'test' }, + expectedError: 'API URL - "test" is not an URL' + }); + }); + + describe('buildInfo', () => { + const badInfo = '{"not":"a build info"}'; + const buildInfo = { extra: {}, lastModified: 23, lastModifiedDate: 'date', rev: 'rev', shortRev: 'short' }; + + testCli('accepts a valid build info object', 'provider', { + args: ['--build-info', JSON.stringify(buildInfo)], + env: { BUILD_INFO: JSON.stringify(buildInfo) }, + expectedArgs: { args: { buildInfo } } + }); + + testCli('expects a JSON encoded string', 'provider', { + args: ['--build-info', 'test'], + env: { BUILD_INFO: 'test' }, + expectedError: 'Invalid JSON format of build-info' + }); + + testCli('expects a valid build info object', 'provider', { + args: ['--build-info', badInfo], + env: { BUILD_INFO: badInfo }, + expectedError: 'is not allowed to have the additional property "not"' + }); + }); + + describe('enableMetrics', () => { + testCli('accepts a valid boolean', 'provider', { + args: ['--enable-metrics', 'true'], + env: { ENABLE_METRICS: 'true' }, + expectedArgs: { args: { enableMetrics: true } } + }); + + testCli('expects a valid boolean', 'provider', { + args: ['--enable-metrics', 'test'], + env: { ENABLE_METRICS: 'test' }, + expectedError: 'Provider server requires a valid Enable Prometheus Metrics program option' + }); + }); + + describe('lastRosEpochs', () => { + testCli('accepts an integer', 'provider', { + args: ['--last-ros-epochs', '23'], + env: { LAST_ROS_EPOCHS: '23' }, + expectedArgs: { args: { lastRosEpochs: 23 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--last-ros-epochs', 'test'], + env: { LAST_ROS_EPOCHS: 'test' }, + expectedError: 'Number of epochs over which lastRos is computed - "test" is not an integer' + }); + }); + + describe('loggerMinSeverity', () => { + testCli('accepts a valid log level', 'provider', { + args: ['--logger-min-severity', 'debug'], + env: { LOGGER_MIN_SEVERITY: 'debug' }, + expectedArgs: { args: { loggerMinSeverity: 'debug' } } + }); + + testCli('expects a valid log level', 'provider', { + args: ['--logger-min-severity', 'test'], + env: { LOGGER_MIN_SEVERITY: 'test' }, + expectedError: 'InvalidLoggerLevel: test is an invalid logger level' + }); + }); + + describe('serviceDiscoveryBackoffFactor', () => { + testCli('accepts a float', 'provider', { + args: ['--service-discovery-backoff-factor', '.23'], + env: { SERVICE_DISCOVERY_BACKOFF_FACTOR: '0.23' }, + expectedArgs: { args: { serviceDiscoveryBackoffFactor: 0.23 } } + }); + + testCli('expects a float', 'provider', { + args: ['--service-discovery-backoff-factor', 'test'], + env: { SERVICE_DISCOVERY_BACKOFF_FACTOR: 'test' }, + expectedError: 'Exponential backoff factor for service discovery - "test" is not a number' + }); + }); + + describe('serviceDiscoveryTimeout', () => { + testCli('accepts an integer', 'provider', { + args: ['--service-discovery-timeout', '23'], + env: { SERVICE_DISCOVERY_TIMEOUT: '23' }, + expectedArgs: { args: { serviceDiscoveryTimeout: 23 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--service-discovery-timeout', 'test'], + env: { SERVICE_DISCOVERY_TIMEOUT: 'test' }, + expectedError: 'Timeout for service discovery attempts - "test" is not an integer' + }); + }); }); - describe('start-provider-server', () => { - let apiPort: number; - let apiUrl: string; - let ogmiosServer: http.Server; - let proc: ChildProcess; + describe('withHandlePolicyIdsOptions', () => { + describe('handlePolicyIds', () => { + testCli('accepts a valid PolicyId array', 'provider', { + args: ['--handle-policy-ids', HANDLE_POLICY_IDS], + env: { HANDLE_POLICY_IDS }, + expectedArgs: { args: { handlePolicyIds: [HANDLE_POLICY_IDS] } } + }); - beforeAll(async () => { - db = new Pool({ connectionString: process.env.POSTGRES_CONNECTION_STRING_DB_SYNC, max: 1, min: 1 }); - fixtureBuilder = new AssetFixtureBuilder(db, logger); - lastBlock = (await db!.query(findLedgerTip)).rows[0]; + testCli('expects a valid PolicyId array', 'provider', { + args: ['--handle-policy-ids', 'test'], + env: { HANDLE_POLICY_IDS: 'test' }, + expectedError: 'Invalid string: "expected length \'56\', got 4"' + }); }); - beforeEach(async () => { - apiPort = await getRandomPort(); - apiUrl = `http://localhost:${apiPort}`; + describe('handlePolicyIds', () => { + testCli('accepts any string', 'provider', { + args: ['--handle-policy-ids-file', 'test'], + env: { HANDLE_POLICY_IDS_FILE: 'test' }, + expectedArgs: { args: { handlePolicyIdsFile: 'test' } } + }); }); - afterEach(() => { - if (proc !== undefined) proc.kill(); - if (ogmiosServer !== undefined) { - return serverClosePromise(ogmiosServer); - } + describe('conflicts', () => { + test('handlePolicyIds conflicts with handlePolicyIdsFile', () => + runCli('provider', { + env: { HANDLE_POLICY_IDS, HANDLE_POLICY_IDS_FILE: 'test' }, + expectedError: "'HANDLE_POLICY_IDS_FILE' cannot be used with environment variable 'HANDLE_POLICY_IDS'" + })); }); + }); + + describe('withOgmiosOptions', () => { + const ogmiosUrl = 'wss://test/'; + + describe('ogmiosUrl', () => { + testCli('accepts an URL', 'provider', { + args: ['--ogmios-url', ogmiosUrl], + env: { OGMIOS_URL: ogmiosUrl }, + expectedArgs: { args: { ogmiosUrl } } + }); - it('CLI version', (done) => { - proc = withLogging( - fork(exePath, ['--version'], { - stdio: 'pipe' - }) - ); - proc.stdout!.on('data', (data) => { - expect(data.toString()).toBeDefined(); - }); - proc.stdout?.on('end', () => { - done(); - }); - }); - - describe('cli:start-provider-server', () => { - let ogmiosPort: Ogmios.ConnectionConfig['port']; - let ogmiosConnection: Ogmios.Connection; - let cardanoNodeConfigPath: string; - let dbCacheTtl: string; - let postgresSrvServiceName: string; - let postgresDb: string; - let postgresUser: string; - let postgresPassword: string; - let postgresDbFile: string; - let postgresUserFile: string; - let postgresPasswordFile: string; - let postgresSslCaFile: string; - let postgresHost: string; - let postgresPort: string; - let ogmiosSrvServiceName: string; - - beforeAll(async () => { - ogmiosPort = await getRandomPort(); - ogmiosConnection = Ogmios.createConnectionObject({ port: ogmiosPort }); - cardanoNodeConfigPath = process.env.CARDANO_NODE_CONFIG_PATH!; - postgresSrvServiceName = process.env.POSTGRES_SRV_SERVICE_NAME_DB_SYNC!; - postgresDb = process.env.POSTGRES_DB_DB_SYNC!; - postgresDbFile = process.env.POSTGRES_DB_FILE_DB_SYNC!; - postgresUser = process.env.POSTGRES_USER_DB_SYNC!; - postgresUserFile = process.env.POSTGRES_USER_FILE_DB_SYNC!; - postgresPassword = process.env.POSTGRES_PASSWORD_DB_SYNC!; - postgresPasswordFile = process.env.POSTGRES_PASSWORD_FILE_DB_SYNC!; - postgresHost = process.env.POSTGRES_HOST_DB_SYNC!; - postgresPort = process.env.POSTGRES_PORT_DB_SYNC!; - postgresSslCaFile = process.env.POSTGRES_SSL_CA_FILE_DB_SYNC!; - ogmiosSrvServiceName = process.env.OGMIOS_SRV_SERVICE_NAME!; - dbCacheTtl = process.env.DB_CACHE_TTL!; - }); - - describe('with healthy internal providers', () => { - describe('valid configuration', () => { - beforeEach(async () => { - ogmiosServer = createHealthyMockOgmiosServer(); - await listenPromise(ogmiosServer, { port: ogmiosConnection.port }); - await ogmiosServerReady(ogmiosConnection); - }); - - it('exposes a HTTP server at the configured URL with all services attached when using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--enable-metrics', - 'true', - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - '--handle-policy-ids', - HANDLE_POLICY_IDS, - '--handle-provider-server-url', - HANDLE_PROVIDER_SERVER_URL, - ServiceNames.Asset, - ServiceNames.ChainHistory, - ServiceNames.NetworkInfo, - ServiceNames.StakePool, - ServiceNames.TxSubmit, - ServiceNames.Utxo, - ServiceNames.Rewards - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - await assertServiceHealthy(apiUrl, services.chainHistory, lastBlock); - await assertServiceHealthy(apiUrl, services.networkInfo, lastBlock); - await assertServiceHealthy(apiUrl, services.stakePool, lastBlock); - await assertServiceHealthy(apiUrl, services.txSubmit, lastBlock, { withTip: false }); - await assertServiceHealthy(apiUrl, services.utxo, lastBlock); - await assertServiceHealthy(apiUrl, services.rewards, lastBlock); - }); - - it('exposes a HTTP server at the configured URL with all services attached when using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - DB_CACHE_TTL: dbCacheTtl, - ENABLE_METRICS: 'true', - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_CONNECTION_STRING_HANDLE: postgresConnectionStringHandle, - SERVICE_NAMES: `${ServiceNames.Asset},${ServiceNames.ChainHistory},${ServiceNames.NetworkInfo},${ServiceNames.StakePool},${ServiceNames.TxSubmit},${ServiceNames.Utxo},${ServiceNames.Rewards}` - }, - stdio: 'pipe' - }) - ); - - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - await assertServiceHealthy(apiUrl, services.chainHistory, lastBlock); - await assertServiceHealthy(apiUrl, services.networkInfo, lastBlock); - await assertServiceHealthy(apiUrl, services.stakePool, lastBlock); - await assertServiceHealthy(apiUrl, services.txSubmit, lastBlock, { withTip: false }); - await assertServiceHealthy(apiUrl, services.utxo, lastBlock); - await assertServiceHealthy(apiUrl, services.rewards, lastBlock); - }); - - it('exposes a HTTP server with /metrics endpoint using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--enable-metrics', - 'true', - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - '--handle-policy-ids', - HANDLE_POLICY_IDS, - '--handle-provider-server-url', - HANDLE_PROVIDER_SERVER_URL, - ServiceNames.Asset, - ServiceNames.ChainHistory, - ServiceNames.NetworkInfo, - ServiceNames.StakePool, - ServiceNames.TxSubmit, - ServiceNames.Utxo, - ServiceNames.Rewards - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertMetricsEndpoint(apiUrl, true); - }); - - it('exposes a HTTP server with /metrics endpoint using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - DB_CACHE_TTL: dbCacheTtl, - ENABLE_METRICS: 'true', - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_CONNECTION_STRING_HANDLE: postgresConnectionStringHandle, - SERVICE_NAMES: `${ServiceNames.Asset},${ServiceNames.ChainHistory},${ServiceNames.NetworkInfo},${ServiceNames.StakePool},${ServiceNames.TxSubmit},${ServiceNames.Utxo},${ServiceNames.Rewards}` - }, - stdio: 'pipe' - }) - ); - - await assertMetricsEndpoint(apiUrl, true); - }); - - it('exposes a HTTP server without /metrics endpoint when env set to false', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - DB_CACHE_TTL: dbCacheTtl, - ENABLE_METRICS: 'false', - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_CONNECTION_STRING_HANDLE: postgresConnectionStringHandle, - SERVICE_NAMES: `${ServiceNames.Asset},${ServiceNames.ChainHistory},${ServiceNames.NetworkInfo},${ServiceNames.StakePool},${ServiceNames.TxSubmit},${ServiceNames.Utxo},${ServiceNames.Rewards}` - }, - stdio: 'pipe' - }) - ); - - await assertMetricsEndpoint(apiUrl, false); - }); - - describe('exposes a HTTP server with CORS header configuration', () => { - const allowedOrigin = 'http://cardano.com'; - - it('using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--allowed-origins', - allowedOrigin, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.Asset - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertServerWithCORSHeaders(apiUrl, allowedOrigin); - }); - - it('using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - ALLOWED_ORIGINS: allowedOrigin, - API_URL: apiUrl, - DB_CACHE_TTL: dbCacheTtl, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.Asset - }, - stdio: 'pipe' - }) - ); - - await assertServerWithCORSHeaders(apiUrl, allowedOrigin); - }); - }); - - describe('exposes a HTTP server with /meta endpoint', () => { - const buildInfo = - '{"lastModified":1666954298,"lastModifiedDate":"20221028105138","rev":"65d78fc015bf7bd856c5febe0ba84d3ad18a069c","shortRev":"65d78fc","extra":{ "narHash":"sha256-PN60Ot9hQZIwh4LRgnPd8iiq9F3hFNXP7PYVpBlM9TQ=", "path":"/nix/store/i0sgvj906qpzw1bk7h8b3vij0z477ff6-source","sourceInfo":"/nix/store/i0sgvj906qpzw1bk7h8b3vij0z477ff6-source"}}'; - - const metaResponse: ServerMetadata = { - extra: JSON.parse( - '{"narHash": "sha256-PN60Ot9hQZIwh4LRgnPd8iiq9F3hFNXP7PYVpBlM9TQ=", "path":"/nix/store/i0sgvj906qpzw1bk7h8b3vij0z477ff6-source", "sourceInfo":"/nix/store/i0sgvj906qpzw1bk7h8b3vij0z477ff6-source"}' - ), - lastModified: 1_666_954_298, - lastModifiedDate: '20221028105138', - rev: '65d78fc015bf7bd856c5febe0ba84d3ad18a069c', - shortRev: '65d78fc', - startupTime: 1_673_353_278_641 - }; - - it('using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--build-info', - buildInfo, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.Utxo - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertMetaEndpoint(apiUrl, metaResponse); - }); - - it('using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - BUILD_INFO: buildInfo, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - DB_CACHE_TTL: dbCacheTtl, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: `${ServiceNames.Utxo}` - }, - stdio: 'pipe' - }) - ); - - await assertMetaEndpoint(apiUrl, metaResponse); - }); - - it('defaults if build info is not provided on startup', async () => { - const defaultServerMeta: ServerMetadata = { startupTime: 1_234_567 }; - - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.Utxo - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertMetaEndpoint(apiUrl, defaultServerMeta); - }); - - it('exits with code 1 with provided invalid build info JSON format', (done) => { - const invalidBuildInfo = '{"lastModified":}'; - - callCliAndAssertExit( - { - args: [ - ...baseArgs, - '--api-url', - apiUrl, - '--build-info', - invalidBuildInfo, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.Utxo - ], - dataMatchOnError: 'Invalid JSON format of process.env.BUILD_INFO' - }, - done - ); - }); - - it('exits with code 1 when the provided build info does not follow the JSON schema', (done) => { - const buildInfoWithWrongProp = '{"lastModified1111": 1673457714494}'; - - callCliAndAssertExit( - { - args: [ - ...baseArgs, - '--api-url', - apiUrl, - '--build-info', - buildInfoWithWrongProp, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.Utxo - ], - dataMatchOnError: 'is not allowed to have the additional property "lastModified1111"' - }, - done - ); - }); - }); - - it('setting the service names via env variable takes preference over command line argument', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.Utxo - ], - { - env: { - SERVICE_NAMES: `${ServiceNames.Utxo},${ServiceNames.Rewards}` - }, - stdio: 'pipe' - } - ) - ); - - await assertServiceHealthy(apiUrl, services.utxo, lastBlock); - await assertServiceHealthy(apiUrl, services.rewards, lastBlock); - }); - - it('exposes a HTTP server with /stake-pool/search endpoint that includes metrics.apy, by default', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.StakePool - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertStakePoolApyInResponse(apiUrl, true); - }); - - it('exposes a HTTP server with /stake-pool/search endpoint that disables metrics.apy, when configured via CLI option', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--disable-stake-pool-metric-apy', - 'true', - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - dbCacheTtl, - ServiceNames.StakePool - ], - { env: {}, stdio: 'pipe' } - ) - ); - - await assertStakePoolApyInResponse(apiUrl, false); - }); - - it('exposes a HTTP server with /stake-pool/search endpoint that disables metrics.apy when using env', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - DB_CACHE_TTL: dbCacheTtl, - DISABLE_STAKE_POOL_METRIC_APY: 'true', - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: `${ServiceNames.StakePool}` - }, - stdio: 'pipe' - }) - ); - - await assertStakePoolApyInResponse(apiUrl, false); - }); - }); - - describe('specifying a PostgreSQL-dependent service', () => { - describe('without provided static nor service discovery config', () => { - it('stake-pool exits with code 1', (done) => { - callCliAndAssertExit( - { - args: ['--service-names', ServiceNames.StakePool], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - - it('network-info exits with code 1', (done) => { - callCliAndAssertExit( - { - args: [ - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--service-names', - ServiceNames.NetworkInfo - ], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - - it('network-info exits with code 1 when cache TTL is out of range', (done) => { - const cacheTtlOutOfRange = '3000'; - callCliAndAssertExit( - { - args: [ - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--db-cache-ttl', - cacheTtlOutOfRange, - '--service-names', - ServiceNames.NetworkInfo - ], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - - it('utxo exits with code 1', (done) => { - callCliAndAssertExit( - { - args: ['--service-names', ServiceNames.Utxo], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - - it('rewards exits with code 1', (done) => { - callCliAndAssertExit( - { - args: ['--service-names', ServiceNames.Rewards], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - - it('chain-history exits with code 1', (done) => { - callCliAndAssertExit( - { - args: ['--service-names', ServiceNames.ChainHistory], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - - it('asset exits with code 1', (done) => { - callCliAndAssertExit( - { - args: ['--service-names', ServiceNames.Asset], - dataMatchOnError: REQUIRES_PG_CONNECTION - }, - done - ); - }); - }); - - describe('with provided static config', () => { - beforeEach(async () => { - ogmiosServer = createHealthyMockOgmiosServer(); - await listenPromise(ogmiosServer, { port: ogmiosConnection.port }); - await ogmiosServerReady(ogmiosConnection); - }); - - it('exposes a HTTP server when using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--postgres-db-db-sync', - postgresDb, - '--postgres-user-db-sync', - postgresUser, - '--postgres-password-db-sync', - postgresPassword, - '--postgres-host-db-sync', - postgresHost, - '--postgres-pool-max-db-sync', - '50', - '--postgres-port-db-sync', - postgresPort, - ServiceNames.Utxo - ], - { - env: {}, - stdio: 'pipe' - } - ) - ); - - await assertServiceHealthy(apiUrl, services.utxo, lastBlock); - }); - - it('exposes a HTTP server when using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_DB_DB_SYNC: postgresDb, - POSTGRES_HOST_DB_SYNC: postgresHost, - POSTGRES_PASSWORD_DB_SYNC: postgresPassword, - POSTGRES_POOL_MAX_DB_SYNC: '50', - POSTGRES_PORT_DB_SYNC: postgresPort, - POSTGRES_USER_DB_SYNC: postgresUser, - SERVICE_NAMES: ServiceNames.Utxo - }, - stdio: 'pipe' - }) - ); - - await assertServiceHealthy(apiUrl, services.utxo, lastBlock); - }); - }); - - describe('with provided service discovery config', () => { - it('exits with code 1 if DNS server not reachable when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-srv-service-name-db-sync', - postgresSrvServiceName, - '--postgres-db-db-sync', - postgresDb, - '--postgres-user-db-sync', - postgresUser, - '--postgres-password-db-sync', - postgresPassword, - '--service-discovery-timeout', - '1000', - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: DNS_SERVER_NOT_REACHABLE_ERROR - }, - done - ); - }); - - it('exits with code 1 if DNS server not reachable when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: DNS_SERVER_NOT_REACHABLE_ERROR, - env: { - POSTGRES_DB_DB_SYNC: postgresDb, - POSTGRES_PASSWORD_DB_SYNC: postgresPassword, - POSTGRES_SRV_SERVICE_NAME_DB_SYNC: postgresSrvServiceName, - POSTGRES_USER_DB_SYNC: postgresUser, - SERVICE_DISCOVERY_TIMEOUT: '1000', - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both postgres srv service name and connection string', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-srv-service-name-db-sync', - postgresSrvServiceName, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_SRV_SERVICE_NAME_DB_SYNC: postgresSrvServiceName, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both postgres srv service name and host', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-host-db-sync', - postgresHost, - '--postgres-srv-service-name-db-sync', - postgresSrvServiceName, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_HOST_DB_SYNC: postgresHost, - POSTGRES_SRV_SERVICE_NAME_DB_SYNC: postgresSrvServiceName, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both postgres srv service name and port', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-port-db-sync', - postgresPort, - '--postgres-srv-service-name-db-sync', - postgresSrvServiceName, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_PORT_DB_SYNC: postgresPort, - POSTGRES_SRV_SERVICE_NAME_DB_SYNC: postgresSrvServiceName, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres db name', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-db-db-sync', - postgresDb, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_DB_DB_SYNC: postgresDb, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres db name file', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-db-file-db-sync', - postgresDbFile, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_DB_FILE_DB_SYNC: postgresDbFile, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres user', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-user-db-sync', - postgresUser, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_USER_DB_SYNC: postgresUser, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres user file', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-user-file-db-sync', - postgresUserFile, - '--service-names', - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_USER_FILE_DB_SYNC: postgresUserFile, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres password', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-password-db-sync', - postgresPassword, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_PASSWORD_DB_SYNC: postgresPassword, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres password file', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-password-file-db-sync', - postgresPasswordFile, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_PASSWORD_FILE_DB_SYNC: postgresPasswordFile, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres host', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-host-db-sync', - postgresHost, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_HOST_DB_SYNC: postgresHost, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both connection string and postgres port', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-port-db-sync', - postgresPort, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_PORT_DB_SYNC: postgresPort, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both postgres db name from config and from file', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-db-db-sync', - postgresDb, - '--postgres-db-file-db-sync', - postgresDbFile, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_DB_DB_SYNC: postgresDb, - POSTGRES_DB_FILE_DB_SYNC: postgresDbFile, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both postgres user from config and from file', () => { - it('throws a CLI validation error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-user-db-sync', - postgresUser, - '--postgres-user-file-db-sync', - postgresUserFile, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_USER_DB_SYNC: postgresUser, - POSTGRES_USER_FILE_DB_SYNC: postgresUserFile, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - - describe('with both postgres password from config and from file', () => { - it('throws a CLI validation error and exits with code 1 whens use CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-password-db-sync', - postgresPassword, - '--postgres-password-file-db-sync', - postgresPasswordFile, - ServiceNames.Utxo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - POSTGRES_PASSWORD_DB_SYNC: postgresPassword, - POSTGRES_PASSWORD_FILE_DB_SYNC: postgresPasswordFile, - SERVICE_NAMES: ServiceNames.Utxo - } - }, - done - ); - }); - }); - }); - - describe('specifying a Cardano-Configurations-dependent service without providing the node config path', () => { - it('network-info exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: ['--postgres-connection-string-db-sync', postgresConnectionString, ServiceNames.NetworkInfo], - dataMatchOnError: 'network-info requires the Cardano node config path program option' - }, - done - ); - }); - - it('network-info exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: 'network-info requires the Cardano node config path program option', - env: { - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.NetworkInfo - } - }, - done - ); - }); - }); - - describe('specifying an Ogmios-dependent service', () => { - beforeEach(async () => { - ogmiosServer = createHealthyMockOgmiosServer(); - // ws://localhost:1337 - ogmiosConnection = Ogmios.createConnectionObject(); - await listenPromise(ogmiosServer, { port: ogmiosConnection.port }); - await ogmiosServerReady(ogmiosConnection); - }); - - describe('without providing the Ogmios URL', () => { - it('network-info uses the default Ogmios configuration if not specified when using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--handle-policy-ids', - HANDLE_POLICY_IDS, - '--handle-provider-server-url', - HANDLE_PROVIDER_SERVER_URL, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--cardano-node-config-path', - cardanoNodeConfigPath, - ServiceNames.NetworkInfo - ], - { - env: {}, - stdio: 'pipe' - } - ) - ); - await assertServiceHealthy(apiUrl, services.networkInfo, lastBlock); - }); - - it('network-info uses the default Ogmios configuration if not specified using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - LOGGER_MIN_SEVERITY: 'error', - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.NetworkInfo - }, - stdio: 'pipe' - }) - ); - await assertServiceHealthy(apiUrl, services.networkInfo, lastBlock); - }); - - it('tx-submit uses the default Ogmios configuration if not specified when using CLI options', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--handle-policy-ids', - HANDLE_POLICY_IDS, - '--handle-provider-server-url', - HANDLE_PROVIDER_SERVER_URL, - ServiceNames.TxSubmit - ], - { - env: {}, - stdio: 'pipe' - } - ) - ); - await assertServiceHealthy(apiUrl, services.txSubmit, lastBlock, { withTip: false }); - }); - - it('exposes a HTTP server with /tx-submit/health endpoint when SUBMIT_VALIDATE_HANDLES is true', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - DB_CACHE_TTL: dbCacheTtl, - HANDLE_POLICY_IDS, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_HANDLE: postgresConnectionStringHandle, - SERVICE_NAMES: `${ServiceNames.TxSubmit}`, - SUBMIT_VALIDATE_HANDLES: 'true' - }, - stdio: 'pipe' - }) - ); - - await assertServiceHealthy(apiUrl, services.txSubmit, lastBlock, { withTip: false }); - }); - - it('tx-submit uses the default Ogmios configuration if not specified when using env variables', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - LOGGER_MIN_SEVERITY: 'error', - SERVICE_NAMES: ServiceNames.TxSubmit - }, - stdio: 'pipe' - }) - ); - await assertServiceHealthy(apiUrl, services.txSubmit, lastBlock, { withTip: false }); - }); - }); - - describe('with service discovery', () => { - it('network-info throws DNS SRV error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--postgres-srv-service-name-db-sync', - postgresSrvServiceName, - '--postgres-db-db-sync', - postgresDb, - '--postgres-user-db-sync', - postgresUser, - '--postgres-password-db-sync', - postgresPassword, - '--ogmios-srv-service-name', - ogmiosSrvServiceName, - '--service-discovery-timeout', - '1000', - '--service-names', - ServiceNames.NetworkInfo - ], - dataMatchOnError: DNS_SERVER_NOT_REACHABLE_ERROR - }, - done - ); - }); - - it('network-info throws DNS SRV error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: DNS_SERVER_NOT_REACHABLE_ERROR, - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - OGMIOS_SRV_SERVICE_NAME: ogmiosSrvServiceName, - POSTGRES_DB_DB_SYNC: postgresDb, - POSTGRES_PASSWORD_DB_SYNC: postgresPassword, - POSTGRES_SRV_SERVICE_NAME_DB_SYNC: postgresSrvServiceName, - POSTGRES_USER_DB_SYNC: postgresUser, - SERVICE_DISCOVERY_TIMEOUT: '1000', - SERVICE_NAMES: ServiceNames.NetworkInfo - } - }, - done - ); - }); - - it('tx-submit throws DNS SRV error and exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--handle-policy-ids', - HANDLE_POLICY_IDS, - '--handle-provider-server-url', - HANDLE_PROVIDER_SERVER_URL, - '--ogmios-srv-service-name', - ogmiosSrvServiceName, - '--service-discovery-timeout', - '1000', - '--service-names', - ServiceNames.TxSubmit - ], - dataMatchOnError: DNS_SERVER_NOT_REACHABLE_ERROR - }, - done - ); - }); - - it('tx-submit throws DNS SRV error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: DNS_SERVER_NOT_REACHABLE_ERROR, - env: { - API_URL: apiUrl, - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - OGMIOS_SRV_SERVICE_NAME: ogmiosSrvServiceName, - POSTGRES_CONNECTION_STRING_HANDLE: postgresConnectionStringHandle, - SERVICE_DISCOVERY_TIMEOUT: '1000', - SERVICE_NAMES: ServiceNames.TxSubmit - } - }, - done - ); - }); - }); - - describe('with providing both Ogmios URL and SRV service name', () => { - it('network-info throws a CLI validation error and exits with code 1 whens use CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-password-db-sync', - postgresPassword, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--ogmios-srv-service-name', - ogmiosSrvServiceName, - '--service-names', - ServiceNames.NetworkInfo - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('network-info throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - API_URL: apiUrl, - OGMIOS_SRV_SERVICE_NAME: ogmiosSrvServiceName, - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.NetworkInfo - } - }, - done - ); - }); - - it('tx-submit throws a CLI validation error and exits with code 1 whens use CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--handle-policy-ids', - HANDLE_POLICY_IDS, - '--handle-provider-server-url', - HANDLE_PROVIDER_SERVER_URL, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--ogmios-srv-service-name', - ogmiosSrvServiceName, - '--service-names', - ServiceNames.TxSubmit - ], - dataMatchOnError: CLI_CONFLICTING_OPTIONS_ERROR_MESSAGE - }, - done - ); - }); - - it('tx-submit throws a CLI validation error and exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: CLI_CONFLICTING_ENV_VARS_ERROR_MESSAGE, - env: { - API_URL: apiUrl, - HANDLE_POLICY_IDS, - HANDLE_PROVIDER_SERVER_URL, - OGMIOS_SRV_SERVICE_NAME: ogmiosSrvServiceName, - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_HANDLE: postgresConnectionStringHandle, - SERVICE_NAMES: ServiceNames.TxSubmit - } - }, - done - ); - }); - }); - - describe('specifying ssl ca file path that does not exist', () => { - const invalidFilePath = 'this-is-not-a-valid-file-path'; - - it('exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--postgres-ssl-ca-file-db-sync', - invalidFilePath, - '--service-names', - ServiceNames.NetworkInfo - ], - dataMatchOnError: 'ENOENT: no such file or directory' - }, - done - ); - }); - - it('exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: 'ENOENT: no such file or directory', - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_SSL_CA_FILE_DB_SYNC: invalidFilePath, - SERVICE_NAMES: ServiceNames.NetworkInfo - } - }, - done - ); - }); - }); - - describe('specifying ssl ca file path to an invalid cert', () => { - it('exits with code 1 when using CLI options', (done) => { - callCliAndAssertExit( - { - args: [ - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--postgres-ssl-ca-file-db-sync', - postgresSslCaFile, - '--service-names', - ServiceNames.NetworkInfo - ], - dataMatchOnError: 'The server does not support SSL connections' - }, - done - ); - }); - - it('exits with code 1 when using env variables', (done) => { - callCliAndAssertExit( - { - dataMatchOnError: 'The server does not support SSL connections', - env: { - API_URL: apiUrl, - CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath, - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - POSTGRES_SSL_CA_FILE_DB_SYNC: postgresSslCaFile, - SERVICE_NAMES: ServiceNames.NetworkInfo - } - }, - done - ); - }); - }); - - describe('specifying a Token-Registry-dependent service', () => { - const tokenMetadataRequestTimeout = '3000'; - let closeMock: () => Promise = jest.fn(); - let tokenMetadataServerUrl = ''; - let serverUrl = ';'; - let asset: AssetData; - let record: any; - - beforeAll(async () => { - asset = (await fixtureBuilder.getAssets(1, { with: [AssetWith.CIP25Metadata] }))[0]; - record = { - name: { value: asset.name }, - subject: asset.id - }; - - ({ closeMock, serverUrl } = await mockTokenRegistry(async () => ({ - body: { subjects: [record] } - }))); - tokenMetadataServerUrl = serverUrl; - }); - - afterAll(async () => await closeMock()); - - it('exposes a HTTP server with healthy state when using CLI options, and /get-asset returns successfully', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--token-metadata-server-url', - tokenMetadataServerUrl, - '--token-metadata-request-timeout', - tokenMetadataRequestTimeout, - ServiceNames.Asset - ], - { env: {}, stdio: 'pipe' } - ) - ); - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - - const res = await axios.post(`${apiUrl}${services.asset.versionPath}/asset/get-asset`, { - assetId: asset.id, - extraData: { tokenMetadata: true } - }); - - const { tokenMetadata } = fromSerializableObject(res.data); - expect(tokenMetadata).toStrictEqual({ assetId: asset.id, name: asset.name }); - }); - - it('exposes a HTTP server with healthy state when using env variables and reaching /get-asset', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.Asset, - TOKEN_METADATA_REQUEST_TIMEOUT: tokenMetadataRequestTimeout, - TOKEN_METADATA_SERVER_URL: tokenMetadataServerUrl - }, - stdio: 'pipe' - }) - ); - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - - const res = await axios.post(`${apiUrl}${services.asset.versionPath}/asset/get-asset`, { - assetId: asset.id, - extraData: { tokenMetadata: true } - }); - - const { tokenMetadata } = fromSerializableObject(res.data); - expect(tokenMetadata).toStrictEqual({ assetId: asset.id, name: asset.name }); - }); - - it('exposes a HTTP server with healthy state when using CLI options, and /get-assets returns successfully', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--token-metadata-server-url', - tokenMetadataServerUrl, - '--token-metadata-request-timeout', - tokenMetadataRequestTimeout, - ServiceNames.Asset - ], - { env: {}, stdio: 'pipe' } - ) - ); - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - - const res = await axios.post( - `${apiUrl}${services.asset.versionPath}/asset/get-assets`, - { - assetIds: [asset.id], - extraData: { tokenMetadata: true } - } - ); - - const { tokenMetadata } = fromSerializableObject(res.data[0]); - expect(tokenMetadata).toStrictEqual({ assetId: asset.id, name: asset.name }); - }); - - it('exposes a HTTP server with healthy state when using env variables and reaching /get-assets', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.Asset, - TOKEN_METADATA_REQUEST_TIMEOUT: tokenMetadataRequestTimeout, - TOKEN_METADATA_SERVER_URL: tokenMetadataServerUrl - }, - stdio: 'pipe' - }) - ); - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - - const res = await axios.post( - `${apiUrl}${services.asset.versionPath}/asset/get-assets`, - { - assetIds: [asset.id], - extraData: { tokenMetadata: true } - } - ); - - const { tokenMetadata } = fromSerializableObject(res.data[0]); - expect(tokenMetadata).toStrictEqual({ assetId: asset.id, name: asset.name }); - }); - - it('loads a stub asset metadata service when TOKEN_METADATA_SERVER_URL starts with "stub:"', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - LOGGER_MIN_SEVERITY: 'error', - OGMIOS_URL: ogmiosConnection.address.webSocket, - POSTGRES_CONNECTION_STRING_DB_SYNC: postgresConnectionString, - SERVICE_NAMES: ServiceNames.Asset, - TOKEN_METADATA_SERVER_URL: 'stub://' - }, - stdio: 'pipe' - }) - ); - await assertServiceHealthy(apiUrl, services.asset, lastBlock); - - const res = await axios.post(`${apiUrl}${services.asset.versionPath}/asset/get-asset`, { - assetId: asset.id, - extraData: { tokenMetadata: true } - }); - - const { tokenMetadata } = fromSerializableObject(res.data); - expect(tokenMetadata).toBeNull(); - }); - }); - }); - }); - - describe('with unhealthy internal providers', () => { - beforeEach(() => { - ogmiosServer = createUnhealthyMockOgmiosServer(); - }); - - it('starts and can be queried for health status', (done) => { - ogmiosServer.listen(ogmiosConnection.port, async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-handle', - postgresConnectionStringHandle, - '--cardano-node-config-path', - cardanoNodeConfigPath, - '--ogmios-url', - ogmiosConnection.address.webSocket, - '--service-names', - ServiceNames.StakePool, - ServiceNames.TxSubmit - ], - { - env: {}, - stdio: 'pipe' - } - ) - ); - - await assertServiceHealthy(apiUrl, services.stakePool, lastBlock, { unhealthy: true }); - done(); - }); - }); - }); - - describe('specifying an unknown service', () => { - beforeEach(() => { - ogmiosServer = createHealthyMockOgmiosServer(); - }); - - it('cli:start-provider-server exits with code 1', (done) => { - ogmiosServer.listen(ogmiosConnection.port, () => { - callCliAndAssertExit( - { - args: [ - '--ogmios-url', - ogmiosConnection.address.webSocket, - 'some-unknown-service', - ServiceNames.TxSubmit - ], - dataMatchOnError: 'UnknownServiceName: some-unknown-service is an unknown service' - }, - done - ); - }); - }); - }); - - describe('with typeorm', () => { - it('stakepool provider server', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-stake-pool', - postgresConnectionStringStakePool, - '--use-typeorm-stake-pool-provider', - 'true', - '--service-names', - ServiceNames.StakePool - ], - { env: {}, stdio: 'pipe' } - ) - ); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - const res = await axios.post(`${apiUrl}${services.stakePool.versionPath}/${ServiceNames.StakePool}/health`, { - headers - }); - expect(res.status).toBe(200); - }); - - describe('asset provider server', () => { - let conn: ReturnType; - - beforeEach(() => { - conn = connString.parse(postgresConnectionStringAsset); - }); - - describe('with cli arguments', () => { - it('starts with connection string', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-connection-string-asset', - postgresConnectionStringAsset, - '--use-typeorm-asset-provider', - 'true', - '--service-names', - ServiceNames.Asset - ], - { env: {}, stdio: 'pipe' } - ) - ); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - const res = await axios.post(`${apiUrl}${services.asset.versionPath}/${ServiceNames.Asset}/health`, { - headers - }); - expect(res.status).toBe(200); - }); - - it('starts with granular connection parameters', async () => { - proc = withLogging( - fork( - exePath, - [ - ...baseArgs, - '--api-url', - apiUrl, - '--postgres-host-asset', - conn.host!, - '--postgres-port-asset', - conn.port!, - '--postgres-db-asset', - conn.database!, - '--postgres-user-asset', - conn.user!, - '--postgres-password-asset', - conn.password!, - '--use-typeorm-asset-provider', - 'true', - '--service-names', - ServiceNames.Asset - ], - { env: {}, stdio: 'pipe' } - ) - ); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - const res = await axios.post(`${apiUrl}${services.asset.versionPath}/${ServiceNames.Asset}/health`, { - headers - }); - expect(res.status).toBe(200); - }); - }); - - describe('with env variables', () => { - it('starts with granular connection parameters', async () => { - proc = withLogging( - fork(exePath, ['start-provider-server'], { - env: { - API_URL: apiUrl, - LOGGER_MIN_SEVERITY: 'error', - POSTGRES_DB_ASSET: conn.database!, - POSTGRES_HOST_ASSET: conn.host!, - POSTGRES_PASSWORD_ASSET: conn.password!, - POSTGRES_PORT_ASSET: conn.port!, - POSTGRES_USER_ASSET: conn.user!, - SERVICE_NAMES: ServiceNames.Asset, - USE_TYPEORM_ASSET_PROVIDER: 'true' - }, - stdio: 'pipe' - }) - ); - await serverStarted(apiUrl); - const headers = { 'Content-Type': 'application/json' }; - const res = await axios.post(`${apiUrl}${services.asset.versionPath}/${ServiceNames.Asset}/health`, { - headers - }); - expect(res.status).toBe(200); - }); - }); - }); + testCli('expects an URL', 'provider', { + args: ['--ogmios-url', 'test'], + env: { OGMIOS_URL: 'test' }, + expectedError: 'Ogmios URL - "test" is not an URL' }); }); + + describe('conflicts', () => { + test('ogmiosUrl conflicts with ogmiosSrvServiceName', () => + runCli('provider', { + env: { OGMIOS_SRV_SERVICE_NAME: 'test', OGMIOS_URL: ogmiosUrl }, + expectedError: "'OGMIOS_URL' cannot be used with environment variable 'OGMIOS_SRV_SERVICE_NAME'" + })); + }); }); - describe('start-projector', () => { - let apiUrl: string; - let proc: ChildProcess; + describe('withPostgresOptionsDbSync', () => { + describe('postgresDbFile', () => { + testCli('accepts an existing file', 'provider', { + args: ['--postgres-db-file-db-sync', 'test/policy_ids'], + env: { POSTGRES_DB_FILE_DB_SYNC: 'test/policy_ids' }, + expectedArgs: { args: { postgresDbFileDbSync: 'test/policy_ids' } } + }); - const assertServerAlive = async () => { - await serverStarted(apiUrl); - const res = await axios.get(`${apiUrl}${baseVersionPath}/live`); - expect(res.status).toBe(200); - }; + testCli('expects an existing file', 'provider', { + args: ['--postgres-db-file-db-sync', 'test/file'], + env: { POSTGRES_DB_FILE_DB_SYNC: 'test/file' }, + expectedError: 'No file exists at test/file' + }); + }); + + describe('postgresPoolMaxDbSync', () => { + testCli('accepts an integer', 'provider', { + args: ['--postgres-pool-max-db-sync', '23'], + env: { POSTGRES_POOL_MAX_DB_SYNC: '23' }, + expectedArgs: { args: { postgresPoolMaxDbSync: 23 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--postgres-pool-max-db-sync', 'test'], + env: { POSTGRES_POOL_MAX_DB_SYNC: 'test' }, + expectedError: 'Maximum number of clients in the PostgreSQL pool for db sync - "test" is not an integer' + }); + }); + + describe('conflicts', () => { + test('postgresConnectionStringDbSync conflicts with postgresDbDbSync', () => + runCli('provider', { + env: { POSTGRES_CONNECTION_STRING_DB_SYNC, POSTGRES_DB_DB_SYNC: 'test' }, + expectedError: + "'POSTGRES_CONNECTION_STRING_DB_SYNC' cannot be used with environment variable 'POSTGRES_DB_DB_SYNC'" + })); + + test('postgresDbDbSync conflicts with postgresDbFileDbSync', () => + runCli('provider', { + env: { POSTGRES_DB_DB_SYNC: 'test', POSTGRES_DB_FILE_DB_SYNC: 'test/policy_ids' }, + expectedError: "'POSTGRES_DB_DB_SYNC' cannot be used with environment variable 'POSTGRES_DB_FILE_DB_SYNC'" + })); + }); + }); + + describe('withStakePoolMetadataOptions', () => { + describe('metadataFetchMode', () => { + testCli('accepts a valid metadata fetch mode', 'pgboss', { + args: ['--queues', QUEUES, '--metadata-fetch-mode', 'direct'], + env: { METADATA_FETCH_MODE: 'direct', QUEUES }, + expectedArgs: { args: { metadataFetchMode: 'direct' } } + }); + + testCli('expects a valid metadata fetch mode', 'pgboss', { + args: ['--queues', QUEUES, '--metadata-fetch-mode', 'test'], + env: { METADATA_FETCH_MODE: 'test', QUEUES }, + expectedError: 'Allowed choices are direct, smash' + }); + }); + + describe('smashUrl', () => { + const smashUrl = 'wss://test/'; + + testCli('accepts an URL', 'pgboss', { + args: ['--queues', QUEUES, '--smash-url', smashUrl], + env: { QUEUES, SMASH_URL: smashUrl }, + expectedArgs: { args: { smashUrl } } + }); + + testCli('expects an URL', 'pgboss', { + args: ['--queues', QUEUES, '--smash-url', 'test'], + env: { QUEUES, SMASH_URL: 'test' }, + expectedError: 'SMASH server api url - "test" is not an URL' + }); + }); - beforeAll(async () => { - const port = await getRandomPort(); - apiUrl = `http://localhost:${port}`; - }); - - afterEach((done) => { - if (proc?.kill()) proc.on('close', () => done()); - else done(); - }); - - describe('with cli arguments', () => { - let commonArgs: string[]; - const startProjector = (extraArgs: string[]) => { - proc = withLogging(fork(exePath, [...commonArgs, ...extraArgs], { env: {}, stdio: 'pipe' })); - }; - - beforeEach(() => { - commonArgs = ['start-projector', '--logger-min-severity', 'error', '--dry-run', 'true', '--api-url', apiUrl]; - }); - - describe('with predefined ogmios url and postgres connection string', () => { - test('with a single projection', async () => { - startProjector([ - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - ProjectionName.UTXO - ]); - await assertServerAlive(); - }); - - test('with multiple projections as a last argument', async () => { - startProjector([ - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - `${ProjectionName.UTXO},${ProjectionName.StakePool}` - ]); - await assertServerAlive(); - }); - - test('with multiple projections as --projection-names argument', async () => { - startProjector([ - '--projection-names', - `${ProjectionName.UTXO},${ProjectionName.StakePool}`, - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection - ]); - await assertServerAlive(); - }); - - it('accepts --drop-schema true', async () => { - startProjector([ - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - '--drop-schema', - 'true', - ProjectionName.UTXO - ]); - await assertServerAlive(); - }); - }); - - it('can be started in SRV discovery mode', async () => { - startProjector([ - '--postgres-db', - 'dbname', - '--postgres-password', - 'password', - '--postgres-srv-service-name', - 'postgres.projection.com', - '--postgres-user', - 'username', - '--ogmios-srv-service-name', - 'ogmios.projection.com', - '--service-discovery-timeout', - '1000', - ProjectionName.UTXO - ]); - await assertServerAlive(); - }); - - test('with handle projection and handle policy ids option', async () => { - startProjector([ - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - '--handle-policy-ids', - 'f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a', - ProjectionName.Handle - ]); - await assertServerAlive(); - }); - - test('with handle projection and handle policy ids file option', async () => { - const chunks: string[] = []; - - startProjector([ - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - '--logger-min-severity', - 'debug', - '--handle-policy-ids-file', - path.join(__dirname, 'policy_ids'), - ProjectionName.Handle - ]); - - proc.stdout?.on('data', (data: Buffer) => chunks.push(data.toString('utf8'))); - - await assertServerAlive(); - - expect(chunks.join('')).toMatch( - 'Creating projection with policyIds [\\"f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a\\"]' - ); - }); - - it('exits with code 1 with handle projection without handle policy ids option', (done) => { - expect.assertions(2); - proc = withLogging( - fork( - exePath, - [ - ...commonArgs, - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - ProjectionName.Handle - ], - { env: {}, stdio: 'pipe' } - ), - true - ); - proc.stderr!.on('data', (data) => - expect(data.toString()).toMatch( - 'MissingProgramOption: handle requires the Handle policy Ids or Handle policy Ids file program option' - ) - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 with handle projection and invalid policy ids', (done) => { - expect.assertions(2); - proc = withLogging( - fork( - exePath, - [ - ...commonArgs, - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - '--handle-policy-ids', - 'policyId', - ProjectionName.Handle - ], - { env: {}, stdio: 'pipe' } - ), - true - ); - proc.stderr!.on('data', (data) => - expect(data.toString()).toMatch("InvalidStringError: Invalid string: \"expected length '56', got 8") - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - test('uses the configured blocks buffer length', async () => { - const chunks: string[] = []; - - startProjector([ - '--ogmios-url', - 'ws://localhost:1234', - '--postgres-connection-string', - postgresConnectionStringProjection, - '--logger-min-severity', - 'debug', - '--blocks-buffer-length', - '23', - ProjectionName.StakePool - ]); - - proc.stdout?.on('data', (data: Buffer) => chunks.push(data.toString('utf8'))); - - await assertServerAlive(); - - expect(chunks.join('')).toMatch('Using a 23 blocks buffer'); - }); - }); - - describe('with environment variables', () => { - let commonEnv: any; - const startProjector = (extraEnv: T, extraArgs: string[] = []) => { - proc = withLogging( - fork(exePath, ['start-projector', ...extraArgs], { - env: { - ...commonEnv, - ...extraEnv - }, - stdio: 'pipe' - }) - ); - }; - - beforeEach(() => { - commonEnv = { API_URL: apiUrl, DRY_RUN: 'true', LOGGER_MIN_SEVERITY: 'error' }; - }); - - describe('with predefined ogmios url and postgres connection string', () => { - test('with a single projection', async () => { - startProjector( - { - OGMIOS_URL: 'ws://localhost:1234', - POSTGRES_CONNECTION_STRING: postgresConnectionStringProjection - }, - [ProjectionName.UTXO] - ); - await assertServerAlive(); - }); - - test('with multiple projections as a last argument', async () => { - startProjector( - { - OGMIOS_URL: 'ws://localhost:1234', - POSTGRES_CONNECTION_STRING: postgresConnectionStringProjection - }, - [`${ProjectionName.UTXO},${ProjectionName.StakePool}`] - ); - await assertServerAlive(); - }); - - test('with multiple projections as --projection-names argument', async () => { - startProjector({ - OGMIOS_URL: 'ws://localhost:1234', - POSTGRES_CONNECTION_STRING: postgresConnectionStringProjection, - PROJECTION_NAMES: `${ProjectionName.UTXO},${ProjectionName.StakePool}` - }); - await assertServerAlive(); - }); - }); - - it('can be started in SRV discovery mode', async () => { - startProjector( - { - OGMIOS_SRV_SERVICE_NAME: 'ogmios.projection.com', - POSTGRES_DB: 'dbname', - POSTGRES_PASSWORD: 'password', - POSTGRES_SRV_SERVICE_NAME: 'postgres.projection.com', - POSTGRES_USER: 'username', - SERVICE_DISCOVERY_TIMEOUT: '1000' + describe('required combinations', () => { + test('metadata-fetch-mode smash requires smashUrl', () => + runCli('pgboss', { + env: { + METADATA_FETCH_MODE: 'smash', + POSTGRES_CONNECTION_STRING_DB_SYNC, + POSTGRES_CONNECTION_STRING_STAKE_POOL, + QUEUES }, - [ProjectionName.UTXO] - ); - await assertServerAlive(); + expectedOutput: 'pool-metadata requires the smash-url to be set when metadata-fetch-mode is smash', + notDump + })); + }); + }); + + describe('blockfrost worker', () => { + describe('blockfrostApiFile', () => { + testBlockfrost('accepts any string', 'blockfrost', { + args: ['--blockfrost-api-file', 'test'], + env: { BLOCKFROST_API_FILE: 'test' }, + expectedArgs: { args: { blockfrostApiFile: 'test' } } + }); + }); + + describe('blockfrostApiKey', () => { + testBlockfrost('accepts any string', 'blockfrost', { + args: ['--blockfrost-api-key', 'test'], + env: { BLOCKFROST_API_KEY: 'test' }, + expectedArgs: { args: { blockfrostApiKey: 'test' } } + }); + }); + + describe('cacheTtl', () => { + testBlockfrost('accepts an integer', 'blockfrost', { + args: ['--cache-ttl', '23'], + env: { CACHE_TTL: '23' }, + expectedArgs: { args: { cacheTtl: 23 } } + }); + + testBlockfrost('expects an integer', 'blockfrost', { + args: ['--cache-ttl', 'test'], + env: { CACHE_TTL: 'test' }, + expectedError: 'TTL of blockfrost cached metrics in minutes - "test" is not an integer' + }); + }); + + describe('createSchema', () => { + testBlockfrost('accepts a boolean', 'blockfrost', { + args: ['--create-schema', 'true'], + env: { CREATE_SCHEMA: 'true' }, + expectedArgs: { args: { createSchema: true } } + }); + + testBlockfrost('expects a boolean', 'blockfrost', { + args: ['--create-schema', 'test'], + env: { CREATE_SCHEMA: 'test' }, + expectedError: + 'Blockfrost worker requires a valid create the schema; useful for development program option. Expected: false, true' + }); + }); + + describe('dropSchema', () => { + testBlockfrost('accepts a boolean', 'blockfrost', { + args: ['--drop-schema', 'true'], + env: { DROP_SCHEMA: 'true' }, + expectedArgs: { args: { dropSchema: true } } + }); + + testBlockfrost('expects a boolean', 'blockfrost', { + args: ['--drop-schema', 'test'], + env: { DROP_SCHEMA: 'test' }, + expectedError: + 'Blockfrost worker requires a valid drop the schema; useful for development program option. Expected: false, true' + }); + }); + + describe('dryRun', () => { + testBlockfrost('accepts a boolean', 'blockfrost', { + args: ['--dry-run', 'true'], + env: { DRY_RUN: 'true' }, + expectedArgs: { args: { dryRun: true } } + }); + + testBlockfrost('expects a boolean', 'blockfrost', { + args: ['--dry-run', 'test'], + env: { DRY_RUN: 'test' }, + expectedError: + 'Blockfrost worker requires a valid dry run; useful for tests program option. Expected: false, true' + }); + }); + + describe('network', () => { + testCli('accepts a valid network', 'blockfrost', { + args: ['--network', 'mainnet'], + env: { NETWORK: 'mainnet' }, + expectedArgs: { args: { network: 'mainnet' } } + }); + + testCli('expects a valid network', 'blockfrost', { + args: ['--network', 'test'], + env: { NETWORK: 'test' }, + expectedError: 'Unknown network: test' }); + + testCli('is mandatory', 'blockfrost', { expectedError: "required option '--network ' not specified" }); + }); + + describe('scanInterval', () => { + testBlockfrost('accepts an integer', 'blockfrost', { + args: ['--scan-interval', '23'], + env: { SCAN_INTERVAL: '23' }, + expectedArgs: { args: { scanInterval: 23 } } + }); + + testBlockfrost('expects an integer', 'blockfrost', { + args: ['--scan-interval', 'test'], + env: { SCAN_INTERVAL: 'test' }, + expectedError: 'interval between a scan and the next one in minutes - "test" is not an integer' + }); + }); + + describe('conflicts', () => { + test('blockfrostApiFile conflicts with blockfrostApiKey', () => + runCli('blockfrost', { + env: { BLOCKFROST_API_FILE: 'test', BLOCKFROST_API_KEY: 'test', NETWORK: 'mainnet' }, + expectedError: "'BLOCKFROST_API_FILE' cannot be used with environment variable 'BLOCKFROST_API_KEY'" + })); + }); + + describe('required combinations', () => { + test('requires blockfrostApiFile or blockfrostApiKey', () => + runCli('blockfrost', { + env: { NETWORK: 'mainnet' }, + expectedError: + 'Blockfrost worker requires the Blockfrost API Key file path or Blockfrost API Key program option', + notDump + })); + + test('requires DB connection config', () => + runCli('blockfrost', { + env: { BLOCKFROST_API_KEY: 'test', NETWORK: 'mainnet' }, + expectedError: + 'Blockfrost worker requires the PostgreSQL Connection string or Postgres SRV service name, db, user and password program option', + notDump + })); }); }); - describe('start-blockfrost-worker', () => { - const commonArgs = ['start-blockfrost-worker', '--logger-min-severity', 'info', '--dry-run', 'true']; - let port: number; - let proc: ChildProcess; - - beforeAll(async () => { - port = await getRandomPort(); - }); - - afterEach((done) => { - if (proc?.kill()) proc.on('close', () => done()); - else done(); - }); - - // Tests without any assertion fail if they get timeout - it('exits with code 1 without api key', (done) => { - expect.assertions(2); - proc = withLogging(fork(exePath, commonArgs, { env: {}, stdio: 'pipe' }), true); - proc.stderr!.on('data', (data) => - expect(data.toString()).toMatch( - 'MissingProgramOption: Blockfrost worker requires the Blockfrost API Key file path or Blockfrost API Key program option' - ) - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 without network', (done) => { - expect.assertions(2); - proc = withLogging( - fork(exePath, [...commonArgs, '--blockfrost-api-key', 'abc'], { env: {}, stdio: 'pipe' }), - true - ); - proc.stderr!.on('data', (data) => - expect(data.toString()).toMatch( - 'MissingProgramOption: Blockfrost worker requires the network to run against program option' - ) - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 with wrong network', (done) => { - expect.assertions(2); - proc = withLogging( - fork(exePath, [...commonArgs, '--blockfrost-api-key', 'abc', '--network', 'none'], { - env: {}, - stdio: 'pipe' - }), - true - ); - proc.stderr!.on('data', (data) => expect(data.toString()).toMatch('Error: Unknown network: none')); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 without db connection string', (done) => { - expect.assertions(2); - proc = withLogging( - fork(exePath, [...commonArgs, '--blockfrost-api-key', 'abc', '--network', 'mainnet'], { - env: {}, - stdio: 'pipe' - }), - true - ); - proc.stderr!.on('data', (data) => expect(data.toString()).toMatch(REQUIRES_PG_CONNECTION)); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('dry run', (done) => { - proc = withLogging( - fork( - exePath, - [ - ...commonArgs, - '--blockfrost-api-key', - 'abc', - '--network', - 'mainnet', - '--postgres-connection-string-db-sync', - process.env.POSTGRES_CONNECTION_STRING_DB_SYNC!, - '--api-url', - `http://localhost:${port}/` - ], - { env: {}, stdio: 'pipe' } - ) - ); - proc.stdout!.on('data', (data) => { - // eslint-disable-next-line unicorn/prefer-regexp-test - if (data.toString('utf8').match(/Sleeping for \d+ milliseconds to start next run/)) done(); + describe('pg-boss worker', () => { + describe('parallelJobs', () => { + testPgBoss('accepts an integer', 'pgboss', { + args: ['--parallel-jobs', '23'], + env: { PARALLEL_JOBS: '23' }, + expectedArgs: { args: { parallelJobs: 23 } } + }); + + testPgBoss('expects an integer', 'pgboss', { + args: ['--parallel-jobs', 'test'], + env: { PARALLEL_JOBS: 'test' }, + expectedError: 'Parallel jobs to run - "test" is not an integer' + }); + }); + + describe('queues', () => { + testCli('accepts an array of valid queues', 'pgboss', { + args: ['--queues', QUEUES], + env: { QUEUES }, + expectedArgs: { args: { queues } } + }); + + testCli('expects an array of valid queues', 'pgboss', { + args: ['--queues', 'test'], + env: { QUEUES: 'test' }, + expectedError: "Unknown queue name: 'test'" + }); + + testCli('is mandatory', 'pgboss', { expectedError: "required option '--queues ' not specified" }); + }); + + describe('schedules', () => { + testPgBoss('accepts a valid schedule file', 'pgboss', { + args: ['--schedules', 'environments/.schedule.local.json'], + env: { SCHEDULES: 'environments/.schedule.local.json' }, + expectedArgs: { + args: { schedules: [{ cron: '0 * * * *', queue: 'pool-delist-schedule', scheduleOptions: {} }] } + } + }); + + testPgBoss('expects a file', 'pgboss', { + args: ['--schedules', 'unknown'], + env: { SCHEDULES: 'unknown' }, + expectedError: 'File does not exist: unknown' + }); + + testPgBoss('expects a valid schedule file', 'pgboss', { + args: ['--schedules', 'test/policy_ids'], + env: { SCHEDULES: 'test/policy_ids' }, + expectedError: 'Failed to parse the schedule config from file: test/policy_ids' }); }); + + describe('required combinations', () => { + test('requires DB connection config', () => + runCli('pgboss', { + env: { QUEUES }, + expectedError: + 'pg-boss-worker requires the postgresConnectionString or postgresSrvServiceName or postgresUser or postgresDb or postgresPassword program option', + notDump + })); + + test('pool-metrics requires stakePoolProviderUrl', () => + runCli('pgboss', { + env: { POSTGRES_CONNECTION_STRING_DB_SYNC, POSTGRES_CONNECTION_STRING_STAKE_POOL, QUEUES }, + expectedOutput: 'pool-metrics requires the stake-pool provider URL program option', + notDump + })); + + test('pool-rewards requires networkInfoProviderUrl', () => + runCli('pgboss', { + env: { + POSTGRES_CONNECTION_STRING_DB_SYNC, + POSTGRES_CONNECTION_STRING_STAKE_POOL, + QUEUES, + STAKE_POOL_PROVIDER_URL: 'http://test/' + }, + expectedOutput: 'pool-rewards requires the network-info provider URL program option', + notDump + })); + }); }); - describe('start-pg-boss-worker', () => { - const commonArgs = ['start-pg-boss-worker', '--logger-min-severity', 'info']; - let proc: ChildProcess; - - afterEach((done) => { - if (proc?.kill()) proc.on('close', () => done()); - else done(); - }); - - // Tests without any assertion fail if they get timeout - it('exits with code 1 without queues', (done) => { - expect.assertions(2); - proc = withLogging(fork(exePath, commonArgs, { env: {}, stdio: 'pipe' }), true); - proc.stderr!.on('data', (data) => - expect(data.toString()).toMatch("required option '--queues ' not specified") - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 with a wrong queue', (done) => { - expect.assertions(2); - proc = withLogging(fork(exePath, [...commonArgs, '--queues', 'abc'], { env: {}, stdio: 'pipe' }), true); - proc.stderr!.on('data', (data) => expect(data.toString()).toMatch("Error: Unknown queue name: 'abc'")); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 without a valid connection string', (done) => { - expect.assertions(2); - proc = withLogging(fork(exePath, [...commonArgs, '--queues', 'pool-metadata'], { env: {}, stdio: 'pipe' }), true); - proc.stderr!.on('data', (data) => - expect(data.toString()).toMatch('pg-boss-worker requires the postgresConnectionString') - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 with metrics queue and without a stake pool provider url', (done) => { - proc = withLogging( - fork( - exePath, - [ - ...commonArgs, - '--queues', - 'pool-metrics', - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-stake-pool', - postgresConnectionStringStakePool - ], - { env: {}, stdio: 'pipe' } - ), - true - ); - - const chunks: string[] = []; - - proc.stdout!.on('data', (data: Buffer) => chunks.push(data.toString())); - proc.on('exit', (code) => { - expect(chunks.join('')).toMatch( - 'MissingProgramOption: pool-metrics requires the stake-pool provider URL program option' - ); - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 with an invalid SMASH_URL when metadata-fetch-mode=smash', (done) => { - expect.assertions(2); - proc = withLogging( - fork( - exePath, - [ - ...commonArgs, - '--queues', - 'pool-metadata', - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-stake-pool', - postgresConnectionStringStakePool, - '--stake-pool-provider-url', - 'http://localhost:4000/stake-pool', - '--metadata-fetch-mode', - 'smash', - '--smash-url', - 'invalid-url' - ], - { env: {}, stdio: 'pipe' } - ), - true - ); - proc.stderr!.on('data', (data) => expect(data.toString()).toContain('[ERR_INVALID_URL]')); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); - }); - }); - - it('exits with code 1 with an invalid SCHEDULES path', (done) => { - expect.assertions(2); - proc = withLogging( - fork( - exePath, - [ - ...commonArgs, - '--queues', - 'pool-metadata', - '--postgres-connection-string-db-sync', - postgresConnectionString, - '--postgres-connection-string-stake-pool', - postgresConnectionStringStakePool, - '--stake-pool-provider-url', - 'http://localhost:4000/stake-pool', - '--schedules', - 'does_not_exist' - ], - { env: {}, stdio: 'pipe' } - ), - true - ); - proc.stderr!.on('data', (data) => - expect(data.toString()).toContain('Error: File does not exist: does_not_exist') - ); - proc.on('exit', (code) => { - expect(code).toBe(1); - done(); + describe('projector', () => { + describe('blocksBufferLength', () => { + testCli('accepts an integer', 'projector', { + args: ['--blocks-buffer-length', '23'], + env: { BLOCKS_BUFFER_LENGTH: '23' }, + expectedArgs: { args: { blocksBufferLength: 23 } } + }); + + testCli('expects an integer', 'projector', { + args: ['--blocks-buffer-length', 'test'], + env: { BLOCKS_BUFFER_LENGTH: 'test' }, + expectedError: 'Chain sync event (blocks) buffer length - "test" is not an integer' }); }); + + describe('dropSchema', () => { + testCli('accepts a boolean', 'projector', { + args: ['--drop-schema', 't'], + env: { DROP_SCHEMA: 't' }, + expectedArgs: { args: { dropSchema: true } } + }); + + testCli('expects a boolean', 'projector', { + args: ['--drop-schema', 'test'], + env: { DROP_SCHEMA: 'test' }, + expectedError: 'requires a valid Drop and recreate database schema to project from origin' + }); + }); + + describe('dryRun', () => { + testCli('accepts a boolean', 'projector', { + args: ['--dry-run', 't'], + env: { DRY_RUN: 't' }, + expectedArgs: { args: { dryRun: true } } + }); + + testCli('expects a boolean', 'projector', { + args: ['--dry-run', 'test'], + env: { DRY_RUN: 'test' }, + expectedError: 'requires a valid Initialize the projection, but do not start it' + }); + }); + + describe('exitAtBlockNo', () => { + testCli('accepts an integer', 'projector', { + args: ['--exit-at-block-no', '23'], + env: { EXIT_AT_BLOCK_NO: '23' }, + expectedArgs: { args: { exitAtBlockNo: 23 } } + }); + + testCli('expects an integer', 'projector', { + args: ['--exit-at-block-no', 'test'], + env: { EXIT_AT_BLOCK_NO: 'test' }, + expectedError: 'Exit after processing this block. Intended for benchmark testing - "test" is not an integer' + }); + + test('defaults to 0', () => runCli('projector', { expectedArgs: { args: { exitAtBlockNo: 0 } } })); + }); + + describe('metadataJobRetryDelay', () => { + testCli('accepts an integer', 'projector', { + args: ['--metadata-job-retry-delay', '23'], + env: { METADATA_JOB_RETRY_DELAY: '23' }, + expectedArgs: { args: { metadataJobRetryDelay: 23 } } + }); + + testCli('expects an integer', 'projector', { + args: ['--metadata-job-retry-delay', 'test'], + env: { METADATA_JOB_RETRY_DELAY: 'test' }, + expectedError: 'Retry delay for metadata fetch job in seconds - "test" is not an integer' + }); + }); + + describe('poolsMetricsInterval', () => { + testCli('accepts an integer', 'projector', { + args: ['--pools-metrics-interval', '23'], + env: { POOLS_METRICS_INTERVAL: '23' }, + expectedArgs: { args: { poolsMetricsInterval: 23 } } + }); + + testCli('expects an integer', 'projector', { + args: ['--pools-metrics-interval', 'test'], + env: { POOLS_METRICS_INTERVAL: 'test' }, + expectedError: + 'Interval in number of blocks between two stake pools metrics jobs to update all metrics - "test" is not an integer' + }); + }); + + describe('poolsMetricsOutdatedInterval', () => { + testCli('accepts an integer', 'projector', { + args: ['--pools-metrics-outdated-interval', '23'], + env: { POOLS_METRICS_OUTDATED_INTERVAL: '23' }, + expectedArgs: { args: { poolsMetricsOutdatedInterval: 23 } } + }); + + testCli('expects an integer', 'projector', { + args: ['--pools-metrics-outdated-interval', 'test'], + env: { POOLS_METRICS_OUTDATED_INTERVAL: 'test' }, + expectedError: + 'Interval in number of blocks between two stake pools metrics jobs to update only outdated metrics - "test" is not an integer' + }); + }); + + describe('projectionNames', () => { + const projectionNames = ['asset', 'handle', 'stake-pool']; + const goodNames = projectionNames.join(','); + const badNames = 'asset,unknown'; + const expectedError = 'Unknown projection name "unknown"'; + + testCli('accepts an array of projections', 'projector', { + args: ['--projection-names', goodNames], + env: { PROJECTION_NAMES: goodNames }, + expectedArgs: { args: { projectionNames } } + }); + + testCli('expects an array of projections', 'projector', { + args: ['--projection-names', badNames], + env: { PROJECTION_NAMES: badNames }, + expectedError + }); + + test('arg - accepts an array of projections', () => + runCli('projector', { args: [goodNames], expectedArgs: { projectionNames } })); + + test('arg - expects an array of projections', () => runCli('projector', { args: [badNames], expectedError })); + }); + + describe('synchronize', () => { + testCli('accepts a boolean', 'projector', { + args: ['--synchronize', 't'], + env: { SYNCHRONIZE: 't' }, + expectedArgs: { args: { synchronize: true } } + }); + + testCli('expects a boolean', 'projector', { + args: ['--synchronize', 'test'], + env: { SYNCHRONIZE: 'test' }, + expectedError: 'Projector requires a valid Synchronize the schema from the models' + }); + }); + + describe('required combinations', () => { + test('handle projection requires handlePolicyIds or handlePolicyIdsFile', () => + runCli('projector', { + env: { PROJECTION_NAMES: 'handle', QUEUES }, + expectedError: 'handle requires the Handle policy Ids or Handle policy Ids file program option', + notDump + })); + }); + }); + + describe('provider server', () => { + describe('allowedOrigins', () => { + const allowedOrigins = ['origin 1', 'test origin', 'origin n']; + const origins = allowedOrigins.join(','); + + testCli('accepts an array of origins', 'provider', { + args: ['--allowed-origins', origins], + env: { ALLOWED_ORIGINS: origins }, + expectedArgs: { args: { allowedOrigins } } + }); + }); + + describe('assetCacheTtl', () => { + testCli('accepts an integer between the edges', 'provider', { + args: ['--asset-cache-ttl', '123'], + env: { ASSET_CACHE_TTL: '123' }, + expectedArgs: { args: { assetCacheTtl: 123 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--asset-cache-ttl', 'test'], + env: { ASSET_CACHE_TTL: 'test' }, + expectedError: 'Asset info and NFT Metadata cache TTL in seconds (600 by default) - "test" is not an integer' + }); + + testCli('expects an integer between the edges', 'provider', { + args: ['--asset-cache-ttl', '23'], + env: { ASSET_CACHE_TTL: '23' }, + expectedError: + 'Asset info and NFT Metadata cache TTL in seconds (600 by default) - 23 must be between 60 and 172800' + }); + }); + + describe('cardanoNodeConfigPath', () => { + const cardanoNodeConfigPath = 'path/to/config'; + + testCli('accepts a path', 'provider', { + args: ['--cardano-node-config-path', cardanoNodeConfigPath], + env: { CARDANO_NODE_CONFIG_PATH: cardanoNodeConfigPath }, + expectedArgs: { args: { cardanoNodeConfigPath } } + }); + }); + + describe('dbCacheTtl', () => { + testCli('accepts an integer between the edges', 'provider', { + args: ['--db-cache-ttl', '123'], + env: { DB_CACHE_TTL: '123' }, + expectedArgs: { args: { dbCacheTtl: 123 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--db-cache-ttl', 'test'], + env: { DB_CACHE_TTL: 'test' }, + expectedError: + 'Cache TTL in seconds between 60 and 172800 (two days), an option for database related operations - "test" is not an integer' + }); + + testCli('expects an integer between the edges', 'provider', { + args: ['--db-cache-ttl', '23'], + env: { DB_CACHE_TTL: '23' }, + expectedError: + 'Cache TTL in seconds between 60 and 172800 (two days), an option for database related operations - 23 must be between 60 and 172800' + }); + }); + + describe('disableDbCache', () => { + testCli('accepts a boolean', 'provider', { + args: ['--disable-db-cache', 'true'], + env: { DISABLE_DB_CACHE: 'true' }, + expectedArgs: { args: { disableDbCache: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--disable-db-cache', 'test'], + env: { DISABLE_DB_CACHE: 'test' }, + expectedError: 'Provider server requires a valid Disable DB cache program option. Expected: false, true' + }); + }); + + describe('disableStakePoolMetricApy', () => { + testCli('accepts a boolean', 'provider', { + args: ['--disable-stake-pool-metric-apy', 'true'], + env: { DISABLE_STAKE_POOL_METRIC_APY: 'true' }, + expectedArgs: { args: { disableStakePoolMetricApy: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--disable-stake-pool-metric-apy', 'test'], + env: { DISABLE_STAKE_POOL_METRIC_APY: 'test' }, + expectedError: + 'Provider server requires a valid Omit this metric for improved query performance program option. Expected: false, true' + }); + }); + + describe('epochPollInterval', () => { + testCli('accepts an integer', 'provider', { + args: ['--epoch-poll-interval', '23'], + env: { EPOCH_POLL_INTERVAL: '23' }, + expectedArgs: { args: { epochPollInterval: 23 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--epoch-poll-interval', 'test'], + env: { EPOCH_POLL_INTERVAL: 'test' }, + expectedError: 'Epoch poll interval - "test" is not an integer' + }); + }); + + describe('handleProviderServerUrl', () => { + testCli('accepts any string', 'provider', { + args: ['--handle-provider-server-url', 'test'], + env: { HANDLE_PROVIDER_SERVER_URL: 'test' }, + expectedArgs: { args: { handleProviderServerUrl: 'test' } } + }); + }); + + describe('healthCheckCacheTtl', () => { + testCli('accepts an integer between the edges', 'provider', { + args: ['--health-check-cache-ttl', '23'], + env: { HEALTH_CHECK_CACHE_TTL: '23' }, + expectedArgs: { args: { healthCheckCacheTtl: 23 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--health-check-cache-ttl', 'test'], + env: { HEALTH_CHECK_CACHE_TTL: 'test' }, + expectedError: 'Health check cache TTL in seconds between 1 and 10 - "test" is not an integer' + }); + + testCli('expects an integer between the edges', 'provider', { + args: ['--health-check-cache-ttl', '123'], + env: { HEALTH_CHECK_CACHE_TTL: '123' }, + expectedError: 'Health check cache TTL in seconds between 1 and 10 - 123 must be between 1 and 120' + }); + }); + + describe('paginationPageSizeLimit', () => { + testCli('accepts an integer between the edges', 'provider', { + args: ['--pagination-page-size-limit', '23'], + env: { PAGINATION_PAGE_SIZE_LIMIT: '23' }, + expectedArgs: { args: { paginationPageSizeLimit: 23 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--health-check-cache-ttl', 'test'], + env: { HEALTH_CHECK_CACHE_TTL: 'test' }, + expectedError: 'Health check cache TTL in seconds between 1 and 10 - "test" is not an integer' + }); + }); + + describe('submitApiUrl', () => { + const submitApiUrl = 'https://test/'; + + testCli('accepts an URL', 'provider', { + args: ['--submit-api-url', submitApiUrl], + env: { SUBMIT_API_URL: submitApiUrl }, + expectedArgs: { args: { submitApiUrl } } + }); + + testCli('expects an URL', 'provider', { + args: ['--submit-api-url', 'test'], + env: { SUBMIT_API_URL: 'test' }, + expectedError: 'cardano-submit-api URL - "test" is not an URL' + }); + }); + + describe('tokenMetadataCacheTtl', () => { + testCli('accepts an integer between the edges', 'provider', { + args: ['--token-metadata-cache-ttl', '123'], + env: { TOKEN_METADATA_CACHE_TTL: '123' }, + expectedArgs: { args: { tokenMetadataCacheTtl: 123 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--token-metadata-cache-ttl', 'test'], + env: { TOKEN_METADATA_CACHE_TTL: 'test' }, + expectedError: 'Token Metadata API cache TTL in seconds - "test" is not an integer' + }); + + testCli('expects an integer between the edges', 'provider', { + args: ['--token-metadata-cache-ttl', '23'], + env: { TOKEN_METADATA_CACHE_TTL: '23' }, + expectedError: 'Token Metadata API cache TTL in seconds - 23 must be between 60 and 172800' + }); + }); + + describe('tokenMetadataRequestTimeout', () => { + testCli('accepts an integer', 'provider', { + args: ['--token-metadata-request-timeout', '23000'], + env: { TOKEN_METADATA_REQUEST_TIMEOUT: '23000' }, + expectedArgs: { args: { tokenMetadataRequestTimeout: 23_000 } } + }); + + testCli('expects an integer', 'provider', { + args: ['--token-metadata-request-timeout', 'test'], + env: { TOKEN_METADATA_REQUEST_TIMEOUT: 'test' }, + expectedError: 'Token Metadata request timeout in milliseconds - "test" is not an integer' + }); + }); + + describe('tokenMetadataServerUrl', () => { + const tokenMetadataServerUrl = 'https://test/'; + + testCli('accepts an URL', 'provider', { + args: ['--token-metadata-server-url', tokenMetadataServerUrl], + env: { TOKEN_METADATA_SERVER_URL: tokenMetadataServerUrl }, + expectedArgs: { args: { tokenMetadataServerUrl } } + }); + + testCli('expects an URL', 'provider', { + args: ['--token-metadata-server-url', 'test'], + env: { TOKEN_METADATA_SERVER_URL: 'test' }, + expectedError: 'Token Metadata API server URL - "test" is not an URL' + }); + }); + + describe('serviceNames', () => { + const serviceNames = ['asset', 'handle', 'stake-pool']; + const goodNames = serviceNames.join(','); + const badNames = 'asset,unknown'; + const expectedError = 'Unknown service name "unknown"'; + + testCli('accepts an array of services', 'provider', { + args: ['--service-names', goodNames], + env: { SERVICE_NAMES: goodNames }, + expectedArgs: { args: { serviceNames } } + }); + + testCli('expects an array of services', 'provider', { + args: ['--service-names', badNames], + env: { SERVICE_NAMES: badNames }, + expectedError + }); + + test('arg - accepts an array of services', () => + runCli('provider', { args: [goodNames], expectedArgs: { serviceNames } })); + + test('arg - expects an array of services', () => runCli('provider', { args: [badNames], expectedError })); + }); + + describe('submitValidateHandles', () => { + testCli('accepts a boolean', 'provider', { + args: ['--submit-validate-handles', 'true'], + env: { SUBMIT_VALIDATE_HANDLES: 'true' }, + expectedArgs: { args: { submitValidateHandles: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--submit-validate-handles', 'test'], + env: { SUBMIT_VALIDATE_HANDLES: 'test' }, + expectedError: + 'Provider server requires a valid Validate handle resolutions before submitting transactions. Requires handle provider options (USE_KORA_LABS or POSTGRES options with HANDLE suffix). program option. Expected: false, true' + }); + }); + + describe('useBlockfrost', () => { + testCli('accepts a boolean', 'provider', { + args: ['--use-blockfrost', 'true'], + env: { USE_BLOCKFROST: 'true' }, + expectedArgs: { args: { useBlockfrost: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--use-blockfrost', 'test'], + env: { USE_BLOCKFROST: 'test' }, + expectedError: + 'Provider server requires a valid Enables Blockfrost cached data DB program option. Expected: false, true' + }); + }); + + describe('useKoraLabs', () => { + testCli('accepts a boolean', 'provider', { + args: ['--use-kora-labs', 'true'], + env: { USE_KORA_LABS: 'true' }, + expectedArgs: { args: { useKoraLabs: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--use-kora-labs', 'test'], + env: { USE_KORA_LABS: 'test' }, + expectedError: + 'Provider server requires a valid Use the KoraLabs handle provider program option. Expected: false, true' + }); + }); + + describe('useSubmitApi', () => { + testCli('accepts a boolean', 'provider', { + args: ['--use-submit-api', 'true'], + env: { USE_SUBMIT_API: 'true' }, + expectedArgs: { args: { useSubmitApi: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--use-submit-api', 'test'], + env: { USE_SUBMIT_API: 'test' }, + expectedError: + 'Provider server requires a valid Use cardano-submit-api provider program option. Expected: false, true' + }); + }); + + describe('useTypeormAssetProvider', () => { + testCli('accepts a boolean', 'provider', { + args: ['--use-typeorm-asset-provider', 'true'], + env: { USE_TYPEORM_ASSET_PROVIDER: 'true' }, + expectedArgs: { args: { useTypeormAssetProvider: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--use-typeorm-asset-provider', 'test'], + env: { USE_TYPEORM_ASSET_PROVIDER: 'test' }, + expectedError: + 'Provider server requires a valid Use the TypeORM Asset Provider (default is db-sync) program option. Expected: false, true' + }); + }); + + describe('useTypeormStakePoolProvider', () => { + testCli('accepts a boolean', 'provider', { + args: ['--use-typeorm-stake-pool-provider', 'true'], + env: { USE_TYPEORM_STAKE_POOL_PROVIDER: 'true' }, + expectedArgs: { args: { useTypeormStakePoolProvider: true } } + }); + + testCli('expects a boolean', 'provider', { + args: ['--use-typeorm-stake-pool-provider', 'test'], + env: { USE_TYPEORM_STAKE_POOL_PROVIDER: 'test' }, + expectedError: + 'Provider server requires a valid Enables the TypeORM Stake Pool Provider program option. Expected: false, true' + }); + }); + + describe('required combinations', () => { + test('DB dependant services require DB connection configuration', () => + runCli('provider', { + env: { SERVICE_NAMES: 'network-info' }, + expectedError: + 'network-info requires the PostgreSQL Connection string or Postgres SRV service name, db, user and password program option', + notDump + })); + + test('cardano configuration dependant services require cardanoNodeConfigPath', () => + runCli('provider', { + env: { POSTGRES_CONNECTION_STRING_DB_SYNC, SERVICE_NAMES: 'network-info' }, + expectedError: 'network-info requires the Cardano node config path program option', + notDump + })); + }); }); });