[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-typescript-core] 03/03: harness: implement simple exchange linter
From: |
gnunet |
Subject: |
[taler-typescript-core] 03/03: harness: implement simple exchange linter |
Date: |
Mon, 27 Jan 2025 12:26:29 +0100 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository taler-typescript-core.
commit 122d7370e81f0df3f906de37187ade7e4ddb91c1
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Jan 27 12:26:21 2025 +0100
harness: implement simple exchange linter
---
packages/taler-harness/src/index.ts | 19 ++-
.../integrationtests/test-exchange-timetravel.ts | 10 +-
packages/taler-harness/src/lint.ts | 157 ++++++++++++++++++++-
3 files changed, 169 insertions(+), 17 deletions(-)
diff --git a/packages/taler-harness/src/index.ts
b/packages/taler-harness/src/index.ts
index 4ae4e4562..31ff52336 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -88,7 +88,7 @@ import {
} from "./harness/harness.js";
import { AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG } from
"./integrationtests/test-kyc-new-measures-prog.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
-import { lintExchangeDeployment } from "./lint.js";
+import { lintExchangeDeployment, lintExchangeUrl } from "./lint.js";
const logger = new Logger("taler-harness:index.ts");
@@ -604,8 +604,8 @@ deploymentCli
});
deploymentCli
- .subcommand("lintExchange", "lint-exchange", {
- help: "Run checks on the exchange deployment.",
+ .subcommand("lintExchangeConfig", "lint-exchange-config", {
+ help: "Run checks on the exchange deployment running on the current
machine.",
})
.flag("cont", ["--continue"], {
help: "Continue after errors if possible",
@@ -615,11 +615,20 @@ deploymentCli
})
.action(async (args) => {
await lintExchangeDeployment(
- args.lintExchange.debug,
- args.lintExchange.cont,
+ args.lintExchangeConfig.debug,
+ args.lintExchangeConfig.cont,
);
});
+deploymentCli
+ .subcommand("lintExchangeUrl", "lint-exchange-url", {
+ help: "Run checks on a remote exchange deployment.",
+ })
+ .requiredArgument("baseUrl", clk.STRING)
+ .action(async (args) => {
+ await lintExchangeUrl(args.lintExchangeUrl.baseUrl);
+ });
+
deploymentCli
.subcommand("waitService", "wait-taler-service", {
help: "Wait for the config endpoint of a Taler-style service to be
available",
diff --git
a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
index c501baf8d..764b1ae55 100644
--- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -19,11 +19,11 @@
*/
import {
AbsoluteTime,
- codecForExchangeKeysJson,
+ codecForExchangeKeysResponse,
DenominationPubKey,
DenomKeyType,
Duration,
- ExchangeKeysJson,
+ ExchangeKeysResponse,
Logger,
TalerCorebankApiClient,
} from "@gnu-taler/taler-util";
@@ -53,7 +53,7 @@ interface DenomInfo {
expireDeposit: string;
}
-function getDenomInfoFromKeys(ek: ExchangeKeysJson): DenomInfo[] {
+function getDenomInfoFromKeys(ek: ExchangeKeysResponse): DenomInfo[] {
const denomInfos: DenomInfo[] = [];
for (const denomGroup of ek.denominations) {
switch (denomGroup.cipher) {
@@ -201,7 +201,7 @@ export async function runExchangeTimetravelTest(t:
GlobalTestState) {
const keysResp1 = await http.fetch(exchange.baseUrl + "keys");
const keys1 = await readSuccessResponseJsonOrThrow(
keysResp1,
- codecForExchangeKeysJson(),
+ codecForExchangeKeysResponse(),
);
console.log(
"keys 1 (before time travel):",
@@ -223,7 +223,7 @@ export async function runExchangeTimetravelTest(t:
GlobalTestState) {
const keysResp2 = await http.fetch(exchange.baseUrl + "keys");
const keys2 = await readSuccessResponseJsonOrThrow(
keysResp2,
- codecForExchangeKeysJson(),
+ codecForExchangeKeysResponse(),
);
console.log(
"keys 2 (after time travel):",
diff --git a/packages/taler-harness/src/lint.ts
b/packages/taler-harness/src/lint.ts
index bb6d70ed4..ade31c088 100644
--- a/packages/taler-harness/src/lint.ts
+++ b/packages/taler-harness/src/lint.ts
@@ -32,18 +32,24 @@
* Imports.
*/
import {
- codecForExchangeKeysJson,
+ AbsoluteTime,
+ canonicalizeBaseUrl,
+ codecForExchangeKeysResponse,
codecForKeysManagementResponse,
Configuration,
decodeCrock,
+ narrowOpSuccessOrThrow,
+ parsePaytoUri,
+ TalerExchangeHttpClient,
+ TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
-import { URL } from "node:url";
-import { spawn } from "child_process";
-import { delayMs } from "./harness/harness.js";
import {
createPlatformHttpLib,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
+import { spawn } from "child_process";
+import { URL } from "node:url";
+import { delayMs } from "./harness/harness.js";
interface BasicConf {
mainCurrency: string;
@@ -430,7 +436,10 @@ export async function checkExchangeHttpd(
{
const keysUrl = new URL("keys", baseUrl);
- const resp = await Promise.race([httpLib.fetch(keysUrl.href),
delayMs(2000)]);
+ const resp = await Promise.race([
+ httpLib.fetch(keysUrl.href),
+ delayMs(2000),
+ ]);
if (!resp) {
context.numErr++;
@@ -446,7 +455,7 @@ export async function checkExchangeHttpd(
} else {
const keys = await readSuccessResponseJsonOrThrow(
resp,
- codecForExchangeKeysJson(),
+ codecForExchangeKeysResponse(),
);
if (keys.master_public_key !== pubConf.masterPublicKey) {
@@ -466,7 +475,10 @@ export async function checkExchangeHttpd(
{
const keysUrl = new URL("wire", baseUrl);
- const resp = await Promise.race([httpLib.fetch(keysUrl.href),
delayMs(2000)]);
+ const resp = await Promise.race([
+ httpLib.fetch(keysUrl.href),
+ delayMs(2000),
+ ]);
if (!resp) {
context.numErr++;
@@ -534,3 +546,134 @@ export async function lintExchangeDeployment(
process.exit(1);
}
}
+
+function isCurrentlyValid(
+ start: TalerProtocolTimestamp,
+ end: TalerProtocolTimestamp,
+): boolean {
+ if (!AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(start))) {
+ return false;
+ }
+ if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(end))) {
+ return false;
+ }
+ return true;
+}
+
+class LintReporter {
+ numErrors = 0;
+ reportError(s: string): void {
+ this.numErrors++;
+ console.log(`ERROR: ${s}`);
+ }
+ reportWarning(s: string): void {
+ console.log(`WARNING: ${s}`);
+ }
+ reportInfo(s: string): void {
+ console.log(`INFO: ${s}`);
+ }
+}
+
+export async function lintExchangeUrl(exchangeBaseUrl: string): Promise<void> {
+ const canonUrl = canonicalizeBaseUrl(exchangeBaseUrl);
+ if (exchangeBaseUrl != canonUrl) {
+ console.error(`invalid or non-canonical base url: ${exchangeBaseUrl}`);
+ process.exit(1);
+ }
+ const exchangeClient = new TalerExchangeHttpClient(canonUrl);
+ const configResp = await exchangeClient.getConfig();
+ narrowOpSuccessOrThrow("", configResp);
+ const currency = configResp.body.currency;
+ const keysResp = await exchangeClient.getKeys();
+ narrowOpSuccessOrThrow("", configResp);
+
+ const reporter = new LintReporter();
+
+ if (keysResp.body.currency != currency) {
+ reporter.reportError("currency mismatch between /config and /keys");
+ }
+
+ if (keysResp.body.signkeys.length == 0) {
+ reporter.reportError("exchange has no signing keys");
+ }
+ // Check for valid signing key
+ {
+ let haveValidSk = false;
+ for (const sk of keysResp.body.signkeys) {
+ if (isCurrentlyValid(sk.stamp_start, sk.stamp_end)) {
+ haveValidSk = true;
+ break;
+ }
+ }
+ if (!haveValidSk) {
+ reporter.reportError("no signing key is valid");
+ }
+ }
+ let numDenomWithdraw = 0;
+ let numDenomDeposit = 0;
+ let numDenomLost = 0;
+ // Check denominations
+ {
+ for (const denomGroup of keysResp.body.denominations) {
+ for (const denom of denomGroup.denoms) {
+ if (denom.lost) {
+ numDenomLost++;
+ }
+ if (
+ !denom.lost &&
+ isCurrentlyValid(denom.stamp_start, denom.stamp_expire_withdraw)
+ ) {
+ numDenomWithdraw++;
+ }
+ if (isCurrentlyValid(denom.stamp_start, denom.stamp_expire_deposit)) {
+ numDenomDeposit++;
+ }
+ }
+ }
+ if (numDenomWithdraw == 0) {
+ reporter.reportWarning("no valid denomination available for withdrawal");
+ }
+ if (numDenomDeposit == 0) {
+ reporter.reportWarning("no valid denomination available for deposit");
+ }
+ if (numDenomLost > 0) {
+ reporter.reportWarning(`have lost denominations (${numDenomLost})`);
+ }
+ }
+ {
+ let haveGlobalFee = false;
+ for (const gf of keysResp.body.global_fees) {
+ if (isCurrentlyValid(gf.start_date, gf.end_date)) {
+ haveGlobalFee = true;
+ break;
+ }
+ }
+ if (!haveGlobalFee) {
+ reporter.reportError("no valid global fees for current time");
+ }
+ }
+ if (keysResp.body.accounts.length === 0) {
+ reporter.reportError("no wire accounts configured");
+ }
+ let wireTypesSet = new Set<string>();
+ for (const acc of keysResp.body.accounts) {
+ const payto = parsePaytoUri(acc.payto_uri);
+ if (!payto) {
+ reporter.reportError(`invalid account payto URI: ${acc.payto_uri}`);
+ continue;
+ }
+ wireTypesSet.add(payto?.targetType);
+ }
+ for (const wireType of wireTypesSet) {
+ let haveAccountFees = false;
+ for (const wf of keysResp.body.wire_fees[wireType]) {
+ if (isCurrentlyValid(wf.start_date, wf.end_date)) {
+ haveAccountFees = true;
+ break;
+ }
+ }
+ if (!haveAccountFees) {
+ reporter.reportError(`missing wire account fees for ${wireType}`);
+ }
+ }
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.