#include "bank.hpp" #include #include #include #include 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 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 : {"", "bad gateway", "{", "null", "{\"ok\":true}"}) assert(!parse_acknowledgement(text).valid); assert(!parse_acknowledgement(std::string(0x5000,' ')+valid.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"; }