use assert_cmd::Command;
use predicates::prelude::*;
#[allow(unused_imports)]
use tempfile;
struct TestServer {
port: u16,
child: std::process::Child,
}
impl TestServer {
fn start(extra_args: &[&str]) -> Option<Self> {
let bin = assert_cmd::cargo::cargo_bin("fints-server");
if !bin.exists() {
return None;
}
let mut child = std::process::Command::new(&bin)
.args(["--port", "0", "--print-ready"])
.args(extra_args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
use std::io::BufRead;
let stdout = child.stdout.take()?;
let mut reader = std::io::BufReader::new(stdout);
let mut line = String::new();
reader.read_line(&mut line).ok()?;
let port: u16 = line.trim().strip_prefix("READY:")?.parse().ok()?;
std::thread::sleep(std::time::Duration::from_millis(100));
Some(TestServer { port, child })
}
fn url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}
}
impl Drop for TestServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn test_server_starts_and_responds() {
let server = match TestServer::start(&[]) {
Some(s) => s,
None => {
eprintln!("Skipping: fints-server not built");
return;
}
};
assert!(server.port > 0, "Server port should be non-zero");
use std::io::Write;
let addr = format!("127.0.0.1:{}", server.port);
match std::net::TcpStream::connect(&addr) {
Ok(mut stream) => {
let request = "POST / HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\nContent-Type: text/plain\r\n\r\n";
let _ = stream.write_all(request.as_bytes());
}
Err(e) => {
eprintln!("Could not connect to server at {}: {}", addr, e);
}
}
}
#[test]
fn test_client_banks_command() {
let bin = assert_cmd::cargo::cargo_bin("fints-client");
if !bin.exists() {
eprintln!("Skipping: fints-client not built");
return;
}
let mut cmd = Command::new(&bin);
cmd.arg("banks");
cmd.assert()
.success()
.stdout(predicate::str::contains("DKB").or(predicate::str::contains("dkb")));
}
#[test]
fn test_client_sync_against_mock_server() {
let server = match TestServer::start(&["--tan-mode", "none"]) {
Some(s) => s,
None => {
eprintln!("Skipping: fints-server not built");
return;
}
};
let bin = assert_cmd::cargo::cargo_bin("fints-client");
if !bin.exists() {
eprintln!("Skipping: fints-client not built");
return;
}
let url = server.url();
let tmp_dir = tempfile::tempdir().expect("tmpdir");
let mut cmd = Command::new(&bin);
cmd.args([
"--bank",
"custom",
"--url",
&url,
"--blz",
"12345678",
"--user",
"test1",
"--pin",
"1234",
"--session-dir",
tmp_dir.path().to_str().unwrap(),
"sync",
"--iban",
"DE89370400440532013000",
"--bic",
"GENODE23X42",
]);
let output = cmd.output().expect("Failed to run fints-client");
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1,523")
|| stdout.contains("1523")
|| stdout.contains("Balance")
|| stdout.contains("balance"),
"Expected balance output containing balance amount, got: {}",
stdout
);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!(
"fints-client sync exited with non-zero: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
stderr
);
}
}
#[test]
fn test_client_wrong_pin() {
let server = match TestServer::start(&[]) {
Some(s) => s,
None => {
eprintln!("Skipping: fints-server not built");
return;
}
};
let bin = assert_cmd::cargo::cargo_bin("fints-client");
if !bin.exists() {
eprintln!("Skipping: fints-client not built");
return;
}
let url = server.url();
let tmp_dir = tempfile::tempdir().expect("tmpdir");
let mut cmd = Command::new(&bin);
cmd.args([
"--bank",
"custom",
"--url",
&url,
"--blz",
"12345678",
"--user",
"test1",
"--pin",
"wrong_pin_1234",
"--session-dir",
tmp_dir.path().to_str().unwrap(),
"balance",
"--iban",
"DE89370400440532013000",
"--bic",
"GENODE23X42",
]);
let output = cmd.output().expect("Failed to run fints-client");
if output.status.success() {
eprintln!("Warning: server accepted wrong PIN (may not validate PINs)");
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stdout, stderr);
assert!(
combined.to_lowercase().contains("pin")
|| combined.to_lowercase().contains("auth")
|| combined.to_lowercase().contains("9340")
|| combined.to_lowercase().contains("error")
|| !combined.is_empty(),
"Expected error output for wrong PIN, got stdout='{}' stderr='{}'",
stdout,
stderr
);
}
}
#[test]
fn test_client_balance_json_output() {
let server = match TestServer::start(&["--tan-mode", "none"]) {
Some(s) => s,
None => {
eprintln!("Skipping: fints-server not built");
return;
}
};
let bin = assert_cmd::cargo::cargo_bin("fints-client");
if !bin.exists() {
eprintln!("Skipping: fints-client not built");
return;
}
let url = server.url();
let tmp_dir = tempfile::tempdir().expect("tmpdir");
let mut cmd = Command::new(&bin);
cmd.args([
"--bank",
"custom",
"--url",
&url,
"--blz",
"12345678",
"--user",
"test1",
"--pin",
"1234",
"--session-dir",
tmp_dir.path().to_str().unwrap(),
"--output",
"json",
"balance",
"--iban",
"DE89370400440532013000",
"--bic",
"GENODE23X42",
]);
let output = cmd.output().expect("Failed to run fints-client");
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(stdout.trim());
if let Ok(json) = parsed {
assert!(
json.get("amount").is_some() || json.to_string().contains("amount"),
"JSON output should contain 'amount' field: {}",
stdout
);
} else {
eprintln!("fints-client balance did not output JSON: {}", stdout);
}
} else {
eprintln!("fints-client balance failed — server may not support this command");
}
}
#[test]
fn test_client_decode_command() {
let bin = assert_cmd::cargo::cargo_bin("fints-client");
if !bin.exists() {
eprintln!("Skipping: fints-client not built");
return;
}
let mut cmd = Command::new(&bin);
cmd.arg("decode");
cmd.write_stdin("HNHBS:5:1+2'");
let output = cmd.output().expect("Failed to run fints-client decode");
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("HNHBS"),
"Decode output should contain 'HNHBS', got: {}",
stdout
);
} else {
eprintln!("fints-client decode not available or failed");
}
}
#[test]
fn test_server_audit_mode() {
let server = match TestServer::start(&["--audit", "--verbose"]) {
Some(s) => s,
None => {
eprintln!("Skipping: fints-server not built or --audit not supported");
return;
}
};
assert!(
server.port > 0,
"Server with --audit should start on a valid port"
);
use std::io::Write;
let addr = format!("127.0.0.1:{}", server.port);
if let Ok(mut stream) = std::net::TcpStream::connect(&addr) {
let request = "POST / HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\nContent-Type: text/plain\r\n\r\n";
let _ = stream.write_all(request.as_bytes());
}
drop(server);
}
#[test]
fn test_client_transactions_json() {
let server = match TestServer::start(&["--tan-mode", "none"]) {
Some(s) => s,
None => {
eprintln!("Skipping: fints-server not built");
return;
}
};
let bin = assert_cmd::cargo::cargo_bin("fints-client");
if !bin.exists() {
eprintln!("Skipping: fints-client not built");
return;
}
let url = server.url();
let tmp_dir = tempfile::tempdir().expect("tmpdir");
let mut cmd = Command::new(&bin);
cmd.args([
"--bank",
"custom",
"--url",
&url,
"--blz",
"12345678",
"--user",
"test1",
"--pin",
"1234",
"--session-dir",
tmp_dir.path().to_str().unwrap(),
"--output",
"json",
"transactions",
"--iban",
"DE89370400440532013000",
"--bic",
"GENODE23X42",
]);
let output = cmd.output().expect("Failed to run fints-client");
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(stdout.trim());
if let Ok(json) = parsed {
assert!(
json.is_array() || json.is_object(),
"JSON output should be an array or object, got: {}",
stdout
);
} else {
eprintln!("fints-client transactions did not output JSON: {}", stdout);
}
} else {
eprintln!("fints-client transactions failed — server may not support this command");
}
}