aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/demobank-ui/src/demobank-ui-settings.js2
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts594
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts10
-rw-r--r--packages/taler-util/src/http-client/test.cli.ts77
-rw-r--r--packages/taler-util/src/http-impl.node.ts18
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) => {