taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 084fa34900ac5303db42fd121cb461055ad2eb1e
parent a86cae90440002dfcd2528aa79c31e6168c1c80c
Author: Florian Dold <florian@dold.me>
Date:   Mon, 23 Sep 2024 20:05:43 +0200

harness: expand merchant KYC test

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mpackages/taler-util/src/types-taler-merchant.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 232 insertions(+), 20 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts @@ -18,14 +18,22 @@ * Imports. */ import { - LegacyAccountKycRedirects, - codecForLegacyAccountKycRedirects, + codecForKycProcessClientInformation, + codecForMerchantAccountKycRedirectsResponse, + codecForQueryInstancesResponse, Duration, + encodeCrock, + hashPaytoUri, j2s, Logger, + MerchantAccountKycRedirectsResponse, TalerCorebankApiClient, + WireGatewayApiClient, } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + readResponseJsonOrThrow, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-util/http"; import { createSyncCryptoApi, EddsaKeyPairStrings, @@ -45,7 +53,11 @@ import { WalletClient, WalletService, } from "../harness/harness.js"; -import { EnvOptions, withdrawViaBankV3 } from "../harness/helpers.js"; +import { + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; const logger = new Logger(`test-kyc-merchant-deposit.ts`); @@ -86,13 +98,15 @@ async function createKycTestkudosEnvironment( let exchangeBankPassword = "mypw"; let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + const wireGatewayApiBaseUrl = new URL( + `accounts/${exchangeBankUsername}/taler-wire-gateway/`, + bank.corebankApiBaseUrl, + ).href; + await exchange.addBankAccount("1", { accountName: exchangeBankUsername, accountPassword: exchangeBankPassword, - wireGatewayApiBaseUrl: new URL( - "accounts/exchange/taler-wire-gateway/", - bank.baseUrl, - ).href, + wireGatewayApiBaseUrl, accountPaytoUri: exchangePaytoUri, }); @@ -239,7 +253,7 @@ async function createKycTestkudosEnvironment( accountName: "", accountPassword: "", accountPaytoUri: "", - wireGatewayApiBaseUrl: "", + wireGatewayApiBaseUrl, }, }; } @@ -247,8 +261,36 @@ async function createKycTestkudosEnvironment( export async function runKycMerchantDepositTest(t: GlobalTestState) { // Set up test environment - const { merchant, walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironment(t); + const { + merchant, + walletClient, + bankClient, + exchange, + exchangeBankAccount, + amlKeypair, + } = await createKycTestkudosEnvironment(t); + + let accountPub: string; + + { + const instanceUrl = new URL("private", merchant.makeInstanceBaseUrl()); + const resp = await harnessHttpLib.fetch(instanceUrl.href); + const parsedResp = await readSuccessResponseJsonOrThrow( + resp, + codecForQueryInstancesResponse(), + ); + accountPub = parsedResp.merchant_pub; + } + + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: "admin", + password: "adminpw", + }, + }, + ); // Withdraw digital cash into the wallet. @@ -261,20 +303,17 @@ export async function runKycMerchantDepositTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; - const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl()) - .href; - - let kycResp: LegacyAccountKycRedirects | undefined = undefined; + let kycRespOne: MerchantAccountKycRedirectsResponse | undefined = undefined; while (1) { + const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl()) + .href; logger.info(`requesting GET ${kycStatusUrl}`); const resp = await harnessHttpLib.fetch(kycStatusUrl); - logger.info(`mechant kyc status: ${resp.status}`); if (resp.status === 200) { - console.log(j2s(await resp.json())); - kycResp = await readSuccessResponseJsonOrThrow( + kycRespOne = await readSuccessResponseJsonOrThrow( resp, - codecForLegacyAccountKycRedirects(), + codecForMerchantAccountKycRedirectsResponse(), ); break; } @@ -284,7 +323,91 @@ export async function runKycMerchantDepositTest(t: GlobalTestState) { }); } - t.assertTrue(!!kycResp); + t.assertTrue(!!kycRespOne); + + logger.info(`mechant kyc status: ${j2s(kycRespOne)}`); + + await wireGatewayApiClient.adminAddKycauth({ + amount: "TESTKUDOS:0.1", + debitAccountPayto: kycRespOne.kyc_data[0].payto_uri, + accountPub, + }); + + let kycRespTwo: MerchantAccountKycRedirectsResponse | undefined = undefined; + + // We do this in a loop as a work-around. + // Not exactly the correct behavior from the merchant right now. + while (true) { + const kycStatusLongpollUrl = new URL( + "private/kyc", + merchant.makeInstanceBaseUrl(), + ); + kycStatusLongpollUrl.searchParams.set("lpt", "1"); + const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href); + t.assertDeepEqual(resp.status, 200); + const parsedResp = await readSuccessResponseJsonOrThrow( + resp, + codecForMerchantAccountKycRedirectsResponse(), + ); + logger.info(`kyc resp 2: ${j2s(parsedResp)}`); + if (parsedResp.kyc_data[0].payto_kycauths == null) { + kycRespTwo = parsedResp; + break; + } + // Wait 500ms + await new Promise<void>((resolve) => { + setTimeout(() => resolve(), 500); + }); + } + + t.assertTrue(!!kycRespTwo); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: encodeCrock(hashPaytoUri(kycRespTwo.kyc_data[0].payto_uri)), + }); + + // We do this in a loop as a work-around. + // Not exactly the correct behavior from the merchant right now. + while (true) { + const kycStatusLongpollUrl = new URL( + "private/kyc", + merchant.makeInstanceBaseUrl(), + ); + kycStatusLongpollUrl.searchParams.set("lpt", "3"); + const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href); + t.assertDeepEqual(resp.status, 200); + const parsedResp = await readSuccessResponseJsonOrThrow( + resp, + codecForMerchantAccountKycRedirectsResponse(), + ); + logger.info(`kyc resp 3: ${j2s(parsedResp)}`); + if ((parsedResp.kyc_data[0].limits?.length ?? 0) == 0) { + break; + } + + const accessToken = parsedResp.kyc_data[0].access_token; + + t.assertTrue(!!accessToken); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + + logger.info(`kyc-info: ${j2s(clientInfo)}`); + + // Wait 500ms + await new Promise<void>((resolve) => { + setTimeout(() => resolve(), 500); + }); + } } runKycMerchantDepositTest.suites = ["wallet", "merchant", "kyc"]; diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -1313,6 +1313,75 @@ export interface QueryInstancesResponse { }; } +export interface MerchantAccountKycRedirectsResponse { + // Array of KYC status information for + // the exchanges and bank accounts selected + // by the query. + kyc_data: MerchantAccountKycRedirect[]; +} + +export interface MerchantAccountKycRedirect { + // Our bank wire account this is about. + payto_uri: string; + + // Base URL of the exchange this is about. + exchange_url: string; + + // HTTP status code returned by the exchange when we asked for + // information about the KYC status. + // Since protocol **v17**. + exchange_http_status: number; + + // Set to true if we did not get a /keys response from + // the exchange and thus cannot do certain checks, such as + // determining default account limits or account eligibility. + no_keys: boolean; + + // Set to true if the given account cannot to KYC at the + // given exchange because no wire method exists that could + // be used to do the KYC auth wire transfer. + auth_conflict: boolean; + + // Numeric error code indicating errors the exchange + // returned, or TALER_EC_INVALID for none. + // Optional (as there may not always have + // been an error code). Since protocol **v17**. + exchange_code?: number; + + // Access token needed to open the KYC SPA and/or + // access the /kyc-info/ endpoint. + access_token?: string; + + // Array with limitations that currently apply to this + // account and that may be increased or lifted if the + // KYC check is passed. + // Note that additional limits *may* exist and not be + // communicated to the client. If such limits are + // reached, this *may* be indicated by the account + // going into aml_review state. However, it is + // also possible that the exchange may legally have + // to deny operations without being allowed to provide + // any justification. + // The limits should be used by the client to + // possibly structure their operations (e.g. withdraw + // what is possible below the limit, ask the user to + // pass KYC checks or withdraw the rest after the time + // limit is passed, warn the user to not withdraw too + // much or even prevent the user from generating a + // request that would cause it to exceed hard limits). + limits?: AccountLimit[]; + + // Array of wire transfer instructions (including + // optional amount and subject) for a KYC auth wire + // transfer. Set only if this is required + // to get the given exchange working. + // Array because the exchange may have multiple + // bank accounts, in which case any of these + // accounts will do. + // Optional. Since protocol **v17**. + payto_kycauths?: string[]; +} + /** * @deprecated */ @@ -2988,6 +3057,26 @@ export const codecForQueryInstancesResponse = ) .build("TalerMerchantApi.QueryInstancesResponse"); +export const codecForMerchantAccountKycRedirectsResponse = + (): Codec<MerchantAccountKycRedirectsResponse> => + buildCodecForObject<MerchantAccountKycRedirectsResponse>() + .property("kyc_data", codecForList(codecForMerchantAccountKycRedirect())) + .build("MerchantAccountKycRedirectsResponse"); + +export const codecForMerchantAccountKycRedirect = + (): Codec<MerchantAccountKycRedirect> => + buildCodecForObject<MerchantAccountKycRedirect>() + .property("limits", codecOptional(codecForList(codecForAccountLimit()))) + .property("exchange_url", codecForURLString()) + .property("exchange_code", codecOptional(codecForNumber())) + .property("exchange_http_status", codecForNumber()) + .property("payto_uri", codecForPaytoString()) + .property("payto_kycauths", codecOptional(codecForList(codecForString()))) + .property("access_token", codecOptional(codecForString())) + .property("auth_conflict", codecForBoolean()) + .property("no_keys", codecForBoolean()) + .build("MerchantAccountKycRedirect"); + /** * @deprecated */