mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 22:01:12 +00:00
371 lines
14 KiB
C++
371 lines
14 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "ccm/games/pokemonjp/JapanesePokemonSetSource.hpp"
|
|
#include "ccm/ports/IHttpClient.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
using namespace ccm;
|
|
|
|
namespace {
|
|
|
|
class RoutingHttpClient final : public IHttpClient {
|
|
public:
|
|
std::unordered_map<std::string, std::string> bodies;
|
|
std::string lastUrl;
|
|
bool ok = true;
|
|
Result<std::string> get(std::string_view url) override {
|
|
lastUrl = std::string(url);
|
|
if (!ok) return Result<std::string>::err("offline");
|
|
const auto it = bodies.find(lastUrl);
|
|
if (it == bodies.end()) return Result<std::string>::err("unknown url");
|
|
return Result<std::string>::ok(it->second);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_SUITE("JapanesePokemonSetSource helpers") {
|
|
TEST_CASE("excludes CS* set ids") {
|
|
CHECK(JapanesePokemonSetSource::shouldExcludeSetId("CS1a"));
|
|
CHECK(JapanesePokemonSetSource::shouldExcludeSetId("CS4a"));
|
|
CHECK_FALSE(JapanesePokemonSetSource::shouldExcludeSetId("PMCG1"));
|
|
CHECK_FALSE(JapanesePokemonSetSource::shouldExcludeSetId("SV1a"));
|
|
}
|
|
|
|
TEST_CASE("applies SV4a name override") {
|
|
CHECK(JapanesePokemonSetSource::applySetNameOverride("SV4a", "wrong") ==
|
|
"シャイニートレジャーex");
|
|
CHECK(JapanesePokemonSetSource::applySetNameOverride("PMCG1", "拡張パック") ==
|
|
"拡張パック");
|
|
}
|
|
|
|
TEST_CASE("rewrites release date separators") {
|
|
CHECK(JapanesePokemonSetSource::rewriteReleaseDate("1996-10-20") == "1996/10/20");
|
|
}
|
|
|
|
TEST_CASE("buildSetDetailUrl percent-encodes id") {
|
|
CHECK(JapanesePokemonSetSource::buildSetDetailUrl("SV1a") ==
|
|
"https://api.tcgdex.net/v2/ja/sets/SV1a");
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("JapanesePokemonSetSource::parseListResponse") {
|
|
TEST_CASE("maps id/name and drops CS* entries") {
|
|
const std::string json = R"([
|
|
{"id":"PMCG1","name":"拡張パック","cardCount":{"total":102,"official":102}},
|
|
{"id":"CS1a","name":"トリプレットビート","cardCount":{"total":1,"official":1}},
|
|
{"id":"SV4a","name":"レイジングサーフ","cardCount":{"total":320,"official":190}}
|
|
])";
|
|
const auto out = JapanesePokemonSetSource::parseListResponse(json);
|
|
REQUIRE(out.isOk());
|
|
// 2 from TCGdex + 11 curated products omitted by TCGdex.
|
|
REQUIRE(out.value().size() == 13);
|
|
CHECK(out.value()[0].id == "PMCG1");
|
|
CHECK(out.value()[0].name == "拡張パック");
|
|
CHECK(out.value()[1].id == "SV4a");
|
|
CHECK(out.value()[1].name == "シャイニートレジャーex");
|
|
}
|
|
|
|
TEST_CASE("injects classic City Gym and Expansion Sheet products") {
|
|
const auto out = JapanesePokemonSetSource::parseListResponse("[]");
|
|
REQUIRE(out.isOk());
|
|
REQUIRE(out.value().size() == 11);
|
|
const auto hasId = [&](const char* id) {
|
|
return std::any_of(out.value().begin(), out.value().end(),
|
|
[&](const Set& s) { return s.id == id; });
|
|
};
|
|
CHECK(hasId("UnnumberedPromo"));
|
|
CHECK(hasId("TamamushiCG"));
|
|
CHECK(hasId("NiviCG"));
|
|
CHECK(hasId("HanadaCG"));
|
|
CHECK(hasId("KuchibaCG"));
|
|
CHECK(hasId("YamabukiCG"));
|
|
CHECK(hasId("GurenTG"));
|
|
CHECK(hasId("ExpSheet1"));
|
|
CHECK(hasId("ExpSheet2"));
|
|
CHECK(hasId("ExpSheet3"));
|
|
CHECK(hasId("SouthernIslands"));
|
|
const Set* unnumbered = nullptr;
|
|
const Set* tama = nullptr;
|
|
for (const auto& s : out.value()) {
|
|
if (s.id == "UnnumberedPromo") unnumbered = &s;
|
|
if (s.id == "TamamushiCG") tama = &s;
|
|
}
|
|
REQUIRE(unnumbered != nullptr);
|
|
CHECK(unnumbered->name == "Unnumbered Promotional cards");
|
|
CHECK(unnumbered->releaseDate == "1997/03/06");
|
|
REQUIRE(tama != nullptr);
|
|
CHECK(tama->name == "Tamamushi City Gym");
|
|
CHECK(tama->releaseDate == "1998/07/25");
|
|
}
|
|
|
|
TEST_CASE("does not duplicate classic products already in the list") {
|
|
const std::string json = R"([
|
|
{"id":"TamamushiCG","name":"already-present"}
|
|
])";
|
|
const auto out = JapanesePokemonSetSource::parseListResponse(json);
|
|
REQUIRE(out.isOk());
|
|
int tamaCount = 0;
|
|
for (const auto& s : out.value()) {
|
|
if (s.id == "TamamushiCG") ++tamaCount;
|
|
}
|
|
CHECK(tamaCount == 1);
|
|
// Curated EN name / release date overwrite a stale upstream label.
|
|
CHECK(out.value().front().name == "Tamamushi City Gym");
|
|
CHECK(out.value().front().releaseDate == "1998/07/25");
|
|
}
|
|
|
|
TEST_CASE("empty array still injects classic products") {
|
|
const auto out = JapanesePokemonSetSource::parseListResponse("[]");
|
|
REQUIRE(out.isOk());
|
|
CHECK_FALSE(out.value().empty());
|
|
}
|
|
|
|
TEST_CASE("non-array is an error") {
|
|
CHECK(JapanesePokemonSetSource::parseListResponse(R"({"data":[]})").isErr());
|
|
}
|
|
|
|
TEST_CASE("invalid JSON is an error") {
|
|
CHECK(JapanesePokemonSetSource::parseListResponse("{not json").isErr());
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("JapanesePokemonSetSource::parseReleaseDate") {
|
|
TEST_CASE("extracts and rewrites releaseDate") {
|
|
const auto out = JapanesePokemonSetSource::parseReleaseDate(
|
|
R"({"id":"PMCG1","releaseDate":"1996-10-20"})");
|
|
REQUIRE(out.isOk());
|
|
CHECK(out.value() == "1996/10/20");
|
|
}
|
|
|
|
TEST_CASE("missing releaseDate yields empty string") {
|
|
const auto out = JapanesePokemonSetSource::parseReleaseDate(R"({"id":"X"})");
|
|
REQUIRE(out.isOk());
|
|
CHECK(out.value().empty());
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("JapanesePokemonSetSource::fetchAll") {
|
|
TEST_CASE("enriches from catalog and sorts by release date") {
|
|
RoutingHttpClient http;
|
|
http.bodies[JapanesePokemonSetSource::kListEndpoint] = R"([
|
|
{"id":"SV1a","name":"トリプレットビート"},
|
|
{"id":"PMCG1","name":"拡張パック"},
|
|
{"id":"PMCG2","name":"ポケモンジャングル"},
|
|
{"id":"CS1a","name":"junk"}
|
|
])";
|
|
// Catalog supplies dates so detail GETs are skipped.
|
|
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
|
|
"sets": {
|
|
"PMCG1": {"name_en":"Expansion Pack","name_ja":"拡張パック","releaseDate":"1996/10/20"},
|
|
"PMCG2": {"name_en":"Pokémon Jungle","name_ja":"ポケモンジャングル","releaseDate":"1997/03/05"},
|
|
"SV1a": {"name_en":"Triplet Beat","name_ja":"トリプレットビート","releaseDate":"2023/03/10"}
|
|
},
|
|
"prints": []
|
|
})");
|
|
REQUIRE(catalog.isOk());
|
|
JapanesePokemonSetSource src{http, catalog.value()};
|
|
const auto out = src.fetchAll();
|
|
REQUIRE(out.isOk());
|
|
// CS* dropped; 3 TCGdex + 11 curated injections.
|
|
REQUIRE(out.value().size() == 14);
|
|
// Expansion Pack → Jungle → UnnumberedPromo (day after Jungle).
|
|
CHECK(out.value()[0].id == "PMCG1");
|
|
CHECK(out.value()[1].id == "PMCG2");
|
|
CHECK(out.value()[1].name == "Pokémon Jungle");
|
|
CHECK(out.value()[2].id == "UnnumberedPromo");
|
|
CHECK(out.value()[2].name == "Unnumbered Promotional cards");
|
|
CHECK(out.value()[2].releaseDate == "1997/03/06");
|
|
const Set* pmcg1 = nullptr;
|
|
bool foundSv = false;
|
|
bool foundTama = false;
|
|
for (const auto& s : out.value()) {
|
|
if (s.id == "PMCG1") {
|
|
pmcg1 = &s;
|
|
CHECK(s.name == "Expansion Pack");
|
|
CHECK(s.releaseDate == "1996/10/20");
|
|
}
|
|
if (s.id == "SV1a") {
|
|
foundSv = true;
|
|
CHECK(s.name == "Triplet Beat");
|
|
}
|
|
if (s.id == "TamamushiCG") {
|
|
foundTama = true;
|
|
CHECK(s.name == "Tamamushi City Gym");
|
|
}
|
|
}
|
|
REQUIRE(pmcg1 != nullptr);
|
|
CHECK(foundSv);
|
|
CHECK(foundTama);
|
|
}
|
|
|
|
TEST_CASE("fetches set detail when catalog lacks release date") {
|
|
RoutingHttpClient http;
|
|
http.bodies[JapanesePokemonSetSource::kListEndpoint] =
|
|
R"([{"id":"PMCG1","name":"拡張パック"}])";
|
|
http.bodies[JapanesePokemonSetSource::buildSetDetailUrl("PMCG1")] =
|
|
R"({"id":"PMCG1","releaseDate":"1996-10-20","cards":[]})";
|
|
JapanesePokemonEnCatalog empty;
|
|
JapanesePokemonSetSource src{http, empty};
|
|
const auto out = src.fetchAll();
|
|
REQUIRE(out.isOk());
|
|
REQUIRE(out.value().size() == 12); // PMCG1 + 11 curated
|
|
const Set* pmcg1 = nullptr;
|
|
for (const auto& s : out.value()) {
|
|
if (s.id == "PMCG1") {
|
|
pmcg1 = &s;
|
|
break;
|
|
}
|
|
}
|
|
REQUIRE(pmcg1 != nullptr);
|
|
CHECK(pmcg1->releaseDate == "1996/10/20");
|
|
// Without catalog EN, never keep Japanese TCGdex names in Set.name.
|
|
CHECK(pmcg1->name == "PMCG1");
|
|
}
|
|
|
|
TEST_CASE("CJK set names fall back to set id even without catalog") {
|
|
RoutingHttpClient http;
|
|
http.bodies[JapanesePokemonSetSource::kListEndpoint] =
|
|
R"([{"id":"PMCG3","name":"化石の秘密"}])";
|
|
http.bodies[JapanesePokemonSetSource::buildSetDetailUrl("PMCG3")] =
|
|
R"({"id":"PMCG3","releaseDate":"1997-06-21"})";
|
|
JapanesePokemonEnCatalog empty;
|
|
JapanesePokemonSetSource src{http, empty};
|
|
const auto out = src.fetchAll();
|
|
REQUIRE(out.isOk());
|
|
const Set* pmcg3 = nullptr;
|
|
for (const auto& s : out.value()) {
|
|
if (s.id == "PMCG3") {
|
|
pmcg3 = &s;
|
|
break;
|
|
}
|
|
}
|
|
REQUIRE(pmcg3 != nullptr);
|
|
CHECK(pmcg3->name == "PMCG3");
|
|
}
|
|
|
|
TEST_CASE("network error on list is surfaced") {
|
|
RoutingHttpClient http;
|
|
http.ok = false;
|
|
JapanesePokemonEnCatalog empty;
|
|
JapanesePokemonSetSource src{http, empty};
|
|
CHECK(src.fetchAll().isErr());
|
|
}
|
|
|
|
TEST_CASE("augmentCachedSets injects classic products into a cached list") {
|
|
RoutingHttpClient http;
|
|
JapanesePokemonEnCatalog empty;
|
|
JapanesePokemonSetSource src{http, empty};
|
|
std::vector<Set> cached;
|
|
Set pmcg2;
|
|
pmcg2.id = "PMCG2";
|
|
pmcg2.name = "Pokémon Jungle";
|
|
pmcg2.releaseDate = "1997/03/05";
|
|
cached.push_back(std::move(pmcg2));
|
|
src.augmentCachedSets(cached);
|
|
REQUIRE(cached.size() == 12);
|
|
// Jungle stays first; UnnumberedPromo (1997/03/06) is immediately after.
|
|
CHECK(cached[0].id == "PMCG2");
|
|
CHECK(cached[1].id == "UnnumberedPromo");
|
|
CHECK(cached[1].releaseDate == "1997/03/06");
|
|
bool foundTama = false;
|
|
bool foundUnnumbered = false;
|
|
for (const auto& s : cached) {
|
|
if (s.id == "TamamushiCG") {
|
|
foundTama = true;
|
|
CHECK(s.name == "Tamamushi City Gym");
|
|
}
|
|
if (s.id == "UnnumberedPromo") {
|
|
foundUnnumbered = true;
|
|
CHECK(s.name == "Unnumbered Promotional cards");
|
|
}
|
|
}
|
|
CHECK(foundTama);
|
|
CHECK(foundUnnumbered);
|
|
}
|
|
|
|
TEST_CASE("augmentCachedSets restores English names from catalog") {
|
|
RoutingHttpClient http;
|
|
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
|
|
"sets": {
|
|
"PMCG2": {"name_en":"Pokémon Jungle","name_ja":"ポケモンジャングル","releaseDate":"1997/03/05"}
|
|
},
|
|
"prints": []
|
|
})");
|
|
REQUIRE(catalog.isOk());
|
|
JapanesePokemonSetSource src{http, catalog.value()};
|
|
std::vector<Set> cached;
|
|
Set pmcg2;
|
|
pmcg2.id = "PMCG2";
|
|
pmcg2.name = "PMCG2"; // stale cache stored the id as the display name
|
|
pmcg2.releaseDate = "1997/03/05";
|
|
cached.push_back(std::move(pmcg2));
|
|
src.augmentCachedSets(cached);
|
|
const Set* jungle = nullptr;
|
|
for (const auto& s : cached) {
|
|
if (s.id == "PMCG2") {
|
|
jungle = &s;
|
|
break;
|
|
}
|
|
}
|
|
REQUIRE(jungle != nullptr);
|
|
CHECK(jungle->name == "Pokémon Jungle");
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("JapanesePokemonSetSource::parseCatalogPackFromSetDetail") {
|
|
TEST_CASE("builds checklist from cards[] and prefers EN catalog names") {
|
|
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
|
|
"sets": {"PMCG1": {"name_en":"Expansion Pack","name_ja":"拡張パック"}},
|
|
"prints": [
|
|
{"set_id":"PMCG1","local_id":"001","name_en":"Charmander","name_ja":"ヒトカゲ"},
|
|
{"set_id":"PMCG1","local_id":"099","name_en":"Catalog Only","name_ja":""}
|
|
]
|
|
})");
|
|
REQUIRE(catalog.isOk());
|
|
Set set;
|
|
set.id = "PMCG1";
|
|
set.name = "Expansion Pack";
|
|
const std::string detail = R"({
|
|
"id":"PMCG1",
|
|
"name":"拡張パック",
|
|
"cards":[
|
|
{"localId":"001","name":"ヒトカゲ"},
|
|
{"localId":"002","name":"リザード"}
|
|
]
|
|
})";
|
|
const auto pack = JapanesePokemonSetSource::parseCatalogPackFromSetDetail(
|
|
detail, set, catalog.value());
|
|
REQUIRE(pack.isOk());
|
|
REQUIRE(pack.value().cards.size() == 3);
|
|
CHECK(pack.value().cards[0].setNo == "001");
|
|
CHECK(pack.value().cards[0].name == "Charmander");
|
|
CHECK(pack.value().cards[1].setNo == "002");
|
|
CHECK(pack.value().cards[1].name == "リザード");
|
|
CHECK(pack.value().cards[2].setNo == "099");
|
|
CHECK(pack.value().cards[2].name == "Catalog Only");
|
|
}
|
|
|
|
TEST_CASE("catalogPackFromEnCatalog covers classic-only products") {
|
|
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
|
|
"sets": {},
|
|
"prints": [
|
|
{"set_id":"UnnumberedPromo","local_id":"001","name_en":"Pikachu"},
|
|
{"set_id":"UnnumberedPromo","local_id":"002","name_en":"Mewtwo"}
|
|
]
|
|
})");
|
|
REQUIRE(catalog.isOk());
|
|
Set set;
|
|
set.id = "UnnumberedPromo";
|
|
set.name = "Unnumbered Promotional cards";
|
|
const auto pack =
|
|
JapanesePokemonSetSource::catalogPackFromEnCatalog(set, catalog.value());
|
|
REQUIRE(pack.cards.size() == 2);
|
|
CHECK(pack.cards[0].setNo == "001");
|
|
CHECK(pack.cards[1].setNo == "002");
|
|
}
|
|
}
|