mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
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:
+3
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"},
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user