Skip to content

Add FinTS mock test server for integration testing#40

Closed
Copilot wants to merge 3 commits into
masterfrom
copilot/setup-test-fints-server
Closed

Add FinTS mock test server for integration testing#40
Copilot wants to merge 3 commits into
masterfrom
copilot/setup-test-fints-server

Conversation

Copilot AI commented Apr 15, 2026

Copy link
Copy Markdown

Testing this library requires a real bank connection. This adds a self-contained FinTS 3.0 mock server (packages/fints-test-server) that speaks the actual protocol over HTTP, enabling deterministic integration tests against PinTanClient.

Server implementation

  • protocol.ts — FinTS message parser/encoder (Base64, escape sequences, @length@ binary data)
  • message-builder.ts — Constructs spec-compliant response envelopes (HNHBK/HNVSK/HNVSD/HNHBS) with proper segment numbering and length calculation
  • request-handler.ts — Stateful dialog session management, processes all major segments: HKSYN, HKIDN/HKVVB, HKSPA, HKSAL (v4-v7), HKKAZ (v4-v7 with MT940), HKCDB, HKCCS, HKDSE, HKTAN, HKEND
  • test-data.ts — Configurable fixtures (accounts, balances, MT940 transactions) with sensible defaults
  • server.ts — HTTP POST endpoint accepting/returning Base64-encoded messages per FinTS transport spec

Key design decisions

  • Account lookup handles both v≤6 (accountNumber:sub:country:blz) and v7 (iban:bic:accountNumber:...) formats
  • Dialog counter is instance-scoped for proper test isolation
  • TAN requirement is configurable per-instance (requireTan: true) to test challenge flows
  • PIN validation rejects unknown credentials with proper 9931 error codes

Usage

const server = new FinTSServer();
await server.start();

const client = new PinTanClient({
  blz: "12345678", name: "testuser", pin: "12345",
  url: server.url, productId: "fints",
});

const accounts = await client.accounts();       // 2 accounts
const balance = await client.balance(accounts[0]); // 1234.56 EUR
const stmts = await client.statements(accounts[0], start, end); // MT940

await server.stop();

Tests

  • 30 unit tests (protocol parsing, message building, request handling)
  • 8 integration tests exercising PinTanClient end-to-end (accounts, balance, statements, standing orders, TAN flow, invalid PIN, custom config)
  • All 309 existing tests unaffected

Copilot AI and others added 3 commits April 14, 2026 23:59
@larsdecker larsdecker marked this pull request as ready for review April 15, 2026 18:41
Copilot AI review requested due to automatic review settings April 15, 2026 18:41

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33e8705160

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

profileVersion, msgNo, "9210", "Konto nicht gefunden");
}

const balance = this.config.balances.find((b) => b.accountNumber === accountNumber);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use matched account number for statement balance lookup

When an HKKAZ v7 request is matched via IBAN fallback, account is resolved correctly but balance is still looked up with the original accountNumber token from the request. In IBAN-only requests that token is the IBAN, so the lookup misses and openingBalance falls back to 0, which yields incorrect MT940 balances for a valid account. Use the resolved account.accountNumber for the balance query.

Useful? React with 👍 / 👎.

req.on("end", () => {
try {
// Decode the Base64 request
const requestStr = decodeBase64(body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse form body before Base64 decoding

The server claims to accept FinTS HTTP requests as application/x-www-form-urlencoded, but it always runs decodeBase64(body) on the raw body. For standard form payloads like msg=<base64>, this produces an invalid FinTS message and the request path returns 500, so form-encoded clients cannot use this mock endpoint. Extract and URL-decode the form field before Base64 decoding.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new self-contained FinTS 3.0 mock HTTP server (packages/fints-test-server) intended to enable deterministic integration tests for PinTanClient without requiring a real bank connection.

Changes:

  • Introduces fints-test-server package: FinTS protocol parsing, response message building, and a basic HTTP endpoint.
  • Implements a stateful request handler for core dialog flow and major business segments (HKSPA/HKSAL/HKKAZ/HKCDB/HKCCS/HKDSE/HKTAN/HKEND).
  • Adds unit + integration tests for the protocol, message builder, request handler, and end-to-end client interactions.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
yarn.lock Updates dependency lockfile to include the new workspace package and related dependency graph changes.
packages/fints-test-server/package.json Defines the new package metadata, scripts, and dependencies.
packages/fints-test-server/tsconfig.json TypeScript compiler configuration for the new package.
packages/fints-test-server/jest.config.js Jest/ts-jest configuration for the new package’s tests.
packages/fints-test-server/README.md Documentation for using/configuring the test server.
packages/fints-test-server/src/index.ts Public exports for server, handler, protocol helpers, and test-data helpers.
packages/fints-test-server/src/server.ts HTTP POST server that decodes Base64 requests and returns Base64 responses.
packages/fints-test-server/src/request-handler.ts Core FinTS message processing, dialog state, and segment handling.
packages/fints-test-server/src/message-builder.ts FinTS envelope + segment builder utilities (HNHBK/HNVSK/HNVSD/HNHBS, HIRMG/HIRMS, etc.).
packages/fints-test-server/src/protocol.ts FinTS parsing/escaping and Base64 encode/decode utilities.
packages/fints-test-server/src/test-data.ts Default fixtures + MT940 generator for statements.
packages/fints-test-server/src/tests/test-protocol.ts Unit tests for parser/encoding helpers.
packages/fints-test-server/src/tests/test-message-builder.ts Unit tests for message construction.
packages/fints-test-server/src/tests/test-request-handler.ts Unit tests for request handling + dialog flow.
packages/fints-test-server/src/tests/test-server-integration.ts Integration tests running PinTanClient against the mock server.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to +174
// Decode the Base64 request
const requestStr = decodeBase64(body);
Comment on lines +228 to +230
? [{ code: "0901", message: "*PIN gultig." }]
: [{ code: "9931", message: "PIN ungueltig." }]),
{ code: "0020", message: "*Dialoginitialisierung erfolgreich" },
Comment on lines +90 to +122
// Count total inner segments (HNSHK + payload + HNSHA)
const totalInnerCount = 2 + innerSegments.length; // HNSHK + payload segments + HNSHA
const segCount = totalInnerCount + 1; // +1 because HNSHA segNo = 2 + innerSegments.length

// HNSHA: Signature footer
const hnsha = buildSegment("HNSHA", 2 + innerSegments.length + 1, 2, [
formatNum(secRef),
]);

// All content inside HNVSD
const innerContent = hnshk + innerSegments.join("") + hnsha;

// HNVSD: Encrypted data container
const hnvsd = buildSegment("HNVSD", 999, 1, [
formatStringWithLength(innerContent),
]);

// HNVSK: Encryption header
const hnvsk = buildSegment("HNVSK", 998, 3, [
["PIN", formatNum(profileVersion)],
formatNum(998),
formatNum(1),
["1", "", systemId],
["1", dateStr, timeStr],
["2", "2", "13", formatStringWithLength("00000000"), "5", "1"],
[formatNum(COUNTRY_CODE), blz, escapeFinTS(userName), "V", formatNum(0), formatNum(0)],
formatNum(0),
]);

// HNHBS: Message footer
const hnhbs = buildSegment("HNHBS", segCount + 2, 1, [
formatNum(msgNo),
]);
{ code: "0010", message: "Nachricht entgegengenommen." },
]));
innerSegs.push(buildHIRMS(segNo++, [
{ code: "0030", message: "Auftrag empfangen - Loss bitte eine TAN ein" },
Comment on lines +2 to +13
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
},
"files": [
"dist",
"LICENSE",
): string {
if (!isValidUser) {
return this.buildErrorMessage("0", userName, "0", profileVersion, msgNo,
"9931", "PIN ungueltig.");
Comment on lines +45 to +47
/** Date of the transaction (yyyyMMdd) */
date: string;
/** Booking date (yyyyMMdd) */
"^.+\\.ts$": ["ts-jest", {
tsconfig: {
experimentalDecorators: true,
strict: false,
@larsdecker larsdecker closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants