#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 { return gameId == Game::Magic ? "magic" : "pokemon"; } std::string displayName() const override { return dirName(); } ISetSource& setSource() override { return source; } }; class InMemSetRepo final : public ISetRepository { public: std::vector stored; bool hasStored = false; Result> load(Game) override { if (!hasStored) return Result>::err("no cache"); return Result>::ok(stored); } Result save(Game, const std::vector& s) override { 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); } }