#include #include "ccm/games/magic/MagicCardPreviewSource.hpp" #include "ccm/ports/IHttpClient.hpp" #include using namespace ccm; namespace { class FixedHttpClient final : public IHttpClient { public: std::string lastUrl; std::string body; bool ok = true; Result get(std::string_view url) override { lastUrl = std::string(url); return ok ? Result::ok(body) : Result::err("offline"); } }; } // namespace TEST_SUITE("MagicCardPreviewSource::buildSearchUrl") { TEST_CASE("simple name and set produce a percent-encoded query") { const auto url = MagicCardPreviewSource::buildSearchUrl("Lightning Bolt", "lea"); // Spaces -> %20, quotes -> %22, colons -> %3A. setId stays as-is when // it only contains unreserved chars. CHECK(url.find("https://api.scryfall.com/cards/search?q=") == 0); CHECK(url.find("%22Lightning%20Bolt%22") != std::string::npos); CHECK(url.find("set%3Alea") != std::string::npos); } TEST_CASE("ampersand in card name is replaced with 'and' before encoding") { const auto url = MagicCardPreviewSource::buildSearchUrl("Fire & Ice", "abc"); CHECK(url.find("Fire%20and%20Ice") != std::string::npos); CHECK(url.find("%26") == std::string::npos); } TEST_CASE("unreserved characters in setId are preserved") { const auto url = MagicCardPreviewSource::buildSearchUrl("X", "swsh10"); CHECK(url.find("set%3Aswsh10") != std::string::npos); } } TEST_SUITE("MagicCardPreviewSource::parseResponse") { TEST_CASE("happy path returns image_uris.normal") { const std::string json = R"({ "data": [ { "name": "Lightning Bolt", "image_uris": { "small": "https://img.scryfall.io/small.jpg", "normal": "https://img.scryfall.io/normal.jpg", "large": "https://img.scryfall.io/large.jpg" } } ] })"; const auto out = MagicCardPreviewSource::parseResponse(json); REQUIRE(out.isOk()); CHECK(out.value() == "https://img.scryfall.io/normal.jpg"); } TEST_CASE("empty data array returns an error") { const auto out = MagicCardPreviewSource::parseResponse(R"({"data":[]})"); CHECK(out.isErr()); } TEST_CASE("missing data array returns an error") { const auto out = MagicCardPreviewSource::parseResponse(R"({"meta":{}})"); CHECK(out.isErr()); } TEST_CASE("entry without image_uris returns an error (double-faced cards)") { const std::string json = R"({ "data": [ {"name":"DoubleFace","card_faces":[{"image_uris":{"normal":"x"}}]} ] })"; const auto out = MagicCardPreviewSource::parseResponse(json); CHECK(out.isErr()); } TEST_CASE("invalid JSON returns an error") { const auto out = MagicCardPreviewSource::parseResponse("{not json"); CHECK(out.isErr()); } } TEST_SUITE("MagicCardPreviewSource::fetchImageUrl") { TEST_CASE("network error is surfaced as a Result error") { FixedHttpClient http; http.ok = false; MagicCardPreviewSource src{http}; CHECK(src.fetchImageUrl("Lightning Bolt", "lea", "").isErr()); } TEST_CASE("network success is parsed end-to-end and uses the encoded URL") { FixedHttpClient http; http.ok = true; http.body = R"({"data":[{"image_uris":{"normal":"https://img/normal.jpg"}}]})"; MagicCardPreviewSource src{http}; const auto out = src.fetchImageUrl("Lightning Bolt", "lea", ""); REQUIRE(out.isOk()); CHECK(out.value() == "https://img/normal.jpg"); CHECK(http.lastUrl.find("%22Lightning%20Bolt%22") != std::string::npos); CHECK(http.lastUrl.find("set%3Alea") != std::string::npos); } }