Merge branch 'master' into tooomm-patch-33

This commit is contained in:
tooomm 2026-09-19 15:48:39 +02:00 committed by GitHub
commit e35c63a592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
171 changed files with 4899 additions and 701 deletions

View file

@ -24,6 +24,19 @@ target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatri
add_test(NAME latency_tracker_test COMMAND latency_tracker_test)
add_executable(latency_tracker_test latency_tracker_test.cpp)
add_test(NAME loader_local_matching_test COMMAND loader_local_matching_test)
add_executable(
${VERSION_STRING_CPP}
loader_local_matching_test
loader_local_matching_test.cpp
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/client/release_channel.cpp
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/cache_settings.cpp
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/card_counter_settings.cpp
${CMAKE_SOURCE_DIR}/cockatrice/src/client/settings/shortcuts_settings.cpp
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
)
target_include_directories(loader_local_matching_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
add_test(NAME metrics_registry_test COMMAND metrics_registry_test)
add_executable(metrics_registry_test ../servatrice/src/metrics_registry.cpp metrics_registry_test.cpp)
@ -42,6 +55,9 @@ add_executable(server_counter_test server_counter_test.cpp)
add_test(NAME server_developer_role_test COMMAND server_developer_role_test)
add_executable(server_developer_role_test server_developer_role_test.cpp)
add_test(NAME server_game_join_test COMMAND server_game_join_test)
add_executable(server_game_join_test server_game_join_test.cpp)
add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test)
add_executable(server_rate_limiter_test server_rate_limiter_test.cpp)
@ -78,20 +94,22 @@ if(NOT GTEST_FOUND)
set(GTEST_INCLUDE_DIRS "${CMAKE_BINARY_DIR}/gtest-src/include")
set(GTEST_BOTH_LIBRARIES gtest)
add_dependencies(dummy_test gtest)
add_dependencies(expression_test gtest)
add_dependencies(clamped_arithmetic_test gtest)
add_dependencies(test_age_formatting gtest)
add_dependencies(password_hash_test gtest)
add_dependencies(playmat_resolver_test gtest)
add_dependencies(deck_hash_performance_test gtest)
add_dependencies(server_card_counter_test gtest)
add_dependencies(server_counter_test gtest)
add_dependencies(server_rate_limiter_test gtest)
add_dependencies(server_developer_role_test gtest)
add_dependencies(warning_categories_test gtest)
add_dependencies(expression_test gtest)
add_dependencies(lag_monitor_test gtest)
add_dependencies(latency_tracker_test gtest)
add_dependencies(loader_local_matching_test gtest)
add_dependencies(metrics_registry_test gtest)
add_dependencies(password_hash_test gtest)
add_dependencies(playmat_resolver_test gtest)
add_dependencies(server_card_counter_test gtest)
add_dependencies(server_counter_test gtest)
add_dependencies(server_developer_role_test gtest)
add_dependencies(server_game_join_test gtest)
add_dependencies(server_rate_limiter_test gtest)
add_dependencies(test_age_formatting gtest)
add_dependencies(warning_categories_test gtest)
endif()
include_directories(${GTEST_INCLUDE_DIRS})
@ -127,6 +145,10 @@ target_link_libraries(
server_developer_role_test libcockatrice_network libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
${TEST_QT_MODULES}
)
target_link_libraries(
server_game_join_test libcockatrice_network_server_remote libcockatrice_rng Threads::Threads ${GTEST_BOTH_LIBRARIES}
${TEST_QT_MODULES}
)
target_link_libraries(
warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
)
@ -136,9 +158,12 @@ target_link_libraries(
)
target_include_directories(metrics_registry_test PRIVATE ${CMAKE_SOURCE_DIR}/servatrice/src)
target_link_libraries(metrics_registry_test ${TEST_QT_MODULES} Threads::Threads ${GTEST_BOTH_LIBRARIES})
target_link_libraries(
loader_local_matching_test libcockatrice_settings Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}
)
add_subdirectory(card_zone_algorithms)
add_subdirectory(carddatabase)
add_subdirectory(card_zone_algorithms)
add_subdirectory(deck_list_model)
add_subdirectory(deck_list_zones)
add_subdirectory(loading_from_clipboard)

View file

@ -2,9 +2,14 @@
#include "test_card_database_path_provider.h"
#include "gtest/gtest.h"
#include <QTemporaryDir>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_data.h>
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
#include <libcockatrice/card/lazy_properties_hash.h>
#include <libcockatrice/card/printing/printing_info.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
namespace
{
@ -33,6 +38,49 @@ TEST(CardDatabaseTest, LoadXml)
ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear";
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
}
TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
{
NoopCardSetPriorityController controller;
CardSetPtr set =
CardSet::newInstance(&controller, "TST", "Test Set", "expansion", QDate(), CardSet::PriorityPrimary);
QHash<QString, QString> props;
props["manacost"] = "1R";
PrintingInfo printing(set, LazyPropertiesHash(props));
SetToPrintingsMap setsInfo;
setsInfo["TST"].append(printing);
CardInfo::UiAttributes attributes = {.tableRow = 1};
CardInfoPtr card =
CardInfo::newInstance("Lightning Bolt", "Deal 3 damage.", false, {}, {}, {}, setsInfo, attributes);
card->setLocalizedName("de", "Blitzschlag");
card->setLocalizedText("de", "Blitzschlag fügt 3 Schadenspunkte zu.");
SetNameMap sets;
sets.insert("TST", set);
CardNameMap cards;
cards.insert("Lightning Bolt", card);
QTemporaryDir tempDir;
const QString fileName = tempDir.filePath("cards.xml");
NoopCardPreferenceProvider prefProvider;
CockatriceXml4Parser writer(&prefProvider, &controller);
ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName));
CardDatabaseData data;
CockatriceXml4Parser parser(&prefProvider, &controller);
QFile file(fileName);
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
parser.parseFileInto(file, data);
CardInfoPtr loaded = data.cards.value("Lightning Bolt");
ASSERT_FALSE(loaded.isNull());
ASSERT_EQ(loaded->getName(), "Lightning Bolt");
ASSERT_EQ(loaded->getLocalizedName("de"), "Blitzschlag");
ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage.");
}
} // namespace
int main(int argc, char **argv)

View file

@ -0,0 +1,187 @@
#include "client/settings/cache_settings.h"
#include "interface/card_picture_loader/card_picture_loader_local.h"
#include "gtest/gtest.h"
#include <QColor>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QImage>
#include <QImageWriter>
#include <QLoggingCategory>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <libcockatrice/card/lazy_properties_hash.h>
#include <libcockatrice/card/printing/exact_card.h>
#include <libcockatrice/card/set/card_set.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/settings/paths_settings.h>
namespace
{
/**
* @brief Builds an ExactCard with the requested identity fields.
*
* Mirrors how the client constructs cards: the set short name feeds tryLoad()'s
* setName, and the "num" printing property feeds the collector number.
*/
ExactCard cardFor(const QString &name, const QString &setShortName, const QString &collectorNumber)
{
CardSetPtr set;
if (!setShortName.isEmpty()) {
set = CardSet::newInstance(new NoopCardSetPriorityController(), setShortName, setShortName);
}
LazyPropertiesHash properties;
if (!collectorNumber.isEmpty()) {
properties.insert("num", collectorNumber);
}
return ExactCard(CardInfo::newInstance(name), PrintingInfo(set, properties));
}
class LocalMatcherTest : public ::testing::Test
{
protected:
QTemporaryDir tempDir; ///< Sandboxed "pics" root for every test.
CardPictureLoaderLocal *loader = nullptr; ///< Constructed per test against the sandboxed paths.
QString picsPath() const
{
return tempDir.path() + "/pics";
}
void SetUp() override
{
// The loader ctor snapshots the global picture paths once, so point them at the
// sandbox before constructing it.
SettingsCache::instance().paths().setPicsPath(picsPath());
SettingsCache::instance().paths().setCustomPicsPath(picsPath() + "/CUSTOM/");
loader = new CardPictureLoaderLocal(nullptr);
}
void TearDown() override
{
delete loader;
loader = nullptr;
}
/**
* @brief Writes a valid 1x1 PNG under the sandboxed pics path.
*/
void writePngUnderPics(const QString &relativePath, const QColor &color = Qt::red)
{
const QString fullPath = picsPath() + "/" + relativePath;
ASSERT_TRUE(QDir().mkpath(QFileInfo(fullPath).absolutePath()));
QImage image(1, 1, QImage::Format_RGB32);
image.fill(color);
QImageWriter writer(fullPath, "PNG");
ASSERT_TRUE(writer.write(image));
}
};
TEST_F(LocalMatcherTest, ExactMatchBareFileWinsOverSuffixedCompanion)
{
writePngUnderPics("downloadedPics/TestCard.png");
writePngUnderPics("downloadedPics/TestCard (1).png");
const QImage image = loader->tryLoad(cardFor("TestCard", "", ""));
EXPECT_FALSE(image.isNull()) << "The bare TestCard.png must be picked over its suffixed companion";
}
TEST_F(LocalMatcherTest, SuffixedFileWithoutExactMatchIsNotLoaded)
{
// The pre-refactor prefix match would have accepted "TestCard (1).png" for "TestCard".
writePngUnderPics("downloadedPics/TestCard (1).png");
const QImage image = loader->tryLoad(cardFor("TestCard", "", ""));
EXPECT_TRUE(image.isNull()) << "A suffixed file must not satisfy an exact card-name lookup";
}
TEST_F(LocalMatcherTest, SetFolderLookupIgnoresSuffixedFiles)
{
writePngUnderPics("M10/TestCard (1).png");
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", ""));
EXPECT_TRUE(image.isNull()) << "Set-folder lookups must also require an exact name match";
}
TEST_F(LocalMatcherTest, SetFolderLookupStillResolvesExactFile)
{
writePngUnderPics("M10/TestCard.png");
writePngUnderPics("M10/TestCard (1).png");
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", ""));
EXPECT_FALSE(image.isNull()) << "The exact file in the set folder must still resolve";
}
TEST_F(LocalMatcherTest, RootDownloadedPicsFallbackResolvesSchemeFilename)
{
// Non-set-folder export schemes (Name_Set_Collector) write straight into downloadedPics/.
writePngUnderPics("downloadedPics/TestCard_M10_1.png");
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
EXPECT_FALSE(image.isNull()) << "downloadedPics/TestCard_M10_1.png must resolve via the root fallback";
}
TEST_F(LocalMatcherTest, RootFallbackResolvesDashSeparatedVariant)
{
writePngUnderPics("downloadedPics/TestCard-M10-1.png");
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
EXPECT_FALSE(image.isNull()) << "The dash-separated import variant must resolve via the root fallback";
}
TEST_F(LocalMatcherTest, DownloadedPicsSetSubfolderStillResolves)
{
writePngUnderPics("downloadedPics/M10/TestCard_M10_1.png");
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
EXPECT_FALSE(image.isNull()) << "The set-subfolder export scheme must keep resolving";
}
TEST_F(LocalMatcherTest, SetFolderCandidateTakesPrecedenceOverRootFallback)
{
writePngUnderPics("M10/TestCard_M10_1.png", Qt::red);
writePngUnderPics("downloadedPics/TestCard_M10_1.png", Qt::blue);
const QImage image = loader->tryLoad(cardFor("TestCard", "M10", "1"));
ASSERT_FALSE(image.isNull());
EXPECT_EQ(image.pixelColor(0, 0), QColor(Qt::red)) << "The set-folder candidate must be preferred";
}
} // namespace
int main(int argc, char **argv)
{
// Redirect SettingsCache reads/writes (app-data location) away from the real user profile.
QStandardPaths::setTestModeEnabled(true);
// Some CI containers run as a uid without a passwd entry (e.g. GitHub's docker
// runner), so HOME resolves to "/" and the test-mode qttest data dir cannot be
// created. SettingsCache's QSettings then silently drops every write, reads come
// back empty, and the paths the loader searches are "". Give the test a writable
// HOME for the duration of the run so settings behave like on a normal machine.
QTemporaryDir home;
if (home.isValid()) {
qputenv("HOME", home.path().toLocal8Bit());
}
QCoreApplication app(argc, argv);
QLoggingCategory::setFilterRules("card_picture_loader.*=false\nsettings_cache.*=false");
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -72,6 +72,18 @@ protected:
return card;
}
// Helper: build a single MTGJSON foreignData entry
QJsonObject makeForeignEntry(const QString &language, const QString &name, const QString &text)
{
QJsonObject entry;
entry["language"] = language;
entry["name"] = name;
if (!text.isEmpty()) {
entry["text"] = text;
}
return entry;
}
NoopCardSetPriorityController *controller;
OracleImporter *importer;
CardSetPtr set;
@ -872,6 +884,243 @@ TEST_F(OracleImporterTest, DisablingProgressReportingSuppressesScanEmissions)
ASSERT_GT(emissions, 0);
}
// Localized card text tests
// ============================================================================
TEST_F(OracleImporterTest, ImportsLocalizedTextForRequestedLanguage)
{
QJsonObject card = makeCard("Lightning Bolt");
card["foreignData"] =
QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")};
QJsonArray cards{card};
importer->setCardLang("de");
importer->importCardsFromSet(set, cards);
importer->applyLocalizedData();
auto result = importer->getCardList().value("Lightning Bolt");
ASSERT_FALSE(result.isNull());
ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag");
ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
// English identity untouched
ASSERT_EQ(result->getName(), "Lightning Bolt");
ASSERT_EQ(result->getText(), "Rules text.");
}
TEST_F(OracleImporterTest, ImportsLocalizedNameAndTextForMultiFaceCards)
{
// MTGJSON reports multi-face cards (adventure/split/aftermath/prepare) as one
// card object per face; every face carries the joined name but only its own
// face's rules text in foreignData. The importer joins the per-face texts with
// the same separator as the English merge.
QJsonObject front = makeCard("Disruptive Stormbrood // Petty Revenge");
front["layout"] = "adventure";
front["faceName"] = "Disruptive Stormbrood";
front["side"] = "a";
front["foreignData"] = QJsonArray{
makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache",
"Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine "
"Verzauberung deiner Wahl.")};
QJsonObject back = makeCard("Disruptive Stormbrood // Petty Revenge");
back["layout"] = "adventure";
back["faceName"] = "Petty Revenge";
back["side"] = "b";
back["text"] = "Destroy target creature.";
back["foreignData"] = QJsonArray{makeForeignEntry("German", "Disruptive Stormbrood // Kleinliche Rache",
"Zerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger.")};
QJsonArray cards{front, back};
importer->setCardLang("de");
importer->importCardsFromSet(set, cards);
importer->applyLocalizedData();
auto result = importer->getCardList().value("Disruptive Stormbrood // Petty Revenge");
ASSERT_FALSE(result.isNull());
ASSERT_EQ(result->getLocalizedName("de"), "Disruptive Stormbrood // Kleinliche Rache");
ASSERT_EQ(result->getLocalizedText("de"),
"Fliegend\nWenn diese Kreatur ins Spiel kommt, zerstöre bis zu ein Artefakt oder eine Verzauberung "
"deiner Wahl.\n\n---\n\nZerstöre eine Kreatur deiner Wahl mit Stärke 3 oder weniger.");
// English identity untouched
ASSERT_EQ(result->getName(), "Disruptive Stormbrood // Petty Revenge");
ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nDestroy target creature.");
}
TEST_F(OracleImporterTest, MultiFaceCardsWithoutCompleteForeignTextKeepEnglishText)
{
// Both faces must carry a foreignData text for the joined text; otherwise the
// rules text stays English while the localized name (from a later complete
// printing) is still applied.
QJsonObject front = makeCard("Wear // Tear");
front["layout"] = "split";
front["faceName"] = "Wear";
front["side"] = "a";
front["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Verschleiß-Text.")};
QJsonObject back = makeCard("Wear // Tear");
back["layout"] = "split";
back["faceName"] = "Tear";
back["side"] = "b";
back["text"] = "Tear rules text.";
back["foreignData"] = QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "")};
QJsonArray cards{front, back};
importer->setCardLang("de");
importer->importCardsFromSet(set, cards);
importer->applyLocalizedData();
auto result = importer->getCardList().value("Wear // Tear");
ASSERT_FALSE(result.isNull());
// The joined name is still applied.
ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung");
// The incomplete text must not become the card's localized text.
ASSERT_TRUE(result->getLocalizedTexts().isEmpty());
ASSERT_EQ(result->getText(), "Rules text.\n\n---\n\nTear rules text.");
}
TEST_F(OracleImporterTest, DefaultLanguageSkipsForeignData)
{
QJsonObject card = makeCard("Lightning Bolt");
card["foreignData"] =
QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")};
QJsonArray cards{card};
// cardLang defaults to "en" — foreignData must never be imported
importer->importCardsFromSet(set, cards);
importer->applyLocalizedData();
auto result = importer->getCardList().value("Lightning Bolt");
ASSERT_FALSE(result.isNull());
ASSERT_TRUE(result->getLocalizedNames().isEmpty());
ASSERT_TRUE(result->getLocalizedTexts().isEmpty());
}
TEST_F(OracleImporterTest, UnsupportedLanguageSkipsForeignData)
{
QJsonObject card = makeCard("Lightning Bolt");
card["foreignData"] = QJsonArray{makeForeignEntry("xx", "Kochanie", "Grzmot uderza.")};
QJsonArray cards{card};
importer->setCardLang("xx");
importer->importCardsFromSet(set, cards);
importer->applyLocalizedData();
auto result = importer->getCardList().value("Lightning Bolt");
ASSERT_FALSE(result.isNull());
ASSERT_TRUE(result->getLocalizedNames().isEmpty());
}
TEST_F(OracleImporterTest, NonMatchingLanguageNotCollected)
{
QJsonObject card = makeCard("Lightning Bolt");
card["foreignData"] = QJsonArray{makeForeignEntry("French", "Éclair", "L'Éclair inflige 3 blessures.")};
QJsonArray cards{card};
importer->setCardLang("de");
importer->importCardsFromSet(set, cards);
importer->applyLocalizedData();
auto result = importer->getCardList().value("Lightning Bolt");
ASSERT_FALSE(result.isNull());
ASSERT_TRUE(result->getLocalizedNames().isEmpty());
}
TEST_F(OracleImporterTest, HigherPrioritySetWinsForReprint)
{
// First printing in a reprint set, then another in a (more authoritative)
// core set: the core set's German text must win even though it was seen later.
QJsonObject reprintCard = makeCard("Lightning Bolt");
reprintCard["foreignData"] = QJsonArray{makeForeignEntry("German", "Blitzschlag", "Älterer deutscher Text.")};
CardSetPtr reprintSet =
CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint);
importer->setCardLang("de");
importer->importCardsFromSet(reprintSet, QJsonArray{reprintCard});
QJsonObject primaryCard = makeCard("Lightning Bolt");
primaryCard["foreignData"] =
QJsonArray{makeForeignEntry("German", "Blitzschlag", "Blitzschlag fügt 3 Schadenspunkte zu.")};
CardSetPtr primarySet =
CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary);
importer->importCardsFromSet(primarySet, QJsonArray{primaryCard});
importer->applyLocalizedData();
auto result = importer->getCardList().value("Lightning Bolt");
ASSERT_FALSE(result.isNull());
ASSERT_EQ(result->getLocalizedName("de"), "Blitzschlag");
ASSERT_EQ(result->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
ASSERT_EQ(importer->getCardList().size(), 1);
}
TEST_F(OracleImporterTest, HigherPrioritySplitSetWinsForReprint)
{
// Split cards print each face as its own card object; the joined text is
// collected per set with the same priority policy as single-face cards, so a
// reprint set's German text must yield to the core set's even when reprints
// are imported first.
QJsonObject reprintFront = makeCard("Wear // Tear");
reprintFront["layout"] = "split";
reprintFront["faceName"] = "Wear";
reprintFront["side"] = "a";
reprintFront["foreignData"] =
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear alter Text.")};
QJsonObject reprintBack = makeCard("Wear // Tear");
reprintBack["layout"] = "split";
reprintBack["faceName"] = "Tear";
reprintBack["side"] = "b";
reprintBack["foreignData"] =
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear alter Text.")};
CardSetPtr reprintSet =
CardSet::newInstance(controller, "TS2", "Second Set", QString(), QDate(), CardSet::PriorityReprint);
importer->setCardLang("de");
importer->importCardsFromSet(reprintSet, QJsonArray{reprintFront, reprintBack});
QJsonObject primaryFront = makeCard("Wear // Tear");
primaryFront["layout"] = "split";
primaryFront["faceName"] = "Wear";
primaryFront["side"] = "a";
primaryFront["foreignData"] =
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Wear neuer Text.")};
QJsonObject primaryBack = makeCard("Wear // Tear");
primaryBack["layout"] = "split";
primaryBack["faceName"] = "Tear";
primaryBack["side"] = "b";
primaryBack["foreignData"] =
QJsonArray{makeForeignEntry("German", "Verschleiß // Zerrreißung", "Tear neuer Text.")};
CardSetPtr primarySet =
CardSet::newInstance(controller, "TS3", "Third Set", QString(), QDate(), CardSet::PriorityPrimary);
importer->importCardsFromSet(primarySet, QJsonArray{primaryFront, primaryBack});
importer->applyLocalizedData();
auto result = importer->getCardList().value("Wear // Tear");
ASSERT_FALSE(result.isNull());
ASSERT_EQ(result->getLocalizedName("de"), "Verschleiß // Zerrreißung");
ASSERT_EQ(result->getLocalizedText("de"), "Wear neuer Text.\n\n---\n\nTear neuer Text.");
ASSERT_EQ(importer->getCardList().size(), 1);
}
TEST_F(OracleImporterTest, StartImportAppliesLocalizedData)
{
QJsonObject card = makeCard("Lightning Bolt");
card["foreignData"] =
QJsonArray{makeForeignEntry("Portuguese (Brazil)", "Raio", "Raio causa 3 de dano a qualquer alvo.")};
QJsonObject dataSet;
dataSet["code"] = "tst";
dataSet["name"] = "Test Set";
dataSet["type"] = "expansion";
dataSet["releaseDate"] = "2024-01-01";
dataSet["cards"] = QJsonArray{card};
QJsonObject root;
root["data"] = QJsonObject{{"TST", dataSet}};
importer->setCardLang("pt");
ASSERT_TRUE(importer->readSetsFromByteArray(QJsonDocument(root).toJson(QJsonDocument::Compact)));
ASSERT_EQ(importer->startImport(), 1);
auto result = importer->getCardList().value("Lightning Bolt");
ASSERT_FALSE(result.isNull());
ASSERT_EQ(result->getLocalizedName("pt"), "Raio");
ASSERT_EQ(result->getLocalizedText("pt"), "Raio causa 3 de dano a qualquer alvo.");
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);

View file

@ -0,0 +1,174 @@
/** @file server_game_join_test.cpp
* @brief Tests for the moderator/judge game-entry restriction override in Server_Game::checkJoin.
* @ingroup Tests
*/
#include "game/server_game.h"
#include "server.h"
#include "server_database_interface.h"
#include "server_room.h"
#include <gtest/gtest.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/rng/rng_abstract.h>
RNG_Abstract *rng = nullptr; // referenced by the server_remote library
namespace
{
class MockDatabaseInterface : public Server_DatabaseInterface
{
public:
AuthenticationResult checkUserPassword(Server_ProtocolHandler *,
const QString &,
const QString &,
const QString &,
QString &,
int &,
bool) override
{
return NotLoggedIn;
}
int getNextReplayId() override
{
return 1;
}
int getNextGameId() override
{
return 1;
}
int getActiveUserCount(QString) override
{
return 0;
}
ServerInfo_User getUserData(const QString &, bool) override
{
return ServerInfo_User();
}
};
class FakeServer : public Server
{
public:
FakeServer()
{
setDatabaseInterface(new MockDatabaseInterface());
}
};
class GameJoinOverrideTest : public ::testing::Test
{
protected:
FakeServer server;
Server_Room room{0, 0, "", "", "", "", false, "", {}, &server};
ServerInfo_User creator;
ServerInfo_User plainUser;
ServerInfo_User unregisteredJudge;
ServerInfo_User moderator;
ServerInfo_User judge;
Server_Game *game = nullptr;
void SetUp() override
{
creator.set_name("creator");
creator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
plainUser.set_name("plain-user");
plainUser.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
unregisteredJudge.set_name("unregistered-judge");
unregisteredJudge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsJudge);
moderator.set_name("moderator");
moderator.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered |
ServerInfo_User::IsModerator);
judge.set_name("judge");
judge.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsJudge);
}
void TearDown() override
{
delete game;
}
Server_Game *makeGame(bool passwordProtected, bool onlyRegistered, bool onlyBuddies, bool spectatorsAllowed)
{
GameConfig config{.creatorInfo = creator,
.gameId = 1,
.description = QString(),
.password = passwordProtected ? "secret" : QString(),
.maxPlayers = 2,
.gameTypes = QList<int>(),
.onlyBuddies = onlyBuddies,
.onlyRegistered = onlyRegistered,
.spectatorsAllowed = spectatorsAllowed,
.spectatorsNeedPassword = true,
.spectatorsCanTalk = false,
.spectatorsSeeEverything = false,
.startingLifeTotal = 20,
.shareDecklistsOnLoad = false};
return new Server_Game(config, &room);
}
};
TEST_F(GameJoinOverrideTest, StaffBypassPasswordRestriction)
{
game = makeGame(true, false, false, true);
// A plain user cannot override the password even with the override flag set.
EXPECT_EQ(game->checkJoin(&plainUser, "wrong", false, true, false), Response::RespWrongPassword);
// Moderators and judges may enter any game regardless of the password.
EXPECT_EQ(game->checkJoin(&moderator, "wrong", false, true, false), Response::RespOk);
EXPECT_EQ(game->checkJoin(&judge, "wrong", false, true, false), Response::RespOk);
// Without the override flag judges are still subject to the password.
EXPECT_EQ(game->checkJoin(&judge, "wrong", false, false, true), Response::RespWrongPassword);
EXPECT_EQ(game->checkJoin(&judge, "secret", false, false, true), Response::RespOk);
}
TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction)
{
game = makeGame(false, true, false, true);
// Without the override flag the only-registered restriction still applies.
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, false, false), Response::RespUserLevelTooLow);
// An unregistered judge may enter when overriding restrictions.
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk);
}
TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction)
{
game = makeGame(false, false, true, true);
// A plain user who is not on the creator's buddy list gets rejected.
EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, false), Response::RespOnlyBuddies);
// Moderators and judges bypass the buddies-only restriction.
EXPECT_EQ(game->checkJoin(&moderator, QString(), false, true, false), Response::RespOk);
EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, false), Response::RespOk);
}
TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction)
{
game = makeGame(false, false, false, false);
// A plain user cannot spectate when the game disallows spectators.
EXPECT_EQ(game->checkJoin(&plainUser, QString(), true, false, false), Response::RespSpectatorsNotAllowed);
// Moderators and judges may spectate any game regardless of the password
// and the spectator restriction.
EXPECT_EQ(game->checkJoin(&moderator, "wrong", true, true, false), Response::RespOk);
EXPECT_EQ(game->checkJoin(&judge, "wrong", true, true, false), Response::RespOk);
}
TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser)
{
game = makeGame(false, false, false, true);
// joining with join_as_judge still requires the judge flag even when overriding.
EXPECT_EQ(game->checkJoin(&plainUser, QString(), false, true, true), Response::RespUserLevelTooLow);
EXPECT_EQ(game->checkJoin(&judge, QString(), false, true, true), Response::RespOk);
}
} // namespace
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -386,6 +386,19 @@ TEST_F(SettingsDefaultsTest, Appearance_HomeTabDisplayCardName_Default)
ASSERT_EQ(s.getHomeTabDisplayCardName(), true);
}
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_Default)
{
AppearanceSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHomeTabBackgroundDim(), true);
}
TEST_F(SettingsDefaultsTest, Appearance_HomeTabBackgroundDim_SetAndGet)
{
AppearanceSettings s(settingsPath, nullptr);
s.setHomeTabBackgroundDim(false);
ASSERT_EQ(s.getHomeTabBackgroundDim(), false);
}
// --- InterfaceSettings ---
TEST_F(SettingsDefaultsTest, Interface_ShowStatusBar_Default)
@ -549,6 +562,19 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default)
ASSERT_EQ(s.getArrowDrawAnimation(), true);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getCardLang(), QString("en"));
}
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setCardLang("de");
ASSERT_EQ(s.getCardLang(), QString("de"));
}
// --- VisualDeckStorageSettings ---
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)