mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-29 00:01:07 +00:00
fix: unittests
This commit is contained in:
@@ -24,8 +24,8 @@ add_executable(ccm_core_tests
|
||||
yugioh_card_preview_source_tests.cpp
|
||||
card_sorter_tests.cpp
|
||||
card_filter_tests.cpp
|
||||
game_module_tests.cpp
|
||||
std_file_system_tests.cpp
|
||||
http_get_mapping_tests.cpp
|
||||
cpr_http_client_tests.cpp
|
||||
|
||||
main.cpp
|
||||
)
|
||||
|
||||
@@ -118,6 +118,18 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class AlwaysNegativeUrlCache final : public IPreviewByteCache {
|
||||
public:
|
||||
[[nodiscard]] LoadResult load(std::string_view key) override {
|
||||
if (!key.empty() && key.front() == 'u') {
|
||||
return {HitKind::NegativeHit, {}};
|
||||
}
|
||||
return {HitKind::Miss, {}};
|
||||
}
|
||||
void store(std::string_view, const std::string&) override {}
|
||||
void storeNegative(std::string_view) override {}
|
||||
};
|
||||
|
||||
// Minimal IGameModule fake that exposes a configurable preview source.
|
||||
class FakeGameModule final : public IGameModule {
|
||||
public:
|
||||
@@ -655,6 +667,64 @@ TEST_SUITE("CardPreviewService caching") {
|
||||
CHECK(second.value() == "card-back-bytes");
|
||||
CHECK(http.calls == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("empty HTTP body is rejected and not cached") {
|
||||
FakeSource source;
|
||||
source.url = "https://example.com/empty.png";
|
||||
FakeGameModule module;
|
||||
module.gameId = Game::Magic;
|
||||
module.preview = &source;
|
||||
|
||||
FixedHttpClient http;
|
||||
http.body = "";
|
||||
|
||||
CardPreviewService svc{http};
|
||||
svc.registerModule(module);
|
||||
|
||||
const auto first = svc.fetchPreviewBytes(Game::Magic, "Any", "set", "1");
|
||||
CHECK(first.isErr());
|
||||
CHECK(first.error().find("Empty response body") != std::string::npos);
|
||||
CHECK(http.calls == 1);
|
||||
|
||||
const auto second = svc.fetchPreviewBytes(Game::Magic, "Any", "set", "1");
|
||||
CHECK(second.isErr());
|
||||
CHECK(http.calls == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("url negative entry on disk is treated as miss and refetched") {
|
||||
FixedHttpClient http;
|
||||
http.body = "card-back";
|
||||
AlwaysNegativeUrlCache disk;
|
||||
CardPreviewService svc{http, &disk};
|
||||
|
||||
const auto out = svc.fetchImageBytesByUrl("https://cdn.example/back.png");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "card-back");
|
||||
CHECK(http.calls == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("in-memory LRU evicts oldest entry after exceeding capacity") {
|
||||
FakeSource source;
|
||||
FakeGameModule module;
|
||||
module.gameId = Game::Magic;
|
||||
module.preview = &source;
|
||||
|
||||
FixedHttpClient http;
|
||||
http.body = "x";
|
||||
|
||||
CardPreviewService svc{http};
|
||||
svc.registerModule(module);
|
||||
|
||||
const auto cap = CardPreviewService::kCacheCapacity;
|
||||
for (std::size_t i = 0; i < cap + 1; ++i) {
|
||||
const std::string name = std::string("LRU-") + std::to_string(i);
|
||||
REQUIRE(svc.fetchPreviewBytes(Game::Magic, name, "lea", "").isOk());
|
||||
}
|
||||
REQUIRE(http.calls == cap + 1);
|
||||
|
||||
REQUIRE(svc.fetchPreviewBytes(Game::Magic, "LRU-0", "lea", "").isOk());
|
||||
CHECK(http.calls == cap + 2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("CardPreviewService::detectFirstPrint") {
|
||||
@@ -692,6 +762,16 @@ TEST_SUITE("CardPreviewService::detectFirstPrint") {
|
||||
CHECK(out.isErr());
|
||||
CHECK(out.error().find("not enabled") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("unregistered game returns explicit error") {
|
||||
FixedHttpClient http;
|
||||
CardPreviewService svc{http};
|
||||
const auto out =
|
||||
svc.detectFirstPrint(Game::YuGiOh, "Dark Magician", "LOB");
|
||||
CHECK(out.isErr());
|
||||
CHECK(out.error().find("No preview source registered") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("CardPreviewService::detectPrintVariants") {
|
||||
@@ -729,4 +809,14 @@ TEST_SUITE("CardPreviewService::detectPrintVariants") {
|
||||
CHECK(out.isErr());
|
||||
CHECK(out.error().find("not enabled") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("unregistered game returns explicit error") {
|
||||
FixedHttpClient http;
|
||||
CardPreviewService svc{http};
|
||||
const auto out =
|
||||
svc.detectPrintVariants(Game::YuGiOh, "Dark Magician", "LOB");
|
||||
CHECK(out.isErr());
|
||||
CHECK(out.error().find("No preview source registered") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "ccm/infra/CprHttpClient.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using namespace ccm;
|
||||
|
||||
TEST_SUITE("CprHttpClient injected executor") {
|
||||
TEST_CASE("forwards URL to injected executor and returns payload") {
|
||||
std::string seenUrl;
|
||||
CprHttpClient client{
|
||||
[&seenUrl](std::string_view url) -> Result<std::string> {
|
||||
seenUrl = std::string(url);
|
||||
return Result<std::string>::ok("body");
|
||||
}
|
||||
};
|
||||
|
||||
const auto out = client.get("https://example.com/api?q=1");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "body");
|
||||
CHECK(seenUrl == "https://example.com/api?q=1");
|
||||
}
|
||||
|
||||
TEST_CASE("propagates injected executor error as-is") {
|
||||
CprHttpClient client{
|
||||
[](std::string_view) -> Result<std::string> {
|
||||
return Result<std::string>::err("HTTP 503 from https://example.com");
|
||||
}
|
||||
};
|
||||
|
||||
const auto out = client.get("https://example.com");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error() == "HTTP 503 from https://example.com");
|
||||
}
|
||||
}
|
||||
+93
-12
@@ -169,24 +169,17 @@ TEST_SUITE("Configuration JSON matches Rust serde aliases") {
|
||||
CHECK(back == cfg);
|
||||
}
|
||||
|
||||
TEST_CASE("theme defaults to Light when missing from payload") {
|
||||
TEST_CASE("missing theme key defaults to Light") {
|
||||
const nlohmann::json j = {
|
||||
{"dataStorage", "/storage"},
|
||||
{"defaultGame", "Magic"},
|
||||
{"dataStorage", "/portable/data"},
|
||||
{"defaultGame", "YuGiOh"},
|
||||
};
|
||||
|
||||
const auto cfg = j.get<Configuration>();
|
||||
CHECK(cfg.dataStorage == "/storage");
|
||||
CHECK(cfg.defaultGame == Game::Magic);
|
||||
CHECK(cfg.dataStorage == "/portable/data");
|
||||
CHECK(cfg.defaultGame == Game::YuGiOh);
|
||||
CHECK(cfg.theme == Theme::Light);
|
||||
}
|
||||
|
||||
TEST_CASE("missing required keys still throws") {
|
||||
const nlohmann::json j = {
|
||||
{"theme", "Dark"},
|
||||
};
|
||||
CHECK_THROWS(j.get<Configuration>());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhCard JSON") {
|
||||
@@ -215,4 +208,92 @@ TEST_SUITE("YuGiOhCard JSON") {
|
||||
const YuGiOhCard back = j.get<YuGiOhCard>();
|
||||
CHECK(back == c);
|
||||
}
|
||||
|
||||
TEST_CASE("missing rarityCode in legacy rows is accepted and mapped to empty") {
|
||||
const nlohmann::json j = {
|
||||
{"id", 1},
|
||||
{"amount", 1},
|
||||
{"name", "Dark Magician"},
|
||||
{"set", nlohmann::json{
|
||||
{"id", "SDY"},
|
||||
{"name", "Starter Deck: Yugi"},
|
||||
{"releaseDate", "2002/03/29"},
|
||||
}},
|
||||
{"setNo", "SDY-006"},
|
||||
{"rarity", "Ultra Rare"},
|
||||
{"note", ""},
|
||||
{"images", nlohmann::json::array()},
|
||||
{"language", "English"},
|
||||
{"condition", "NearMint"},
|
||||
{"firstEdition", false},
|
||||
{"signed", false},
|
||||
{"altered", false},
|
||||
};
|
||||
|
||||
const auto card = j.get<YuGiOhCard>();
|
||||
CHECK(card.rarity == "Ultra Rare");
|
||||
CHECK(card.rarityCode.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("Domain JSON required fields") {
|
||||
TEST_CASE("Set missing required key throws") {
|
||||
const nlohmann::json j = {
|
||||
{"id", "lea"},
|
||||
{"name", "Limited Edition Alpha"},
|
||||
};
|
||||
CHECK_THROWS(j.get<Set>());
|
||||
}
|
||||
|
||||
TEST_CASE("MagicCard missing required key throws") {
|
||||
const nlohmann::json j = {
|
||||
{"id", 10},
|
||||
{"amount", 1},
|
||||
{"name", "Lightning Bolt"},
|
||||
{"set", nlohmann::json{
|
||||
{"id", "lea"},
|
||||
{"name", "Limited Edition Alpha"},
|
||||
{"releaseDate", "1993/08/05"},
|
||||
}},
|
||||
// note missing on purpose
|
||||
{"images", nlohmann::json::array()},
|
||||
{"language", "English"},
|
||||
{"condition", "NearMint"},
|
||||
{"foil", false},
|
||||
{"signed", false},
|
||||
{"altered", false},
|
||||
};
|
||||
CHECK_THROWS(j.get<MagicCard>());
|
||||
}
|
||||
|
||||
TEST_CASE("PokemonCard missing required key throws") {
|
||||
const nlohmann::json j = {
|
||||
{"id", 7},
|
||||
{"amount", 1},
|
||||
{"name", "Charizard"},
|
||||
{"set", nlohmann::json{
|
||||
{"id", "base1"},
|
||||
{"name", "Base Set"},
|
||||
{"releaseDate", "1999/01/09"},
|
||||
}},
|
||||
{"setNo", "4/102"},
|
||||
{"note", ""},
|
||||
{"images", nlohmann::json::array()},
|
||||
{"language", "English"},
|
||||
{"condition", "Excellent"},
|
||||
{"firstEdition", true},
|
||||
// holo missing on purpose
|
||||
{"signed", false},
|
||||
{"altered", false},
|
||||
};
|
||||
CHECK_THROWS(j.get<PokemonCard>());
|
||||
}
|
||||
|
||||
TEST_CASE("Configuration missing required key throws") {
|
||||
const nlohmann::json j = {
|
||||
{"defaultGame", "Magic"},
|
||||
{"theme", "Dark"},
|
||||
};
|
||||
CHECK_THROWS(j.get<Configuration>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "ccm/util/HttpGetMapping.hpp"
|
||||
|
||||
using namespace ccm;
|
||||
|
||||
TEST_SUITE("mapHttpGetResponse") {
|
||||
TEST_CASE("curl transport error ignores HTTP status and body") {
|
||||
const auto out =
|
||||
mapHttpGetResponse(true, "connection refused", 0, "ignored", "http://x");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error() == "HTTP error: connection refused");
|
||||
}
|
||||
|
||||
TEST_CASE("HTTP status below 200 is an error") {
|
||||
const auto out =
|
||||
mapHttpGetResponse(false, {}, 199, "body", "http://example/a");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error() == "HTTP 199 from http://example/a");
|
||||
}
|
||||
|
||||
TEST_CASE("HTTP status 200 returns body") {
|
||||
const auto out =
|
||||
mapHttpGetResponse(false, {}, 200, "payload", "http://example/a");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "payload");
|
||||
}
|
||||
|
||||
TEST_CASE("HTTP status 299 returns body") {
|
||||
const auto out =
|
||||
mapHttpGetResponse(false, {}, 299, "ok", "http://example/a");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "ok");
|
||||
}
|
||||
|
||||
TEST_CASE("HTTP status 300 and above is an error") {
|
||||
const auto out =
|
||||
mapHttpGetResponse(false, {}, 300, "redirect", "http://example/a");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error() == "HTTP 300 from http://example/a");
|
||||
}
|
||||
|
||||
TEST_CASE("HTTP 404 formats URL into message") {
|
||||
const auto out =
|
||||
mapHttpGetResponse(false, {}, 404, "", "https://api.example/r");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error() == "HTTP 404 from https://api.example/r");
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,26 @@ TEST_SUITE("MagicCardPreviewSource::parseResponse") {
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("'data' present but not an array is Transient") {
|
||||
const auto out = MagicCardPreviewSource::parseResponse(R"({"data":{}})");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("image_uris present but not an object is NotFound") {
|
||||
const auto out = MagicCardPreviewSource::parseResponse(
|
||||
R"({"data":[{"name":"X","image_uris":[]}]})");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("'normal' present but not a string is NotFound") {
|
||||
const auto out = MagicCardPreviewSource::parseResponse(
|
||||
R"({"data":[{"image_uris":{"normal":null}}]})");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("entry without image_uris is classified as NotFound (double-faced cards)") {
|
||||
const std::string json = R"({
|
||||
"data": [
|
||||
|
||||
@@ -54,6 +54,13 @@ TEST_SUITE("PokemonCardPreviewSource::buildSearchUrl") {
|
||||
"Mr. Mime", "base1", "");
|
||||
CHECK(url.find("%22Mr.%20Mime%22") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("empty setId omits the set.id clause") {
|
||||
const auto url =
|
||||
PokemonCardPreviewSource::buildSearchUrl("Pikachu", "", "25");
|
||||
CHECK(url.find("set.id") == std::string::npos);
|
||||
CHECK(url.find("number%3A25") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("PokemonCardPreviewSource::parseResponse") {
|
||||
@@ -97,6 +104,35 @@ TEST_SUITE("PokemonCardPreviewSource::parseResponse") {
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("'data' present but not an array is Transient") {
|
||||
const auto out = PokemonCardPreviewSource::parseResponse(R"({"data":{}})");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("'images' present but not an object is NotFound") {
|
||||
const auto out =
|
||||
PokemonCardPreviewSource::parseResponse(R"({"data":[{"images":[]}]})");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("large unusable type falls back to small string") {
|
||||
const auto out = PokemonCardPreviewSource::parseResponse(R"({
|
||||
"data":[{"images":{"large":123,"small":"https://only.small/img.png"}}]
|
||||
})");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "https://only.small/img.png");
|
||||
}
|
||||
|
||||
TEST_CASE("no usable large or small string yields NotFound") {
|
||||
const auto out = PokemonCardPreviewSource::parseResponse(R"({
|
||||
"data":[{"images":{"large":null,"small":false}}]
|
||||
})");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("entry without images is classified as NotFound") {
|
||||
const auto out = PokemonCardPreviewSource::parseResponse(
|
||||
R"({"data":[{"name":"Pikachu"}]})");
|
||||
|
||||
@@ -54,6 +54,23 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// First GET (filtered `cardset=` URL) fails; second GET (unfiltered) succeeds.
|
||||
// Exercises `YuGiOhCardPreviewSource::detectPrintVariants` narrow-query fallback.
|
||||
class FailFilteredThenOkHttpClient final : public IHttpClient {
|
||||
public:
|
||||
std::string unfilteredBody;
|
||||
int calls{0};
|
||||
|
||||
Result<std::string> get(std::string_view url) override {
|
||||
++calls;
|
||||
std::string u(url);
|
||||
if (u.find("cardset=") != std::string::npos) {
|
||||
return Result<std::string>::err("filtered endpoint unavailable");
|
||||
}
|
||||
return Result<std::string>::ok(unfilteredBody);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_SUITE("ygoPrintingSlotsMatch") {
|
||||
@@ -250,6 +267,90 @@ TEST_SUITE("YuGiOhCardPreviewSource::parseYugipediaResponse") {
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("missing top-level query object is Transient") {
|
||||
const auto out = YuGiOhCardPreviewSource::parseYugipediaResponse(
|
||||
R"({"not_query":{}})", {"File.png"});
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("query.pages not an object is Transient") {
|
||||
const auto out = YuGiOhCardPreviewSource::parseYugipediaResponse(
|
||||
R"({"query":{"pages":[]}})", {"X.png"});
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhCardPreviewSource::parseFallbackImageUrl") {
|
||||
TEST_CASE("missing data array is Transient") {
|
||||
const auto out = YuGiOhCardPreviewSource::parseFallbackImageUrl(R"({"meta":{}})", "Dark Magician");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("data present but not an array is Transient") {
|
||||
const auto out =
|
||||
YuGiOhCardPreviewSource::parseFallbackImageUrl(R"({"data":{}})", "X");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
|
||||
TEST_CASE("empty data array is NotFound") {
|
||||
const auto out = YuGiOhCardPreviewSource::parseFallbackImageUrl(R"({"data":[]})", "X");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("prefers exact-name row with image_url_small when image_url absent") {
|
||||
const std::string json = R"({"data":[
|
||||
{"name":"Dark Magician Girl","card_images":[{"image_url_small":"https://small.only/a.jpg"}]},
|
||||
{"name":"Dark Magician","card_images":[{"image_url":"https://ignored/wrong.jpg"}]}
|
||||
]})";
|
||||
const auto out = YuGiOhCardPreviewSource::parseFallbackImageUrl(json, "Dark Magician Girl");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "https://small.only/a.jpg");
|
||||
}
|
||||
|
||||
TEST_CASE("exact-name match uses image_url_cropped when earlier slots absent") {
|
||||
const std::string json = R"({"data":[{
|
||||
"name":"Slifer",
|
||||
"card_images":[{"image_url_cropped":"https://crop/z.jpg"}]
|
||||
}]})";
|
||||
const auto out = YuGiOhCardPreviewSource::parseFallbackImageUrl(json, "Slifer");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "https://crop/z.jpg");
|
||||
}
|
||||
|
||||
TEST_CASE("no exact name match falls back to first ranked card_images row") {
|
||||
const std::string json = R"({"data":[{
|
||||
"name":"Dark Magician Girl",
|
||||
"card_images":[{"image_url":"https://images/std-from-ranked-first.jpg"}]
|
||||
}]})";
|
||||
const auto out =
|
||||
YuGiOhCardPreviewSource::parseFallbackImageUrl(json, "Dark Magician");
|
||||
REQUIRE(out.isOk());
|
||||
CHECK(out.value() == "https://images/std-from-ranked-first.jpg");
|
||||
}
|
||||
|
||||
TEST_CASE("matching cards without usable images is NotFound") {
|
||||
const std::string json = R"({"data":[{
|
||||
"name":"Empty Card",
|
||||
"card_images":[{}]
|
||||
}]})";
|
||||
const auto out =
|
||||
YuGiOhCardPreviewSource::parseFallbackImageUrl(json, "Empty Card");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::NotFound);
|
||||
}
|
||||
|
||||
TEST_CASE("malformed JSON is Transient") {
|
||||
const auto out =
|
||||
YuGiOhCardPreviewSource::parseFallbackImageUrl("{not json", "Any");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().kind == PreviewLookupError::Kind::Transient);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhCardPreviewSource::parseFirstPrint") {
|
||||
@@ -268,6 +369,18 @@ TEST_SUITE("YuGiOhCardPreviewSource::parseFirstPrint") {
|
||||
CHECK(out.value().setNo == "LOB-001");
|
||||
CHECK(out.value().rarity == "Ultra Rare");
|
||||
}
|
||||
|
||||
TEST_CASE("empty data array yields error") {
|
||||
const auto out =
|
||||
YuGiOhCardPreviewSource::parseFirstPrint(R"({"data":[]})", "Any Set");
|
||||
CHECK(out.isErr());
|
||||
}
|
||||
|
||||
TEST_CASE("card row without card_sets yields error") {
|
||||
const auto out = YuGiOhCardPreviewSource::parseFirstPrint(
|
||||
R"({"data":[{"name":"Solo"}]})", "Any Display Set");
|
||||
CHECK(out.isErr());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhCardPreviewSource::parsePrintVariants") {
|
||||
@@ -326,6 +439,34 @@ TEST_SUITE("YuGiOhCardPreviewSource::parsePrintVariants") {
|
||||
CHECK(out.value()[0].setNo == "SDY-043");
|
||||
CHECK(out.value()[0].rarity == "Super Rare");
|
||||
}
|
||||
|
||||
TEST_CASE("malformed JSON surfaces as parse error") {
|
||||
const auto out =
|
||||
YuGiOhCardPreviewSource::parsePrintVariants("{bad json", "Mega Pack", "X");
|
||||
REQUIRE(out.isErr());
|
||||
CHECK(out.error().find("YGOPRODeck JSON parse error") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("YuGiOhCardPreviewSource::detectPrintVariants HTTP fallback") {
|
||||
TEST_CASE("retries without cardset when the filtered request fails") {
|
||||
FailFilteredThenOkHttpClient http;
|
||||
http.unfilteredBody = R"({
|
||||
"data":[{
|
||||
"name":"Test Goblin",
|
||||
"card_sets":[
|
||||
{"set_name":"Mega Pack","set_code":"MP21-EN001","set_rarity":"Common"}
|
||||
]
|
||||
}]
|
||||
})";
|
||||
|
||||
YuGiOhCardPreviewSource src{http};
|
||||
const auto out = src.detectPrintVariants("Test Goblin", "Mega Pack");
|
||||
REQUIRE(out.isOk());
|
||||
REQUIRE(out.value().size() == 1);
|
||||
CHECK(out.value()[0].setNo == "MP21-EN001");
|
||||
REQUIRE(http.calls == 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers aligned with external fixture `yugioh_same_card_set_variant_tests`
|
||||
|
||||
Reference in New Issue
Block a user