Files
josh 463b4bcc51 Add safe bank failure diagnostics for T5 and T6
Address #14 with request correlation, bounded parser reasons, startup version reporting, and rate-limited storage and settlement logs. Preserve accounting and API identity contracts, extend portable tests, and update release versions and rollout guidance.
2026-10-13 10:02:36 -04:00

137 lines
6.1 KiB
C++

#include "bank.hpp"
#include <json.hpp>
#include <cassert>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
using utils::bank::outbox;
using utils::bank::parse_acknowledgement;
const std::string server = "test-server", endpoint = "https://example.test/api/bank";
std::string body(const std::string& id, const std::string& decision)
{
return nlohmann::json{{"server",server},{"guid","18446744073709551615"},
{"match","match"},{"request_id",id},{"operation","deposit"},
{"amount",2000},{"capacity",1000000},{"decision",decision}}.dump();
}
template <typename F> void must_fail(F action)
{
bool failed = false;
try { action(); } catch (const std::exception&) { failed = true; }
assert(failed);
}
int main(int argc, char** argv)
{
assert(argc >= 2);
const fs::path root(argv[1]);
const outbox queue(root,server,endpoint);
if (argc == 3)
{
const std::string mode(argv[2]);
if (mode == "produce")
{
queue.store(body("uncertain-prepare","cancel"));
queue.store(body("applied-points","cancel"));
queue.store(body("applied-points","commit"));
}
else if (mode == "recover")
{
const auto entries = queue.load();
assert(entries.size() == 2);
assert(entries[0].decision == "cancel" && entries[1].decision == "commit");
for (const auto& entry : entries) queue.erase(entry.id);
}
else assert(mode == "empty" && queue.load().empty());
return 0;
}
fs::create_directories(root);
assert(queue.load().empty());
queue.store(body("one","cancel"));
queue.store(body("one","cancel"));
assert(queue.load().size() == 1);
queue.store(body("one","commit"));
queue.store(body("one","commit"));
assert(queue.load()[0].decision == "commit");
must_fail([&]{queue.store(body("one","cancel"));});
auto changed = nlohmann::json::parse(body("one","commit"));
changed["amount"] = 100;
must_fail([&]{queue.store(changed.dump());});
assert(queue.load()[0].body == body("one","commit"));
const outbox isolated(root,"another-server",endpoint);
assert(isolated.load().empty());
const outbox another_endpoint(root,server,endpoint+"/other");
assert(another_endpoint.load().empty());
// An interrupted temp write cannot replace the committed decision.
const auto file = fs::directory_iterator(root)->path();
{ std::ofstream stream(file.string()+".tmp"); stream << "incomplete"; }
assert(queue.load()[0].decision == "commit");
fs::remove(file.string()+".tmp");
// A failed replacement preserves cancellation and must stop points moving.
queue.erase("one");
queue.store(body("two","cancel"));
fs::create_directory(file.string()+".tmp");
must_fail([&]{queue.store(body("two","commit"));});
assert(queue.load()[0].decision == "cancel");
fs::remove(file.string()+".tmp");
queue.erase("two");
queue.erase("two");
assert(queue.load().empty());
auto valid = nlohmann::json{{"ok",true},{"state","prepared"},{"request_id","one"},
{"code","ok"},{"amount",2000},{"balance",3000}};
const auto ack = parse_acknowledgement(valid.dump());
assert(ack.valid && ack.amount == 2000 && ack.balance == 3000 && ack.id == "one");
auto empty = valid;
empty["state"] = "balance"; empty["request_id"] = "";
empty["amount"] = 0; empty["balance"] = 0;
const auto zero = parse_acknowledgement(empty.dump());
assert(zero.valid && zero.amount == 0 && zero.balance == 0);
empty["ok"] = false; empty["state"] = "rejected";
empty["request_id"] = "one"; empty["code"] = "insufficient_balance";
assert(parse_acknowledgement(empty.dump()).valid);
for (const auto& field : {"amount","balance"})
for (const auto& bad : {nlohmann::json(-1),nlohmann::json(1.5),nlohmann::json("100"),nlohmann::json(1000001)})
{
auto value = valid; value[field] = bad;
assert(!parse_acknowledgement(value.dump()).valid);
}
auto bad = valid; bad["ok"] = false;
assert(!parse_acknowledgement(bad.dump()).valid);
bad = valid; bad["request_id"] = "";
assert(!parse_acknowledgement(bad.dump()).valid);
bad = valid; bad["state"] = "unknown";
assert(!parse_acknowledgement(bad.dump()).valid);
for (const auto& text : {"", "<html>bad gateway</html>", "{", "null", "{\"ok\":true}"})
assert(!parse_acknowledgement(text).valid);
assert(!parse_acknowledgement(std::string(0x5000,' ')+valid.dump()).valid);
// Every validation failure exposes a fixed reason and clears response data.
const auto invalid = [](const std::string& text, const std::string& reason)
{
const auto result = parse_acknowledgement(text);
assert(!result.valid && result.reason == reason);
assert(result.state.empty() && result.code.empty() && result.id.empty());
};
assert(ack.reason == "ok");
invalid("", "empty_body");
invalid(std::string(0x5000, 'x'), "body_limit");
invalid("secret-token-not-json", "invalid_json");
invalid("null", "schema");
invalid("{}", "schema");
bad = valid; bad["code"] = std::string(65, 'x'); invalid(bad.dump(), "field_limit");
bad = valid; bad["state"] = "secret-state"; invalid(bad.dump(), "state");
bad = valid; bad["amount"] = -1; invalid(bad.dump(), "amount_range");
bad = valid; bad["ok"] = false; invalid(bad.dump(), "ok_state");
bad = valid; bad["request_id"] = ""; invalid(bad.dump(), "missing_id");
bad = valid; bad["amount"] = 0; invalid(bad.dump(), "zero_transfer");
bad = valid; bad["code"] = "private-token\nAuthorization: secret";
// Compatibility: valid but unknown API codes remain available to callers;
// scripts permit only fixed known codes in diagnostics.
assert(parse_acknowledgement(bad.dump()).valid);
{ std::ofstream stream(file); stream << "corrupt"; }
must_fail([&]{queue.load();});
must_fail([&]{queue.store(body("three","cancel"));});
must_fail([&]{queue.erase("three");});
fs::remove(file);
std::cout << "bank decisions, corruption, cancellation, response guards passed\n";
}