Minor: Additional functions (#25)

* several functions. fixes #1 and #2

* multi selection functionality
This commit is contained in:
Sebastian Dine
2026-08-03 08:35:38 +02:00
committed by GitHub
parent 2eb7c59f78
commit d3b4762b76
60 changed files with 1859 additions and 172 deletions
+1
View File
@@ -43,6 +43,7 @@ add_executable(ccm_core_tests
card_sorter_tests.cpp
card_filter_tests.cpp
ascii_utils_tests.cpp
card_lookup_detect_tests.cpp
http_get_mapping_tests.cpp
cpr_http_client_tests.cpp
+33
View File
@@ -0,0 +1,33 @@
#include <doctest/doctest.h>
#include "ccm/util/CardLookupDetect.hpp"
using namespace ccm;
TEST_SUITE("preferDetectBySetNo") {
TEST_CASE("only set number filled uses reverse lookup") {
CHECK(preferDetectBySetNo(true, false, CardLookupEditField::None));
CHECK(preferDetectBySetNo(true, false, CardLookupEditField::Name));
CHECK(preferDetectBySetNo(true, false, CardLookupEditField::SetNo));
}
TEST_CASE("only name filled uses name lookup") {
CHECK_FALSE(preferDetectBySetNo(false, true, CardLookupEditField::None));
CHECK_FALSE(preferDetectBySetNo(false, true, CardLookupEditField::Name));
CHECK_FALSE(preferDetectBySetNo(false, true, CardLookupEditField::SetNo));
}
TEST_CASE("both empty does not prefer reverse") {
CHECK_FALSE(preferDetectBySetNo(true, true, CardLookupEditField::None));
CHECK_FALSE(preferDetectBySetNo(true, true, CardLookupEditField::SetNo));
}
TEST_CASE("both filled: last edited SetNo prefers reverse") {
CHECK(preferDetectBySetNo(false, false, CardLookupEditField::SetNo));
}
TEST_CASE("both filled: Name or None keeps name lookup") {
CHECK_FALSE(preferDetectBySetNo(false, false, CardLookupEditField::Name));
CHECK_FALSE(preferDetectBySetNo(false, false, CardLookupEditField::None));
}
}
+56
View File
@@ -34,6 +34,7 @@ public:
std::string lastSetNo;
std::string detectLastName;
std::string detectLastSetId;
std::string detectLastSetNo;
AutoDetectedPrint detectedPrint{"LOB-001", "Ultra Rare"};
bool allowAutoDetect{true};
@@ -67,6 +68,23 @@ public:
return Result<std::vector<AutoDetectedPrint>>::ok(std::move(v));
}
Result<AutoDetectedPrint> detectBySetNo(std::string_view setId,
std::string_view setNo) override {
detectLastSetId = std::string(setId);
detectLastSetNo = std::string(setNo);
return Result<AutoDetectedPrint>::ok(detectedPrint);
}
Result<std::vector<AutoDetectedPrint>> detectVariantsBySetNo(
std::string_view setId,
std::string_view setNo) override {
detectLastSetId = std::string(setId);
detectLastSetNo = std::string(setNo);
std::vector<AutoDetectedPrint> v;
v.push_back(detectedPrint);
return Result<std::vector<AutoDetectedPrint>>::ok(std::move(v));
}
[[nodiscard]] bool supportsAutoDetectPrint() const noexcept override {
return allowAutoDetect;
}
@@ -992,3 +1010,41 @@ TEST_SUITE("CardPreviewService::detectPrintVariants") {
std::string::npos);
}
}
TEST_SUITE("CardPreviewService::detectVariantsBySetNo") {
TEST_CASE("routes set-scoped reverse lookup to registered source") {
FakeSource source;
source.detectedPrint = AutoDetectedPrint{"4", "Rare", "Pikachu"};
FakeGameModule module;
module.gameId = Game::Pokemon;
module.preview = &source;
FixedHttpClient http;
CardPreviewService svc{http};
svc.registerModule(module);
const auto out = svc.detectVariantsBySetNo(Game::Pokemon, "base1", "4");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Pikachu");
CHECK(out.value()[0].setNo == "4");
CHECK(source.detectLastSetId == "base1");
CHECK(source.detectLastSetNo == "4");
}
TEST_CASE("returns error when game does not enable auto-detect") {
FakeSource source;
source.allowAutoDetect = false;
FakeGameModule module;
module.gameId = Game::Magic;
module.preview = &source;
FixedHttpClient http;
CardPreviewService svc{http};
svc.registerModule(module);
const auto out = svc.detectVariantsBySetNo(Game::Magic, "lea", "1");
CHECK(out.isErr());
CHECK(out.error().find("not enabled") != std::string::npos);
}
}
@@ -177,3 +177,56 @@ TEST_SUITE("DigiBattle99CardPreviewSource::detectPrintVariants") {
CHECK(out.value()[1].setNo == "ST-126");
}
}
TEST_SUITE("DigiBattle99CardPreviewSource::detectVariantsBySetNo") {
TEST_CASE("card id search fills name within pack") {
FixedHttpClient http;
http.body = R"([
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]}
])";
DigiBattle99CardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("Series 1 Starter Set", "st-01");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Agumon");
CHECK(out.value()[0].setNo == "ST-01");
CHECK(http.lastUrl.find("card=ST-01") != std::string::npos);
}
TEST_CASE("digits-only 1 matches ST-01 not ST-11 from fuzzy API hits") {
FixedHttpClient http;
http.body = R"([
{"name":"Patamon","id":"ST-11","set_name":["Series 1 Starter Set"]},
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"Other","id":"BO-1","set_name":["Booster 1"]}
])";
DigiBattle99CardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("Series 1 Starter Set", "1");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Agumon");
CHECK(out.value()[0].setNo == "ST-01");
}
TEST_CASE("digits-only 11 matches ST-11 not ST-01") {
FixedHttpClient http;
http.body = R"([
{"name":"Agumon","id":"ST-01","set_name":["Series 1 Starter Set"]},
{"name":"Patamon","id":"ST-11","set_name":["Series 1 Starter Set"]}
])";
DigiBattle99CardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("Series 1 Starter Set", "11");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Patamon");
CHECK(out.value()[0].setNo == "ST-11");
}
TEST_CASE("empty pack is rejected") {
FixedHttpClient http;
DigiBattle99CardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("", "ST-01");
REQUIRE(out.isErr());
CHECK(out.error().find("set") != std::string::npos);
}
}
+14 -2
View File
@@ -404,20 +404,32 @@ TEST_SUITE("YuGiOhSetCatalog JSON") {
YuGiOhSetCatalogPack pack;
pack.setId = "LOB";
pack.setName = "Legend of Blue Eyes White Dragon";
pack.cards.push_back(YuGiOhCatalogCard{"LOB-001", "Blue-Eyes White Dragon"});
pack.cards.push_back(YuGiOhCatalogCard{"LOB-EN005", "Dark Magician"});
pack.cards.push_back(YuGiOhCatalogCard{"LOB-001", "Blue-Eyes White Dragon", "Ultra Rare"});
pack.cards.push_back(YuGiOhCatalogCard{"LOB-EN005", "Dark Magician", "Ultra Rare"});
catalog.packs.push_back(std::move(pack));
nlohmann::json j = catalog;
CHECK(j.at("packs").is_array());
CHECK(j.at("packs").at(0).at("id") == "LOB");
CHECK(j.at("packs").at(0).at("cards").at(0).at("setNo") == "LOB-001");
CHECK(j.at("packs").at(0).at("cards").at(0).at("rarity") == "Ultra Rare");
const YuGiOhSetCatalog back = j.get<YuGiOhSetCatalog>();
CHECK(back == catalog);
CHECK(back.findPack("LOB") != nullptr);
CHECK(back.findPack("missing") == nullptr);
}
TEST_CASE("legacy catalog JSON without rarity still loads") {
const auto j = nlohmann::json::parse(R"({
"packs":[{"id":"LOB","name":"Legend of Blue Eyes White Dragon",
"cards":[{"setNo":"LOB-001","name":"Blue-Eyes White Dragon"}]}]
})");
const YuGiOhSetCatalog back = j.get<YuGiOhSetCatalog>();
REQUIRE(back.packs.size() == 1);
REQUIRE(back.packs[0].cards.size() == 1);
CHECK(back.packs[0].cards[0].rarity.empty());
}
}
TEST_SUITE("PokemonSetCatalog JSON") {
+7
View File
@@ -28,6 +28,13 @@ TEST_SUITE("FsNames::formatTextForFs") {
TEST_CASE("idempotent on already-clean strings") {
CHECK(formatTextForFs("AlreadyClean") == "AlreadyClean");
}
TEST_CASE("male and female signs become male/female") {
CHECK(formatTextForFs("\xE2\x99\x82") == "male");
CHECK(formatTextForFs("\xE2\x99\x80") == "female");
CHECK(formatTextForFs("Nidoran \xE2\x99\x82") == "Nidoranmale");
CHECK(formatTextForFs("Nidoran \xE2\x99\x80") == "Nidoranfemale");
}
}
TEST_SUITE("FsNames::parseIndexFromFilename") {
@@ -591,3 +591,58 @@ TEST_SUITE("JapanesePokemonCardPreviewSource::detectPrintVariants catalog-only")
CHECK(shining.value()[0].setNo == "013");
}
}
TEST_SUITE("JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog") {
TEST_CASE("resolves English name from setId + localId") {
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
"sets": {},
"prints": [
{"set_id":"PMCG1","local_id":"001","name_en":"Bulbasaur","name_ja":"フシギダネ"}
]
})");
REQUIRE(catalog.isOk());
const auto out = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog(
"PMCG1", "001", catalog.value());
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Bulbasaur");
CHECK(out.value()[0].setNo == "001");
}
TEST_CASE("leading-zero-insensitive localId still resolves") {
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
"sets": {},
"prints": [
{"set_id":"PMCG1","local_id":"001","name_en":"Bulbasaur","name_ja":"フシギダネ"},
{"set_id":"PMCG1","local_id":"011","name_en":"Weedle","name_ja":"ビードル"}
]
})");
REQUIRE(catalog.isOk());
const auto byOne = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog(
"PMCG1", "1", catalog.value());
REQUIRE(byOne.isOk());
REQUIRE(byOne.value().size() == 1);
CHECK(byOne.value()[0].name == "Bulbasaur");
CHECK(byOne.value()[0].setNo == "001");
const auto byEleven =
JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog(
"PMCG1", "11", catalog.value());
REQUIRE(byEleven.isOk());
REQUIRE(byEleven.value().size() == 1);
CHECK(byEleven.value()[0].name == "Weedle");
}
TEST_CASE("unknown localId is an error") {
const auto catalog = JapanesePokemonEnCatalog::parse(R"({
"sets": {},
"prints": [
{"set_id":"PMCG1","local_id":"001","name_en":"Bulbasaur","name_ja":"フシギダネ"}
]
})");
REQUIRE(catalog.isOk());
const auto out = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog(
"PMCG1", "999", catalog.value());
REQUIRE(out.isErr());
}
}
@@ -323,3 +323,78 @@ TEST_SUITE("PokemonCardPreviewSource::detectPrintVariants") {
CHECK(http.calls == 2);
}
}
TEST_SUITE("PokemonCardPreviewSource::detectVariantsBySetNo") {
TEST_CASE("card-by-id fills name from TCGdex response") {
FixedHttpClient http;
http.body = R"({
"id":"base1-4",
"localId":"4",
"name":"Charmander",
"rarity":"Common",
"image":"https://assets.tcgdex.net/en/base/base1/4"
})";
PokemonCardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("base1", "4");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Charmander");
CHECK(out.value()[0].setNo == "4");
CHECK(out.value()[0].rarity == "Common");
CHECK(http.lastUrl.find("/v2/en/cards/base1-4") != std::string::npos);
}
TEST_CASE("search fallback rejects localIds that only share a digit prefix") {
struct ScriptedHttp : IHttpClient {
int n = 0;
Result<std::string> get(std::string_view) override {
++n;
if (n == 1) {
return Result<std::string>::err("not found");
}
// Fuzzy search returns both "14" and "4"; only "4" may be kept.
return Result<std::string>::ok(R"([
{"id":"base1-14","localId":"14","name":"Wrong","rarity":"Common"},
{"id":"base1-4","localId":"4","name":"Charmander","rarity":"Common"}
])");
}
} http;
PokemonCardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("base1", "4");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Charmander");
CHECK(out.value()[0].setNo == "4");
}
TEST_CASE("card-by-id response with mismatched localId falls through to search") {
struct ScriptedHttp : IHttpClient {
int n = 0;
Result<std::string> get(std::string_view) override {
++n;
if (n == 1) {
return Result<std::string>::ok(R"({
"id":"base1-14","localId":"14","name":"Wrong","rarity":"Rare"
})");
}
return Result<std::string>::ok(R"([
{"id":"base1-4","localId":"4","name":"Charmander","rarity":"Common"}
])");
}
} http;
PokemonCardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("base1", "4");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Charmander");
CHECK(out.value()[0].setNo == "4");
}
TEST_CASE("empty set id is rejected") {
FixedHttpClient http;
PokemonCardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("", "4");
REQUIRE(out.isErr());
CHECK(out.error().find("set") != std::string::npos);
}
}
+113
View File
@@ -1,5 +1,6 @@
#include <doctest/doctest.h>
#include "ccm/domain/YuGiOhSetCatalog.hpp"
#include "ccm/games/yugioh/YuGiOhCardPreviewSource.hpp"
#include "ccm/ports/IHttpClient.hpp"
#include "ccm/util/YuGiOhPrintingSlot.hpp"
@@ -86,6 +87,15 @@ TEST_SUITE("ygoPrintingSlotsMatch") {
CHECK(ygoPrintingSlotsMatch("RA04-001", "RA04-EN001"));
}
TEST_CASE("matches collector numbers ignoring leading zeros") {
CHECK(ygoPrintingSlotsMatch("LOB-5", "LOB-005"));
CHECK(ygoPrintingSlotsMatch("LOB-EN005", "LOB-5"));
CHECK(ygoCollectorDigitsEqual("005", "5"));
CHECK(ygoCollectorDigitsEqual("LOB-EN005", "5"));
CHECK(ygoCollectorDigitsFromInput("005") == "005");
CHECK(ygoCollectorDigitsFromInput("LOB-EN005") == "005");
}
TEST_CASE("detects European alternate numbering suffix E+digit vs EN/DE") {
CHECK(ygoLikelyEuropeanRegionalSetCode("LOB-E003"));
CHECK_FALSE(ygoLikelyEuropeanRegionalSetCode("LOB-EN005"));
@@ -935,3 +945,106 @@ TEST_SUITE("YuGiOhCardPreviewSource::detectFirstPrint") {
CHECK(out.error() == "offline");
}
}
TEST_SUITE("YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog") {
TEST_CASE("matches digits within pack to card name") {
YuGiOhSetCatalog catalog;
YuGiOhSetCatalogPack pack;
pack.setId = "LOB";
pack.setName = "Legend of Blue Eyes White Dragon";
pack.cards.push_back(YuGiOhCatalogCard{"LOB-005", "Dark Magician", "Ultra Rare"});
pack.cards.push_back(YuGiOhCatalogCard{"LOB-EN001", "Blue-Eyes White Dragon", "Ultra Rare"});
catalog.packs.push_back(std::move(pack));
const auto byDigits =
YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "LOB", "005");
REQUIRE(byDigits.isOk());
REQUIRE(byDigits.value().size() == 1);
CHECK(byDigits.value()[0].name == "Dark Magician");
CHECK(byDigits.value()[0].setNo == "LOB-005");
CHECK(byDigits.value()[0].rarity == "Ultra Rare");
const auto byUnpadded =
YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "LOB", "5");
REQUIRE(byUnpadded.isOk());
REQUIRE(byUnpadded.value().size() == 1);
CHECK(byUnpadded.value()[0].name == "Dark Magician");
const auto byRegionCode =
YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "LOB", "LOB-001");
REQUIRE(byRegionCode.isOk());
REQUIRE(byRegionCode.value().size() == 1);
CHECK(byRegionCode.value()[0].name == "Blue-Eyes White Dragon");
}
TEST_CASE("digits-only 1 does not match collector 011") {
YuGiOhSetCatalog catalog;
YuGiOhSetCatalogPack pack;
pack.setId = "LOB";
pack.cards.push_back(YuGiOhCatalogCard{"LOB-005", "Dark Magician"});
pack.cards.push_back(YuGiOhCatalogCard{"LOB-011", "Hitotsu-Me Giant"});
catalog.packs.push_back(std::move(pack));
CHECK(YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "LOB", "1")
.isErr());
const auto byEleven =
YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "LOB", "11");
REQUIRE(byEleven.isOk());
REQUIRE(byEleven.value().size() == 1);
CHECK(byEleven.value()[0].name == "Hitotsu-Me Giant");
}
TEST_CASE("unknown pack or number returns error") {
YuGiOhSetCatalog catalog;
YuGiOhSetCatalogPack pack;
pack.setId = "LOB";
pack.cards.push_back(YuGiOhCatalogCard{"LOB-005", "Dark Magician"});
catalog.packs.push_back(std::move(pack));
CHECK(YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "SDK", "001")
.isErr());
CHECK(YuGiOhCardPreviewSource::detectVariantsBySetNoFromCatalog(catalog, "LOB", "999")
.isErr());
}
TEST_CASE("detectVariantsBySetNo falls back to cardset HTTP without catalog") {
FixedHttpClient http;
http.body = R"({
"data":[
{"name":"Dark Magician",
"card_sets":[
{"set_name":"Legend of Blue Eyes White Dragon","set_code":"LOB-EN005","set_rarity":"Ultra Rare"}
]}
]
})";
YuGiOhCardPreviewSource src{http};
const auto out = src.detectVariantsBySetNo("Legend of Blue Eyes White Dragon", "5");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Dark Magician");
CHECK(out.value()[0].setNo == "LOB-EN005");
CHECK(out.value()[0].rarity == "Ultra Rare");
CHECK(http.lastUrl.find("cardset=") != std::string::npos);
CHECK(http.lastUrl.find("fname=") == std::string::npos);
}
TEST_CASE("cardset reverse keeps distinct rarities for the same set code") {
const std::string body = R"({
"data":[
{"name":"Elemental HERO Bubbleman",
"card_sets":[
{"set_name":"Soul of the Duelist","set_code":"SOD-EN015","set_rarity":"Ultra Rare"},
{"set_name":"Soul of the Duelist","set_code":"SOD-EN015","set_rarity":"Ultimate Rare"}
]}
]
})";
const auto out = YuGiOhCardPreviewSource::detectVariantsBySetNoFromCardset(
body, "Soul of the Duelist", "15");
REQUIRE(out.isOk());
REQUIRE(out.value().size() == 2);
CHECK(out.value()[0].name == "Elemental HERO Bubbleman");
CHECK(out.value()[0].rarity == "Ultra Rare");
CHECK(out.value()[1].rarity == "Ultimate Rare");
}
}
+5
View File
@@ -213,11 +213,16 @@ TEST_SUITE("YuGiOhSetSource::parseCatalog") {
}
CHECK(sawBe);
CHECK(sawDm);
for (const auto& c : lob->cards) {
if (c.name == "Blue-Eyes White Dragon") CHECK(c.rarity == "Ultra Rare");
if (c.name == "Dark Magician") CHECK(c.rarity == "Ultra Rare");
}
const auto* mrd = out.value().findPack("MRD");
REQUIRE(mrd != nullptr);
REQUIRE(mrd->cards.size() == 1);
CHECK(mrd->cards[0].setNo == "MRD-010");
CHECK(mrd->cards[0].rarity == "Ultra Rare");
}
TEST_CASE("missing data array returns error") {
@@ -124,6 +124,34 @@ TEST_SUITE("YuGiOhBandaiCardPreviewSource helpers") {
CHECK(out.value()[0].setId == "ban1");
}
TEST_CASE("parseAskResponse drops results whose Bandai number does not match") {
const std::string body = R"JSON({
"query": {
"results": {
"Card Eleven (Bandai)": {
"printouts": {
"English name": ["Card Eleven"],
"Bandai number": [11],
"Rarity": [{"fulltext": "Common"}]
}
},
"Card One (Bandai)": {
"printouts": {
"English name": ["Card One"],
"Bandai number": [1],
"Rarity": [{"fulltext": "Common"}]
}
}
}
}
})JSON";
auto out = YuGiOhBandaiCardPreviewSource::parseAskResponse(body, "ban1", "1");
REQUIRE(out);
REQUIRE(out.value().size() == 1);
CHECK(out.value()[0].name == "Card One");
CHECK(out.value()[0].setNo == "1");
}
TEST_CASE("fetchImageUrl uses pageimages URL") {
FixedHttpClient http;
http.body = R"JSON({
@@ -182,7 +210,7 @@ TEST_SUITE("YuGiOhBandaiCardPreviewSource helpers") {
}
})JSON";
YuGiOhBandaiCardPreviewSource src(http);
auto out = src.detectBySetNo("014");
auto out = src.detectBySetNo("ban1", "014");
REQUIRE(out);
CHECK(out.value().name == "Dark Magician");
CHECK(http.lastUrl.find("action=ask") != std::string::npos);
@@ -196,7 +224,7 @@ TEST_SUITE("YuGiOhBandaiCardPreviewSource helpers") {
}
})JSON";
YuGiOhBandaiCardPreviewSource src(http);
auto out = src.detectBySetNo("ta2");
auto out = src.detectBySetNo("banpromo-ta", "ta2");
REQUIRE(out);
CHECK(out.value().name == "Blue-Eyes White Dragon's 3-Body Connection");
CHECK(out.value().setNo == "TA2");
@@ -206,6 +234,27 @@ TEST_SUITE("YuGiOhBandaiCardPreviewSource helpers") {
CHECK(http.lastUrl.find("Promotional") != std::string::npos);
}
TEST_CASE("detectBySetNo filters out prints from other sets") {
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("bansealdass", "14");
CHECK_FALSE(out);
CHECK(out.error().find("selected set") != std::string::npos);
}
TEST_CASE("isAlphanumericPromoNumber detects Jump and Toei codes") {
CHECK(YuGiOhBandaiCardPreviewSource::isAlphanumericPromoNumber("TA2"));
CHECK(YuGiOhBandaiCardPreviewSource::isAlphanumericPromoNumber("j1"));