minor: New Game Digimon Digi-Battle (#17)

* digimon digi battle added to supported games

* sonarqube update

* readme update

---------

Co-authored-by: sdine <sdine@sdine.com>
This commit is contained in:
Sebastian Dine
2026-07-19 12:06:57 +02:00
committed by GitHub
parent 42926f2fb5
commit e5c830e945
53 changed files with 2170 additions and 49 deletions
+3 -1
View File
@@ -21,9 +21,11 @@
- `std_file_system_tests.cpp``StdFileSystem` directly (`exists`, `isDirectory`, `ensureDirectory`, `readText`, `writeText`, `copyFile`, `remove`, `listDirectory`) under a unique `temp_directory_path()/ccm_std_fs_test_*` directory per case; scope matches the real-disk exception documented for preview-cache tests.
- `pokemon_set_source_tests.cpp``PokemonSetSource::parseResponse` (api.pokemontcg.io/v2/sets shape — `data[].id`, `name`, `releaseDate` already in `YYYY/MM/DD`) + sort-by-release-date stability. Drives `fetchAll` via `FixedHttpClient` and asserts the public endpoint URL.
- `pokemon_card_preview_source_tests.cpp``PokemonCardPreviewSource::buildSearchUrl` (percent-encoded `name:` / `set.id:` / `number:` triple, with collector-number `4/102` -> `4` normalization) + `parseResponse` (`data[0].images.large` with `images.small` fallback). Drives `fetchImageUrl` via `FixedHttpClient`.
- `digibattle99_set_source_tests.cpp``DigiBattle99SetSource::parseResponse` derives unique packs from digimoncard.io search arrays, slugifies `Set.id`, applies curated release dates, and sorts chronologically. Drives `fetchAll` via `FixedHttpClient`.
- `digibattle99_card_preview_source_tests.cpp` — CDN image URL from `setNo`, search URL encoding (`series`/`n`/`pack`/`card`), `parseImageUrlFromSearch` NotFound vs Transient, and auto-detect print variants. Drives `fetchImageUrl` / `detectPrintVariants` via `FixedHttpClient`.
- `yugioh_set_source_tests.cpp``YuGiOhSetSource::parseResponse` for YGOPRODeck `cardsets.php` (`set_code`, `set_name`, `tcg_date`) including `YYYY-MM-DD` -> `YYYY/MM/DD` rewrite and chronological sort checks.
- `yugioh_set_lookup_tests.cpp``lookupYuGiOhSetByShorthand` / helpers in `ccm/util/YuGiOhSetLookup.hpp` (trim, ASCII case-fold, exact `Set.id` match, not-found vs ambiguous).
- `game_module_tests.cpp` — smoke tests that each concrete `IGameModule` (Magic / Pokemon / Yu-Gi-Oh) reports stable `id()`, `dirName()`, `displayName()`, and a non-null `cardPreviewSource()` when constructed with a noop `IHttpClient`.
- `game_module_tests.cpp` — smoke tests that each concrete `IGameModule` (Magic / Pokemon / Yu-Gi-Oh / DigiBattle99) reports stable `id()`, `dirName()`, `displayName()`, and a non-null `cardPreviewSource()` when constructed with a noop `IHttpClient`.
- `yugioh_card_preview_source_tests.cpp``YuGiOhCardPreviewSource` Yugipedia + YGOPRODeck unit coverage. Helper-level tests pin down `normalizeName` (whitespace + Yugipedia-policy punctuation stripping), `ygoRarityShortCode` + `rarityCodeFor` (CCM3 dialog rarity names → canonical short codes used by both the YGO overview table and Yugipedia filename generation; unknown rarity falls through), `extractSetCode` (`LOB-005` / `LOB-DE005``LOB`), `buildCandidateFilenames` (printed-edition first, EN/NA/EU/AU + png/jpg, rarity-less fallback round, empty list when slug or set code is missing), `buildYugipediaQueryUrl` (single `titles=File:A|File:B` batch, percent-encoded), and `parseYugipediaResponse` (returns the URL of the highest-priority filename that resolved, errors when every candidate is `missing`). End-to-end `fetchImageUrl` cases use a `RoutingHttpClient` to verify Yugipedia is queried first and the per-printing scan is returned when found, that empty/error Yugipedia responses fall through to the YGOPRODeck `card_images[0]` fallback, that the YGOPRODeck error is propagated when both upstreams fail, and that an empty `setNo` skips Yugipedia entirely. `parseFirstPrint` preferred-`set_name` lookup is also covered for the auto-detect path. `parsePrintVariants` includes synthetic scenarios aligned with the `yugioh_same_card_set_variant_tests` fixture (dual-rarity vs multi-code within one display set, duplicate suppression, and no merge across unrelated `set_name` rows when the picker label matches nothing).
- `card_sorter_tests.cpp``sortMagicCards` / `sortPokemonCards` per-column behavior. Pin-down tests for `byField`-equivalent semantics: case-insensitive strings, chronological set sort via `set.releaseDate`, numeric `amount`, `false < true` boolean order, stable composition (sort by name then by set keeps inner-name order). Update this file whenever you add a new column / sort key.
- `card_filter_tests.cpp``matchesMagicFilter` / `matchesPokemonFilter` / `matchesYuGiOhFilter` row-matcher behavior. Pin-down tests for `applyFilter`-equivalent semantics: case-insensitive substring match across `tableFields` valueKeys (name, set.name, language, condition, amount-as-string, note; Pokemon adds `setNo`; Yu-Gi-Oh adds `setNo` + `rarity`), boolean flag columns intentionally excluded, empty filter matches everything. Update this file whenever you add a new searchable column.
+2
View File
@@ -21,6 +21,8 @@ add_executable(ccm_core_tests
std_file_system_tests.cpp
pokemon_set_source_tests.cpp
pokemon_card_preview_source_tests.cpp
digibattle99_set_source_tests.cpp
digibattle99_card_preview_source_tests.cpp
icard_preview_source_tests.cpp
yugioh_set_source_tests.cpp
yugioh_set_lookup_tests.cpp
+38
View File
@@ -6,6 +6,7 @@
#include <doctest/doctest.h>
#include "ccm/domain/Enums.hpp"
#include "ccm/domain/DigiBattle99Card.hpp"
#include "ccm/domain/MagicCard.hpp"
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/domain/YuGiOhCard.hpp"
@@ -238,3 +239,40 @@ TEST_SUITE("CardFilter::matchesYuGiOhFilter") {
CHECK_FALSE(matchesYuGiOhFilter(c, "zzznomatch"));
}
}
TEST_SUITE("CardFilter::matchesDigiBattle99Filter") {
TEST_CASE("matches by name and set.name") {
DigiBattle99Card c;
c.name = "Agumon";
c.set.name = "Series 1 Starter Set";
CHECK(matchesDigiBattle99Filter(c, "agu"));
CHECK(matchesDigiBattle99Filter(c, "STARTER"));
CHECK_FALSE(matchesDigiBattle99Filter(c, "greymon"));
}
TEST_CASE("includes setNo in searchable columns") {
DigiBattle99Card c;
c.name = "Agumon";
c.set.name = "Series 1 Starter Set";
c.setNo = "ST-01";
CHECK(matchesDigiBattle99Filter(c, "st-01"));
CHECK(matchesDigiBattle99Filter(c, "ST-"));
}
TEST_CASE("empty filter matches everything") {
DigiBattle99Card c;
c.name = "Agumon";
CHECK(matchesDigiBattle99Filter(c, ""));
}
TEST_CASE("boolean flag columns are not matched") {
DigiBattle99Card c;
c.name = "Agumon";
c.holo = true;
c.firstEdition = true;
c.signed_ = true;
c.altered = true;
CHECK_FALSE(matchesDigiBattle99Filter(c, "true"));
CHECK(matchesDigiBattle99Filter(c, "agu"));
}
}
+64
View File
@@ -6,6 +6,7 @@
#include <doctest/doctest.h>
#include "ccm/domain/Enums.hpp"
#include "ccm/domain/DigiBattle99Card.hpp"
#include "ccm/domain/MagicCard.hpp"
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/domain/YuGiOhCard.hpp"
@@ -94,6 +95,30 @@ YuGiOhCard yc(std::uint32_t id, std::string name,
return c;
}
DigiBattle99Card db(std::uint32_t id, std::string name,
std::string setName, std::string releaseDate,
std::uint8_t amount = 1,
bool holo = false, bool firstEdition = false,
bool sgnd = false, bool altered = false,
Language lang = Language::English,
Condition cond = Condition::NearMint,
std::string note = "") {
DigiBattle99Card c;
c.id = id;
c.name = std::move(name);
c.set.name = std::move(setName);
c.set.releaseDate = std::move(releaseDate);
c.amount = amount;
c.holo = holo;
c.firstEdition = firstEdition;
c.signed_ = sgnd;
c.altered = altered;
c.language = lang;
c.condition = cond;
c.note = std::move(note);
return c;
}
std::vector<std::uint32_t> ids(const std::vector<MagicCard>& v) {
std::vector<std::uint32_t> out;
out.reserve(v.size());
@@ -115,6 +140,13 @@ std::vector<std::uint32_t> ids(const std::vector<YuGiOhCard>& v) {
return out;
}
std::vector<std::uint32_t> ids(const std::vector<DigiBattle99Card>& v) {
std::vector<std::uint32_t> out;
out.reserve(v.size());
for (const auto& c : v) out.push_back(c.id);
return out;
}
} // namespace
TEST_SUITE("CardSorter - Magic columns") {
@@ -451,3 +483,35 @@ TEST_SUITE("CardSorter - YuGiOh columns") {
CHECK(ids(v) == std::vector<std::uint32_t>{3, 1, 2});
}
}
TEST_SUITE("CardSorter - DigiBattle99 columns") {
TEST_CASE("Holo and FirstEdition sort false before true") {
std::vector<DigiBattle99Card> v = {
db(1, "a", "X", "2000/01/01", 1, /*holo=*/true, /*first=*/false),
db(2, "b", "X", "2000/01/01", 1, /*holo=*/false, /*first=*/true),
db(3, "c", "X", "2000/01/01", 1, /*holo=*/false, /*first=*/false),
};
sortDigiBattle99Cards(v, DigiBattle99SortColumn::Holo, /*ascending=*/true);
CHECK(ids(v) == std::vector<std::uint32_t>{2, 3, 1});
sortDigiBattle99Cards(v, DigiBattle99SortColumn::FirstEdition, /*ascending=*/true);
CHECK(ids(v) == std::vector<std::uint32_t>{3, 1, 2});
}
TEST_CASE("Set column sorts by release date") {
std::vector<DigiBattle99Card> v = {
db(1, "x", "Late", "2001/01/01"),
db(2, "y", "Early", "1999/06/01"),
};
sortDigiBattle99Cards(v, DigiBattle99SortColumn::SetReleaseDate, /*ascending=*/true);
CHECK(ids(v) == std::vector<std::uint32_t>{2, 1});
}
TEST_CASE("Name sorts case-insensitively") {
std::vector<DigiBattle99Card> v = {
db(1, "greymon", "X", "2000/01/01"),
db(2, "Agumon", "X", "2000/01/01"),
};
sortDigiBattle99Cards(v, DigiBattle99SortColumn::Name, /*ascending=*/true);
CHECK(ids(v) == std::vector<std::uint32_t>{2, 1});
}
}
@@ -0,0 +1,179 @@
#include <doctest/doctest.h>
#include "ccm/games/digibattle99/DigiBattle99CardPreviewSource.hpp"
#include "ccm/ports/IHttpClient.hpp"
#include <string>
using namespace ccm;
namespace {
class FixedHttpClient final : public IHttpClient {
public:
std::string lastUrl;
std::string body;
bool ok = true;
Result<std::string> get(std::string_view url) override {
lastUrl = std::string(url);
return ok ? Result<std::string>::ok(body)
: Result<std::string>::err("offline");
}
};
} // namespace
TEST_SUITE("DigiBattle99CardPreviewSource::normalizeCardNumber") {
TEST_CASE("uppercases alphabetic prefix without zero-padding") {
CHECK(DigiBattle99CardPreviewSource::normalizeCardNumber("bo-88") == "BO-88");
CHECK(DigiBattle99CardPreviewSource::normalizeCardNumber("st-01") == "ST-01");
CHECK(DigiBattle99CardPreviewSource::normalizeCardNumber(" MO-06 ") == "MO-06");
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::buildImageUrl") {
TEST_CASE("builds CDN jpeg URL from card id") {
CHECK(DigiBattle99CardPreviewSource::buildImageUrl("ST-01") ==
"https://images.digimoncard.io/images/cards/ST-01.jpg");
CHECK(DigiBattle99CardPreviewSource::buildImageUrl("bo-115") ==
"https://images.digimoncard.io/images/cards/BO-115.jpg");
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::buildSearchUrl") {
TEST_CASE("percent-encodes name pack and series") {
const auto url = DigiBattle99CardPreviewSource::buildSearchUrl(
"Agumon", "Series 1 Starter Set", "");
CHECK(url.find("https://digimoncard.io/api-public/search.php?series=") == 0);
CHECK(url.find("Digimon%20Digi-Battle%20Card%20Game") != std::string::npos);
CHECK(url.find("&n=Agumon") != std::string::npos);
CHECK(url.find("&pack=Series%201%20Starter%20Set") != std::string::npos);
CHECK(url.find("&card=") == std::string::npos);
}
TEST_CASE("includes card= when setNo is present") {
const auto url = DigiBattle99CardPreviewSource::buildSearchUrl(
"Agumon", "Series 1 Starter Set", "st-01");
CHECK(url.find("&card=ST-01") != std::string::npos);
}
TEST_CASE("empty pack omits pack clause") {
const auto url =
DigiBattle99CardPreviewSource::buildSearchUrl("Agumon", "", "ST-01");
CHECK(url.find("&pack=") == std::string::npos);
CHECK(url.find("&card=ST-01") != std::string::npos);
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::parseImageUrlFromSearch") {
TEST_CASE("returns CDN URL for first exact name match") {
const std::string json = R"([
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"Agumon","id":"BO-115","set_name":["Series 1 Booster Pack"]}
])";
const auto out =
DigiBattle99CardPreviewSource::parseImageUrlFromSearch(json, "Agumon");
REQUIRE(out.isOk());
CHECK(out.value() == "https://images.digimoncard.io/images/cards/ST-01.jpg");
}
TEST_CASE("empty array is NotFound") {
const auto out =
DigiBattle99CardPreviewSource::parseImageUrlFromSearch("[]", "Agumon");
REQUIRE(out.isErr());
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
}
TEST_CASE("API error object is NotFound") {
const auto out = DigiBattle99CardPreviewSource::parseImageUrlFromSearch(
R"({"error":"No cards found for this search."})", "Agumon");
REQUIRE(out.isErr());
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
}
TEST_CASE("non-array is Transient") {
const auto out =
DigiBattle99CardPreviewSource::parseImageUrlFromSearch(R"({"meta":{}})", "Agumon");
REQUIRE(out.isErr());
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
}
TEST_CASE("malformed JSON is Transient") {
const auto out =
DigiBattle99CardPreviewSource::parseImageUrlFromSearch("{not json", "Agumon");
REQUIRE(out.isErr());
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::fetchImageUrl") {
TEST_CASE("setNo present skips HTTP and returns CDN URL") {
FixedHttpClient http;
DigiBattle99CardPreviewSource src{http};
const auto out = src.fetchImageUrl("Agumon", "Series 1 Starter Set", "st-01");
REQUIRE(out.isOk());
CHECK(out.value() == "https://images.digimoncard.io/images/cards/ST-01.jpg");
CHECK(http.lastUrl.empty());
}
TEST_CASE("empty setNo searches and parses") {
FixedHttpClient http;
http.body = R"([{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]}])";
DigiBattle99CardPreviewSource src{http};
const auto out = src.fetchImageUrl("Agumon", "Series 1 Starter Set", "");
REQUIRE(out.isOk());
CHECK(out.value() == "https://images.digimoncard.io/images/cards/ST-01.jpg");
CHECK(http.lastUrl.find("search.php") != std::string::npos);
CHECK(http.lastUrl.find("n=Agumon") != std::string::npos);
}
TEST_CASE("HTTP failure is Transient") {
FixedHttpClient http;
http.ok = false;
DigiBattle99CardPreviewSource src{http};
const auto out = src.fetchImageUrl("Agumon", "Series 1 Starter Set", "");
REQUIRE(out.isErr());
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::parsePrintVariants") {
TEST_CASE("collects distinct card ids for name+pack") {
const std::string json = R"([
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"Agumon","id":"BO-115","set_name":["Series 1 Booster Pack"]},
{"name":"Greymon","id":"ST-02","set_name":["Series 1 Starter Set"]}
])";
const auto out = DigiBattle99CardPreviewSource::parsePrintVariants(
json, "Series 1 Starter Set", "Agumon");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].setNo == "ST-01");
}
TEST_CASE("pack miss with exact name returns error") {
const std::string json = R"([
{"name":"Agumon","id":"BO-115","set_name":["Series 1 Booster Pack"]}
])";
const auto out = DigiBattle99CardPreviewSource::parsePrintVariants(
json, "Series 1 Starter Set", "Agumon");
CHECK(out.isErr());
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::detectPrintVariants") {
TEST_CASE("round-trips through FixedHttpClient") {
FixedHttpClient http;
http.body = R"([
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"Agumon","id":"ST-126","set_name":["Series 1 Starter Set"]}
])";
DigiBattle99CardPreviewSource src{http};
const auto out = src.detectPrintVariants("Agumon", "Series 1 Starter Set");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 2);
CHECK(out.value()[0].setNo == "ST-01");
CHECK(out.value()[1].setNo == "ST-126");
}
}
+112
View File
@@ -0,0 +1,112 @@
#include <doctest/doctest.h>
#include "ccm/games/digibattle99/DigiBattle99SetSource.hpp"
#include "ccm/ports/IHttpClient.hpp"
using namespace ccm;
namespace {
class FixedHttpClient final : public IHttpClient {
public:
std::string lastUrl;
std::string body;
bool ok = true;
Result<std::string> get(std::string_view url) override {
lastUrl = std::string(url);
return ok ? Result<std::string>::ok(body)
: Result<std::string>::err("offline");
}
};
} // namespace
TEST_SUITE("DigiBattle99SetSource::slugifyPackName") {
TEST_CASE("slugifies pack display names") {
CHECK(DigiBattle99SetSource::slugifyPackName("Series 1 Starter Set") ==
"series-1-starter-set");
CHECK(DigiBattle99SetSource::slugifyPackName("Digimon The Movie Promo Cards") ==
"digimon-the-movie-promo-cards");
}
}
TEST_SUITE("DigiBattle99SetSource::parseResponse") {
TEST_CASE("derives unique packs with curated release dates") {
const std::string json = R"([
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"MetalGreymon","id":"BO-01","set_name":["Series 1 Booster Pack"]},
{"name":"Agumon","id":"ST-126","set_name":["Series 1 Starter Set"]},
{"name":"Promo","id":"MO-06","set_name":["Digimon The Movie Promo Cards"]}
])";
const auto out = DigiBattle99SetSource::parseResponse(json);
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 3);
// Same curated date for Series 1 products → secondary sort by name:
// "Booster" before "Starter".
CHECK(out.value()[0].id == "series-1-booster-pack");
CHECK(out.value()[0].name == "Series 1 Booster Pack");
CHECK(out.value()[0].releaseDate == "1999/06/01");
CHECK(out.value()[1].id == "series-1-starter-set");
CHECK(out.value()[1].name == "Series 1 Starter Set");
CHECK(out.value()[1].releaseDate == "1999/06/01");
CHECK(out.value()[2].id == "digimon-the-movie-promo-cards");
CHECK(out.value()[2].releaseDate == "2000/10/01");
}
TEST_CASE("sorts by release date then name") {
const std::string json = R"([
{"name":"A","id":"ST-1","set_name":["Street Starter Set 2"]},
{"name":"B","id":"ST-2","set_name":["Series 1 Starter Set"]},
{"name":"C","id":"ST-3","set_name":["Street Starter Set 1"]}
])";
const auto out = DigiBattle99SetSource::parseResponse(json);
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 3);
CHECK(out.value()[0].name == "Series 1 Starter Set");
CHECK(out.value()[1].name == "Street Starter Set 1");
CHECK(out.value()[2].name == "Street Starter Set 2");
}
TEST_CASE("empty array returns an empty list") {
const auto out = DigiBattle99SetSource::parseResponse("[]");
REQUIRE(out.isOk());
CHECK(out.value().empty());
}
TEST_CASE("non-array object with error is an error") {
const auto out = DigiBattle99SetSource::parseResponse(
R"({"error":"No cards found for this search."})");
CHECK(out.isErr());
}
TEST_CASE("missing top-level array returns an error") {
const auto out = DigiBattle99SetSource::parseResponse(R"({"meta":{}})");
CHECK(out.isErr());
}
TEST_CASE("invalid JSON returns an error") {
const auto out = DigiBattle99SetSource::parseResponse("{not json");
CHECK(out.isErr());
}
}
TEST_SUITE("DigiBattle99SetSource::fetchAll") {
TEST_CASE("network error is surfaced as a Result error") {
FixedHttpClient http;
http.ok = false;
DigiBattle99SetSource src{http};
CHECK(src.fetchAll().isErr());
}
TEST_CASE("network success hits the Digi-Battle search endpoint") {
FixedHttpClient http;
http.ok = true;
http.body = R"([{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]}])";
DigiBattle99SetSource src{http};
const auto out = src.fetchAll();
REQUIRE(out.isOk());
CHECK(out.value().front().id == "series-1-starter-set");
CHECK(http.lastUrl == DigiBattle99SetSource::kEndpoint);
}
}
+62
View File
@@ -1,6 +1,7 @@
#include <doctest/doctest.h>
#include "ccm/domain/Configuration.hpp"
#include "ccm/domain/DigiBattle99Card.hpp"
#include "ccm/domain/Enums.hpp"
#include "ccm/domain/MagicCard.hpp"
#include "ccm/domain/PokemonCard.hpp"
@@ -23,6 +24,9 @@ TEST_SUITE("domain enums round-trip JSON as strings") {
nlohmann::json jYgo = "YuGiOh";
CHECK(jYgo.get<Game>() == Game::YuGiOh);
nlohmann::json jDigi = "DigiBattle99";
CHECK(jDigi.get<Game>() == Game::DigiBattle99);
nlohmann::json j3 = Theme::Dark;
CHECK(j3.get<std::string>() == "Dark");
CHECK(j3.get<Theme>() == Theme::Dark);
@@ -153,6 +157,34 @@ TEST_SUITE("PokemonCard JSON") {
}
}
TEST_SUITE("DigiBattle99Card JSON") {
TEST_CASE("uses 'setNo' and 'firstEdition' aliases") {
DigiBattle99Card c;
c.id = 3;
c.amount = 2;
c.name = "Agumon";
c.set = Set{"series-1-starter-set", "Series 1 Starter Set", "1999/06/01"};
c.setNo = "ST-01";
c.note = "starter";
c.images = {"a.png"};
c.language = Language::English;
c.condition = Condition::NearMint;
c.firstEdition = false;
c.holo = true;
c.signed_ = true;
c.altered = false;
nlohmann::json j = c;
CHECK(j.at("setNo") == "ST-01");
CHECK(j.at("firstEdition") == false);
CHECK(j.at("holo") == true);
CHECK(j.at("signed") == true);
const DigiBattle99Card back = j.get<DigiBattle99Card>();
CHECK(back == c);
}
}
TEST_SUITE("Configuration JSON matches Rust serde aliases") {
TEST_CASE("dataStorage / defaultGame / theme keys are present") {
Configuration cfg;
@@ -497,6 +529,36 @@ TEST_SUITE("Domain JSON required fields") {
}
}
TEST_CASE("DigiBattle99Card missing each required key throws") {
const nlohmann::json full = {
{"id", 3},
{"amount", 1},
{"name", "Agumon"},
{"set", nlohmann::json{
{"id", "series-1-starter-set"},
{"name", "Series 1 Starter Set"},
{"releaseDate", "1999/06/01"},
}},
{"setNo", "ST-01"},
{"note", ""},
{"images", nlohmann::json::array()},
{"language", "English"},
{"condition", "NearMint"},
{"firstEdition", false},
{"holo", true},
{"signed", false},
{"altered", false},
};
for (const char* key :
{"id", "amount", "name", "set", "setNo", "note", "images", "language", "condition",
"firstEdition", "holo", "signed", "altered"}) {
nlohmann::json partial = full;
partial.erase(key);
CHECK_THROWS(partial.get<DigiBattle99Card>());
}
}
TEST_CASE("Configuration missing required key throws") {
const nlohmann::json j = {
{"defaultGame", "Magic"},
+12
View File
@@ -1,5 +1,6 @@
#include <doctest/doctest.h>
#include "ccm/games/digibattle99/DigiBattle99GameModule.hpp"
#include "ccm/games/magic/MagicGameModule.hpp"
#include "ccm/games/pokemon/PokemonGameModule.hpp"
#include "ccm/games/yugioh/YuGiOhGameModule.hpp"
@@ -51,4 +52,15 @@ TEST_SUITE("game modules expose stable identity and wiring") {
CHECK(module.cardPreviewSource() != nullptr);
CHECK(static_cast<void*>(&module.setSource()) != static_cast<void*>(module.cardPreviewSource()));
}
TEST_CASE("DigiBattle99 module reports canonical metadata") {
NoopHttpClient http;
DigiBattle99GameModule module(http);
CHECK(module.id() == Game::DigiBattle99);
CHECK(module.dirName() == "digibattle99");
CHECK(module.displayName() == "Digimon (Digi-Battle)");
CHECK(module.cardPreviewSource() != nullptr);
CHECK(static_cast<void*>(&module.setSource()) != static_cast<void*>(module.cardPreviewSource()));
}
}
+1
View File
@@ -17,6 +17,7 @@ std::string dirNameForGame(Game g) {
case Game::Magic: return "magic";
case Game::Pokemon: return "pokemon";
case Game::YuGiOh: return "yugioh";
case Game::DigiBattle99: return "digibattle99";
}
return "magic";
}
+31
View File
@@ -148,6 +148,37 @@ TEST_SUITE("SetService") {
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<std::vector<Set>>::ok({{"lea", "Alpha", "1993/08/05"}});
FakeGameModule pokemon{Game::Pokemon};
pokemon.source.result = Result<std::vector<Set>>::ok({{"base1", "Base", "1999/01/09"}});
FakeGameModule yugioh{Game::YuGiOh};
yugioh.source.result = Result<std::vector<Set>>::ok({{"LOB", "Legend of Blue Eyes", "2002/03/08"}});
FakeGameModule digi{Game::DigiBattle99};
digi.source.result = Result<std::vector<Set>>::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("updateSets propagates repository save failures") {
InMemSetRepo repo;
repo.failSave = true;