#include #include "ccm/games/IGameModule.hpp" #include "ccm/ports/ISetRepository.hpp" #include "ccm/services/SetService.hpp" #include using namespace ccm; namespace { class FakeSetSource final : public ISetSource { public: Result> result = Result>::err("not set"); int calls = 0; Result> fetchAll() override { ++calls; return result; } }; class FakeGameModule final : public IGameModule { public: FakeSetSource source; Game gameId; explicit FakeGameModule(Game id) : gameId(id) {} Game id() const noexcept override { return gameId; } std::string dirName() const override { if (gameId == Game::Magic) return "magic"; if (gameId == Game::Pokemon) return "pokemon"; if (gameId == Game::YuGiOh) return "yugioh"; if (gameId == Game::DigiBattle99) return "digibattle99"; if (gameId == Game::YuGiOhBandai) return "yugiohbandai"; if (gameId == Game::JapanesePokemon) return "pokemon"; return "yugioh"; } std::string displayName() const override { return dirName(); } ISetSource& setSource() override { return source; } }; class InMemSetRepo final : public ISetRepository { public: std::vector stored; bool hasStored = false; bool failSave = false; Result> load(Game) override { if (!hasStored) return Result>::err("no cache"); return Result>::ok(stored); } Result save(Game, const std::vector& s) override { if (failSave) return Result::err("save failed"); stored = s; hasStored = true; return Result::ok(); } }; } // namespace TEST_SUITE("SetService") { TEST_CASE("updateSets fetches via the registered module and persists the result") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({ {"lea", "Alpha", "1993/08/05"}, }); svc.registerModule(&magic); const auto out = svc.updateSets(Game::Magic); REQUIRE(out.isOk()); CHECK(out.value().size() == 1); CHECK(magic.source.calls == 1); // Cache is now warm. const auto cached = svc.getSets(Game::Magic); REQUIRE(cached.isOk()); CHECK(cached.value() == out.value()); } TEST_CASE("updateSets fails cleanly for unregistered games") { InMemSetRepo repo; SetService svc{repo}; const auto out = svc.updateSets(Game::Pokemon); CHECK(out.isErr()); } TEST_CASE("propagates upstream fetch errors without persisting") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule pokemon{Game::Pokemon}; pokemon.source.result = Result>::err("upstream is down"); svc.registerModule(&pokemon); const auto out = svc.updateSets(Game::Pokemon); CHECK(out.isErr()); CHECK_FALSE(repo.hasStored); } TEST_CASE("Pokemon module is routed independently of Magic") { // Both modules registered; updating Pokemon must hit the Pokemon // source and persist under the Pokemon Game key without disturbing // a previously cached Magic list. InMemSetRepo repo; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({ {"lea", "Alpha", "1993/08/05"}, }); FakeGameModule pokemon{Game::Pokemon}; pokemon.source.result = Result>::ok({ {"base1", "Base", "1999/01/09"}, }); svc.registerModule(&magic); svc.registerModule(&pokemon); REQUIRE(svc.updateSets(Game::Magic).isOk()); const auto poke = svc.updateSets(Game::Pokemon); REQUIRE(poke.isOk()); REQUIRE(poke.value().size() == 1); CHECK(poke.value().front().id == "base1"); CHECK(magic.source.calls == 1); CHECK(pokemon.source.calls == 1); } TEST_CASE("YuGiOh module routes independently when all games are registered") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({{"lea", "Alpha", "1993/08/05"}}); FakeGameModule pokemon{Game::Pokemon}; pokemon.source.result = Result>::ok({{"base1", "Base", "1999/01/09"}}); FakeGameModule yugioh{Game::YuGiOh}; yugioh.source.result = Result>::ok({{"LOB", "Legend of Blue Eyes", "2002/03/08"}}); svc.registerModule(&magic); svc.registerModule(&pokemon); svc.registerModule(&yugioh); REQUIRE(svc.updateSets(Game::Magic).isOk()); REQUIRE(svc.updateSets(Game::Pokemon).isOk()); const auto ygo = svc.updateSets(Game::YuGiOh); REQUIRE(ygo.isOk()); CHECK(ygo.value().front().id == "LOB"); CHECK(magic.source.calls == 1); CHECK(pokemon.source.calls == 1); CHECK(yugioh.source.calls == 1); } TEST_CASE("DigiBattle99 module routes independently when all games are registered") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({{"lea", "Alpha", "1993/08/05"}}); FakeGameModule pokemon{Game::Pokemon}; pokemon.source.result = Result>::ok({{"base1", "Base", "1999/01/09"}}); FakeGameModule yugioh{Game::YuGiOh}; yugioh.source.result = Result>::ok({{"LOB", "Legend of Blue Eyes", "2002/03/08"}}); FakeGameModule digi{Game::DigiBattle99}; digi.source.result = Result>::ok( {{"series-1-starter-set", "Series 1 Starter Set", "1999/06/01"}}); svc.registerModule(&magic); svc.registerModule(&pokemon); svc.registerModule(&yugioh); svc.registerModule(&digi); REQUIRE(svc.updateSets(Game::Magic).isOk()); REQUIRE(svc.updateSets(Game::Pokemon).isOk()); REQUIRE(svc.updateSets(Game::YuGiOh).isOk()); const auto out = svc.updateSets(Game::DigiBattle99); REQUIRE(out.isOk()); CHECK(out.value().front().id == "series-1-starter-set"); CHECK(magic.source.calls == 1); CHECK(pokemon.source.calls == 1); CHECK(yugioh.source.calls == 1); CHECK(digi.source.calls == 1); } TEST_CASE("YuGiOhBandai module routes independently when all games are registered") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({{"lea", "Alpha", "1993/08/05"}}); FakeGameModule pokemon{Game::Pokemon}; pokemon.source.result = Result>::ok({{"base1", "Base", "1999/01/09"}}); FakeGameModule yugioh{Game::YuGiOh}; yugioh.source.result = Result>::ok({{"LOB", "Legend of Blue Eyes", "2002/03/08"}}); FakeGameModule digi{Game::DigiBattle99}; digi.source.result = Result>::ok( {{"series-1-starter-set", "Series 1 Starter Set", "1999/06/01"}}); FakeGameModule bandai{Game::YuGiOhBandai}; bandai.source.result = Result>::ok( {{"ban1", "1st Generation", "1998/09/01"}}); svc.registerModule(&magic); svc.registerModule(&pokemon); svc.registerModule(&yugioh); svc.registerModule(&digi); svc.registerModule(&bandai); REQUIRE(svc.updateSets(Game::Magic).isOk()); REQUIRE(svc.updateSets(Game::Pokemon).isOk()); REQUIRE(svc.updateSets(Game::YuGiOh).isOk()); REQUIRE(svc.updateSets(Game::DigiBattle99).isOk()); const auto out = svc.updateSets(Game::YuGiOhBandai); REQUIRE(out.isOk()); CHECK(out.value().front().id == "ban1"); CHECK(magic.source.calls == 1); CHECK(pokemon.source.calls == 1); CHECK(yugioh.source.calls == 1); CHECK(digi.source.calls == 1); CHECK(bandai.source.calls == 1); } TEST_CASE("JapanesePokemon module routes independently when all games are registered") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({{"lea", "Alpha", "1993/08/05"}}); FakeGameModule pokemon{Game::Pokemon}; pokemon.source.result = Result>::ok({{"base1", "Base", "1999/01/09"}}); FakeGameModule yugioh{Game::YuGiOh}; yugioh.source.result = Result>::ok({{"LOB", "Legend of Blue Eyes", "2002/03/08"}}); FakeGameModule digi{Game::DigiBattle99}; digi.source.result = Result>::ok( {{"series-1-starter-set", "Series 1 Starter Set", "1999/06/01"}}); FakeGameModule jp{Game::JapanesePokemon}; jp.source.result = Result>::ok( {{"PMCG1", "Expansion Pack", "1996/10/20"}}); svc.registerModule(&magic); svc.registerModule(&pokemon); svc.registerModule(&yugioh); svc.registerModule(&digi); svc.registerModule(&jp); REQUIRE(svc.updateSets(Game::Magic).isOk()); REQUIRE(svc.updateSets(Game::Pokemon).isOk()); REQUIRE(svc.updateSets(Game::YuGiOh).isOk()); REQUIRE(svc.updateSets(Game::DigiBattle99).isOk()); const auto out = svc.updateSets(Game::JapanesePokemon); REQUIRE(out.isOk()); CHECK(out.value().front().id == "PMCG1"); CHECK(jp.source.calls == 1); CHECK(magic.source.calls == 1); CHECK(pokemon.source.calls == 1); } TEST_CASE("updateSets propagates repository save failures") { InMemSetRepo repo; repo.failSave = true; SetService svc{repo}; FakeGameModule magic{Game::Magic}; magic.source.result = Result>::ok({{"lea", "Alpha", "1993/08/05"}}); svc.registerModule(&magic); const auto out = svc.updateSets(Game::Magic); REQUIRE(out.isErr()); CHECK(out.error() == "save failed"); } TEST_CASE("registering a second module for same game id overwrites previous one") { InMemSetRepo repo; SetService svc{repo}; FakeGameModule firstMagic{Game::Magic}; firstMagic.source.result = Result>::ok({{"a", "First", "2000/01/01"}}); FakeGameModule secondMagic{Game::Magic}; secondMagic.source.result = Result>::ok({{"b", "Second", "2001/01/01"}}); svc.registerModule(&firstMagic); svc.registerModule(&secondMagic); const auto out = svc.updateSets(Game::Magic); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value().front().id == "b"); CHECK(firstMagic.source.calls == 0); CHECK(secondMagic.source.calls == 1); } }