@mojaloop/central-ledger 19.14.0 → 20.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.grype.yaml +3 -0
- package/.license-scanner.json +16 -0
- package/.ncurc.yaml +19 -13
- package/.nvmrc +1 -1
- package/.nycrc.yml +14 -2
- package/CHANGELOG.md +51 -0
- package/Dockerfile +13 -10
- package/README.md +39 -89
- package/audit-ci.jsonc +1 -1
- package/config/default.json +60 -3
- package/config/deprecated.default-settlement.json +160 -0
- package/docker/ml-api-adapter/default.json +28 -0
- package/docker-compose.yml +6 -0
- package/nodemon.json +6 -0
- package/package.json +128 -64
- package/{sbom-v19.13.2.csv → sbom-v20.0.0.csv} +886 -736
- package/scripts/_wait4_all.js +5 -5
- package/scripts/transferSettlementTemp/interchangeFeeCalculation.js +51 -0
- package/src/api/index.ts +30 -0
- package/src/api/interface/swagger.json +2249 -2054
- package/src/api/participants/routes.js +1 -1
- package/src/api/root/handler.int.ts +55 -0
- package/src/api/routes.js +7 -6
- package/src/api/server.ts +27 -0
- package/src/api/server.unit.ts +16 -0
- package/src/api/settlementModels/routes.js +4 -4
- package/src/domain/participant/index.d.ts +47 -0
- package/src/domain/participant/index.int.ts +724 -0
- package/src/domain/participant/index.js +4 -5
- package/src/domain/position/fx-prepare.js +1 -1
- package/src/domain/position/fx-timeout-reserved.js +6 -0
- package/src/domain/position/prepare.js +1 -1
- package/src/handlers/admin/handler.js +1 -1
- package/src/handlers/index.js +30 -0
- package/src/handlers/positions/handler.js +0 -2
- package/src/handlers/positions/handlerBatch.js +5 -5
- package/src/handlers/register.js +44 -29
- package/src/handlers/timeouts/handler.js +1 -4
- package/src/handlers/transfers/fx-abort.int.ts +209 -0
- package/src/handlers/transfers/fx-handlers.int.ts +275 -0
- package/src/handlers/transfers/fx-timeout.int.ts +284 -0
- package/src/handlers/transfers/handler.js +1 -3
- package/src/handlers/transfers/handlers.int.ts +538 -0
- package/src/handlers/transfers/prepare.int.ts +1283 -0
- package/src/handlers/transfers/prepare.js +2 -2
- package/src/knexfile.js +19 -0
- package/src/lib/cache.js +1 -1
- package/src/lib/config/index.ts +60 -0
- package/src/lib/config/resolver.ts +231 -0
- package/src/lib/config/resolver.unit.ts +72 -0
- package/src/lib/config/types.ts +372 -0
- package/src/lib/config/util.ts +523 -0
- package/src/lib/config/util.unit.ts +825 -0
- package/src/lib/migrator.js +3 -3
- package/src/lib/proxyCache.js +3 -3
- package/{migrations → src/migrations}/100100_event.js +1 -1
- package/{migrations → src/migrations}/110100_contactType.js +1 -1
- package/{migrations → src/migrations}/110200_currency.js +1 -1
- package/{migrations → src/migrations}/110201_currency-scale.js +1 -1
- package/{migrations → src/migrations}/110300_endpointType.js +1 -1
- package/{migrations → src/migrations}/110400_ledgerEntryType.js +1 -1
- package/{migrations → src/migrations}/110450_ledgerAccountType.js +1 -1
- package/{migrations → src/migrations}/110500_participantLimitType.js +1 -1
- package/{migrations → src/migrations}/110600_transferParticipantRoleType.js +1 -1
- package/{migrations → src/migrations}/110700_transferState.js +1 -1
- package/{migrations → src/migrations}/110900_settlementState.js +1 -1
- package/{migrations → src/migrations}/112000_bulkTransferState.js +1 -1
- package/{migrations → src/migrations}/112100_bulkProcessingState.js +1 -1
- package/{migrations → src/migrations}/200100_participant.js +1 -1
- package/{migrations → src/migrations}/200200_participantContact.js +1 -1
- package/{migrations → src/migrations}/200300_participantEndpoint.js +1 -1
- package/{migrations → src/migrations}/200400_participantParty.js +1 -1
- package/{migrations → src/migrations}/200600_token.js +1 -1
- package/{migrations → src/migrations}/200601_token-indexes.js +1 -1
- package/{migrations → src/migrations}/300100_transferDuplicateCheck.js +2 -2
- package/{migrations → src/migrations}/300150_bulkTransferDuplicateCheck.js +2 -2
- package/{migrations → src/migrations}/300200_transfer.js +2 -2
- package/{migrations → src/migrations}/300202_transfer-decimal184.js +1 -1
- package/{migrations → src/migrations}/300250_bulkTransfer.js +2 -3
- package/{migrations → src/migrations}/300275_bulkTransferAssociation.js +2 -3
- package/{migrations → src/migrations}/300300_ilpPacket.js +1 -1
- package/{migrations → src/migrations}/300400_transferStateChange.js +1 -1
- package/{migrations → src/migrations}/300450_bulkTransferStateChange.js +1 -1
- package/{migrations → src/migrations}/300500_segment.js +1 -1
- package/{migrations → src/migrations}/310100_participantCurrency.js +1 -1
- package/{migrations → src/migrations}/310200_transferParticipant.js +1 -1
- package/{migrations → src/migrations}/310202_transferParticipant-decimal184.js +1 -1
- package/{migrations → src/migrations}/310204_transferParticipant-participantId.js +2 -2
- package/{migrations → src/migrations}/310300_participantPosition.js +1 -1
- package/{migrations → src/migrations}/310302_participantPosition-decimal184.js +1 -1
- package/{migrations → src/migrations}/310400_participantPositionChange.js +1 -1
- package/{migrations → src/migrations}/310402_participantPositionChange-decimal184.js +1 -1
- package/{migrations → src/migrations}/310403_participantPositionChange-participantCurrencyId.js +2 -2
- package/{migrations → src/migrations}/310404_participantPositionChange-change.js +2 -2
- package/{migrations → src/migrations}/310500_participantLimit.js +1 -1
- package/{migrations → src/migrations}/310502_participantLimit-decimal184.js +1 -1
- package/{migrations → src/migrations}/400100_settlementWindow.js +1 -1
- package/{migrations → src/migrations}/400200_settlement.js +1 -1
- package/{migrations → src/migrations}/400300_settlementSettlementWindow.js +1 -1
- package/{migrations → src/migrations}/400301_settlementSettlementWindow-indexes.js +0 -1
- package/{migrations → src/migrations}/400400_settlementWindowStateChange.js +1 -1
- package/{migrations → src/migrations}/400500_settlementTransferParticipant.js +1 -1
- package/{migrations → src/migrations}/400502_settlementTransferParticipant-decimal184.js +1 -1
- package/{migrations → src/migrations}/400600_settlementParticipantCurrency.js +1 -1
- package/{migrations → src/migrations}/400602_settlementParticipantCurrency-decimal184.js +1 -1
- package/{migrations → src/migrations}/400700_settlementParticipantCurrencyStateChange.js +1 -1
- package/{migrations → src/migrations}/400800_settlementStateChange.js +1 -1
- package/{migrations → src/migrations}/400900_settlementWindowContent.js +1 -1
- package/{migrations → src/migrations}/401000_settlementWindowContentStateChange.js +1 -1
- package/{migrations → src/migrations}/401001_settlementWindowContentStateChange-indexes.js +6 -6
- package/{migrations → src/migrations}/401200_settlementContentAggregation.js +1 -1
- package/{migrations → src/migrations}/401300_settlementModel.js +1 -1
- package/{migrations → src/migrations}/410100_transferFulfilmentDuplicateCheck.js +1 -1
- package/{migrations → src/migrations}/410102_transferFulfilmentDuplicateCheck-dropForeignTransferId.js +1 -1
- package/{migrations → src/migrations}/410103_transferFulfilmentDuplicateCheck-hashNullable.js +1 -1
- package/{migrations → src/migrations}/410200_transferFulfilment.js +1 -1
- package/{migrations → src/migrations}/410202_transferFulfilment-ilpFulfilment.js +1 -1
- package/{migrations → src/migrations}/410203_transferFulfilment-foreignTransferFulfilmentId.js +1 -1
- package/{migrations → src/migrations}/410240_bulkTransferFulfilmentDuplicateCheck.js +1 -1
- package/{migrations → src/migrations}/410250_bulkTransferFulfilment.js +1 -1
- package/{migrations → src/migrations}/410300_transferExtension.js +1 -1
- package/{migrations → src/migrations}/410302_transferExtension-transferErrorId.js +1 -1
- package/{migrations → src/migrations}/410350_bulkTransferExtension.js +1 -1
- package/{migrations → src/migrations}/410400_transferTimeout.js +1 -1
- package/{migrations → src/migrations}/410450_transferErrorDuplicateCheck.js +1 -1
- package/{migrations → src/migrations}/410500_transferError.js +1 -1
- package/{migrations → src/migrations}/410502_transferError-foreignTransferErrorDuplicateCheckId.js +1 -1
- package/{migrations → src/migrations}/410550_bulkTransferError.js +1 -1
- package/{migrations → src/migrations}/410600_expiringTransfer.js +1 -1
- package/{migrations → src/migrations}/500401_quote-hotfix-2719.js +8 -8
- package/{migrations → src/migrations}/500501_feature-fixSubIdRef.js +12 -12
- package/{migrations → src/migrations}/500601_party-2480.js +3 -3
- package/{migrations → src/migrations}/501001_quoteExtension-quote-service-174.js +1 -2
- package/{migrations → src/migrations}/501002_quoteExtension-2522.js +3 -3
- package/{migrations → src/migrations}/600010_fxTransferType.js +1 -1
- package/{migrations → src/migrations}/600012_fxParticipantCurrencyType.js +1 -1
- package/{migrations → src/migrations}/600100_fxTransferDuplicateCheck.js +2 -2
- package/{migrations → src/migrations}/600110_fxTransferErrorDuplicateCheck.js.js +1 -1
- package/{migrations → src/migrations}/600200_fxTransfer.js +2 -2
- package/{migrations → src/migrations}/600400_fxTransferStateChange.js +1 -1
- package/{migrations → src/migrations}/600501_fxWatchList.js +2 -2
- package/{migrations → src/migrations}/600600_fxTransferFulfilmentDuplicateCheck.js +1 -1
- package/{migrations → src/migrations}/600700_fxTransferFulfilment.js +1 -1
- package/{migrations → src/migrations}/600800_fxTransferExtension.js +1 -1
- package/{migrations → src/migrations}/601400_fxTransferTimeout.js +1 -1
- package/{migrations → src/migrations}/601500_fxTransferError.js +1 -1
- package/{migrations → src/migrations}/610200_fxTransferParticipant.js +1 -1
- package/{migrations → src/migrations}/610202_fxTransferParticipant-participantId.js +2 -2
- package/{migrations → src/migrations}/610403_participantPositionChange-fxTransfer.js +1 -1
- package/{migrations → src/migrations}/800100_quotePartyView.js +1 -1
- package/{migrations → src/migrations}/800101_feature-fixSubIdRef.js +1 -1
- package/{migrations → src/migrations}/800200_quoteView.js +1 -1
- package/{migrations → src/migrations}/800300_quoteResponseView.js +1 -1
- package/{migrations → src/migrations}/900100_foreign-keys.js +18 -18
- package/{migrations → src/migrations}/900101_foreign-keys.js +6 -6
- package/{migrations → src/migrations}/910100_feature904DDL.js +7 -7
- package/{migrations → src/migrations}/910101_feature904DataMigration.js +1 -1
- package/{migrations → src/migrations}/910102_feature949DataMigration.js +1 -1
- package/{migrations → src/migrations}/910103_feature1093SettlementByCurrency.js +3 -3
- package/{migrations → src/migrations}/940101_quotePartyIdInfoExtension-2522.js +25 -25
- package/{migrations → src/migrations}/950101_transferParticipantStateChange.js +2 -2
- package/{migrations → src/migrations}/950102_settlementModel-adjustPosition.js +2 -2
- package/{migrations → src/migrations}/950103_dropTransferParticipantStateChange.js +3 -3
- package/{migrations → src/migrations}/950104_settlementModel-settlementAccountTypeId.js +3 -3
- package/src/migrations/950107_settlementWindowContent_foreign_settlementModel.js +63 -0
- package/{migrations → src/migrations}/950108_participantProxy.js +1 -2
- package/{migrations → src/migrations}/950120_fxQuoteConversionTerms_sourceAmount_nullable.js +1 -1
- package/{migrations → src/migrations}/960100_create_externalParticipant.js +2 -2
- package/{migrations → src/migrations}/960110_alter_transferParticipant__addFiled_externalParticipantId.js +2 -2
- package/{migrations → src/migrations}/961001_transferForwarded.js +1 -1
- package/{migrations → src/migrations}/961002_fxTransferForwarded.js +1 -1
- package/src/models/participant/externalParticipant.int.ts +85 -0
- package/src/models/participant/facade.js +4 -2
- package/src/models/participant/participantCurrencyCached.js +1 -1
- package/src/models/position/facade.js +8 -2
- package/src/models/transfer/facade.int.ts +139 -0
- package/src/models/transfer/facade.js +8 -3
- package/src/models/transfer/transferDuplicateCheck.js +2 -2
- package/src/models/transfer/transferError.int.ts +112 -0
- package/src/models/transfer/transferExtension.int.ts +137 -0
- package/src/models/transfer/transferStateChange.int.ts +117 -0
- package/{seeds → src/seeds}/participant.js +1 -1
- package/{seeds → src/seeds}/settlementInterchange.js +1 -0
- package/src/settlement/api/handlerRoutes.js +71 -0
- package/src/settlement/api/handlers/health.js +10 -0
- package/src/settlement/api/handlers/index.js +57 -0
- package/src/settlement/api/handlers/settlementWindows/{id}.js +103 -0
- package/src/settlement/api/handlers/settlementWindows.js +77 -0
- package/src/settlement/api/handlers/settlements/{id}.js +133 -0
- package/src/settlement/api/handlers/settlements/{sid}/participants/{pid}/accounts/{aid}.js +127 -0
- package/src/settlement/api/handlers/settlements/{sid}/participants/{pid}.js +127 -0
- package/src/settlement/api/handlers/settlements.js +114 -0
- package/src/settlement/api/openapiRouting.js +87 -0
- package/src/settlement/api/routes.js +176 -0
- package/src/settlement/domain/rules/index.js +46 -0
- package/src/settlement/domain/settlement/index.js +405 -0
- package/src/settlement/domain/settlementWindow/index.js +117 -0
- package/src/settlement/domain/transactions/index.js +83 -0
- package/src/settlement/domain/transferSettlement/index.js +57 -0
- package/src/settlement/handlers/deferredSettlement/handler.js +180 -0
- package/src/settlement/handlers/grossSettlement/handler.js +180 -0
- package/src/settlement/handlers/index.js +101 -0
- package/src/settlement/handlers/register.js +92 -0
- package/src/settlement/handlers/rules/handler.js +164 -0
- package/src/settlement/interface/swagger-handler.json +34 -0
- package/src/settlement/interface/swagger.json +1560 -0
- package/src/settlement/lib/db.js +32 -0
- package/src/settlement/lib/kafka/index.js +41 -0
- package/src/settlement/lib/kafka/producer.js +142 -0
- package/src/settlement/lib/scriptEngine.js +99 -0
- package/src/settlement/lib/scriptsLoader.js +175 -0
- package/src/settlement/lib/utility.js +243 -0
- package/src/settlement/models/ilpPackets/ilpPacket.js +42 -0
- package/src/settlement/models/lib/enums.js +192 -0
- package/src/settlement/models/misc/migrationLock.js +53 -0
- package/src/settlement/models/settlement/facade.js +1652 -0
- package/src/settlement/models/settlement/index.js +55 -0
- package/src/settlement/models/settlement/participantCurrency.js +48 -0
- package/src/settlement/models/settlement/settlement.js +49 -0
- package/src/settlement/models/settlement/settlementModel.js +44 -0
- package/src/settlement/models/settlement/settlementParticipantCurrency.js +61 -0
- package/src/settlement/models/settlement/settlementStateChange.js +44 -0
- package/src/settlement/models/settlement/settlementTransferParticipant.js +46 -0
- package/src/settlement/models/settlementWindow/facade.js +345 -0
- package/src/settlement/models/settlementWindow/index.js +47 -0
- package/src/settlement/models/settlementWindow/settlementWindowStateChange.js +55 -0
- package/src/settlement/models/settlementWindowContent/facade.js +79 -0
- package/src/settlement/models/settlementWindowContent/index.js +40 -0
- package/src/settlement/models/settlementWindowContent/settlementWindowContentStateChange.js +53 -0
- package/src/settlement/models/transferSettlement/facade.js +363 -0
- package/src/settlement/models/transferSettlement/index.js +42 -0
- package/src/settlement/shared/logger/index.js +7 -0
- package/src/settlement/shared/plugins.js +88 -0
- package/src/settlement/shared/setup.js +224 -0
- package/src/settlement/utils/truthyProperty.js +37 -0
- package/src/shared/legacyUtilShim.js +20 -0
- package/src/shared/plugins.js +3 -3
- package/src/shared/results.ts +22 -0
- package/src/shared/results.unit.ts +38 -0
- package/src/shared/setup.js +14 -1
- package/src/testing/api-helpers.ts +1222 -0
- package/src/testing/exec-async.ts +67 -0
- package/src/testing/harness.snapshot.sql +3895 -0
- package/src/testing/harness.test.ts +15 -0
- package/src/testing/harness.ts +1160 -0
- package/src/testing/kafka.ts +221 -0
- package/src/testing/prng.ts +82 -0
- package/src/testing/rdkafka.test.ts +263 -0
- package/src/testing/run.ts +559 -0
- package/src/testing/snapshot.test.ts +92 -0
- package/src/testing/snapshot.ts +549 -0
- package/src/testing/tap-stream.ts +164 -0
- package/src/testing/types.ts +95 -0
- package/src/testing/util.ts +333 -0
- package/src/types/central-services-error-handling.d.ts +72 -0
- package/src/types/central-services-stream.d.ts +148 -0
- package/test-integration.Dockerfile +0 -2
- package/test.Dockerfile +0 -2
- package/tsconfig.json +40 -0
- package/config/knexfile.js +0 -19
- package/migrations/950107_settlementWindowContent_foreign_settlementModel.js +0 -63
- package/src/api/index.js +0 -16
- package/src/lib/config.js +0 -81
- /package/{migrations → src/migrations}/110101_contactType-indexes.js +0 -0
- /package/{migrations → src/migrations}/110301_endpointType-indexes.js +0 -0
- /package/{migrations → src/migrations}/110401_ledgerEntryType-indexes.js +0 -0
- /package/{migrations → src/migrations}/110451_ledgerAccountType-indexes.js +0 -0
- /package/{migrations → src/migrations}/110501_participantLimitType-indexes.js +0 -0
- /package/{migrations → src/migrations}/110601_transferParticipantRoleType-indexes.js +0 -0
- /package/{migrations → src/migrations}/110800_settlementWindowState.js +0 -0
- /package/{migrations → src/migrations}/111000_amountType.js +0 -0
- /package/{migrations → src/migrations}/111001_amountType-indexes.js +0 -0
- /package/{migrations → src/migrations}/111100_balanceOfPayments.js +0 -0
- /package/{migrations → src/migrations}/111101_balanceOfPayments-indexes.js +0 -0
- /package/{migrations → src/migrations}/111200_partyIdentifierType.js +0 -0
- /package/{migrations → src/migrations}/111201_partyIdentifierType-indexes.js +0 -0
- /package/{migrations → src/migrations}/111300_partyType.js +0 -0
- /package/{migrations → src/migrations}/111301_partyType-indexes.js +0 -0
- /package/{migrations → src/migrations}/111400_quoteDuplicateCheck.js +0 -0
- /package/{migrations → src/migrations}/111500_transactionInitiator.js +0 -0
- /package/{migrations → src/migrations}/111501_transactionInitiator-indexes.js +0 -0
- /package/{migrations → src/migrations}/111600_transactionInitiatorType.js +0 -0
- /package/{migrations → src/migrations}/111601_transactionInitiatorType-indexes.js +0 -0
- /package/{migrations → src/migrations}/111700_settlementDelay.js +0 -0
- /package/{migrations → src/migrations}/111701_settlementDelay-indexes.js +0 -0
- /package/{migrations → src/migrations}/111800_settlementInterchange.js +0 -0
- /package/{migrations → src/migrations}/111801_settlementInterchange-indexes.js +0 -0
- /package/{migrations → src/migrations}/111900_settlementGranularity.js +0 -0
- /package/{migrations → src/migrations}/111901_settlementGranularity-indexes.js +0 -0
- /package/{migrations → src/migrations}/112101_bulkProcessingState-indexes.js +0 -0
- /package/{migrations → src/migrations}/200101_participant-indexes.js +0 -0
- /package/{migrations → src/migrations}/200201_participantContact-indexes.js +0 -0
- /package/{migrations → src/migrations}/200301_participantEndpoint-indexes.js +0 -0
- /package/{migrations → src/migrations}/200401_participantParty-indexes.js +0 -0
- /package/{migrations → src/migrations}/300201_transfer-indexes.js +0 -0
- /package/{migrations → src/migrations}/300251_bulkTransfer-indexes.js +0 -0
- /package/{migrations → src/migrations}/300276_bulkTransferAssociation-indexes.js +0 -0
- /package/{migrations → src/migrations}/300401_transferStateChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/300451_bulkTransferStateChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/300501_segment-indexes.js +0 -0
- /package/{migrations → src/migrations}/310101_participantCurrency-indexes.js +0 -0
- /package/{migrations → src/migrations}/310201_transferParticipant-indexes.js +0 -0
- /package/{migrations → src/migrations}/310203_transferParticipant-indexes.js +0 -0
- /package/{migrations → src/migrations}/310301_participantPosition-indexes.js +0 -0
- /package/{migrations → src/migrations}/310401_participantPositionChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/310405_participantPositionChange-positionChange.js +0 -0
- /package/{migrations → src/migrations}/310406_participantPositionChange-positionChange-revert.js +0 -0
- /package/{migrations → src/migrations}/310501_participantLimit-indexes.js +0 -0
- /package/{migrations → src/migrations}/310503_participantLimit-indexes-composite.js +0 -0
- /package/{migrations → src/migrations}/400401_settlmentWindowStateChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/400501_settlementTransferParticipant-indexes.js +0 -0
- /package/{migrations → src/migrations}/400601_settlementParticipantCurrency-indexes.js +0 -0
- /package/{migrations → src/migrations}/400701_settlementParticipantCurrencyStateChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/400801_settlementStateChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/401002_settlementWindowContent-indexes.js +0 -0
- /package/{migrations → src/migrations}/401201_settlementContentAggregation-indexes.js +0 -0
- /package/{migrations → src/migrations}/401301_settlementModel-indexes.js +0 -0
- /package/{migrations → src/migrations}/410101_transferFulfilmentDuplicateCheck-indexes.js +0 -0
- /package/{migrations → src/migrations}/410201_transferFulfilment-indexes.js +0 -0
- /package/{migrations → src/migrations}/410301_transferExtension-indexes.js +0 -0
- /package/{migrations → src/migrations}/410351_bulkTransferExtension-indexes.js +0 -0
- /package/{migrations → src/migrations}/410401_transferTimeout-indexes.js +0 -0
- /package/{migrations → src/migrations}/410501_transferError-indexes.js +0 -0
- /package/{migrations → src/migrations}/410551_bulkTransferError-indexes.js +0 -0
- /package/{migrations → src/migrations}/410601_expiringTransfer-indexes.js +0 -0
- /package/{migrations → src/migrations}/500100_transactionReference.js +0 -0
- /package/{migrations → src/migrations}/500101_transactionReference-indexes.js +0 -0
- /package/{migrations → src/migrations}/500200_transactionScenario.js +0 -0
- /package/{migrations → src/migrations}/500201_transactionScenario-indexes.js +0 -0
- /package/{migrations → src/migrations}/500300_transactionSubScenario.js +0 -0
- /package/{migrations → src/migrations}/500400_quote.js +0 -0
- /package/{migrations → src/migrations}/500500_quoteParty.js +0 -0
- /package/{migrations → src/migrations}/500600_party.js +0 -0
- /package/{migrations → src/migrations}/500700_quoteResponse.js +0 -0
- /package/{migrations → src/migrations}/500800_quoteResponseIlpPacket.js +0 -0
- /package/{migrations → src/migrations}/500900_geoCode.js +0 -0
- /package/{migrations → src/migrations}/501000_quoteExtension.js +0 -0
- /package/{migrations → src/migrations}/501100_quoteResponseDuplicateCheck.js +0 -0
- /package/{migrations → src/migrations}/501200_quoteError.js +0 -0
- /package/{migrations → src/migrations}/501300_transferRules.js +0 -0
- /package/{migrations → src/migrations}/600011_fxTransferType-indexes.js +0 -0
- /package/{migrations → src/migrations}/600013_fxParticipantCurrencyType-indexes.js +0 -0
- /package/{migrations → src/migrations}/600201_fxTransfer-indexes.js +0 -0
- /package/{migrations → src/migrations}/600401_fxTransferStateChange-indexes.js +0 -0
- /package/{migrations → src/migrations}/600502_fxWatchList-indexes.js +0 -0
- /package/{migrations → src/migrations}/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +0 -0
- /package/{migrations → src/migrations}/600701_fxTransferFulfilment-indexes.js +0 -0
- /package/{migrations → src/migrations}/601401_fxTransferTimeout-indexes.js +0 -0
- /package/{migrations → src/migrations}/601501_fxTransferError-indexes.js +0 -0
- /package/{migrations → src/migrations}/610201_fxTransferParticipant-indexes.js +0 -0
- /package/{migrations → src/migrations}/910102_hotfix954.js +0 -0
- /package/{migrations → src/migrations}/940100_quotePartyIdInfoExtension.js +0 -0
- /package/{migrations → src/migrations}/950100_settlementContentAggregation-decimal184.js +0 -0
- /package/{migrations → src/migrations}/950106_unique_settlement_model_ledger_account_currency.js +0 -0
- /package/{migrations → src/migrations}/950109_fxQuote.js +0 -0
- /package/{migrations → src/migrations}/950110_fxQuoteResponse.js +0 -0
- /package/{migrations → src/migrations}/950111_fxQuoteError.js +0 -0
- /package/{migrations → src/migrations}/950113_fxQuoteDuplicateCheck.js +0 -0
- /package/{migrations → src/migrations}/950114_fxQuoteResponseDuplicateCheck.js +0 -0
- /package/{migrations → src/migrations}/950115_fxQuoteConversionTerms.js +0 -0
- /package/{migrations → src/migrations}/950116_fxQuoteConversionTermsExtension.js +0 -0
- /package/{migrations → src/migrations}/950117_fxQuoteResponseConversionTerms.js +0 -0
- /package/{migrations → src/migrations}/950118_fxQuoteResponseConversionTermsExtension.js +0 -0
- /package/{migrations → src/migrations}/950119_fxCharge.js +0 -0
- /package/{migrations → src/migrations}/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +0 -0
- /package/{migrations → src/migrations}/960112_fxTransfer-indexes2.js +0 -0
- /package/{migrations → src/migrations}/960113_fxTransferExtension-indexes.js +0 -0
- /package/{migrations → src/migrations}/960114_fxTransferTimeout-indexes2.js +0 -0
- /package/{migrations → src/migrations}/961000_participantEndpoint_unique.js +0 -0
- /package/{migrations → src/migrations}/migration.template +0 -0
- /package/{seeds → src/seeds}/amountType.js +0 -0
- /package/{seeds → src/seeds}/balanceOfPayments.js +0 -0
- /package/{seeds → src/seeds}/bulkProcessingState.js +0 -0
- /package/{seeds → src/seeds}/bulkTransferState.js +0 -0
- /package/{seeds → src/seeds}/currency.js +0 -0
- /package/{seeds → src/seeds}/endpointType.js +0 -0
- /package/{seeds → src/seeds}/fxParticipantCurrencyType.js +0 -0
- /package/{seeds → src/seeds}/fxTransferType.js +0 -0
- /package/{seeds → src/seeds}/ledgerAccountType.js +0 -0
- /package/{seeds → src/seeds}/ledgerEntryType.js +0 -0
- /package/{seeds → src/seeds}/participantLimitType.js +0 -0
- /package/{seeds → src/seeds}/partyIdentifierType.js +0 -0
- /package/{seeds → src/seeds}/partyType.js +0 -0
- /package/{seeds → src/seeds}/settlementDelay.js +0 -0
- /package/{seeds → src/seeds}/settlementGranularity.js +0 -0
- /package/{seeds → src/seeds}/settlementState.js +0 -0
- /package/{seeds → src/seeds}/settlementWindow1State.js +0 -0
- /package/{seeds → src/seeds}/settlementWindow2Open.js +0 -0
- /package/{seeds → src/seeds}/transactionInitiator.js +0 -0
- /package/{seeds → src/seeds}/transactionInitiatorType.js +0 -0
- /package/{seeds → src/seeds}/transactionScenario.js +0 -0
- /package/{seeds → src/seeds}/transferParticipantRoleType.js +0 -0
- /package/{seeds → src/seeds}/transferState.js +0 -0
- /package/{seeds → src/seeds}/z1000_settlementModel-deprecated.js- +0 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
/*****
|
|
2
|
+
License
|
|
3
|
+
--------------
|
|
4
|
+
Copyright © 2020-2026 Mojaloop Foundation
|
|
5
|
+
The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
10
|
+
|
|
11
|
+
Contributors
|
|
12
|
+
--------------
|
|
13
|
+
This is the official list of the Mojaloop project contributors for this file.
|
|
14
|
+
Names of the original copyright holders (individuals or organizations)
|
|
15
|
+
should be listed with a '*' in the first column. People who have
|
|
16
|
+
contributed from an organization can be listed under the organization
|
|
17
|
+
that actually holds the copyright for their contributions (see the
|
|
18
|
+
Mojaloop Foundation for an example). Those individuals should have
|
|
19
|
+
their names indented and be marked with a '-'. Email address can be added
|
|
20
|
+
optionally within square brackets <email>.
|
|
21
|
+
|
|
22
|
+
* TigerBeetle
|
|
23
|
+
- Lewis Daly <lewis@tigerbeetle.com>
|
|
24
|
+
--------------
|
|
25
|
+
|
|
26
|
+
******/
|
|
27
|
+
|
|
28
|
+
// Unfortunately each Winston logger created by @mojaloop/central-services-logger registers it's own
|
|
29
|
+
// uncaughtException handler. There's no easy way to pass through a common logger around, so we set
|
|
30
|
+
// the maxListeners to ~13 to avoid the `MaxListenersExceededWarning`.
|
|
31
|
+
process.setMaxListeners(13)
|
|
32
|
+
|
|
33
|
+
// Disable the Event SDK logs globally.
|
|
34
|
+
const EventSdkConfig = require('@mojaloop/event-sdk/dist/lib/config')
|
|
35
|
+
EventSdkConfig.default.EVENT_LOGGER_LOG_FILTER = ''
|
|
36
|
+
const EventSdk = require('@mojaloop/event-sdk')
|
|
37
|
+
const span = EventSdk.Tracer.createSpan('thing')
|
|
38
|
+
span.audit('Hopefully we dont see this!')
|
|
39
|
+
|
|
40
|
+
import assert from "assert"
|
|
41
|
+
import { execAsync } from "./exec-async"
|
|
42
|
+
|
|
43
|
+
import Cache from '../lib/cache'
|
|
44
|
+
import { makeConfig } from "../lib/config/resolver"
|
|
45
|
+
import { deepMerge } from "../lib/config/util"
|
|
46
|
+
import Db from '../lib/db'
|
|
47
|
+
import Enums from '../lib/enumCached'
|
|
48
|
+
import { Enum } from '@mojaloop/central-services-shared'
|
|
49
|
+
|
|
50
|
+
const Metrics = require('@mojaloop/central-services-metrics')
|
|
51
|
+
|
|
52
|
+
// Note: we _must_ use `require()` here, otherwise the global Producer and Consumers are imported
|
|
53
|
+
// as empty objects.
|
|
54
|
+
const KafkaProducer = require('@mojaloop/central-services-stream').Util.Producer
|
|
55
|
+
const KafkaConsumer = require('@mojaloop/central-services-stream').Util.Consumer
|
|
56
|
+
const Utility = require('@mojaloop/central-services-shared').Util.Kafka
|
|
57
|
+
|
|
58
|
+
import AdminHandler from '../handlers/admin/handler'
|
|
59
|
+
import PositionHandler from '../handlers/positions/handler'
|
|
60
|
+
import PositionBatchHandler from '../handlers/positions/handlerBatch'
|
|
61
|
+
|
|
62
|
+
import ParticipantCached from '../models/participant/participantCached'
|
|
63
|
+
import ParticipantCurrencyCached from '../models/participant/participantCurrencyCached'
|
|
64
|
+
import ParticipantLimitCached from '../models/participant/participantLimitCached'
|
|
65
|
+
import ProxyCache from '../lib/proxyCache'
|
|
66
|
+
const BatchPositionModelCached = require('../models/position/batchCached')
|
|
67
|
+
const ExternalParticipantCached = require('../models/participant/externalParticipantCached')
|
|
68
|
+
|
|
69
|
+
import Logger from "@mojaloop/central-services-logger"
|
|
70
|
+
import knex from 'knex'
|
|
71
|
+
import { ApplicationConfig, overrideForTesting, RecursivePartial, resetOverride } from "../lib/config"
|
|
72
|
+
import { randomAvailablePort } from "./util"
|
|
73
|
+
import { Consumer } from "./kafka"
|
|
74
|
+
import { Message } from "node-rdkafka"
|
|
75
|
+
|
|
76
|
+
const logger = Logger.child({ scope: 'harness' })
|
|
77
|
+
|
|
78
|
+
export interface HarnessOptions {
|
|
79
|
+
/**
|
|
80
|
+
* A unique id used in naming and logs to disambiguate between multiple harness runs.
|
|
81
|
+
*/
|
|
82
|
+
id: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @class Harness
|
|
87
|
+
*
|
|
88
|
+
* @description Testing harness used to manage the dependencies for running e2e and integration.
|
|
89
|
+
* ```
|
|
90
|
+
* import Harness from './harness
|
|
91
|
+
* const harness = harness.getInstance()
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Then, in the before() and after() hooks:
|
|
95
|
+
* ```
|
|
96
|
+
* before(async () => {
|
|
97
|
+
* await harness.up()
|
|
98
|
+
* await harness.setupGlobals()
|
|
99
|
+
* })
|
|
100
|
+
*
|
|
101
|
+
* after(async () => {
|
|
102
|
+
* await harness.teardownGlobals()
|
|
103
|
+
* await harness.down()
|
|
104
|
+
* })
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* Depends on:
|
|
108
|
+
* 1. Docker host for `mysql`, `kafka`, `redis` containers.
|
|
109
|
+
* 2. Filesystem access for `tigerbeetle` binary.
|
|
110
|
+
*
|
|
111
|
+
* The goal is that the integration tests are responsible for setting up and tearing down their
|
|
112
|
+
* own environment.
|
|
113
|
+
*/
|
|
114
|
+
export default class Harness {
|
|
115
|
+
private static instance: Harness | null = null;
|
|
116
|
+
private options: HarnessOptions
|
|
117
|
+
private dependencyRedpanda: Redpanda
|
|
118
|
+
private dependencyMySql: MySql
|
|
119
|
+
private dependencyRedis: Redis
|
|
120
|
+
private applicationConfig: ApplicationConfig | null = null;
|
|
121
|
+
private omniConsumer: Consumer | null = null;
|
|
122
|
+
private messageQueue: Array<MojaloopKafkaMessage> = []
|
|
123
|
+
private positionHandlerType: 'NON_BATCH' | 'BATCH' = 'NON_BATCH'
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
*
|
|
127
|
+
* There's 2 types of enums:
|
|
128
|
+
* 1. Statically defined enums in types.
|
|
129
|
+
* 2. Enums we need get from the database and pass around for some reason (I really don't know
|
|
130
|
+
* _why_).
|
|
131
|
+
*
|
|
132
|
+
* We keep a reference to #2 here for convenience.
|
|
133
|
+
*/
|
|
134
|
+
private _enums: any = null;
|
|
135
|
+
|
|
136
|
+
private constructor(options: HarnessOptions) {
|
|
137
|
+
this.options = options
|
|
138
|
+
|
|
139
|
+
this.dependencyRedpanda = new Redpanda({
|
|
140
|
+
harnessId: this.options.id
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
this.dependencyMySql = new MySql({
|
|
144
|
+
harnessId: this.options.id,
|
|
145
|
+
databaseName: 'central_ledger',
|
|
146
|
+
migration: {
|
|
147
|
+
type: 'sql',
|
|
148
|
+
sqlFilePath: './src/testing/harness.snapshot.sql',
|
|
149
|
+
|
|
150
|
+
// Uncomment below to update the harness.snapshot.sql file.
|
|
151
|
+
// You'll want to do this after adding new migrations.
|
|
152
|
+
// type: 'knex',
|
|
153
|
+
// updateSqlFilePath: './src/testing/harness.snapshot.sql'
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
this.dependencyRedis = new Redis({
|
|
158
|
+
harnessId: this.options.id
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public static getInstance(): Harness {
|
|
163
|
+
if (!Harness.instance) {
|
|
164
|
+
let run = Math.floor(Math.random() * (100000 - 10000) + 10000)
|
|
165
|
+
if (process.env.RUN) {
|
|
166
|
+
try {
|
|
167
|
+
run = Number.parseInt(process.env.RUN)
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
throw new Error(`Invalid test run id. process.env.RUN should be an integer.`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Harness.instance = new Harness({
|
|
174
|
+
id: run,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
return Harness.instance;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The BATCH position handler contains a bug which makes it unable to properly process
|
|
182
|
+
* aborted transfers. Since we will be soon removing the position handlers, we work around
|
|
183
|
+
* this bug by allowing the harness to specify whether or not to use the NON_BATCH or BATCH
|
|
184
|
+
* position handler for a specific test.
|
|
185
|
+
*
|
|
186
|
+
* The bug is related to cyrilResult not being set for a non-fx transfer, which causes the
|
|
187
|
+
* position rest message to not be fired.
|
|
188
|
+
*/
|
|
189
|
+
public async up(positionHandlerType: 'NON_BATCH' | 'BATCH' = 'NON_BATCH') {
|
|
190
|
+
const start = performance.now()
|
|
191
|
+
await this.checkEnvironment()
|
|
192
|
+
|
|
193
|
+
const results = await Promise.allSettled([
|
|
194
|
+
this.dependencyRedpanda.up(),
|
|
195
|
+
this.dependencyMySql.up(),
|
|
196
|
+
this.dependencyRedis.up(),
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
let failed = false
|
|
200
|
+
let failedCount = 0
|
|
201
|
+
let errorMessage = ''
|
|
202
|
+
results.forEach(result => {
|
|
203
|
+
if (result.status === 'rejected') {
|
|
204
|
+
failed = true
|
|
205
|
+
failedCount += 1
|
|
206
|
+
errorMessage += `${result.reason}\n`
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
if (failed) {
|
|
211
|
+
logger.error(`Harness.up() encountered: ${failedCount} failure${failed === 1 ? '' : 's'}`)
|
|
212
|
+
logger.error(`Detailed errors: ${errorMessage}`)
|
|
213
|
+
throw new Error(`Harness.up() encountered: ${failedCount} failures.`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const defaultConfig = makeConfig()
|
|
217
|
+
const kafkaBroker = `localhost:${this.dependencyRedpanda.connectionOptions.port}`
|
|
218
|
+
|
|
219
|
+
// Shortcut to make below more bearable.
|
|
220
|
+
const innerKafkaConfig = {
|
|
221
|
+
config: {
|
|
222
|
+
rdkafkaConf: {
|
|
223
|
+
"metadata.broker.list": kafkaBroker
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.positionHandlerType = positionHandlerType
|
|
229
|
+
let positionHandlerOverrides: Record<string, string> = {
|
|
230
|
+
PREPARE: 'topic-transfer-position',
|
|
231
|
+
COMMIT: 'topic-transfer-position',
|
|
232
|
+
RESERVE: 'topic-transfer-position',
|
|
233
|
+
TIMEOUT_RESERVED: 'topic-transfer-position',
|
|
234
|
+
ABORT: 'topic-transfer-position'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (positionHandlerType === 'BATCH') {
|
|
238
|
+
positionHandlerOverrides = {
|
|
239
|
+
PREPARE: 'topic-transfer-position-batch',
|
|
240
|
+
FX_PREPARE: 'topic-transfer-position-batch',
|
|
241
|
+
COMMIT: 'topic-transfer-position-batch',
|
|
242
|
+
RESERVE: 'topic-transfer-position-batch',
|
|
243
|
+
FX_RESERVE: 'topic-transfer-position-batch',
|
|
244
|
+
TIMEOUT_RESERVED: 'topic-transfer-position-batch',
|
|
245
|
+
FX_TIMEOUT_RESERVED: 'topic-transfer-position-batch',
|
|
246
|
+
ABORT: 'topic-transfer-position-batch',
|
|
247
|
+
FX_ABORT: 'topic-transfer-position-batch',
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Override the config based on the harness variables.
|
|
252
|
+
const override: RecursivePartial<ApplicationConfig> = {
|
|
253
|
+
PROXY_CACHE_CONFIG: {
|
|
254
|
+
enabled: true,
|
|
255
|
+
type: 'redis',
|
|
256
|
+
proxyConfig: {
|
|
257
|
+
host: 'localhost',
|
|
258
|
+
port: this.dependencyRedis.connectionOptions.port,
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
DATABASE: {
|
|
262
|
+
connection: {
|
|
263
|
+
user: 'root',
|
|
264
|
+
port: this.dependencyMySql.connectionOptions.port,
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
KAFKA_CONFIG: {
|
|
268
|
+
EVENT_TYPE_ACTION_TOPIC_MAP: {
|
|
269
|
+
POSITION: positionHandlerOverrides,
|
|
270
|
+
},
|
|
271
|
+
CONSUMER: {
|
|
272
|
+
ADMIN: {
|
|
273
|
+
TRANSFER: innerKafkaConfig,
|
|
274
|
+
},
|
|
275
|
+
TRANSFER: {
|
|
276
|
+
PREPARE: innerKafkaConfig,
|
|
277
|
+
FULFIL: innerKafkaConfig,
|
|
278
|
+
// TODO: remove this when we finish fusing the handlers.
|
|
279
|
+
POSITION: innerKafkaConfig,
|
|
280
|
+
POSITION_BATCH: innerKafkaConfig,
|
|
281
|
+
},
|
|
282
|
+
NOTIFICATION: {
|
|
283
|
+
EVENT: innerKafkaConfig
|
|
284
|
+
},
|
|
285
|
+
DEFERREDSETTLEMENT: {
|
|
286
|
+
CLOSE: innerKafkaConfig
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
PRODUCER: {
|
|
290
|
+
ADMIN: {
|
|
291
|
+
TRANSFER: innerKafkaConfig
|
|
292
|
+
},
|
|
293
|
+
TRANSFER: {
|
|
294
|
+
PREPARE: innerKafkaConfig,
|
|
295
|
+
FULFIL: innerKafkaConfig,
|
|
296
|
+
// TODO: remove this when we finish fusing the handlers.
|
|
297
|
+
POSITION: innerKafkaConfig,
|
|
298
|
+
},
|
|
299
|
+
NOTIFICATION: {
|
|
300
|
+
EVENT: innerKafkaConfig
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.applicationConfig = deepMerge(defaultConfig, override)
|
|
306
|
+
|
|
307
|
+
this.omniConsumer = new Consumer('omniconsumer', kafkaBroker)
|
|
308
|
+
await this.omniConsumer?.subscribe([
|
|
309
|
+
'topic-transfer-prepare',
|
|
310
|
+
'topic-transfer-fulfil',
|
|
311
|
+
'topic-transfer-position',
|
|
312
|
+
'topic-transfer-position-batch',
|
|
313
|
+
'topic-notification-event',
|
|
314
|
+
'topic-admin-transfer',
|
|
315
|
+
], (message) => {
|
|
316
|
+
if (message.topic === '__consumer_offsets') {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
this.appendMessageQueue(message)
|
|
322
|
+
} catch (err: any) {
|
|
323
|
+
console.error('Failed to append message to queue:\n')
|
|
324
|
+
console.error(err.message)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const end = performance.now()
|
|
329
|
+
logger.warn(`Harness.up() took: ${(end - start).toFixed(0)} ms.`)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
get mySqlConnectionOptions(): MySqlConnectionOptions {
|
|
333
|
+
return this.dependencyMySql.connectionOptions
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
get redpandaConnectionOptions(): RedpandaConnectionOptions {
|
|
337
|
+
return this.dependencyRedpanda.connectionOptions
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
get config(): ApplicationConfig {
|
|
341
|
+
assert(this.applicationConfig, 'No this.applicationConfig. Did you forget to call up()?')
|
|
342
|
+
return this.applicationConfig
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get enums(): any {
|
|
346
|
+
assert(this._enums, 'Enums not initalized. Did you forget to call setupGlobals()?')
|
|
347
|
+
return this._enums
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
get topicTransferPrepare(): { topicName: string } {
|
|
351
|
+
return Utility.createGeneralTopicConf(
|
|
352
|
+
this.config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE,
|
|
353
|
+
Enum.Events.Event.Type.TRANSFER,
|
|
354
|
+
Enum.Events.Event.Type.PREPARE
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get topicTransferFulfil(): { topicName: string } {
|
|
359
|
+
return Utility.createGeneralTopicConf(
|
|
360
|
+
this.config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE,
|
|
361
|
+
Enum.Events.Event.Type.TRANSFER,
|
|
362
|
+
Enum.Events.Event.Type.FULFIL,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private appendMessageQueue(message: Message): void {
|
|
367
|
+
assert(message)
|
|
368
|
+
assert(message.value)
|
|
369
|
+
const messageValueStr = message.value.toString()
|
|
370
|
+
const parsed = JSON.parse(messageValueStr)
|
|
371
|
+
assert(parsed)
|
|
372
|
+
|
|
373
|
+
assert(message.timestamp, 'message.timestamp is not defined.')
|
|
374
|
+
|
|
375
|
+
const mojaloopKafkaMessage = {
|
|
376
|
+
...message,
|
|
377
|
+
valueStr: messageValueStr,
|
|
378
|
+
valueParsed: parsed
|
|
379
|
+
} as MojaloopKafkaMessage
|
|
380
|
+
const lastMessage = this.peekMessageQueue()
|
|
381
|
+
const lastTimestamp = lastMessage ? lastMessage.timestamp : 0
|
|
382
|
+
// Even if the message is outdated, still append to the message queue instead of dropping it.
|
|
383
|
+
// This warning will help us catch tests that have overlapping messages.
|
|
384
|
+
if (mojaloopKafkaMessage.timestamp < lastTimestamp) {
|
|
385
|
+
const message = `appendMessageQueue() inserted a stale message with timestamp:\
|
|
386
|
+
${mojaloopKafkaMessage.timestamp} after message with timestamp: ${lastTimestamp}.`
|
|
387
|
+
logger.warn(message)
|
|
388
|
+
}
|
|
389
|
+
this.messageQueue.push(mojaloopKafkaMessage)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get the last message from the queue.
|
|
394
|
+
*/
|
|
395
|
+
private peekMessageQueue(): MojaloopKafkaMessage | undefined {
|
|
396
|
+
if (this.messageQueue.length === 0) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
return this.messageQueue.at(-1)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Legacy central-ledger code uses a lot of globals everywhere. This is a convenience function
|
|
404
|
+
* so we don't have to call this at the start of each test.
|
|
405
|
+
*/
|
|
406
|
+
public async setupGlobals(): Promise<void> {
|
|
407
|
+
logger.info('setupGlobals()')
|
|
408
|
+
// Override the global config with our testing config.
|
|
409
|
+
overrideForTesting(this.config)
|
|
410
|
+
|
|
411
|
+
await Db.connect(this.config.DATABASE)
|
|
412
|
+
await ParticipantCached.initialize()
|
|
413
|
+
await ParticipantCurrencyCached.initialize()
|
|
414
|
+
await BatchPositionModelCached.initialize()
|
|
415
|
+
ExternalParticipantCached.initialize()
|
|
416
|
+
|
|
417
|
+
await ParticipantLimitCached.initialize()
|
|
418
|
+
await Cache.initCache()
|
|
419
|
+
Metrics.setup(this.config.INSTRUMENTATION_METRICS_CONFIG)
|
|
420
|
+
|
|
421
|
+
Enums.initialize()
|
|
422
|
+
this._enums = await Enums.getEnums('all')
|
|
423
|
+
|
|
424
|
+
// Register the `topic-transfer-position` consumer
|
|
425
|
+
// Because the kafka registration uses global scope, we cannot register both the non batch
|
|
426
|
+
// and batch position handlers, in spite of the fact that they have different topics.
|
|
427
|
+
switch (this.positionHandlerType) {
|
|
428
|
+
case 'NON_BATCH':
|
|
429
|
+
await PositionHandler.registerPositionHandler()
|
|
430
|
+
break;
|
|
431
|
+
case 'BATCH':
|
|
432
|
+
await PositionBatchHandler.registerPositionHandler()
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
await AdminHandler.registerAllHandlers()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public async teardownGlobals(): Promise<void> {
|
|
440
|
+
try {
|
|
441
|
+
logger.info('teardownGlobals()')
|
|
442
|
+
await ProxyCache.disconnect()
|
|
443
|
+
await Cache.destroyCache()
|
|
444
|
+
await Db.disconnect()
|
|
445
|
+
await KafkaProducer.disconnect()
|
|
446
|
+
await KafkaConsumer.disconnectAll()
|
|
447
|
+
resetOverride()
|
|
448
|
+
} catch (err: any) {
|
|
449
|
+
logger.error(`teardownGlobals() failed with error: ${err.message}`)
|
|
450
|
+
throw err
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private async checkEnvironment() {
|
|
455
|
+
try {
|
|
456
|
+
// Check that docker is installed.
|
|
457
|
+
await execAsync(`docker --version`)
|
|
458
|
+
} catch (err: any) {
|
|
459
|
+
logger.error(`command: 'docker --version' failed. Ensure docker is installed in this
|
|
460
|
+
environment!\n ${err.message}`)
|
|
461
|
+
throw new Error('checkEnvironment() failed.')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
// Check that we have a docker daemon running.
|
|
466
|
+
await execAsync(`docker ps`)
|
|
467
|
+
} catch (err: any) {
|
|
468
|
+
logger.error(`command: 'docker ps' failed. Is the docker daemon running?\n ${err.message}`)
|
|
469
|
+
throw new Error('checkEnvironment() failed.')
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
public async down() {
|
|
474
|
+
let forceExit = false
|
|
475
|
+
const start = performance.now()
|
|
476
|
+
logger.warn(`harness.down()`)
|
|
477
|
+
|
|
478
|
+
if (this.omniConsumer) {
|
|
479
|
+
try {
|
|
480
|
+
await this.omniConsumer.disconnect()
|
|
481
|
+
} catch (err: any) {
|
|
482
|
+
forceExit = true
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const results = await Promise.allSettled([
|
|
487
|
+
this.dependencyRedpanda.down(),
|
|
488
|
+
this.dependencyMySql.down(),
|
|
489
|
+
this.dependencyRedis.down(),
|
|
490
|
+
])
|
|
491
|
+
|
|
492
|
+
let failed = false
|
|
493
|
+
let failedCount = 0
|
|
494
|
+
let errorMessage = ''
|
|
495
|
+
results.forEach(result => {
|
|
496
|
+
if (result.status === 'rejected') {
|
|
497
|
+
failed = true
|
|
498
|
+
failedCount += 1
|
|
499
|
+
errorMessage += `${result.reason}\n`
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
if (failed) {
|
|
504
|
+
logger.error(`Harness.down() encountered: ${failedCount} failure${failed === 1 ? '' : 's'}`)
|
|
505
|
+
logger.error(`Detailed errors: ${errorMessage}`)
|
|
506
|
+
throw new Error(`Harness.down() encountered: ${failedCount} failures.`)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const end = performance.now()
|
|
510
|
+
logger.warn(`Harness.down() took: ${(end - start).toFixed(0)} ms.`)
|
|
511
|
+
|
|
512
|
+
if (forceExit) {
|
|
513
|
+
logger.warn('force exiting')
|
|
514
|
+
setTimeout(() => process.kill(process.pid, 'SIGKILL'), 4000);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
public redpandaMark(): number {
|
|
519
|
+
return this.messageQueue.length
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @description Wait for redpanda to produce and consume _n_ messages.
|
|
524
|
+
*/
|
|
525
|
+
public async redpandaDrain(markLast: number, numMessages: number, attempts: number = 25): Promise<void> {
|
|
526
|
+
const start = performance.now()
|
|
527
|
+
assert(markLast >= 0)
|
|
528
|
+
assert(numMessages >= 0)
|
|
529
|
+
|
|
530
|
+
let delayMs = 10
|
|
531
|
+
let markNew = this.messageQueue.length
|
|
532
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
533
|
+
try {
|
|
534
|
+
markNew = this.messageQueue.length
|
|
535
|
+
if (markNew < markLast) {
|
|
536
|
+
throw new Error(`It appears redpanda went backwards! markLast: ${markLast} --> ${markNew}`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (markNew > (markLast + numMessages)) {
|
|
540
|
+
const errorMessage = `Redpanda expected to consume: ${numMessages}, but consumed: ${markNew - markLast}`
|
|
541
|
+
logger.error(errorMessage)
|
|
542
|
+
this.printLast(markNew - markLast)
|
|
543
|
+
throw new Error(errorMessage)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (markNew === (markLast + numMessages)) {
|
|
547
|
+
const end = performance.now()
|
|
548
|
+
logger.info(`Redpanda consumed ${numMessages} message${numMessages === 1 ? ' ' : 's'} after ${(end - start).toFixed(0).padStart(4)}ms.`)
|
|
549
|
+
|
|
550
|
+
// Cool down for 20ms, check that there are no late messages.
|
|
551
|
+
await new Promise(resolve => setTimeout(resolve, 20))
|
|
552
|
+
const extraMessages = this.messageQueue.length - markNew
|
|
553
|
+
assert(extraMessages >= 0)
|
|
554
|
+
if (extraMessages !== 0) {
|
|
555
|
+
const errorMessage = `After cooldown, Redpanda consumed ${extraMessages} extra message${extraMessages === 1 ? ' ' : 's'}.`
|
|
556
|
+
logger.error(errorMessage)
|
|
557
|
+
|
|
558
|
+
this.printLast(numMessages + extraMessages)
|
|
559
|
+
throw new Error(errorMessage)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
throw new Error('Not ready')
|
|
566
|
+
} catch (err: any) {
|
|
567
|
+
if (attempt === attempts) {
|
|
568
|
+
const errorMessage = `redpandaDrain() failed to consume ${numMessages} messages after ${attempts} attempts.\
|
|
569
|
+
Found only ${markNew - markLast} new messages.`
|
|
570
|
+
logger.error(errorMessage)
|
|
571
|
+
this.printLast(markNew - markLast)
|
|
572
|
+
|
|
573
|
+
throw new Error(errorMessage)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (err.message !== 'Not ready') {
|
|
577
|
+
logger.error(err.message)
|
|
578
|
+
throw err
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
logger.info(`redpandaDrain() waiting for Redpanda: [attempt ${`${attempt}`.padStart(3)}/${attempts}, delayMs: ${delayMs}].`)
|
|
582
|
+
// Slowly back off.
|
|
583
|
+
delayMs = Math.floor((delayMs * 1.1) + 10)
|
|
584
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Get the complete message of the last _n_ messages produced across all topics.
|
|
591
|
+
*/
|
|
592
|
+
public spoolLast(numMessages: number): Array<MojaloopKafkaMessage> {
|
|
593
|
+
assert(numMessages > 0)
|
|
594
|
+
|
|
595
|
+
if (numMessages > this.messageQueue.length) {
|
|
596
|
+
throw new Error(`spoolLast() requested: ${numMessages}, but messageQueue only has: ${this.messageQueue.length} messages.`)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const last = this.messageQueue.slice(numMessages * -1)
|
|
600
|
+
assert(last.length === numMessages)
|
|
601
|
+
return last
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* @description Print the last messages in the messageQueue. If `numMessages` is undefined, prints
|
|
606
|
+
* all messages.
|
|
607
|
+
*/
|
|
608
|
+
public printLast(numMessages?: number): void {
|
|
609
|
+
let messages = this.spoolLast(this.messageQueue.length)
|
|
610
|
+
if (numMessages) {
|
|
611
|
+
assert(numMessages >= 0)
|
|
612
|
+
assert(numMessages <= messages.length)
|
|
613
|
+
messages = messages.slice(numMessages * -1)
|
|
614
|
+
}
|
|
615
|
+
logger.warn('printLast() messages:')
|
|
616
|
+
messages.forEach(msg => {
|
|
617
|
+
logger.warn(`\n
|
|
618
|
+
ts: ${msg.timestamp}
|
|
619
|
+
topic: ${msg.topic}
|
|
620
|
+
uriParams: ${msg.valueParsed.content.uriParams ?
|
|
621
|
+
JSON.stringify(msg.valueParsed.content.uriParams) : ''
|
|
622
|
+
}
|
|
623
|
+
valueParsed:
|
|
624
|
+
${JSON.stringify(msg.valueParsed.content.payload, null, 2)}
|
|
625
|
+
`.replaceAll(/^\s{6}/gm, ''))
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Sometimes we just want to check the last topics that were published to.
|
|
631
|
+
*/
|
|
632
|
+
public spoolLastTopic(numMessages: number): Array<string> {
|
|
633
|
+
const last = this.spoolLast(numMessages)
|
|
634
|
+
return last.map(message => message.topic)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Get the payload of the last _n_ messages produced across all topics.
|
|
639
|
+
*/
|
|
640
|
+
public spoolLastPayload(numMessages: number): Array<any> {
|
|
641
|
+
const last = this.spoolLast(numMessages)
|
|
642
|
+
return last.map(message => message.valueParsed.content.payload)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
interface DependencyOptions {
|
|
647
|
+
harnessId: number,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
interface RedpandaConnectionOptions {
|
|
651
|
+
port: number
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export type MojaloopKafkaMessage = {
|
|
655
|
+
topic: string,
|
|
656
|
+
valueStr: string,
|
|
657
|
+
timestamp: number,
|
|
658
|
+
partition: number,
|
|
659
|
+
offset: number,
|
|
660
|
+
valueParsed: {
|
|
661
|
+
content: {
|
|
662
|
+
uriParams: any
|
|
663
|
+
context: any,
|
|
664
|
+
headers: any,
|
|
665
|
+
payload: any,
|
|
666
|
+
},
|
|
667
|
+
metadata: {
|
|
668
|
+
event: any,
|
|
669
|
+
'protocol.createdAt': number,
|
|
670
|
+
trace: any
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Starts a redpanda and redpanda console docker container.
|
|
677
|
+
* Redpanda is much quicker to start up than Kafka, so is more suitable for this testing harness.
|
|
678
|
+
*/
|
|
679
|
+
class Redpanda {
|
|
680
|
+
private logger = logger.child({ scope: 'Redpanda' })
|
|
681
|
+
private options: DependencyOptions
|
|
682
|
+
private containerName: string
|
|
683
|
+
private containerNameConsole: string
|
|
684
|
+
private _connectionOptions: null | RedpandaConnectionOptions
|
|
685
|
+
|
|
686
|
+
private topics = [
|
|
687
|
+
'topic-transfer-prepare',
|
|
688
|
+
'topic-transfer-position',
|
|
689
|
+
'topic-transfer-fulfil',
|
|
690
|
+
'topic-notification-event',
|
|
691
|
+
'topic-admin-transfer',
|
|
692
|
+
'topic-transfer-position-batch',
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
constructor(options: DependencyOptions) {
|
|
696
|
+
assert(options)
|
|
697
|
+
assert(options.harnessId)
|
|
698
|
+
|
|
699
|
+
this.options = options;
|
|
700
|
+
this.containerName = `int_${this.options.harnessId}_redpanda`
|
|
701
|
+
this.containerNameConsole = `int_${this.options.harnessId}_redpanda_console`
|
|
702
|
+
this._connectionOptions = null
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
public async up(): Promise<void> {
|
|
706
|
+
this.logger.debug(`up()`)
|
|
707
|
+
const portRedpanda = await randomAvailablePort()
|
|
708
|
+
const portConsole = await randomAvailablePort()
|
|
709
|
+
|
|
710
|
+
const command = `
|
|
711
|
+
docker rm -f ${this.containerName} ${this.containerNameConsole} 2>/dev/null;
|
|
712
|
+
docker network create harness || echo 'harness exists';
|
|
713
|
+
docker run -d \
|
|
714
|
+
--name ${this.containerName} \
|
|
715
|
+
--network harness \
|
|
716
|
+
-p ${portRedpanda}:9092 \
|
|
717
|
+
--health-cmd="rpk cluster info" \
|
|
718
|
+
--health-interval=1s \
|
|
719
|
+
--health-timeout=2s \
|
|
720
|
+
--health-retries=100 \
|
|
721
|
+
--health-start-period=2s \
|
|
722
|
+
docker.io/redpandadata/redpanda:latest \
|
|
723
|
+
redpanda start \
|
|
724
|
+
--smp 1 \
|
|
725
|
+
--memory 400M \
|
|
726
|
+
--reserve-memory 0M \
|
|
727
|
+
--overprovisioned \
|
|
728
|
+
--node-id 0 \
|
|
729
|
+
--check=false \
|
|
730
|
+
--kafka-addr internal://0.0.0.0:29092,external://0.0.0.0:9092 \
|
|
731
|
+
--advertise-kafka-addr internal://${this.containerName}:29092,external://localhost:${portRedpanda}
|
|
732
|
+
`.replace(/\s+/g, ' ')
|
|
733
|
+
const { stdout, stderr } = await execAsync(command)
|
|
734
|
+
this.logger.info(`Redpanda.up() stdout: ${stdout}`)
|
|
735
|
+
this.logger.info(`Redpanda.up() stderr: ${stderr}`)
|
|
736
|
+
|
|
737
|
+
const commandConsole = `
|
|
738
|
+
docker run -d \
|
|
739
|
+
--name ${this.containerNameConsole} \
|
|
740
|
+
--hostname ${this.containerNameConsole} \
|
|
741
|
+
--restart on-failure \
|
|
742
|
+
--network harness \
|
|
743
|
+
-p ${portConsole}:8080 \
|
|
744
|
+
-e KAFKA_BROKERS=${this.containerName}:29092 \
|
|
745
|
+
docker.redpanda.com/redpandadata/console:latest
|
|
746
|
+
`.replace(/\s+/g, ' ')
|
|
747
|
+
|
|
748
|
+
await execAsync(commandConsole)
|
|
749
|
+
|
|
750
|
+
this._connectionOptions = {
|
|
751
|
+
port: portRedpanda,
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
this.logger.warn(`Redpanda - go to: http://localhost:${portConsole} to see the Redpanda Console`);
|
|
755
|
+
await this.waitForHealthy()
|
|
756
|
+
await this.createTopics()
|
|
757
|
+
this.logger.debug(`up() - Complete.`)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private async waitForHealthy(): Promise<void> {
|
|
761
|
+
assert(this._connectionOptions)
|
|
762
|
+
|
|
763
|
+
let attemptsMax = 75
|
|
764
|
+
let delayMs = 50
|
|
765
|
+
|
|
766
|
+
for (let attempt = 1; attempt <= attemptsMax; attempt++) {
|
|
767
|
+
try {
|
|
768
|
+
const command = `docker inspect --format='{{.State.Health.Status}}' ${this.containerName}`
|
|
769
|
+
const { stdout } = await execAsync(command, { silent: true })
|
|
770
|
+
|
|
771
|
+
if (stdout.trim() !== 'healthy') {
|
|
772
|
+
throw new Error('Not ready.')
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
logger.warn(`Redpanda started after ${attempt} attempts (${attempt * delayMs}ms).`)
|
|
776
|
+
return
|
|
777
|
+
} catch (err: any) {
|
|
778
|
+
if (attempt === attemptsMax) {
|
|
779
|
+
throw new Error(`Redpanda failed to start after ${attemptsMax} attempts.\n${err.message}`)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
logger.debug(`Waiting for Redpanda: [attempt ${`${attempt}`.padStart(3)}/${attemptsMax}]`)
|
|
783
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private async createTopics(): Promise<void> {
|
|
789
|
+
logger.debug(`createTopics() - creating ${this.topics.length} kafka topics`);
|
|
790
|
+
|
|
791
|
+
const commands = this.topics.map(topic =>
|
|
792
|
+
`docker exec ${this.containerName} rpk topic create ${topic}`
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
await Promise.all(commands.map(async cmd => {
|
|
796
|
+
await execAsync(cmd, { silent: true, force: true })
|
|
797
|
+
}))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
get connectionOptions(): MySqlConnectionOptions {
|
|
801
|
+
if (!this._connectionOptions) {
|
|
802
|
+
throw new Error(`this._connectionOptions is null. Did you forget to call up()?`)
|
|
803
|
+
}
|
|
804
|
+
return this._connectionOptions
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
public async down(): Promise<void> {
|
|
808
|
+
this.logger.debug(`down() - stopping and removing containers: ${this.containerName}, ${this.containerNameConsole}.`)
|
|
809
|
+
try {
|
|
810
|
+
await execAsync(`docker stop ${this.containerName} ${this.containerNameConsole}`, { silent: true })
|
|
811
|
+
await execAsync(`docker rm -f ${this.containerName} ${this.containerNameConsole}`, { silent: true })
|
|
812
|
+
this.logger.debug(`down() - Complete.`)
|
|
813
|
+
} catch (err: any) {
|
|
814
|
+
this.logger.error(`down() - failed to remove containers: ${err.message}`)
|
|
815
|
+
throw err
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Get the sum of all watermarks across all topics.
|
|
821
|
+
*/
|
|
822
|
+
public async mark(): Promise<number> {
|
|
823
|
+
let watermarkSum = 0
|
|
824
|
+
for (const topic of this.topics) {
|
|
825
|
+
const cmd = `docker exec ${this.containerName} rpk topic describe ${topic} --format=json`
|
|
826
|
+
const { stdout } = await execAsync(cmd)
|
|
827
|
+
|
|
828
|
+
const describeJson = JSON.parse(stdout)[0].partitions[0]
|
|
829
|
+
watermarkSum += describeJson.high_watermark
|
|
830
|
+
}
|
|
831
|
+
return watermarkSum;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
interface DependencyOptionsMySql extends DependencyOptions {
|
|
836
|
+
databaseName: string,
|
|
837
|
+
migration: MigrationOptions
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
interface MigrationOptionsKnex {
|
|
841
|
+
type: 'knex';
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* If this is set, then after running the knex migration, perform a mysql dump
|
|
845
|
+
* to update the migration file
|
|
846
|
+
*/
|
|
847
|
+
updateSqlFilePath?: string
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
interface MigrationOptionsSql {
|
|
851
|
+
type: 'sql';
|
|
852
|
+
sqlFilePath: string;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
type MigrationOptions = MigrationOptionsKnex | MigrationOptionsSql;
|
|
856
|
+
|
|
857
|
+
interface MySqlConnectionOptions {
|
|
858
|
+
port: number
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
class MySql {
|
|
862
|
+
private logger = logger.child({ scope: 'MySql' })
|
|
863
|
+
private options: DependencyOptionsMySql
|
|
864
|
+
private containerName: string
|
|
865
|
+
private _connectionOptions: MySqlConnectionOptions | null
|
|
866
|
+
|
|
867
|
+
constructor(options: DependencyOptionsMySql) {
|
|
868
|
+
assert(options)
|
|
869
|
+
assert(options.harnessId)
|
|
870
|
+
|
|
871
|
+
this.options = options;
|
|
872
|
+
this.containerName = `int_${this.options.harnessId}_mysql`
|
|
873
|
+
this._connectionOptions = null
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
public async up(): Promise<void> {
|
|
877
|
+
this.logger.debug(`up()`)
|
|
878
|
+
const port = await randomAvailablePort()
|
|
879
|
+
|
|
880
|
+
const command = `
|
|
881
|
+
docker rm -f ${this.containerName} 2>/dev/null;
|
|
882
|
+
docker run -d \
|
|
883
|
+
--name ${this.containerName} \
|
|
884
|
+
--tmpfs /var/lib/mysql:rw,size=256m \
|
|
885
|
+
-e MARIADB_ROOT_PASSWORD=password \
|
|
886
|
+
-e MARIADB_DATABASE=${this.options.databaseName} \
|
|
887
|
+
-p ${port}:3306 \
|
|
888
|
+
mariadb:latest
|
|
889
|
+
`.replace(/\s/g, ' ')
|
|
890
|
+
await execAsync(command)
|
|
891
|
+
|
|
892
|
+
this.logger.info(`MySql starting at localhost:${port}`);
|
|
893
|
+
|
|
894
|
+
this._connectionOptions = { port }
|
|
895
|
+
await this.waitForMySqlReady()
|
|
896
|
+
await this.migrate()
|
|
897
|
+
await this.seed()
|
|
898
|
+
|
|
899
|
+
this.logger.debug(`up() - Complete.`)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
get connectionOptions(): MySqlConnectionOptions {
|
|
903
|
+
if (!this._connectionOptions) {
|
|
904
|
+
throw new Error(`this._connectionOptions is null. Did you forget to call up()?`)
|
|
905
|
+
}
|
|
906
|
+
return this._connectionOptions
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
public async down(): Promise<void> {
|
|
910
|
+
this.logger.debug(`down() - stopping and removing containers: ${this.containerName}.`)
|
|
911
|
+
try {
|
|
912
|
+
await execAsync(`docker stop ${this.containerName}`, { silent: true })
|
|
913
|
+
await execAsync(`docker rm -f ${this.containerName}`, { silent: true })
|
|
914
|
+
this.logger.debug(`down() - Complete.`)
|
|
915
|
+
} catch (err: any) {
|
|
916
|
+
this.logger.error(`down() - failed to remove containers: ${err.message}`)
|
|
917
|
+
throw err
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Call exec on the container to make sure mysql is ready for connections.
|
|
923
|
+
*
|
|
924
|
+
* The probe goes over TCP rather than the unix socket on purpose. The mariadb entrypoint
|
|
925
|
+
* bootstraps the database on a temporary server started with `--skip-networking`, then stops
|
|
926
|
+
* it and starts the real one. A socket probe passes against that temporary server and then
|
|
927
|
+
* migrate() races its shutdown - `ERROR 2002 ... Can't connect to local server through socket`.
|
|
928
|
+
* Only the real server listens on 3306, so a TCP probe cannot succeed too early.
|
|
929
|
+
*/
|
|
930
|
+
private async waitForMySqlReady(): Promise<void> {
|
|
931
|
+
assert(this._connectionOptions)
|
|
932
|
+
|
|
933
|
+
let attemptsMax = 150
|
|
934
|
+
let delayMs = 35
|
|
935
|
+
|
|
936
|
+
for (let attempt = 1; attempt <= attemptsMax; attempt++) {
|
|
937
|
+
try {
|
|
938
|
+
const command = `docker exec ${this.containerName} sh -c \
|
|
939
|
+
'mariadb --protocol=TCP -h 127.0.0.1 -P 3306 -u root -ppassword -e "select 1" ${this.options.databaseName}'
|
|
940
|
+
`
|
|
941
|
+
await execAsync(command)
|
|
942
|
+
logger.warn(`MySql started after ${attempt} attempts (${attempt * delayMs}ms).`)
|
|
943
|
+
return
|
|
944
|
+
} catch (err: any) {
|
|
945
|
+
if (attempt === attemptsMax) {
|
|
946
|
+
throw new Error(`MySql failed to start after ${attemptsMax} attempts.\n${err.message}`)
|
|
947
|
+
}
|
|
948
|
+
// Extra whitespace for better printing.
|
|
949
|
+
logger.debug(`Waiting for MySQL: [attempt ${`${attempt}`.padStart(3)}/${attemptsMax}]`)
|
|
950
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private async migrate(): Promise<void> {
|
|
956
|
+
assert(this._connectionOptions)
|
|
957
|
+
|
|
958
|
+
// Sometimes migration fails even if MySQL is ready, so we wrap this in retries.
|
|
959
|
+
let attemptsMax = 3
|
|
960
|
+
let delayMs = 1000
|
|
961
|
+
|
|
962
|
+
for (let attempt = 1; attempt <= attemptsMax; attempt++) {
|
|
963
|
+
try {
|
|
964
|
+
const type = this.options.migration.type
|
|
965
|
+
switch (type) {
|
|
966
|
+
case "knex":
|
|
967
|
+
await this.migrateKnex()
|
|
968
|
+
break;
|
|
969
|
+
case "sql":
|
|
970
|
+
await this.migrateSql()
|
|
971
|
+
break
|
|
972
|
+
default:
|
|
973
|
+
throw new Error(`Unexpected migration type: ${type}`)
|
|
974
|
+
}
|
|
975
|
+
return
|
|
976
|
+
} catch (err: any) {
|
|
977
|
+
if (attempt === attemptsMax) {
|
|
978
|
+
throw new Error(`migrate failed after ${attemptsMax}.\n${err.message}`)
|
|
979
|
+
}
|
|
980
|
+
logger.debug(`migrate() [attempt ${`${attempt}`.padStart(3)}/${attemptsMax}]`)
|
|
981
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
private getKnexClient() {
|
|
987
|
+
return knex({
|
|
988
|
+
client: 'mysql2',
|
|
989
|
+
connection: {
|
|
990
|
+
host: 'localhost',
|
|
991
|
+
port: this.connectionOptions.port,
|
|
992
|
+
user: 'root',
|
|
993
|
+
password: 'password',
|
|
994
|
+
database: this.options.databaseName,
|
|
995
|
+
},
|
|
996
|
+
migrations: {
|
|
997
|
+
tableName: 'migration',
|
|
998
|
+
directory: './src/migrations'
|
|
999
|
+
},
|
|
1000
|
+
seeds: {
|
|
1001
|
+
directory: './src/seeds'
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* @method migrateKnex()
|
|
1008
|
+
* @description Runs all of the knex migrations. It's quite slow so you probably want to use
|
|
1009
|
+
* migrateSql() to restore the databse from the checkpoint sql file, and check that file into
|
|
1010
|
+
* git.
|
|
1011
|
+
*/
|
|
1012
|
+
private async migrateKnex(): Promise<void> {
|
|
1013
|
+
assert(this.options.migration.type === 'knex')
|
|
1014
|
+
|
|
1015
|
+
const knexClient = this.getKnexClient();
|
|
1016
|
+
try {
|
|
1017
|
+
await knexClient.migrate.latest()
|
|
1018
|
+
logger.debug('migrateKnex() - complete.')
|
|
1019
|
+
|
|
1020
|
+
if (this.options.migration.updateSqlFilePath) {
|
|
1021
|
+
await this.saveDatabaseCheckpoint(this.options.migration.updateSqlFilePath)
|
|
1022
|
+
}
|
|
1023
|
+
} finally {
|
|
1024
|
+
await knexClient.destroy()
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private async saveDatabaseCheckpoint(pathToCheckpoint: string): Promise<void> {
|
|
1029
|
+
try {
|
|
1030
|
+
logger.info(`saveDatabaseCheckpoint() - creating checkpoint at: ${pathToCheckpoint}`)
|
|
1031
|
+
|
|
1032
|
+
// Dump the database inside the container to a known location.
|
|
1033
|
+
const containerTempFile = '/tmp/checkpoint_dump.sql'
|
|
1034
|
+
const dumpCmd = `docker exec ${this.containerName} sh -c \
|
|
1035
|
+
'mariadb-dump -u root -ppassword ${this.options.databaseName} > ${containerTempFile}'`;
|
|
1036
|
+
const { stderr: dumpStderr } = await execAsync(dumpCmd);
|
|
1037
|
+
|
|
1038
|
+
if (dumpStderr && !dumpStderr.includes('Warning: Using a password')) {
|
|
1039
|
+
logger.warn('SQL dump warnings:', dumpStderr);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const copyCmd = `docker cp ${this.containerName}:${containerTempFile} ${pathToCheckpoint}`;
|
|
1043
|
+
const { stderr: copyStderr } = await execAsync(copyCmd);
|
|
1044
|
+
|
|
1045
|
+
if (copyStderr) {
|
|
1046
|
+
logger.warn('Docker copy warnings:', copyStderr);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const addNoticeCmd = `echo "-- Note: This file was generated by ./testing/harness.ts.
|
|
1050
|
+
-- It is used to speed up the integration tests, but needs to be recreated every time a new
|
|
1051
|
+
-- migration is added. Refer to ./src/testing/harness.ts for instructions of how to update this
|
|
1052
|
+
-- snapshot.
|
|
1053
|
+
" > /tmp/checkpoint; \
|
|
1054
|
+
cat ${pathToCheckpoint} >> /tmp/checkpoint; \
|
|
1055
|
+
`.replace(/\ {2,}/g, '')
|
|
1056
|
+
await execAsync(addNoticeCmd, { silent: false })
|
|
1057
|
+
|
|
1058
|
+
await execAsync(`mv /tmp/checkpoint ${pathToCheckpoint}`)
|
|
1059
|
+
logger.info(`SQL checkpoint saved to ${pathToCheckpoint}`)
|
|
1060
|
+
|
|
1061
|
+
} catch (err: any) {
|
|
1062
|
+
logger.error(`saveDatabaseCheckpoint() failed with error: ${err.message}`)
|
|
1063
|
+
throw err
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private async migrateSql(): Promise<void> {
|
|
1068
|
+
assert(this.options.migration.type === 'sql')
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
logger.debug(`migrateSql(): from: ${this.options.migration.sqlFilePath}.`)
|
|
1072
|
+
const cmd = `docker cp ${this.options.migration.sqlFilePath} ${this.containerName}:/tmp/checkpoint.sql && \
|
|
1073
|
+
docker exec -i ${this.containerName} sh -c 'mariadb -u root -ppassword ${this.options.databaseName} < /tmp/checkpoint.sql'
|
|
1074
|
+
`
|
|
1075
|
+
const { stdout, stderr } = await execAsync(cmd);
|
|
1076
|
+
|
|
1077
|
+
if (stderr && !stderr.includes('warning')) {
|
|
1078
|
+
logger.debug('migrateSql() warnings:', stderr);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
logger.debug(`migrateSql() from ${this.options.migration.sqlFilePath} completed`);
|
|
1082
|
+
} catch (err: any) {
|
|
1083
|
+
throw new Error(`migrateSql() from: ${this.options.migration.sqlFilePath} failed with error: ${err.message}.`)
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private async seed(): Promise<void> {
|
|
1088
|
+
const knexClient = this.getKnexClient();
|
|
1089
|
+
try {
|
|
1090
|
+
await knexClient.seed.run()
|
|
1091
|
+
logger.debug('seed() - complete.')
|
|
1092
|
+
} finally {
|
|
1093
|
+
await knexClient.destroy()
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
interface DependencyOptionsRedis extends DependencyOptions {
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
interface RedisConnectionOptions {
|
|
1102
|
+
port: number
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
class Redis {
|
|
1106
|
+
private logger = logger.child({ scope: 'Redis' })
|
|
1107
|
+
private containerName: string
|
|
1108
|
+
private _connectionOptions: RedisConnectionOptions | null
|
|
1109
|
+
|
|
1110
|
+
constructor(private options: DependencyOptionsRedis) {
|
|
1111
|
+
assert(options)
|
|
1112
|
+
assert(options.harnessId)
|
|
1113
|
+
|
|
1114
|
+
this.containerName = `int_${this.options.harnessId}_redis`
|
|
1115
|
+
this._connectionOptions = null
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
public async up(): Promise<void> {
|
|
1119
|
+
this.logger.debug(`up()`)
|
|
1120
|
+
const port = await randomAvailablePort()
|
|
1121
|
+
|
|
1122
|
+
const command = `
|
|
1123
|
+
docker rm -f ${this.containerName} 2>/dev/null;
|
|
1124
|
+
docker run -d \
|
|
1125
|
+
--name ${this.containerName} \
|
|
1126
|
+
-p ${port}:6379 \
|
|
1127
|
+
-e ALLOW_EMPTY_PASSWORD=yes \
|
|
1128
|
+
--health-cmd "redis-cli ping" \
|
|
1129
|
+
--health-timeout 2s \
|
|
1130
|
+
--health-interval 10s \
|
|
1131
|
+
redis:latest
|
|
1132
|
+
`.replace(/\s/g, ' ')
|
|
1133
|
+
await execAsync(command)
|
|
1134
|
+
|
|
1135
|
+
this.logger.info(`Redis starting at localhost:${port}`);
|
|
1136
|
+
|
|
1137
|
+
this._connectionOptions = { port }
|
|
1138
|
+
|
|
1139
|
+
this.logger.debug(`up() - Complete.`)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
get connectionOptions() {
|
|
1143
|
+
if (!this._connectionOptions) {
|
|
1144
|
+
throw new Error(`this._connectionOptions is null. Did you forget to call up()?`)
|
|
1145
|
+
}
|
|
1146
|
+
return this._connectionOptions
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
public async down(): Promise<void> {
|
|
1150
|
+
this.logger.debug(`down() - stopping and removing containers: ${this.containerName}.`)
|
|
1151
|
+
try {
|
|
1152
|
+
await execAsync(`docker stop ${this.containerName}`, { silent: true })
|
|
1153
|
+
await execAsync(`docker rm -f ${this.containerName}`, { silent: true })
|
|
1154
|
+
this.logger.debug(`down() - Complete.`)
|
|
1155
|
+
} catch (err: any) {
|
|
1156
|
+
this.logger.error(`down() - failed to remove containers: ${err.message}`)
|
|
1157
|
+
throw err
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|