mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 22:01:12 +00:00
338 lines
12 KiB
C++
338 lines
12 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "ccm/domain/PokemonCard.hpp"
|
|
#include "ccm/domain/PokemonSetCatalog.hpp"
|
|
#include "ccm/services/ConfigService.hpp"
|
|
#include "ccm/services/PokemonSetCatalogService.hpp"
|
|
#include "ccm/services/PokemonSetCompletion.hpp"
|
|
#include "fakes/InMemoryFileSystem.hpp"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
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;
|
|
}
|
|
|
|
PokemonCard makeOwned(PokemonRegion region, std::string setId, std::string setNo) {
|
|
PokemonCard c;
|
|
c.id = 1;
|
|
c.name = "Owned";
|
|
c.region = region;
|
|
c.set.id = std::move(setId);
|
|
c.set.name = "Set";
|
|
c.setNo = std::move(setNo);
|
|
c.language = Language::English;
|
|
return c;
|
|
}
|
|
|
|
PokemonSetCatalog westCatalog() {
|
|
PokemonSetCatalog catalog;
|
|
PokemonSetCatalogPack base;
|
|
base.setId = "base1";
|
|
base.setName = "Base";
|
|
base.cards = {
|
|
{"4", "Charizard"},
|
|
{"58", "Growlithe"},
|
|
{"59", "Arcanine"},
|
|
};
|
|
catalog.packs.push_back(std::move(base));
|
|
return catalog;
|
|
}
|
|
|
|
PokemonSetCatalog asiaCatalog() {
|
|
PokemonSetCatalog catalog;
|
|
PokemonSetCatalogPack pmcg1;
|
|
pmcg1.setId = "PMCG1";
|
|
pmcg1.setName = "Expansion Pack";
|
|
pmcg1.cards = {
|
|
{"001", "Charmander"},
|
|
{"002", "Charmeleon"},
|
|
{"006", "Charizard"},
|
|
};
|
|
catalog.packs.push_back(std::move(pmcg1));
|
|
return catalog;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_SUITE("computePokemonSetCompletion") {
|
|
TEST_CASE("west pack with owned card appears") {
|
|
const auto west = westCatalog();
|
|
const auto asia = asiaCatalog();
|
|
std::vector<PokemonCard> collection{
|
|
makeOwned(PokemonRegion::West, "base1", "4"),
|
|
};
|
|
const auto rows = computePokemonSetCompletion(collection, west, asia);
|
|
REQUIRE(rows.size() == 1);
|
|
CHECK(rows[0].region == PokemonRegion::West);
|
|
CHECK(rows[0].setId == "base1");
|
|
CHECK(rows[0].ownedUnique == 1);
|
|
CHECK(rows[0].total == 3);
|
|
CHECK(rows[0].percent() == 33);
|
|
}
|
|
|
|
TEST_CASE("asia and west do not cross-count") {
|
|
const auto west = westCatalog();
|
|
const auto asia = asiaCatalog();
|
|
// Same collector-looking number, different region/set.
|
|
PokemonCard westCard = makeOwned(PokemonRegion::West, "base1", "4");
|
|
PokemonCard asiaCard = makeOwned(PokemonRegion::Asia, "PMCG1", "006");
|
|
asiaCard.id = 2;
|
|
|
|
const auto rows = computePokemonSetCompletion({westCard, asiaCard}, west, asia);
|
|
REQUIRE(rows.size() == 2);
|
|
CHECK(rows[0].setId == "base1");
|
|
CHECK(rows[0].ownedUnique == 1);
|
|
CHECK(rows[1].setId == "PMCG1");
|
|
CHECK(rows[1].ownedUnique == 1);
|
|
}
|
|
|
|
TEST_CASE("orders packs by releaseDate then setName") {
|
|
PokemonSetCatalog west;
|
|
PokemonSetCatalogPack newer;
|
|
newer.setId = "sv01";
|
|
newer.setName = "Scarlet & Violet";
|
|
newer.cards = {{"1", "Sprigatito"}};
|
|
PokemonSetCatalogPack older;
|
|
older.setId = "base1";
|
|
older.setName = "Zoo Set"; // would sort after Scarlet by name
|
|
older.cards = {{"4", "Charizard"}};
|
|
west.packs.push_back(std::move(newer));
|
|
west.packs.push_back(std::move(older));
|
|
PokemonSetCatalog emptyAsia;
|
|
|
|
PokemonCard base = makeOwned(PokemonRegion::West, "base1", "4");
|
|
base.set.releaseDate = "1999/01/09";
|
|
PokemonCard sv = makeOwned(PokemonRegion::West, "sv01", "1");
|
|
sv.id = 2;
|
|
sv.set.releaseDate = "2023/03/31";
|
|
|
|
const auto rows = computePokemonSetCompletion({base, sv}, west, emptyAsia);
|
|
REQUIRE(rows.size() == 2);
|
|
CHECK(rows[0].setId == "base1");
|
|
CHECK(rows[0].releaseDate == "1999/01/09");
|
|
CHECK(rows[1].setId == "sv01");
|
|
CHECK(rows[1].releaseDate == "2023/03/31");
|
|
}
|
|
|
|
TEST_CASE("region filter isolates catalogs") {
|
|
const auto west = westCatalog();
|
|
const auto asia = asiaCatalog();
|
|
PokemonCard westCard = makeOwned(PokemonRegion::West, "base1", "4");
|
|
PokemonCard asiaCard = makeOwned(PokemonRegion::Asia, "PMCG1", "001");
|
|
asiaCard.id = 2;
|
|
|
|
const auto westOnly = computePokemonSetCompletion(
|
|
{westCard, asiaCard}, west, asia, PokemonRegion::West);
|
|
REQUIRE(westOnly.size() == 1);
|
|
CHECK(westOnly[0].setId == "base1");
|
|
|
|
const auto asiaOnly = computePokemonSetCompletion(
|
|
{westCard, asiaCard}, west, asia, PokemonRegion::Asia);
|
|
REQUIRE(asiaOnly.size() == 1);
|
|
CHECK(asiaOnly[0].setId == "PMCG1");
|
|
}
|
|
|
|
TEST_CASE("normalizes west 4/102 to 4") {
|
|
const auto west = westCatalog();
|
|
PokemonSetCatalog emptyAsia;
|
|
std::vector<PokemonCard> collection{
|
|
makeOwned(PokemonRegion::West, "base1", "4/102"),
|
|
};
|
|
const auto rows = computePokemonSetCompletion(collection, west, emptyAsia);
|
|
REQUIRE(rows.size() == 1);
|
|
CHECK(rows[0].ownedUnique == 1);
|
|
}
|
|
|
|
TEST_CASE("legacy pokemontcg West set id matches TCGdex catalog pack") {
|
|
PokemonSetCatalog west;
|
|
PokemonSetCatalogPack pack;
|
|
pack.setId = "sv01";
|
|
pack.setName = "Scarlet & Violet";
|
|
pack.cards = {{"6", "Charizard"}};
|
|
west.packs.push_back(std::move(pack));
|
|
PokemonSetCatalog emptyAsia;
|
|
std::vector<PokemonCard> collection{
|
|
makeOwned(PokemonRegion::West, "sv1", "6"),
|
|
};
|
|
const auto rows = computePokemonSetCompletion(collection, west, emptyAsia);
|
|
REQUIRE(rows.size() == 1);
|
|
CHECK(rows[0].setId == "sv01");
|
|
CHECK(rows[0].ownedUnique == 1);
|
|
|
|
const auto checklist = pokemonChecklistForSet(
|
|
collection, west, emptyAsia, PokemonRegion::West, "sv01");
|
|
REQUIRE(checklist.size() == 1);
|
|
CHECK(checklist[0].owned);
|
|
}
|
|
|
|
TEST_CASE("amount does not inflate unique ownership") {
|
|
const auto west = westCatalog();
|
|
PokemonSetCatalog emptyAsia;
|
|
PokemonCard a = makeOwned(PokemonRegion::West, "base1", "4");
|
|
a.amount = 5;
|
|
PokemonCard b = makeOwned(PokemonRegion::West, "base1", "4");
|
|
b.id = 2;
|
|
PokemonCard c = makeOwned(PokemonRegion::West, "base1", "58");
|
|
c.id = 3;
|
|
const auto rows = computePokemonSetCompletion({a, b, c}, west, emptyAsia);
|
|
REQUIRE(rows.size() == 1);
|
|
CHECK(rows[0].ownedUnique == 2);
|
|
}
|
|
|
|
TEST_CASE("language filter hides packs with no matching language") {
|
|
const auto west = westCatalog();
|
|
PokemonSetCatalog emptyAsia;
|
|
PokemonCard en = makeOwned(PokemonRegion::West, "base1", "4");
|
|
en.language = Language::English;
|
|
|
|
CHECK(computePokemonSetCompletion({en}, west, emptyAsia, std::nullopt,
|
|
Language::German)
|
|
.empty());
|
|
REQUIRE(computePokemonSetCompletion({en}, west, emptyAsia, std::nullopt,
|
|
Language::English)
|
|
.size() == 1);
|
|
}
|
|
|
|
TEST_CASE("empty catalog yields no rows") {
|
|
PokemonSetCatalog empty;
|
|
std::vector<PokemonCard> collection{
|
|
makeOwned(PokemonRegion::West, "base1", "4"),
|
|
};
|
|
CHECK(computePokemonSetCompletion(collection, empty, empty).empty());
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("pokemonChecklistForSet") {
|
|
TEST_CASE("marks owned west cards") {
|
|
const auto west = westCatalog();
|
|
PokemonSetCatalog emptyAsia;
|
|
std::vector<PokemonCard> collection{
|
|
makeOwned(PokemonRegion::West, "base1", "58"),
|
|
};
|
|
const auto list = pokemonChecklistForSet(collection, west, emptyAsia,
|
|
PokemonRegion::West, "base1");
|
|
REQUIRE(list.size() == 3);
|
|
CHECK(list[0].setNo == "4");
|
|
CHECK(list[0].owned == false);
|
|
CHECK(list[1].setNo == "58");
|
|
CHECK(list[1].owned == true);
|
|
CHECK(list[2].setNo == "59");
|
|
CHECK(list[2].owned == false);
|
|
}
|
|
|
|
TEST_CASE("orders unpadded set numbers numerically") {
|
|
PokemonSetCatalog west;
|
|
PokemonSetCatalogPack pack;
|
|
pack.setId = "base1";
|
|
pack.setName = "Base";
|
|
// Insert out of order / in lex-favoring order to prove we re-sort.
|
|
pack.cards = {
|
|
{"100", "Lightning Energy"},
|
|
{"1", "Alakazam"},
|
|
{"10", "Mewtwo"},
|
|
{"2", "Blastoise"},
|
|
};
|
|
west.packs.push_back(std::move(pack));
|
|
PokemonSetCatalog emptyAsia;
|
|
|
|
const auto list = pokemonChecklistForSet({}, west, emptyAsia,
|
|
PokemonRegion::West, "base1");
|
|
REQUIRE(list.size() == 4);
|
|
CHECK(list[0].setNo == "1");
|
|
CHECK(list[1].setNo == "2");
|
|
CHECK(list[2].setNo == "10");
|
|
CHECK(list[3].setNo == "100");
|
|
}
|
|
|
|
TEST_CASE("asia card does not mark west checklist") {
|
|
const auto west = westCatalog();
|
|
const auto asia = asiaCatalog();
|
|
std::vector<PokemonCard> collection{
|
|
makeOwned(PokemonRegion::Asia, "PMCG1", "006"),
|
|
};
|
|
const auto list = pokemonChecklistForSet(collection, west, asia,
|
|
PokemonRegion::West, "base1");
|
|
REQUIRE(list.size() == 3);
|
|
CHECK(list[0].owned == false);
|
|
CHECK(list[1].owned == false);
|
|
CHECK(list[2].owned == false);
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("pokemonLanguagesInCollection") {
|
|
TEST_CASE("region filter scopes languages") {
|
|
PokemonCard westEn = makeOwned(PokemonRegion::West, "base1", "4");
|
|
westEn.language = Language::English;
|
|
PokemonCard asiaJp = makeOwned(PokemonRegion::Asia, "PMCG1", "001");
|
|
asiaJp.id = 2;
|
|
asiaJp.language = Language::Japanese;
|
|
|
|
const auto all = pokemonLanguagesInCollection({westEn, asiaJp});
|
|
REQUIRE(all.size() == 2);
|
|
CHECK(all[0] == Language::English);
|
|
CHECK(all[1] == Language::Japanese);
|
|
|
|
const auto westOnly =
|
|
pokemonLanguagesInCollection({westEn, asiaJp}, PokemonRegion::West);
|
|
REQUIRE(westOnly.size() == 1);
|
|
CHECK(westOnly[0] == Language::English);
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("pokemonRegionsInCollection") {
|
|
TEST_CASE("reports regions with matching catalog packs") {
|
|
const auto west = westCatalog();
|
|
const auto asia = asiaCatalog();
|
|
PokemonCard westCard = makeOwned(PokemonRegion::West, "base1", "4");
|
|
PokemonCard asiaCard = makeOwned(PokemonRegion::Asia, "PMCG1", "001");
|
|
asiaCard.id = 2;
|
|
const auto regions =
|
|
pokemonRegionsInCollection({westCard, asiaCard}, west, asia);
|
|
REQUIRE(regions.size() == 2);
|
|
CHECK(regions[0] == PokemonRegion::West);
|
|
CHECK(regions[1] == PokemonRegion::Asia);
|
|
}
|
|
}
|
|
|
|
TEST_SUITE("PokemonSetCatalogService") {
|
|
TEST_CASE("save then load round-trips for west and asia paths") {
|
|
InMemoryFileSystem fs;
|
|
auto config = makeConfig(fs, "/data");
|
|
PokemonSetCatalogService store{fs, config, [](Game) { return "pokemon"; }};
|
|
|
|
CHECK_FALSE(store.exists(PokemonRegion::West));
|
|
CHECK_FALSE(store.exists(PokemonRegion::Asia));
|
|
CHECK(store.load(PokemonRegion::West).isErr());
|
|
|
|
const auto west = westCatalog();
|
|
const auto asia = asiaCatalog();
|
|
REQUIRE(store.save(PokemonRegion::West, west).isOk());
|
|
REQUIRE(store.save(PokemonRegion::Asia, asia).isOk());
|
|
CHECK(store.exists(PokemonRegion::West));
|
|
CHECK(store.exists(PokemonRegion::Asia));
|
|
|
|
const auto loadedWest = store.load(PokemonRegion::West);
|
|
REQUIRE(loadedWest.isOk());
|
|
CHECK(loadedWest.value() == west);
|
|
|
|
const auto loadedAsia = store.load(PokemonRegion::Asia);
|
|
REQUIRE(loadedAsia.isOk());
|
|
CHECK(loadedAsia.value() == asia);
|
|
|
|
CHECK(fs.exists("/data/pokemon/set-catalog-west.json"));
|
|
CHECK(fs.exists("/data/pokemon/set-catalog-asia.json"));
|
|
}
|
|
}
|