mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-30 17:38:48 +00:00
Minor: New Game Yu-Gi-Oh! Bandai (#22)
This commit is contained in:
@@ -26,6 +26,9 @@ add_executable(ccm_core_tests
|
||||
digibattle99_set_source_tests.cpp
|
||||
digibattle99_card_preview_source_tests.cpp
|
||||
digibattle99_set_completion_tests.cpp
|
||||
yugiohbandai_set_source_tests.cpp
|
||||
yugiohbandai_card_preview_source_tests.cpp
|
||||
yugiohbandai_set_completion_tests.cpp
|
||||
yugioh_set_completion_tests.cpp
|
||||
pokemon_set_completion_tests.cpp
|
||||
set_no_natural_tests.cpp
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/services/CardFilter.hpp"
|
||||
|
||||
@@ -287,6 +288,39 @@ TEST_SUITE("CardFilter::matchesDigiBattle99Filter") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("CardFilter::matchesYuGiOhBandaiFilter") {
|
||||
TEST_CASE("matches name set setNo rarity language") {
|
||||
YuGiOhBandaiCard c;
|
||||
c.name = "Dark Magician";
|
||||
c.set.name = "1st Generation";
|
||||
c.setNo = "14";
|
||||
c.rarity = "Rare";
|
||||
c.language = Language::Japanese;
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, "magician"));
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, "1st"));
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, "14"));
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, "rare"));
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, "japanese"));
|
||||
CHECK_FALSE(matchesYuGiOhBandaiFilter(c, "blue-eyes"));
|
||||
}
|
||||
|
||||
TEST_CASE("empty filter matches everything") {
|
||||
YuGiOhBandaiCard c;
|
||||
c.name = "Dark Magician";
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, ""));
|
||||
}
|
||||
|
||||
TEST_CASE("boolean flag columns are not matched") {
|
||||
YuGiOhBandaiCard c;
|
||||
c.name = "Dark Magician";
|
||||
c.holo = true;
|
||||
c.signed_ = true;
|
||||
c.altered = true;
|
||||
CHECK_FALSE(matchesYuGiOhBandaiFilter(c, "true"));
|
||||
CHECK(matchesYuGiOhBandaiFilter(c, "dark"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("CardFilter::matchesJapanesePokemonFilter") {
|
||||
TEST_CASE("matches by name and set.name") {
|
||||
JapanesePokemonCard c;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/domain/Set.hpp"
|
||||
#include "ccm/services/CardSorter.hpp"
|
||||
@@ -548,6 +549,51 @@ TEST_SUITE("CardSorter - DigiBattle99 columns") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("CardSorter - YuGiOhBandai columns") {
|
||||
TEST_CASE("Holo sorts false before true; Rarity and SetNo sort") {
|
||||
YuGiOhBandaiCard a;
|
||||
a.id = 1;
|
||||
a.name = "a";
|
||||
a.set = Set{"ban1", "1st Generation", "1998/09/01"};
|
||||
a.setNo = "14";
|
||||
a.rarity = "Rare";
|
||||
a.holo = true;
|
||||
|
||||
YuGiOhBandaiCard b;
|
||||
b.id = 2;
|
||||
b.name = "b";
|
||||
b.set = Set{"ban1", "1st Generation", "1998/09/01"};
|
||||
b.setNo = "9";
|
||||
b.rarity = "Common";
|
||||
b.holo = false;
|
||||
|
||||
std::vector<YuGiOhBandaiCard> v = {a, b};
|
||||
sortYuGiOhBandaiCards(v, YuGiOhBandaiSortColumn::Holo, /*ascending=*/true);
|
||||
CHECK(v[0].id == 2);
|
||||
CHECK(v[1].id == 1);
|
||||
|
||||
sortYuGiOhBandaiCards(v, YuGiOhBandaiSortColumn::SetNo, /*ascending=*/true);
|
||||
CHECK(v[0].setNo == "14");
|
||||
CHECK(v[1].setNo == "9");
|
||||
|
||||
sortYuGiOhBandaiCards(v, YuGiOhBandaiSortColumn::Rarity, /*ascending=*/true);
|
||||
CHECK(v[0].rarity == "Common");
|
||||
}
|
||||
|
||||
TEST_CASE("Set column sorts by release date") {
|
||||
YuGiOhBandaiCard a;
|
||||
a.id = 1;
|
||||
a.set = Set{"ban3", "3rd Generation", "1999/03/06"};
|
||||
YuGiOhBandaiCard b;
|
||||
b.id = 2;
|
||||
b.set = Set{"ban1", "1st Generation", "1998/09/01"};
|
||||
std::vector<YuGiOhBandaiCard> v = {a, b};
|
||||
sortYuGiOhBandaiCards(v, YuGiOhBandaiSortColumn::SetReleaseDate, /*ascending=*/true);
|
||||
CHECK(v[0].id == 2);
|
||||
CHECK(v[1].id == 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("CardSorter - JapanesePokemon columns") {
|
||||
TEST_CASE("Holo and FirstEdition sort false before true") {
|
||||
std::vector<JapanesePokemonCard> v = {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "ccm/domain/JapanesePokemonCard.hpp"
|
||||
#include "ccm/domain/MagicCard.hpp"
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
|
||||
#include "ccm/domain/YuGiOhCard.hpp"
|
||||
#include "ccm/domain/Set.hpp"
|
||||
|
||||
@@ -31,6 +33,9 @@ TEST_SUITE("domain enums round-trip JSON as strings") {
|
||||
nlohmann::json jDigi = "DigiBattle99";
|
||||
CHECK(jDigi.get<Game>() == Game::DigiBattle99);
|
||||
|
||||
nlohmann::json jBandai = "YuGiOhBandai";
|
||||
CHECK(jBandai.get<Game>() == Game::YuGiOhBandai);
|
||||
|
||||
nlohmann::json jJp = "JapanesePokemon";
|
||||
CHECK(jJp.get<Game>() == Game::JapanesePokemon);
|
||||
|
||||
@@ -77,7 +82,7 @@ TEST_SUITE("domain enums round-trip JSON as strings") {
|
||||
for (const auto game : allGames()) {
|
||||
CHECK(game != Game::JapanesePokemon);
|
||||
}
|
||||
CHECK(allGames().size() == 4);
|
||||
CHECK(allGames().size() == 5);
|
||||
CHECK(gameFromString("JapanesePokemon") == Game::JapanesePokemon);
|
||||
CHECK(pokemonBackendGame(PokemonRegion::West) == Game::Pokemon);
|
||||
CHECK(pokemonBackendGame(PokemonRegion::Asia) == Game::JapanesePokemon);
|
||||
@@ -312,6 +317,87 @@ TEST_SUITE("DigiBattle99SetCatalog JSON") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhBandaiCard JSON") {
|
||||
TEST_CASE("round-trips with setNo, rarity, holo and signed alias") {
|
||||
YuGiOhBandaiCard c;
|
||||
c.id = 14;
|
||||
c.amount = 2;
|
||||
c.name = "Dark Magician";
|
||||
c.set = Set{"ban1", "1st Generation", "1998/09/01"};
|
||||
c.setNo = "14";
|
||||
c.rarity = "Rare";
|
||||
c.note = "classic";
|
||||
c.images = {"a.png"};
|
||||
c.language = Language::Japanese;
|
||||
c.condition = Condition::NearMint;
|
||||
c.holo = true;
|
||||
c.signed_ = true;
|
||||
c.altered = false;
|
||||
|
||||
nlohmann::json j = c;
|
||||
CHECK(j.at("setNo") == "14");
|
||||
CHECK(j.at("rarity") == "Rare");
|
||||
CHECK(j.at("holo") == true);
|
||||
CHECK(j.at("signed") == true);
|
||||
CHECK_FALSE(j.contains("firstEdition"));
|
||||
|
||||
const YuGiOhBandaiCard back = j.get<YuGiOhBandaiCard>();
|
||||
CHECK(back == c);
|
||||
}
|
||||
|
||||
TEST_CASE("missing each required key throws") {
|
||||
const nlohmann::json full = {
|
||||
{"id", 14},
|
||||
{"amount", 1},
|
||||
{"name", "Dark Magician"},
|
||||
{"set", nlohmann::json{
|
||||
{"id", "ban1"},
|
||||
{"name", "1st Generation"},
|
||||
{"releaseDate", "1998/09/01"},
|
||||
}},
|
||||
{"setNo", "14"},
|
||||
{"rarity", "Rare"},
|
||||
{"note", ""},
|
||||
{"images", nlohmann::json::array()},
|
||||
{"language", "Japanese"},
|
||||
{"condition", "NearMint"},
|
||||
{"holo", false},
|
||||
{"signed", false},
|
||||
{"altered", false},
|
||||
};
|
||||
|
||||
for (const char* key : {
|
||||
"id", "amount", "name", "set", "setNo", "rarity", "note", "images",
|
||||
"language", "condition", "holo", "signed", "altered"}) {
|
||||
nlohmann::json partial = full;
|
||||
partial.erase(key);
|
||||
CHECK_THROWS(partial.get<YuGiOhBandaiCard>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhBandaiSetCatalog JSON") {
|
||||
TEST_CASE("round-trips packs with rarity") {
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack pack;
|
||||
pack.setId = "ban1";
|
||||
pack.setName = "1st Generation";
|
||||
pack.cards.push_back(YuGiOhBandaiCatalogCard{"14", "Dark Magician", "Rare"});
|
||||
pack.cards.push_back(YuGiOhBandaiCatalogCard{"9", "Blue-Eyes White Dragon", "Super Rare"});
|
||||
catalog.packs.push_back(std::move(pack));
|
||||
|
||||
nlohmann::json j = catalog;
|
||||
CHECK(j.at("packs").at(0).at("id") == "ban1");
|
||||
CHECK(j.at("packs").at(0).at("cards").at(0).at("setNo") == "14");
|
||||
CHECK(j.at("packs").at(0).at("cards").at(0).at("rarity") == "Rare");
|
||||
|
||||
const YuGiOhBandaiSetCatalog back = j.get<YuGiOhBandaiSetCatalog>();
|
||||
CHECK(back == catalog);
|
||||
CHECK(back.findPack("ban1") != nullptr);
|
||||
CHECK(back.findPack("missing") == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhSetCatalog JSON") {
|
||||
TEST_CASE("round-trips packs and setNo alias") {
|
||||
YuGiOhSetCatalog catalog;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "ccm/games/pokemon/PokemonGameModule.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonGameModule.hpp"
|
||||
#include "ccm/games/yugioh/YuGiOhGameModule.hpp"
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiGameModule.hpp"
|
||||
#include "ccm/ports/IHttpClient.hpp"
|
||||
|
||||
using namespace ccm;
|
||||
@@ -65,6 +66,17 @@ TEST_SUITE("game modules expose stable identity and wiring") {
|
||||
CHECK(static_cast<void*>(&module.setSource()) != static_cast<void*>(module.cardPreviewSource()));
|
||||
}
|
||||
|
||||
TEST_CASE("YuGiOhBandai module reports canonical metadata") {
|
||||
NoopHttpClient http;
|
||||
YuGiOhBandaiGameModule module(http);
|
||||
|
||||
CHECK(module.id() == Game::YuGiOhBandai);
|
||||
CHECK(module.dirName() == "yugiohbandai");
|
||||
CHECK(module.displayName() == "Yu-Gi-Oh! (Bandai)");
|
||||
CHECK(module.cardPreviewSource() != nullptr);
|
||||
CHECK(static_cast<void*>(&module.setSource()) != static_cast<void*>(module.cardPreviewSource()));
|
||||
}
|
||||
|
||||
TEST_CASE("JapanesePokemon module reports canonical metadata") {
|
||||
NoopHttpClient http;
|
||||
JapanesePokemonGameModule module(http);
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
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";
|
||||
}
|
||||
@@ -182,6 +183,43 @@ TEST_SUITE("SetService") {
|
||||
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<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"}});
|
||||
FakeGameModule bandai{Game::YuGiOhBandai};
|
||||
bandai.source.result = Result<std::vector<Set>>::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};
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiCardPreviewSource.hpp"
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace ccm;
|
||||
|
||||
namespace {
|
||||
|
||||
class FixedHttpClient final : public IHttpClient {
|
||||
public:
|
||||
std::string body;
|
||||
std::string lastUrl;
|
||||
bool fail{false};
|
||||
|
||||
Result<std::string> get(std::string_view url) override {
|
||||
lastUrl = std::string(url);
|
||||
if (fail) return Result<std::string>::err("http fail");
|
||||
return Result<std::string>::ok(body);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_SUITE("YuGiOhBandaiCardPreviewSource helpers") {
|
||||
TEST_CASE("preferredPageTitle picks Bandai / English / Sealdass") {
|
||||
CHECK(YuGiOhBandaiCardPreviewSource::preferredPageTitle("Dark Magician", "ban1", "14") ==
|
||||
"Dark Magician (Bandai)");
|
||||
CHECK(YuGiOhBandaiCardPreviewSource::preferredPageTitle("Blue-Eyes White Dragon", "ban3",
|
||||
"118") ==
|
||||
"Blue-Eyes White Dragon (English Bandai)");
|
||||
CHECK(YuGiOhBandaiCardPreviewSource::preferredPageTitle("Dark Magician", "bansealdass",
|
||||
"2") ==
|
||||
"Dark Magician (Bandai Sealdass)");
|
||||
}
|
||||
|
||||
TEST_CASE("buildPageImagesUrl encodes spaces as underscores then percent") {
|
||||
const auto url =
|
||||
YuGiOhBandaiCardPreviewSource::buildPageImagesUrl("Dark Magician (Bandai)");
|
||||
CHECK(url.find("titles=Dark_Magician_%28Bandai%29") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("buildAskByNameUrl includes English name constraint") {
|
||||
const auto url = YuGiOhBandaiCardPreviewSource::buildAskByNameUrl("Dark Magician");
|
||||
CHECK(url.find("action=ask") != std::string::npos);
|
||||
CHECK(url.find("query=") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("parsePageImagesResponse returns original source") {
|
||||
const std::string body = R"JSON({
|
||||
"query": {
|
||||
"pages": {
|
||||
"1": {
|
||||
"title": "Dark Magician (Bandai)",
|
||||
"original": {"source": "https://ms.yugipedia.com/d/d0/DarkMagician.png"}
|
||||
}
|
||||
}
|
||||
}
|
||||
})JSON";
|
||||
auto out = YuGiOhBandaiCardPreviewSource::parsePageImagesResponse(body);
|
||||
REQUIRE(out);
|
||||
CHECK(out.value() == "https://ms.yugipedia.com/d/d0/DarkMagician.png");
|
||||
}
|
||||
|
||||
TEST_CASE("parsePageImagesResponse missing page is NotFound") {
|
||||
const std::string body = R"JSON({
|
||||
"query": { "pages": { "-1": { "missing": true, "title": "Nope" } } }
|
||||
})JSON";
|
||||
auto out = YuGiOhBandaiCardPreviewSource::parsePageImagesResponse(body);
|
||||
REQUIRE_FALSE(out);
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("parseAskResponse fills setNo rarity name and setId") {
|
||||
const std::string body = R"JSON({
|
||||
"query": {
|
||||
"results": {
|
||||
"Dark Magician (Bandai)": {
|
||||
"printouts": {
|
||||
"English name": ["Dark Magician"],
|
||||
"Bandai number": [14],
|
||||
"Rarity": [{"fulltext": "Rare"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})JSON";
|
||||
auto out = YuGiOhBandaiCardPreviewSource::parseAskResponse(body, "ban1");
|
||||
REQUIRE(out);
|
||||
REQUIRE(out.value().size() == 1);
|
||||
CHECK(out.value()[0].name == "Dark Magician");
|
||||
CHECK(out.value()[0].setNo == "14");
|
||||
CHECK(out.value()[0].rarity == "Rare");
|
||||
CHECK(out.value()[0].setId == "ban1");
|
||||
CHECK(out.value()[0].language == "Japanese");
|
||||
}
|
||||
|
||||
TEST_CASE("parseAskResponse prefers Bandai over Sealdass when set is ban1") {
|
||||
const std::string body = R"JSON({
|
||||
"query": {
|
||||
"results": {
|
||||
"Dark Magician (Bandai Sealdass)": {
|
||||
"printouts": {
|
||||
"English name": ["Dark Magician"],
|
||||
"Bandai number": [2],
|
||||
"Rarity": [{"fulltext": "Common"}]
|
||||
}
|
||||
},
|
||||
"Dark Magician (Bandai)": {
|
||||
"printouts": {
|
||||
"English name": ["Dark Magician"],
|
||||
"Bandai number": [14],
|
||||
"Rarity": [{"fulltext": "Rare"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})JSON";
|
||||
auto out = YuGiOhBandaiCardPreviewSource::parseAskResponse(body, "ban1");
|
||||
REQUIRE(out);
|
||||
REQUIRE(out.value().size() == 2);
|
||||
CHECK(out.value()[0].setNo == "14");
|
||||
CHECK(out.value()[0].setId == "ban1");
|
||||
}
|
||||
|
||||
TEST_CASE("fetchImageUrl uses pageimages URL") {
|
||||
FixedHttpClient http;
|
||||
http.body = R"JSON({
|
||||
"query": {
|
||||
"pages": {
|
||||
"1": {
|
||||
"title": "Dark Magician (Bandai)",
|
||||
"original": {"source": "https://ms.yugipedia.com/x.png"}
|
||||
}
|
||||
}
|
||||
}
|
||||
})JSON";
|
||||
YuGiOhBandaiCardPreviewSource src(http);
|
||||
auto out = src.fetchImageUrl("Dark Magician", "ban1", "14");
|
||||
REQUIRE(out);
|
||||
CHECK(out.value() == "https://ms.yugipedia.com/x.png");
|
||||
CHECK(http.lastUrl.find("pageimages") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("detectFirstPrint uses ask response") {
|
||||
FixedHttpClient http;
|
||||
http.body = R"JSON({
|
||||
"query": {
|
||||
"results": {
|
||||
"Dark Magician (Bandai)": {
|
||||
"printouts": {
|
||||
"English name": ["Dark Magician"],
|
||||
"Bandai number": [14],
|
||||
"Rarity": [{"fulltext": "Rare"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})JSON";
|
||||
YuGiOhBandaiCardPreviewSource src(http);
|
||||
auto out = src.detectFirstPrint("Dark Magician", "ban1");
|
||||
REQUIRE(out);
|
||||
CHECK(out.value().setNo == "14");
|
||||
CHECK(out.value().rarity == "Rare");
|
||||
CHECK(out.value().setId == "ban1");
|
||||
}
|
||||
|
||||
TEST_CASE("detectBySetNo uses ask-by-number URL") {
|
||||
FixedHttpClient http;
|
||||
http.body = R"JSON({
|
||||
"query": {
|
||||
"results": {
|
||||
"Dark Magician (Bandai)": {
|
||||
"printouts": {
|
||||
"English name": ["Dark Magician"],
|
||||
"Bandai number": [14],
|
||||
"Rarity": [{"fulltext": "Rare"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})JSON";
|
||||
YuGiOhBandaiCardPreviewSource src(http);
|
||||
auto out = src.detectBySetNo("014");
|
||||
REQUIRE(out);
|
||||
CHECK(out.value().name == "Dark Magician");
|
||||
CHECK(http.lastUrl.find("action=ask") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("detectBySetNo resolves promo TA2 from gallery parse") {
|
||||
FixedHttpClient http;
|
||||
http.body = R"JSON({
|
||||
"parse": {
|
||||
"wikitext": "WickedChain-BAN1-JP-SR.png | [[TA1]] ([[SR]]) {{Gallery card names|Wicked Chain|ja}}\nBlueEyesWhiteDragons3BodyConnection-BAN1-JP-SR.png | [[TA2]] ([[SR]])<br />{{Gallery card names|Blue-Eyes White Dragon's 3-Body Connection|ja}}\n"
|
||||
}
|
||||
})JSON";
|
||||
YuGiOhBandaiCardPreviewSource src(http);
|
||||
auto out = src.detectBySetNo("ta2");
|
||||
REQUIRE(out);
|
||||
CHECK(out.value().name == "Blue-Eyes White Dragon's 3-Body Connection");
|
||||
CHECK(out.value().setNo == "TA2");
|
||||
CHECK(out.value().setId == "banpromo-ta");
|
||||
CHECK(out.value().rarity == "Super Rare");
|
||||
CHECK(http.lastUrl.find("action=parse") != std::string::npos);
|
||||
CHECK(http.lastUrl.find("Promotional") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("isAlphanumericPromoNumber detects Jump and Toei codes") {
|
||||
CHECK(YuGiOhBandaiCardPreviewSource::isAlphanumericPromoNumber("TA2"));
|
||||
CHECK(YuGiOhBandaiCardPreviewSource::isAlphanumericPromoNumber("j1"));
|
||||
CHECK_FALSE(YuGiOhBandaiCardPreviewSource::isAlphanumericPromoNumber("14"));
|
||||
}
|
||||
|
||||
TEST_CASE("preferredPageTitle omits Bandai suffix for promo sets") {
|
||||
CHECK(YuGiOhBandaiCardPreviewSource::preferredPageTitle(
|
||||
"Blue-Eyes White Dragon's 3-Body Connection", "banpromo-ta", "TA2") ==
|
||||
"Blue-Eyes White Dragon's 3-Body Connection");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "ccm/domain/Configuration.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
|
||||
#include "ccm/services/ConfigService.hpp"
|
||||
#include "ccm/services/YuGiOhBandaiSetCatalogService.hpp"
|
||||
#include "ccm/services/YuGiOhBandaiSetCompletion.hpp"
|
||||
#include "fakes/InMemoryFileSystem.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
using namespace ccm;
|
||||
using ccm::testing::InMemoryFileSystem;
|
||||
|
||||
namespace {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
YuGiOhBandaiCard makeOwned(std::string setId, std::string setName, std::string setNo) {
|
||||
YuGiOhBandaiCard c;
|
||||
c.set = Set{std::move(setId), std::move(setName), "1998/09/01"};
|
||||
c.setNo = std::move(setNo);
|
||||
c.name = "Card";
|
||||
c.language = Language::Japanese;
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_SUITE("YuGiOhBandaiSetCompletion") {
|
||||
TEST_CASE("unique setNo within a pack; amount does not inflate") {
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack pack;
|
||||
pack.setId = "ban1";
|
||||
pack.setName = "1st Generation";
|
||||
pack.cards.push_back({"9", "Blue-Eyes", "Super Rare"});
|
||||
pack.cards.push_back({"14", "Dark Magician", "Rare"});
|
||||
catalog.packs.push_back(pack);
|
||||
|
||||
std::vector<YuGiOhBandaiCard> coll;
|
||||
auto a = makeOwned("ban1", "1st Generation", "14");
|
||||
a.amount = 5;
|
||||
coll.push_back(a);
|
||||
coll.push_back(makeOwned("ban1", "1st Generation", "014"));
|
||||
|
||||
auto progress = computeYuGiOhBandaiSetCompletion(coll, catalog);
|
||||
REQUIRE(progress.size() == 1);
|
||||
CHECK(progress[0].ownedUnique == 1);
|
||||
CHECK(progress[0].total == 2);
|
||||
CHECK(progress[0].percent() == 50);
|
||||
}
|
||||
|
||||
TEST_CASE("ownership on one pack does not complete another pack sharing setNo") {
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack ban1;
|
||||
ban1.setId = "ban1";
|
||||
ban1.setName = "1st Generation";
|
||||
ban1.cards.push_back({"14", "Dark Magician", "Rare"});
|
||||
YuGiOhBandaiSetCatalogPack seal;
|
||||
seal.setId = "bansealdass";
|
||||
seal.setName = "Sealdass";
|
||||
seal.cards.push_back({"14", "Other", "Common"});
|
||||
catalog.packs.push_back(ban1);
|
||||
catalog.packs.push_back(seal);
|
||||
|
||||
std::vector<YuGiOhBandaiCard> coll{makeOwned("ban1", "1st Generation", "14")};
|
||||
auto progress = computeYuGiOhBandaiSetCompletion(coll, catalog);
|
||||
REQUIRE(progress.size() == 1);
|
||||
CHECK(progress[0].setId == "ban1");
|
||||
}
|
||||
|
||||
TEST_CASE("checklist marks owned rows") {
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack pack;
|
||||
pack.setId = "ban1";
|
||||
pack.setName = "1st Generation";
|
||||
pack.cards.push_back({"9", "Blue-Eyes", "Super Rare"});
|
||||
pack.cards.push_back({"14", "Dark Magician", "Rare"});
|
||||
catalog.packs.push_back(pack);
|
||||
|
||||
std::vector<YuGiOhBandaiCard> coll{makeOwned("ban1", "1st Generation", "14")};
|
||||
auto list = yuGiOhBandaiChecklistForSet(coll, catalog, "ban1");
|
||||
REQUIRE(list.size() == 2);
|
||||
CHECK(list[0].setNo == "14");
|
||||
CHECK(list[0].owned);
|
||||
CHECK(list[1].setNo == "9");
|
||||
CHECK_FALSE(list[1].owned);
|
||||
}
|
||||
|
||||
TEST_CASE("empty setNo produces no progress rows") {
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack pack;
|
||||
pack.setId = "ban1";
|
||||
pack.setName = "1st Generation";
|
||||
pack.cards.push_back({"14", "Dark Magician", "Rare"});
|
||||
catalog.packs.push_back(pack);
|
||||
|
||||
YuGiOhBandaiCard missingNo = makeOwned("ban1", "1st Generation", "");
|
||||
YuGiOhBandaiCard whitespaceNo = makeOwned("ban1", "1st Generation", " ");
|
||||
std::vector<YuGiOhBandaiCard> coll{missingNo, whitespaceNo};
|
||||
auto progress = computeYuGiOhBandaiSetCompletion(coll, catalog);
|
||||
CHECK(progress.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("set.id plus setNo against catalog pack yields a tile") {
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack pack;
|
||||
pack.setId = "ban2";
|
||||
pack.setName = "2nd Generation";
|
||||
pack.cards.push_back({"47", "Time Wizard", "Super Rare"});
|
||||
pack.cards.push_back({"48", "Polymerization", "Super Rare"});
|
||||
catalog.packs.push_back(pack);
|
||||
|
||||
std::vector<YuGiOhBandaiCard> coll{makeOwned("ban2", "2nd Generation", "047")};
|
||||
auto progress = computeYuGiOhBandaiSetCompletion(coll, catalog);
|
||||
REQUIRE(progress.size() == 1);
|
||||
CHECK(progress[0].setId == "ban2");
|
||||
CHECK(progress[0].setName == "2nd Generation");
|
||||
CHECK(progress[0].ownedUnique == 1);
|
||||
CHECK(progress[0].total == 2);
|
||||
CHECK(progress[0].percent() == 50);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhBandaiSetCatalogService") {
|
||||
TEST_CASE("catalog service round-trips against InMemoryFileSystem") {
|
||||
InMemoryFileSystem fs;
|
||||
auto config = makeConfig(fs, "/data");
|
||||
YuGiOhBandaiSetCatalogService svc(fs, config, [](Game) { return "yugiohbandai"; });
|
||||
|
||||
YuGiOhBandaiSetCatalog catalog;
|
||||
YuGiOhBandaiSetCatalogPack pack;
|
||||
pack.setId = "ban1";
|
||||
pack.setName = "1st Generation";
|
||||
pack.cards.push_back({"14", "Dark Magician", "Rare"});
|
||||
catalog.packs.push_back(pack);
|
||||
|
||||
REQUIRE(svc.save(catalog));
|
||||
CHECK(svc.exists());
|
||||
auto loaded = svc.load();
|
||||
REQUIRE(loaded);
|
||||
CHECK(loaded.value() == catalog);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiSetSource.hpp"
|
||||
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace ccm;
|
||||
|
||||
namespace {
|
||||
|
||||
class FixedHttpClient final : public IHttpClient {
|
||||
public:
|
||||
std::string body;
|
||||
std::string lastUrl;
|
||||
bool fail{false};
|
||||
|
||||
Result<std::string> get(std::string_view url) override {
|
||||
lastUrl = std::string(url);
|
||||
if (fail) return Result<std::string>::err("http fail");
|
||||
return Result<std::string>::ok(body);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_SUITE("YuGiOhBandaiSetSource") {
|
||||
TEST_CASE("parseResponse returns stable manifest ordered by release date") {
|
||||
auto sets = YuGiOhBandaiSetSource::parseResponse({});
|
||||
REQUIRE(sets);
|
||||
REQUIRE(sets.value().size() == 6);
|
||||
CHECK(sets.value()[0].id == "ban1");
|
||||
CHECK(sets.value()[0].name == "1st Generation");
|
||||
CHECK(sets.value()[0].releaseDate == "1998/09/01");
|
||||
CHECK(sets.value()[5].id == "bansealdass");
|
||||
}
|
||||
|
||||
TEST_CASE("normalizeCardNumber strips leading zeros and uppercases prefixes") {
|
||||
CHECK(YuGiOhBandaiSetSource::normalizeCardNumber("014") == "14");
|
||||
CHECK(YuGiOhBandaiSetSource::normalizeCardNumber("#9") == "9");
|
||||
CHECK(YuGiOhBandaiSetSource::normalizeCardNumber("j1") == "J1");
|
||||
CHECK(YuGiOhBandaiSetSource::normalizeCardNumber("ta2") == "TA2");
|
||||
CHECK(YuGiOhBandaiSetSource::normalizeCardNumber(" ") == "");
|
||||
}
|
||||
|
||||
TEST_CASE("expandRarityCode maps gallery abbreviations") {
|
||||
CHECK(YuGiOhBandaiSetSource::expandRarityCode("C") == "Common");
|
||||
CHECK(YuGiOhBandaiSetSource::expandRarityCode("R") == "Rare");
|
||||
CHECK(YuGiOhBandaiSetSource::expandRarityCode("SR") == "Super Rare");
|
||||
CHECK(YuGiOhBandaiSetSource::expandRarityCode("HFR") == "Holo Seal");
|
||||
}
|
||||
|
||||
TEST_CASE("setIdForNumber maps ranges and promo prefixes") {
|
||||
CHECK(YuGiOhBandaiSetSource::setIdForNumber("14") == "ban1");
|
||||
CHECK(YuGiOhBandaiSetSource::setIdForNumber("50") == "ban2");
|
||||
CHECK(YuGiOhBandaiSetSource::setIdForNumber("118") == "ban3");
|
||||
CHECK(YuGiOhBandaiSetSource::setIdForNumber("J1") == "banpromo-j");
|
||||
CHECK(YuGiOhBandaiSetSource::setIdForNumber("TA2") == "banpromo-ta");
|
||||
}
|
||||
|
||||
TEST_CASE("parseGalleryWikitext extracts number rarity and English name") {
|
||||
const std::string wiki =
|
||||
"DarkMagician-BAN1-JP-R.png | {{pound}}014 ([[R]]) "
|
||||
"{{Gallery card names|Dark Magician (Bandai)|ja}}\n"
|
||||
"BlueEyesWhiteDragon-BAN1-JP-SR.png | {{pound}}009 ([[SR]]) "
|
||||
"{{Gallery card names|Blue-Eyes White Dragon (Bandai)|ja}}\n";
|
||||
|
||||
auto cards = YuGiOhBandaiSetSource::parseGalleryWikitext(wiki);
|
||||
REQUIRE(cards);
|
||||
REQUIRE(cards.value().size() == 2);
|
||||
CHECK(cards.value()[0].setNo == "14");
|
||||
CHECK(cards.value()[0].name == "Dark Magician");
|
||||
CHECK(cards.value()[0].rarity == "Rare");
|
||||
CHECK(cards.value()[1].setNo == "9");
|
||||
CHECK(cards.value()[1].rarity == "Super Rare");
|
||||
}
|
||||
|
||||
TEST_CASE("parseGalleryWikitext accepts promo [[TA2]] number format") {
|
||||
const std::string wiki =
|
||||
"WickedChain-BAN1-JP-SR.png | [[TA1]] ([[SR]]) "
|
||||
"{{Gallery card names|Wicked Chain|ja}}\n"
|
||||
"BlueEyesWhiteDragons3BodyConnection-BAN1-JP-SR.png | [[TA2]] ([[SR]]) "
|
||||
"{{Gallery card names|Blue-Eyes White Dragon's 3-Body Connection|ja}}\n"
|
||||
"MirrorForce-BAN1-JP-SR.png | [[J1]] ([[SR]]) "
|
||||
"{{Gallery card names|Mirror Force (Bandai)|ja}}\n";
|
||||
|
||||
auto cards = YuGiOhBandaiSetSource::parseGalleryWikitext(wiki);
|
||||
REQUIRE(cards);
|
||||
REQUIRE(cards.value().size() == 3);
|
||||
CHECK(cards.value()[0].setNo == "TA1");
|
||||
CHECK(cards.value()[0].name == "Wicked Chain");
|
||||
CHECK(cards.value()[0].rarity == "Super Rare");
|
||||
CHECK(cards.value()[1].setNo == "TA2");
|
||||
CHECK(cards.value()[1].name == "Blue-Eyes White Dragon's 3-Body Connection");
|
||||
CHECK(cards.value()[2].setNo == "J1");
|
||||
CHECK(cards.value()[2].name == "Mirror Force");
|
||||
}
|
||||
|
||||
TEST_CASE("parseGalleryWikitext tolerates <br /> between rarity and name template") {
|
||||
// Live Yugipedia promo gallery captions insert <br /> after expansion.
|
||||
const std::string wiki =
|
||||
"<gallery mode=\"packed\">\n"
|
||||
"BlueEyesWhiteDragons3BodyConnection-BAN1-JP-SR.png | [[TA2]] ([[SR]])<br />"
|
||||
"{{Gallery card names|Blue-Eyes White Dragon's 3-Body Connection|ja}}\n"
|
||||
"MirrorForce-BAN1-JP-SR.png | [[J1]] ([[SR]])<br />"
|
||||
"{{Gallery card names|Mirror Force (Bandai)|ja}}\n"
|
||||
"</gallery>\n";
|
||||
|
||||
auto cards = YuGiOhBandaiSetSource::parseGalleryWikitext(wiki);
|
||||
REQUIRE(cards);
|
||||
REQUIRE(cards.value().size() == 2);
|
||||
CHECK(cards.value()[0].setNo == "TA2");
|
||||
CHECK(cards.value()[0].name == "Blue-Eyes White Dragon's 3-Body Connection");
|
||||
CHECK(cards.value()[0].rarity == "Super Rare");
|
||||
CHECK(cards.value()[1].setNo == "J1");
|
||||
}
|
||||
|
||||
TEST_CASE("parseGalleryWikitext empty body yields empty ok") {
|
||||
auto cards = YuGiOhBandaiSetSource::parseGalleryWikitext("");
|
||||
REQUIRE(cards);
|
||||
CHECK(cards.value().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("fetchAll returns manifest without HTTP") {
|
||||
FixedHttpClient http;
|
||||
YuGiOhBandaiSetSource src(http);
|
||||
auto sets = src.fetchAll();
|
||||
REQUIRE(sets);
|
||||
CHECK(sets.value().size() == 6);
|
||||
CHECK(http.lastUrl.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("buildGalleryParseUrl percent-encodes page title") {
|
||||
const auto url = YuGiOhBandaiSetSource::buildGalleryParseUrl(
|
||||
"Set Card Galleries:Yu-Gi-Oh! Bandai OCG: 1st Generation");
|
||||
CHECK(url.find("action=parse") != std::string::npos);
|
||||
CHECK(url.find("prop=wikitext") != std::string::npos);
|
||||
CHECK(url.find("page=") != std::string::npos);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user