diff options
Diffstat (limited to 'packages')
-rw-r--r-- | packages/demobank-ui/src/demobank-ui-settings.js | 2 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-core.ts | 594 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/bank-integration.ts | 10 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/test.cli.ts | 77 | ||||
-rw-r--r-- | packages/taler-util/src/http-impl.node.ts | 18 |
5 files changed, 687 insertions, 14 deletions
diff --git a/packages/demobank-ui/src/demobank-ui-settings.js b/packages/demobank-ui/src/demobank-ui-settings.js index 8a0961831..99c6f3873 100644 --- a/packages/demobank-ui/src/demobank-ui-settings.js +++ b/packages/demobank-ui/src/demobank-ui-settings.js @@ -3,8 +3,6 @@ /** * Global settings for the demobank UI. */ -localStorage.setItem("bank-base-url", "http://bank.taler.test/"); - globalThis.talerDemobankSettings = { backendBaseURL: "http://bank.taler.test/", allowRegistrations: true, diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts index b9cce6c72..de3622b8e 100644 --- a/packages/taler-util/src/http-client/bank-core.ts +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -18,18 +18,25 @@ import { AmountJson, Amounts, HttpStatusCode, - LibtoolVersion + LibtoolVersion, + TalerError, + TalerErrorDetail, + encodeCrock, + getRandomBytes, + parsePaytoUri, + stringifyPaytoUri } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, createPlatformHttpLib } from "@gnu-taler/taler-util/http"; +import { setShowCurlRequest } from "../http-impl.node.js"; +import { TalerAuthenticationHttpClient } from "./authentication.js"; import { TalerBankIntegrationHttpClient } from "./bank-integration.js"; import { TalerRevenueHttpClient } from "./bank-revenue.js"; import { TalerWireGatewayHttpClient } from "./bank-wire.js"; -import { AccessToken, OperationOk, PaginationParams, TalerCorebankApi, UserAndToken, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountGetWithdrawalResponse, codecForBankAccountTransactionInfo, codecForBankAccountTransactionsResponse, codecForCashoutConversionResponse, codecForCashoutPending, codecForCashoutStatusResponse, codecForCashouts, codecForConversionRatesResponse, codecForCoreBankConfig, codecForGlobalCashouts, codecForListBankAccountsResponse, codecForMonitorResponse, codecForPublicAccountsResponse } from "./types.js"; -import { addPaginationParams, opFixedSuccess, opEmptySuccess, opSuccess, opKnownFailure, makeBearerTokenAuthHeader, opUnknownFailure } from "./utils.js"; -import { TalerAuthenticationHttpClient } from "./authentication.js"; +import { AccessToken, OperationOk, OperationResult, PaginationParams, TalerCorebankApi, UserAndToken, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountGetWithdrawalResponse, codecForBankAccountTransactionInfo, codecForBankAccountTransactionsResponse, codecForCashoutConversionResponse, codecForCashoutPending, codecForCashoutStatusResponse, codecForCashouts, codecForCoreBankConfig, codecForGlobalCashouts, codecForListBankAccountsResponse, codecForMonitorResponse, codecForPublicAccountsResponse } from "./types.js"; +import { addPaginationParams, makeBearerTokenAuthHeader, opEmptySuccess, opFixedSuccess, opKnownFailure, opSuccess, opUnknownFailure } from "./utils.js"; type props = keyof TalerCoreBankHttpClient @@ -134,7 +141,11 @@ export class TalerCoreBankHttpClient { return opKnownFailure("unauthorized", resp); } } - case HttpStatusCode.PreconditionFailed: return opKnownFailure("balance-not-zero", resp); + //FIXME: this should be forbidden + case HttpStatusCode.Unauthorized: { + return opKnownFailure("unauthorized", resp); + } + case HttpStatusCode.Conflict: return opKnownFailure("balance-not-zero", resp); default: return opUnknownFailure(resp, await resp.text()) } } @@ -155,7 +166,7 @@ export class TalerCoreBankHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: return opEmptySuccess() case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); //FIXME: missing error code for cases: // * change legal name // * admin tries to change its own account @@ -210,7 +221,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get--accounts * */ - async getAccounts(auth: AccessToken, filter: {account? : string} = {}, pagination?: PaginationParams) { + async getAccounts(auth: AccessToken, filter: { account?: string } = {}, pagination?: PaginationParams) { const url = new URL(`accounts`, this.baseUrl); addPaginationParams(url, pagination) if (filter.account) { @@ -226,7 +237,9 @@ export class TalerCoreBankHttpClient { case HttpStatusCode.Ok: return opSuccess(resp, codecForListBankAccountsResponse()) case HttpStatusCode.NoContent: return opFixedSuccess({ accounts: [] }) case HttpStatusCode.Forbidden: return opKnownFailure("no-rights", resp); - case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.Unauthorized: { + return opKnownFailure("unauthorized", resp); + } default: return opUnknownFailure(resp, await resp.text()) } } @@ -312,6 +325,7 @@ export class TalerCoreBankHttpClient { body, }); switch (resp.status) { + //FIXME: return txid //FIXME: remove this after server has been updated case HttpStatusCode.Ok: return opEmptySuccess() case HttpStatusCode.NoContent: return opEmptySuccess() @@ -342,6 +356,7 @@ export class TalerCoreBankHttpClient { switch (resp.status) { case HttpStatusCode.Ok: return opSuccess(resp, codecForBankAccountCreateWithdrawalResponse()) case HttpStatusCode.PreconditionFailed: return opKnownFailure("insufficient-funds", resp); + case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); //FIXME: remove when server is updated case HttpStatusCode.Forbidden: return opKnownFailure("insufficient-funds", resp); default: return opUnknownFailure(resp, await resp.text()) @@ -359,6 +374,7 @@ export class TalerCoreBankHttpClient { }); switch (resp.status) { case HttpStatusCode.Ok: return opSuccess(resp, codecForBankAccountGetWithdrawalResponse()) + case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) default: return opUnknownFailure(resp, await resp.text()) } @@ -377,6 +393,8 @@ export class TalerCoreBankHttpClient { //FIXME: remove when the server is fixed case HttpStatusCode.Ok: return opEmptySuccess() case HttpStatusCode.NoContent: return opEmptySuccess() + case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) + case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) case HttpStatusCode.Conflict: return opKnownFailure("previously-confirmed", resp); default: return opUnknownFailure(resp, await resp.text()) } @@ -395,6 +413,8 @@ export class TalerCoreBankHttpClient { //FIXME: remove when the server is fixed case HttpStatusCode.Ok: return opEmptySuccess() case HttpStatusCode.NoContent: return opEmptySuccess() + case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) + case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp) case HttpStatusCode.Conflict: return opKnownFailure("previously-aborted", resp); case HttpStatusCode.UnprocessableEntity: return opKnownFailure("no-exchange-or-reserve-selected", resp); default: return opUnknownFailure(resp, await resp.text()) @@ -570,7 +590,7 @@ export class TalerCoreBankHttpClient { * https://docs.taler.net/core/api-corebank.html#get--monitor * */ - async getMonitor(params: { timeframe?: TalerCorebankApi.MonitorTimeframeParam, which?: number }) { + async getMonitor(params: { timeframe?: TalerCorebankApi.MonitorTimeframeParam, which?: number } = {}) { const url = new URL(`monitor`, this.baseUrl); if (params.timeframe) { url.searchParams.set("timeframe", params.timeframe.toString()) @@ -630,4 +650,560 @@ export class TalerCoreBankHttpClient { const url = new URL(`accounts/${username}/`, this.baseUrl); return new TalerAuthenticationHttpClient(url.href, username, this.httpLib,) } + + async testConfig() { + const config = await this.getConfig() + if (!this.isCompatible(config.body.version)) { + throw Error(`not compatible with server ${config.body.version}`) + } + return config.body + } + + async testCashouts(adminPassword: string) { + } + async testMonitor(adminPassword: string) { + const { access_token: adminToken } = await succeedOrThrow(() => + this.getAuthenticationAPI("admin").createAccessToken(adminPassword, { + scope: "readwrite" + }) + ) + + await succeedOrThrow(() => ( + this.getMonitor() + )) + + await succeedOrThrow(() => ( + this.getMonitor({ + timeframe: TalerCorebankApi.MonitorTimeframeParam.day, + which: (new Date()).getDate() -1 + }) + )) + } + + async testAccountManagement(adminPassword: string) { + + const { access_token: adminToken } = await succeedOrThrow(() => + this.getAuthenticationAPI("admin").createAccessToken(adminPassword, { + scope: "readwrite" + }) + ) + + /** + * Create account + */ + { + const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); + + // await failOrThrow("invalid-input",() => + // this.createAccount(adminToken, { + // name: username, + // username, password: "123", + // challenge_contact_data: { + // email: "invalid email", + // phone: "invalid phone", + // } + // }) + // ) + + // await failOrThrow("unable-to-create",() => + // this.createAccount(adminToken, { + // name: "admin", + // username, password: "123" + // }) + // ) + + // await failOrThrow("unable-to-create",() => + // this.createAccount(adminToken, { + // name: "bank", + // username, password: "123" + // }) + // ) + + await succeedOrThrow(() => + this.createAccount(adminToken, { + name: username, + username, password: "123" + }) + ) + + await failOrThrow("already-exist", () => + this.createAccount(adminToken, { + name: username, + username, password: "123" + }) + ); + } + + /** + * Delete account + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + + await failOrThrow("not-found", () => + this.deleteAccount({ username: "not-found", token: adminToken }) + ) + await failOrThrow("unable-to-delete", () => + this.deleteAccount({ username: "admin", token: adminToken }) + ) + await failOrThrow("unable-to-delete", () => + this.deleteAccount({ username: "bank", token: adminToken }) + ) + + await failOrThrow("balance-not-zero", () => + this.deleteAccount({ username, token: adminToken }) + ) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + + const adminInfo = await succeedOrThrow(() => + this.getAccount({ username: "admin", token: adminToken }) + ) + + const adminAccount = parsePaytoUri(adminInfo.payto_uri)! + adminAccount.params["message"] = "all my money" + const withSubject = stringifyPaytoUri(adminAccount) + + await succeedOrThrow(() => + this.createTransaction({ username, token }, { + payto_uri: withSubject, + amount: userInfo.balance.amount + }) + ) + + + const otherUsername = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); + + await succeedOrThrow(() => + this.createAccount(adminToken, { + name: otherUsername, + username: otherUsername, password: "123" + }) + ) + + await failOrThrow("unauthorized", () => + this.deleteAccount({ username: otherUsername, token }) + ) + + await succeedOrThrow(() => + this.deleteAccount({ username, token: adminToken }) + ) + } + + /** + * Update account + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + + await failOrThrow("cant-change-legal-name-or-admin", () => + this.updateAccount({ username, token }, { + name: "something else", + }) + ) + + // await failOrThrow("not-found", () => + // this.updateAccount({ username: "notfound", token }, { + // challenge_contact_data: { + // email: "asd@Aasd.com" + // } + // }) + // ) + + await failOrThrow("unauthorized", () => + this.updateAccount({ username: "notfound", token: "wrongtoken" as AccessToken }, { + challenge_contact_data: { + email: "asd@Aasd.com" + } + }) + ) + + await succeedOrThrow(() => + this.updateAccount({ username, token }, { + challenge_contact_data: { + email: "asd@Aasd.com" + } + }) + ) + } + + /** + * Update password + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + + await succeedOrThrow(() => + this.updatePassword({ username, token }, { + old_password: "123", + new_password: "234" + }) + ) + // await failOrThrow("not-found",() => + // this.updatePassword({ username:"notfound", token: userTempToken }, { + // old_password: "123", + // new_password: "234" + // }) + // ) + await failOrThrow("unauthorized", () => + this.updatePassword({ username: "admin", token }, { + old_password: "123", + new_password: "234" + }) + ) + // await failOrThrow("old-password-invalid-or-not-allowed",() => + // this.updatePassword({ username, token: userTempToken }, { + // old_password: "123", + // new_password: "234" + // }) + // ) + + } + + /** + * public accounts + */ + { + const acs = await succeedOrThrow(() => this.getPublicAccounts()) + + } + /** + * get accounts + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + // await failOrThrow("no-rights",() => + // this.getAccounts(token) + // ) + await failOrThrow("unauthorized", () => + this.getAccounts("ASDASD" as AccessToken) + ) + + const acs = await succeedOrThrow(() => + this.getAccounts(adminToken) + ) + } + + } + + async testWithdrawals(adminPassword: string) { + const { access_token: adminToken } = await succeedOrThrow(() => + this.getAuthenticationAPI("admin").createAccessToken(adminPassword, { + scope: "readwrite" + }) + ) + /** + * create withdrawals + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + + // FIXME: it shoulw warn about not enough balance + // const balance = Amounts.parseOrThrow(userInfo.balance.amount) + // const moreThanBalance = Amounts.stringify(Amounts.add(balance, balance).amount) + // setShowCurlRequest(true) + // await failOrThrow("insufficient-funds", () => + // this.createWithdrawal({ username, token }, { + // amount: moreThanBalance + // }) + // ) + + await failOrThrow("unauthorized", () => + this.createWithdrawal({ username, token: "wrongtoken" as AccessToken }, { + amount: userInfo.balance.amount + }) + ) + + await succeedOrThrow(() => + this.createWithdrawal({ username, token }, { + amount: userInfo.balance.amount + }) + ) + } + + /** + * get withdrawal + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + + const { withdrawal_id } = await succeedOrThrow(() => + this.createWithdrawal({ username, token }, { + amount: userInfo.balance.amount + }) + ) + + await succeedOrThrow(() => + this.getWithdrawalById(withdrawal_id) + ) + + await failOrThrow("invalid-id", () => + this.getWithdrawalById("invalid") + ) + await failOrThrow("not-found", () => + this.getWithdrawalById("11111111-1111-1111-1111-111111111111") + ) + } + + /** + * abort withdrawal + */ + { + const { username:exchangeUser, token: exchangeToken } = await createRandomTestUser(this, adminToken, {is_taler_exchange: true}) + const { username, token } = await createRandomTestUser(this, adminToken) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + const exchangeInfo = await succeedOrThrow(() => + this.getAccount({ username:exchangeUser, token:exchangeToken }) + ) + + await failOrThrow("invalid-id", () => + this.abortWithdrawalById("invalid") + ) + await failOrThrow("not-found", () => + this.abortWithdrawalById("11111111-1111-1111-1111-111111111111") + ) + + const { withdrawal_id:firstWithdrawal } = await succeedOrThrow(() => + this.createWithdrawal({ username, token }, { + amount: userInfo.balance.amount + }) + ) + + await succeedOrThrow(() => + this.abortWithdrawalById(firstWithdrawal) + ) + + const { taler_withdraw_uri: uri, withdrawal_id:secondWithdrawal } = await succeedOrThrow(() => + this.createWithdrawal({ username, token }, { + amount: userInfo.balance.amount + }) + ) + + await succeedOrThrow(() => + this.getIntegrationAPI().completeWithdrawalOperationById(secondWithdrawal, { + reserve_pub: encodeCrock(getRandomBytes(32)), + selected_exchange: exchangeInfo.payto_uri, + }) + ) + await succeedOrThrow(() => + this.confirmWithdrawalById(secondWithdrawal) + ) + await failOrThrow("previously-confirmed", () => + this.abortWithdrawalById(secondWithdrawal) + ) + } + + /** + * confirm withdrawal + */ + { + const { username:exchangeUser, token: exchangeToken } = await createRandomTestUser(this, adminToken, {is_taler_exchange: true}) + const { username, token } = await createRandomTestUser(this, adminToken) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + const exchangeInfo = await succeedOrThrow(() => + this.getAccount({ username:exchangeUser, token:exchangeToken }) + ) + + await failOrThrow("invalid-id", () => + this.confirmWithdrawalById("invalid") + ) + await failOrThrow("not-found", () => + this.confirmWithdrawalById("11111111-1111-1111-1111-111111111111") + ) + + const { withdrawal_id:firstWithdrawal } = await succeedOrThrow(() => + this.createWithdrawal({ username, token }, { + amount: userInfo.balance.amount + }) + ) + + await failOrThrow("no-exchange-or-reserve-selected", () => + this.confirmWithdrawalById(firstWithdrawal) + ) + + await succeedOrThrow(() => + this.getIntegrationAPI().completeWithdrawalOperationById(firstWithdrawal, { + reserve_pub: encodeCrock(getRandomBytes(32)), + selected_exchange: exchangeInfo.payto_uri, + }) + ) + + await succeedOrThrow(() => + this.confirmWithdrawalById(firstWithdrawal) + ) + + const { withdrawal_id:secondWithdrawal } = await succeedOrThrow(() => + this.createWithdrawal({ username, token }, { + amount: userInfo.balance.amount + }) + ) + + await succeedOrThrow(() => + this.abortWithdrawalById(secondWithdrawal) + ) + await failOrThrow("previously-aborted", () => + this.confirmWithdrawalById(secondWithdrawal) + ) + } + } + + async testTransactions(adminPassword: string) { + const { access_token: adminToken } = await succeedOrThrow(() => + this.getAuthenticationAPI("admin").createAccessToken(adminPassword, { + scope: "readwrite" + }) + ) + // get transactions + { + const { username, token } = await createRandomTestUser(this, adminToken) + // await succeedOrThrow(() => this.getTransactions(creds)) + const txs = await succeedOrThrow(() => this.getTransactions({ username, token }, { + limit: 5, + order: "asc" + })) + // await failOrThrow("not-found",() => this.getTransactions({ + // username:"not-found", + // token: creds.token, + // })) + await failOrThrow("unauthorized", () => this.getTransactions({ + username: username, + token: "wrongtoken" as AccessToken, + })) + } + + /** + * getTxby id + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + const { username: otherUser, token: otherToken } = await createRandomTestUser(this, adminToken) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + const otherInfo = await succeedOrThrow(() => + this.getAccount({ username: otherUser, token: otherToken }) + ) + const otherAccount = parsePaytoUri(otherInfo.payto_uri)! + otherAccount.params["message"] = "all" + + await succeedOrThrow(() => + this.createTransaction({ username, token }, { + payto_uri: stringifyPaytoUri(otherAccount), + amount: userInfo.balance.amount + }) + ) + + const txs = await succeedOrThrow(() => this.getTransactions({ username, token }, { + limit: 5, + order: "asc" + })) + const rowId = txs.transactions[0].row_id + + await succeedOrThrow(() => + this.getTransactionById({ username, token }, rowId) + ) + + await failOrThrow("not-found", () => + this.getTransactionById({ username, token }, 123123123) + ) + + await failOrThrow("unauthorized", () => + this.getTransactionById({ username, token: "wrongtoken" as AccessToken }, 123123123) + ) + } + + /** + * create transactions + */ + { + const { username, token } = await createRandomTestUser(this, adminToken) + const { username: otherUser, token: otherToken } = await createRandomTestUser(this, adminToken) + + const userInfo = await succeedOrThrow(() => + this.getAccount({ username, token }) + ) + const otherInfo = await succeedOrThrow(() => + this.getAccount({ username: otherUser, token: otherToken }) + ) + const otherAccount = parsePaytoUri(otherInfo.payto_uri)! + otherAccount.params["message"] = "all" + + await succeedOrThrow(() => + this.createTransaction({ username, token }, { + payto_uri: stringifyPaytoUri(otherAccount), + amount: userInfo.balance.amount + }) + ) + //missing amount + await failOrThrow("invalid-input", () => + this.createTransaction({ username, token }, { + payto_uri: stringifyPaytoUri(otherAccount), + // amount: userInfo.balance.amount + }) + ) + //missing subject + await failOrThrow("invalid-input", () => + this.createTransaction({ username, token }, { + payto_uri: otherInfo.payto_uri, + amount: userInfo.balance.amount + }) + ) + await failOrThrow("unauthorized", () => + this.createTransaction({ username, token: "wrongtoken" as AccessToken }, { + payto_uri: otherInfo.payto_uri, + amount: userInfo.balance.amount + }) + ) + } + } } + +export async function succeedOrThrow<R, E>(cb: () => Promise<OperationResult<R, E>>): Promise<R> { + const resp = await cb() + if (resp.type === "ok") return resp.body + throw TalerError.fromUncheckedDetail({ ...resp.detail, case: resp.case }) +} +export async function failOrThrow<E>(s: E, cb: () => Promise<OperationResult<unknown, E>>): Promise<TalerErrorDetail> { + const resp = await cb() + if (resp.type === "ok") { + throw TalerError.fromException(new Error(`request succeed but failure "${s}" was expected`)) + } + if (resp.case === s) { + return resp.detail + } + throw TalerError.fromException(new Error(`request failed but case "${s}" was expected`)) +} + +export async function createRandomTestUser(api: TalerCoreBankHttpClient, adminToken: AccessToken, options: Partial<TalerCorebankApi.RegisterAccountRequest> = {}) { + const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); + await succeedOrThrow(() => + api.createAccount(adminToken, { + name: username, + username, password: "123", + ...options + }) + ) + const { access_token } = await succeedOrThrow(() => + api.getAuthenticationAPI(username).createAccessToken("123", { + scope: "readwrite" + }) + ) + return { username, token: access_token } +}
\ No newline at end of file diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts index 521b6e34c..887dbed1b 100644 --- a/packages/taler-util/src/http-client/bank-integration.ts +++ b/packages/taler-util/src/http-client/bank-integration.ts @@ -1,10 +1,12 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from "../http-common.js"; +import { HttpStatusCode } from "../http-status-codes.js"; import { createPlatformHttpLib } from "../http.js"; import { TalerBankIntegrationApi, codecForBankWithdrawalOperationPostResponse, codecForBankWithdrawalOperationStatus } from "./types.js"; +import { opSuccess, opUnknownFailure } from "./utils.js"; export class TalerBankIntegrationHttpClient { httpLib: HttpRequestLibrary; @@ -35,12 +37,16 @@ export class TalerBankIntegrationHttpClient { * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid * */ - async completeWithdrawalOperationById(woid: string): Promise<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse> { + async completeWithdrawalOperationById(woid: string, body: TalerBankIntegrationApi.BankWithdrawalOperationPostRequest) { const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", + body, }); - return readSuccessResponseJsonOrThrow(resp, codecForBankWithdrawalOperationPostResponse()); + switch (resp.status) { + case HttpStatusCode.Ok: return opSuccess(resp, codecForBankWithdrawalOperationPostResponse()) + default: return opUnknownFailure(resp, await resp.text()) + } } } diff --git a/packages/taler-util/src/http-client/test.cli.ts b/packages/taler-util/src/http-client/test.cli.ts new file mode 100644 index 000000000..08a0c5fd3 --- /dev/null +++ b/packages/taler-util/src/http-client/test.cli.ts @@ -0,0 +1,77 @@ +import { HttpLibImpl, setShowCurlRequest as setPrintHttpRequestAsCurl } from "../http-impl.node.js" +import { AccessToken, TalerError } from "../index.js" +import { TalerCoreBankHttpClient, createRandomTestUser, succeedOrThrow } from "./bank-core.js" + + +const baseUrl = process.argv[2] +const admin = process.argv[3] as AccessToken +// const usrpwd = process.argv[4] + +if (!baseUrl) { + console.error("missing baseUrl") + process.exit(1) +} + +console.log("trying against ", baseUrl) + +const api = new TalerCoreBankHttpClient(baseUrl, new HttpLibImpl()) +try { + process.stdout.write("config: "); + const config = await api.testConfig() + console.log("ok") + + setPrintHttpRequestAsCurl(true) + + process.stdout.write("account management: "); + const withAdmin = !!admin && admin !== "-" + if (withAdmin) { + await api.testAccountManagement(admin) + console.log("ok") + } else { + console.log("skipped") + } + + process.stdout.write("transactions: "); + if (withAdmin) { + await api.testTransactions(admin) + console.log("ok") + } else { + console.log("skipped") + } + + process.stdout.write("withdrawals: "); + if (withAdmin) { + await api.testWithdrawals(admin) + console.log("ok") + } else { + console.log("skipped") + } + + process.stdout.write("monitor: "); + if (withAdmin && config.have_cashout) { + await api.testMonitor(admin) + console.log("ok") + } else { + console.log("skipped") + } + + process.stdout.write("cashout: "); + if (withAdmin && config.have_cashout) { + await api.testCashouts(admin) + console.log("ok") + } else { + console.log("skipped") + } + +} catch (e: any) { + console.log("") + if (e instanceof TalerError) { + console.error("FAILED", JSON.stringify(e.errorDetail, undefined, 2)) + console.error(e.stack) + } else if (e instanceof Error) { + console.error(`FAILED: ${e.message}`) + console.error(e.stack) + } else { + console.error(`FAILED: ${e}`) + } +} diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts index 528d303be..11ae9480c 100644 --- a/packages/taler-util/src/http-impl.node.ts +++ b/packages/taler-util/src/http-impl.node.ts @@ -56,6 +56,10 @@ if ( const logger = new Logger("http-impl.node.ts"); const textDecoder = new TextDecoder(); +let SHOW_CURL_HTTP_REQUEST = false; +export function setShowCurlRequest(b: boolean) { + SHOW_CURL_HTTP_REQUEST = b +} /** * Implementation of the HTTP request library interface for node. @@ -115,7 +119,7 @@ export class HttpLibImpl implements HttpRequestLibrary { let reqBody: ArrayBuffer | undefined; - if (opt?.method == "POST") { + if (opt?.method == "POST" || opt?.method == "PATCH" || opt?.method == "PUT") { reqBody = encodeBody(opt.body); } @@ -145,6 +149,18 @@ export class HttpLibImpl implements HttpRequestLibrary { const chunks: Uint8Array[] = []; + if (SHOW_CURL_HTTP_REQUEST) { + const payload = !reqBody || reqBody.byteLength === 0 ? undefined : textDecoder.decode(reqBody) + const headers = Object.entries(requestHeadersMap).reduce((prev, [key, value]) => { + return `${prev} -H "${key}: ${value}"` + }, "") + function ifUndefined<T>(arg: string, v: undefined | T): string { + if (v === undefined) return "" + return arg + " '" + String(v) + "'" + } + console.log(`curl -X ${options.method} ${parsedUrl.href} ${ifUndefined("-d", payload)} ${headers}`) + } + return new Promise((resolve, reject) => { const handler = (res: http.IncomingMessage) => { res.on("data", (d) => { |