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.
137 lines
6.1 KiB
C++
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";
|
|
}
|