Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions packages/fints-test-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# FinTS Test Server

A mock FinTS 3.0 banking server for integration testing. Implements the full FinTS protocol over HTTP with configurable test data.

## Features

- Full FinTS 3.0 protocol implementation (PIN/TAN)
- HTTP server accepting Base64-encoded FinTS messages
- Configurable test data (accounts, balances, transactions)
- MT940-format transaction statements
- Dialog lifecycle management (sync → init → request → end)
- PIN authentication validation
- Optional TAN challenge flow
- SEPA account listing, balance queries, statement retrieval
- Standing orders, credit transfers, direct debits

## Supported Operations

| Segment | Operation | Description |
|---------|-----------|-------------|
| HKSYN | Synchronization | System ID, BPD, TAN methods |
| HKIDN/HKVVB | Dialog Init | PIN/TAN authentication |
| HKSPA | SEPA Accounts | Account listing |
| HKSAL | Balance | Account balance query |
| HKKAZ | Statements | Transaction list (MT940) |
| HKCDB | Standing Orders | Recurring payment list |
| HKCCS | Credit Transfer | SEPA transfer |
| HKDSE | Direct Debit | SEPA direct debit |
| HKTAN | TAN | Challenge/response handling |
| HKEND | Dialog End | Session termination |

## Usage

### Basic Usage

```typescript
import { FinTSServer } from "fints-test-server";
import { PinTanClient } from "fints-lib";

// Start test server
const server = new FinTSServer({ port: 3000 });
await server.start();

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

const accounts = await client.accounts();
console.log(accounts);
// [
// { iban: "DE111234567800000001", bic: "GENODE00TES", accountNumber: "1", ... },
// { iban: "DE111234567800000002", bic: "GENODE00TES", accountNumber: "2", ... }
// ]

const balance = await client.balance(accounts[0]);
console.log(balance);
// { bookedBalance: 1234.56, availableBalance: 6234.56, creditLimit: 5000, currency: "EUR" }

await server.stop();
```

### Custom Configuration

```typescript
import { FinTSServer, createDefaultConfig } from "fints-test-server";

const config = createDefaultConfig();

// Customize accounts
config.accounts = [
{
iban: "DE99999999990000000001",
bic: "TESTDEFFXXX",
accountNumber: "42",
subAccount: "",
blz: "12345678",
currency: "EUR",
ownerName: "Custom User",
accountName: "Test Account",
},
];

// Customize balances
config.balances = [
{
accountNumber: "42",
productName: "Test Account",
currency: "EUR",
bookedBalance: 9999.99,
pendingBalance: 9999.99,
creditLimit: 0,
availableBalance: 9999.99,
},
];

// Customize transactions
config.transactions = {
"42": [
{
date: "180901",
bookingDate: "180901",
amount: 1500.00,
currency: "EUR",
counterpartyName: "Employer Inc",
purpose: "Salary September",
eref: "SAL-2018-09",
},
],
};

// Enable TAN requirement for transfers
config.requireTan = true;

const server = new FinTSServer({ config });
await server.start();
```

### Jest Integration

```typescript
import { FinTSServer, createDefaultConfig } from "fints-test-server";
import { PinTanClient } from "fints-lib";

describe("Banking integration", () => {
let server: FinTSServer;

beforeEach(async () => {
server = new FinTSServer();
await server.start();
});

afterEach(async () => {
await server.stop();
});

it("should fetch accounts", async () => {
const client = new PinTanClient({
blz: "12345678",
name: "testuser",
pin: "12345",
url: server.url,
});

const accounts = await client.accounts();
expect(accounts.length).toBe(2);
});
});
```

## Configuration

### `FinTSTestConfig`

| Property | Type | Description |
|----------|------|-------------|
| `blz` | `string` | Bank code (Bankleitzahl) |
| `bankName` | `string` | Bank display name |
| `bpdVersion` | `number` | BPD version number |
| `url` | `string` | Server URL (for HIKOM) |
| `users` | `TestUser[]` | Authorized users (name + PIN) |
| `accounts` | `TestAccount[]` | SEPA accounts |
| `balances` | `TestBalance[]` | Account balances |
| `transactions` | `Record<string, TestTransaction[]>` | Transactions per account |
| `painFormats` | `string[]` | Supported SEPA pain formats |
| `requireTan` | `boolean` | Require TAN for transfers |

## Default Test Data

The default configuration includes:
- **Bank**: "FinTS Test Bank" (BLZ: 12345678)
- **User**: testuser / 12345
- **Account 1**: DE111234567800000001 (Girokonto, balance: 1234.56 EUR)
- **Account 2**: DE111234567800000002 (Tagesgeld, balance: 10000.00 EUR)
- **Transactions**: 5 sample transactions on Account 1, 1 on Account 2
- **TAN Method**: mobile TAN (security function 942)
- **SEPA Formats**: pain.001.001.03, pain.001.002.03, pain.008.002.02, etc.
21 changes: 21 additions & 0 deletions packages/fints-test-server/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/__tests__/**/*.ts"],
collectCoverage: true,
transform: {
"^.+\\.ts$": ["ts-jest", {
tsconfig: {
experimentalDecorators: true,
strict: false,
},
diagnostics: {
pathRegex: ".*test-.*\\.ts$",
},
}],
},
transformIgnorePatterns: [
"/node_modules/(?!fints-lib)",
],
moduleFileExtensions: ["ts", "js", "json"],
};
34 changes: 34 additions & 0 deletions packages/fints-test-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "fints-test-server",
"version": "0.1.0",
"description": "A mock FinTS server for integration testing",
"keywords": [
"fints",
"hbci",
"testing",
"mock-server"
],
"scripts": {
"build": "tsc -p ./tsconfig.json",
"test": "TZ=UTC jest"
},
"files": [
"dist",
"LICENSE",
"package.json",
"README.md"
],
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"jest": "^30.2.0",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
},
"dependencies": {
"iconv-lite": "^0.7.1"
}
}
105 changes: 105 additions & 0 deletions packages/fints-test-server/src/__tests__/test-message-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { buildSegment, buildMessage, buildHIRMG, buildHIRMS, buildHISPA, buildHISAL } from "../message-builder";
import { parse, parseSegment } from "../protocol";

describe("message-builder", () => {
describe("buildSegment", () => {
it("should build a simple segment", () => {
const seg = buildSegment("HNHBS", 5, 1, ["1"]);
expect(seg).toBe("HNHBS:5:1+1'");
});

it("should build segment with data group arrays", () => {
const seg = buildSegment("HKIDN", 2, 2, [["280", "12345678"], "user", "0", "1"]);
expect(seg).toBe("HKIDN:2:2+280:12345678+user+0+1'");
});

it("should build segment with reference", () => {
const seg = buildSegment("HIRMS", 4, 2, [["0020", "", "text"]], 3);
expect(seg).toBe("HIRMS:4:2:3+0020::text'");
});
});

describe("buildMessage", () => {
it("should build a complete message with envelope", () => {
const msg = buildMessage({
dialogId: "DIA_TEST",
msgNo: 1,
blz: "12345678",
userName: "testuser",
systemId: "SYS001",
profileVersion: 1,
innerSegments: [

Check failure on line 31 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Replace `⏎····················buildHIRMG(3,·[{·code:·"0010",·message:·"OK"·}]),⏎················` with `buildHIRMG(3,·[{·code:·"0010",·message:·"OK"·}])`

Check failure on line 31 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Replace `⏎····················buildHIRMG(3,·[{·code:·"0010",·message:·"OK"·}]),⏎················` with `buildHIRMG(3,·[{·code:·"0010",·message:·"OK"·}])`
buildHIRMG(3, [{ code: "0010", message: "OK" }]),
],
});

// Should start with HNHBK
expect(msg.startsWith("HNHBK:1:3+")).toBe(true);
// Should contain HNVSK
expect(msg).toContain("HNVSK:998:3+");
// Should contain HNVSD
expect(msg).toContain("HNVSD:999:1+");
// Should contain HNHBS
expect(msg).toContain("HNHBS:");
// Should contain our inner segment
expect(msg).toContain("HIRMG:");
// Should end with segment terminator
expect(msg.endsWith("'")).toBe(true);
});

it("should be parseable by the FinTS parser", () => {
const msg = buildMessage({
dialogId: "DIA_TEST",
msgNo: 1,
blz: "12345678",
userName: "testuser",
systemId: "0",
profileVersion: 1,
innerSegments: [
buildHIRMG(3, [{ code: "0010", message: "OK" }]),
buildHIRMS(4, [{ code: "0020", message: "Auftrag ausgefuehrt" }], 3),
],
});

// Parse should not throw
const segments = parse(msg);
expect(segments.length).toBeGreaterThan(0);

// First segment should be HNHBK
const hnhbk = parseSegment(segments[0]);
expect(hnhbk.type).toBe("HNHBK");
});
});

describe("buildHISPA", () => {
it("should build SEPA accounts segment", () => {
const seg = buildHISPA(5, [

Check failure on line 76 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Replace `5,` with `⏎················5,⏎···············`

Check failure on line 76 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Replace `5,` with `⏎················5,⏎···············`
{ iban: "DE111234567800000001", bic: "GENODE00TES", accountNumber: "1", subAccount: "", blz: "12345678" },

Check failure on line 77 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Replace `{·iban:·"DE111234567800000001",·bic:·"GENODE00TES",·accountNumber:·"1",·subAccount:·"",·blz:·"12345678"` with `····{⏎························iban:·"DE111234567800000001",⏎························bic:·"GENODE00TES",⏎························accountNumber:·"1",⏎························subAccount:·"",⏎························blz:·"12345678",⏎···················`

Check failure on line 77 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Replace `{·iban:·"DE111234567800000001",·bic:·"GENODE00TES",·accountNumber:·"1",·subAccount:·"",·blz:·"12345678"` with `····{⏎························iban:·"DE111234567800000001",⏎························bic:·"GENODE00TES",⏎························accountNumber:·"1",⏎························subAccount:·"",⏎························blz:·"12345678",⏎···················`
], 3);

Check failure on line 78 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Replace `],·3` with `····],⏎················3,⏎············`

Check failure on line 78 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Replace `],·3` with `····],⏎················3,⏎············`

expect(seg).toContain("HISPA:5:1:3+");
expect(seg).toContain("DE111234567800000001");
expect(seg).toContain("GENODE00TES");
});
});

describe("buildHISAL", () => {
it("should build balance segment", () => {
const seg = buildHISAL(5, {

Check failure on line 88 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Replace `5,` with `⏎················5,⏎···············`

Check failure on line 88 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Replace `5,` with `⏎················5,⏎···············`
accountNumber: "1",

Check failure on line 89 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Insert `····`

Check failure on line 89 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Insert `····`
subAccount: "",

Check failure on line 90 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Insert `····`

Check failure on line 90 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Insert `····`
blz: "12345678",

Check failure on line 91 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Insert `····`

Check failure on line 91 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Insert `····`
productName: "Girokonto",

Check failure on line 92 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Insert `····`

Check failure on line 92 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Insert `····`
currency: "EUR",

Check failure on line 93 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (20)

Insert `····`

Check failure on line 93 in packages/fints-test-server/src/__tests__/test-message-builder.ts

View workflow job for this annotation

GitHub Actions / test (22)

Insert `····`
bookedBalance: 1234.56,
pendingBalance: 1234.56,
creditLimit: 5000,
availableBalance: 6234.56,
}, 3);

expect(seg).toContain("HISAL:5:7:3+");
expect(seg).toContain("1234,56");
expect(seg).toContain("Girokonto");
});
});
});
Loading
Loading