#include #include "ccm/infra/JsonSetRepository.hpp" #include "ccm/services/ConfigService.hpp" #include "fakes/InMemoryFileSystem.hpp" #include using namespace ccm; using ccm::testing::InMemoryFileSystem; namespace { std::string dirNameFn(Game g) { return g == Game::Magic ? "magic" : "pokemon"; } ConfigService makeConfig(InMemoryFileSystem& fs, const std::string& dataDir) { Configuration c; c.dataStorage = dataDir; c.defaultGame = Game::Magic; fs.writeText("/app/config.json", nlohmann::json(c).dump()); ConfigService cfg{fs, "/app/config.json", dataDir}; cfg.initialize(); return cfg; } class FailingSetFs final : public IFileSystem { public: bool ensureOk{true}; bool writeOk{true}; bool readOk{true}; std::string readPayload{"[]"}; std::filesystem::path lastWritePath; std::string lastWriteBody; std::filesystem::path lastReadPath; [[nodiscard]] bool exists(const std::filesystem::path&) const override { return true; } [[nodiscard]] bool isDirectory(const std::filesystem::path&) const override { return true; } Result ensureDirectory(const std::filesystem::path&) override { if (!ensureOk) return Result::err("ensure failed"); return Result::ok(); } Result readText(const std::filesystem::path&) override { lastReadPath = std::filesystem::path("/tracked/read/path"); if (!readOk) return Result::err("read failed"); return Result::ok(readPayload); } Result writeText(const std::filesystem::path& p, std::string_view contents) override { if (!writeOk) return Result::err("write failed"); lastWritePath = p; lastWriteBody = std::string(contents); return Result::ok(); } Result copyFile(const std::filesystem::path&, const std::filesystem::path&, bool) override { return Result::ok(); } Result remove(const std::filesystem::path&) override { return Result::ok(); } Result> listDirectory(const std::filesystem::path&) override { return Result>::ok({}); } }; } // namespace TEST_SUITE("JsonSetRepository") { TEST_CASE("save then load returns identical sets") { InMemoryFileSystem fs; auto cfg = makeConfig(fs, "/data"); JsonSetRepository repo{fs, cfg, dirNameFn}; const std::vector sets = { {"lea", "Limited Edition Alpha", "1993/08/05"}, {"leb", "Limited Edition Beta", "1993/10/04"}, }; REQUIRE(repo.save(Game::Magic, sets).isOk()); const auto loaded = repo.load(Game::Magic); REQUIRE(loaded.isOk()); CHECK(loaded.value() == sets); } TEST_CASE("load before any save reports a clear error") { InMemoryFileSystem fs; auto cfg = makeConfig(fs, "/data"); JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::Pokemon); CHECK(loaded.isErr()); } TEST_CASE("load propagates read errors from filesystem") { InMemoryFileSystem configFs; auto cfg = makeConfig(configFs, "/data"); FailingSetFs fs; fs.readOk = false; JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::Magic); REQUIRE(loaded.isErr()); CHECK(loaded.error() == "read failed"); } TEST_CASE("load reports parse error for malformed sets.json") { InMemoryFileSystem configFs; auto cfg = makeConfig(configFs, "/data"); FailingSetFs fs; fs.readPayload = "{bad json"; JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::Magic); REQUIRE(loaded.isErr()); CHECK(loaded.error().find("sets.json parse error:") != std::string::npos); } TEST_CASE("load reports parse error for wrong JSON shape") { InMemoryFileSystem configFs; auto cfg = makeConfig(configFs, "/data"); FailingSetFs fs; fs.readPayload = R"({"not":"an array"})"; JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::Magic); REQUIRE(loaded.isErr()); CHECK(loaded.error().find("sets.json parse error:") != std::string::npos); } TEST_CASE("save propagates ensureDirectory and writeText failures") { InMemoryFileSystem configFs; auto cfg = makeConfig(configFs, "/data"); FailingSetFs fs; JsonSetRepository repo{fs, cfg, dirNameFn}; const std::vector sets = {{"lea", "Limited Edition Alpha", "1993/08/05"}}; fs.ensureOk = false; const auto ensureFail = repo.save(Game::Magic, sets); REQUIRE(ensureFail.isErr()); CHECK(ensureFail.error() == "ensure failed"); fs.ensureOk = true; fs.writeOk = false; const auto writeFail = repo.save(Game::Magic, sets); REQUIRE(writeFail.isErr()); CHECK(writeFail.error() == "write failed"); } TEST_CASE("paths use region-specific filenames for Pokemon West and Asia") { InMemoryFileSystem fs; auto cfg = makeConfig(fs, "/data"); JsonSetRepository repo{fs, cfg, dirNameFn}; const std::vector west = {{"base1", "Base Set", "1999/01/09"}}; const std::vector asia = {{"SV1a", "Triplet Beat", "2023/01/20"}}; REQUIRE(repo.save(Game::Pokemon, west).isOk()); REQUIRE(repo.save(Game::JapanesePokemon, asia).isOk()); CHECK(fs.files().count("/data/pokemon/sets-west.json") == 1); CHECK(fs.files().count("/data/pokemon/sets-asia.json") == 1); CHECK(fs.files().count("/data/pokemon/sets.json") == 0); } TEST_CASE("load migrates legacy pokemon/sets.json to sets-west.json") { InMemoryFileSystem fs; auto cfg = makeConfig(fs, "/data"); const std::vector sets = {{"base1", "Base Set", "1999/01/09"}}; REQUIRE(fs.writeText("/data/pokemon/sets.json", nlohmann::json(sets).dump(2)).isOk()); JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::Pokemon); REQUIRE(loaded.isOk()); CHECK(loaded.value() == sets); CHECK(fs.files().count("/data/pokemon/sets-west.json") == 1); } TEST_CASE("load migrates legacy pokemonjp/sets.json to sets-asia.json") { InMemoryFileSystem fs; auto cfg = makeConfig(fs, "/data"); const std::vector sets = {{"SV1a", "Triplet Beat", "2023/01/20"}}; REQUIRE(fs.writeText("/data/pokemonjp/sets.json", nlohmann::json(sets).dump(2)).isOk()); JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::JapanesePokemon); REQUIRE(loaded.isOk()); CHECK(loaded.value() == sets); CHECK(fs.files().count("/data/pokemon/sets-asia.json") == 1); } TEST_CASE("load prefers new path over legacy when both exist") { InMemoryFileSystem fs; auto cfg = makeConfig(fs, "/data"); const std::vector legacy = {{"old", "Old", "1999/01/01"}}; const std::vector neu = {{"new", "New", "2024/01/01"}}; REQUIRE(fs.writeText("/data/pokemon/sets.json", nlohmann::json(legacy).dump(2)).isOk()); REQUIRE(fs.writeText("/data/pokemon/sets-west.json", nlohmann::json(neu).dump(2)).isOk()); JsonSetRepository repo{fs, cfg, dirNameFn}; const auto loaded = repo.load(Game::Pokemon); REQUIRE(loaded.isOk()); CHECK(loaded.value() == neu); } }