mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 17:43:23 -08:00
Keep cached image redirects within the inclusive hop limit
Accept a terminal URL reached by exactly the permitted number of redirects, while preserving cycle and overlong-chain invalidation. Match Qt's redirect classification by omitting HTTP 305 from both reply paths. Add focused GoogleTest coverage for the cache resolver and retain current upstream.
This commit is contained in:
commit
4ee7b416a9
451 changed files with 15753 additions and 11787 deletions
|
|
@ -171,13 +171,36 @@ endfunction()
|
|||
|
||||
# --- Unit tests ------------------------------------------------------------------
|
||||
|
||||
cockatrice_add_test(NAME image_redirect_chain_test SOURCES image_redirect_chain_test.cpp)
|
||||
cockatrice_add_test(NAME expression_test SOURCES expression_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME clamped_arithmetic_test SOURCES clamped_arithmetic_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME test_age_formatting SOURCES test_age_formatting.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME svg_recolor_test SOURCES svg_recolor_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
user_games_model_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/server/user/user_games_model.cpp
|
||||
user_games_model_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network_client_abstract
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
${CMAKE_SOURCE_DIR}/libcockatrice_network
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
spoiler_file_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/network/update/card_spoiler/spoiler_file.cpp
|
||||
spoiler_file_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
# The installer's holder probe is Windows-only, but the wording it produces and the
|
||||
# question which programs are the payload's own are plain C++ and are what a stuck user
|
||||
# has to act on, so they are tested everywhere.
|
||||
|
|
@ -208,6 +231,18 @@ cockatrice_add_test(
|
|||
libcockatrice_deck_list
|
||||
libcockatrice_utility
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
card_art_crop_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/cards/card_art_utils.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
card_art_crop_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_rate_limiter_test
|
||||
|
|
@ -328,6 +363,40 @@ cockatrice_add_test(
|
|||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_forced_password_change_test
|
||||
SOURCES
|
||||
server_forced_password_change_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_rng
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
client_login_status_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
client_login_status_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network_client_remote
|
||||
LABELS
|
||||
ci
|
||||
)
|
||||
# The local game runs one client and one server in the same process.
|
||||
# Either of them can be destroyed first; the survivor has to stay usable.
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
local_client_lifetime_test
|
||||
SOURCES
|
||||
local_client_lifetime_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
LABELS
|
||||
ci
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
server_room_channels_test
|
||||
|
|
@ -380,18 +449,10 @@ cockatrice_add_test(
|
|||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
loader_local_matching_test
|
||||
font_config_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.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/client/network/update/client/release_channel.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
loader_local_matching_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network
|
||||
libcockatrice_settings
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp
|
||||
font_config_test.cpp
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
|
|
@ -446,6 +507,19 @@ endif()
|
|||
|
||||
# Guard must match the condition for src/single_instance_manager.cpp in cockatrice/CMakeLists.txt.
|
||||
if(WITH_CLIENT)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
moxfield_signature_spells_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/deck_loader/card_node_function.cpp
|
||||
moxfield_signature_spells_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
single_instance_manager_test
|
||||
|
|
@ -493,7 +567,9 @@ add_subdirectory(card_zone_algorithms)
|
|||
add_subdirectory(carddatabase)
|
||||
add_subdirectory(deck_list_model)
|
||||
add_subdirectory(deck_list_zones)
|
||||
add_subdirectory(icon_assets)
|
||||
add_subdirectory(loading_from_clipboard)
|
||||
add_subdirectory(model_contract)
|
||||
add_subdirectory(movecard_tests)
|
||||
add_subdirectory(oracle)
|
||||
add_subdirectory(settings)
|
||||
|
|
@ -513,3 +589,46 @@ endif()
|
|||
if(WITH_ORACLE OR WITH_CLIENT)
|
||||
add_subdirectory(picture_loader_benchmark)
|
||||
endif()
|
||||
|
||||
# loader_local_matching_test and deck_state_manager_test links libcockatrice_settings,
|
||||
# which only exists when a client/UI-capable target is being built.
|
||||
if(WITH_ORACLE OR WITH_CLIENT)
|
||||
set(SETTINGS_SOURCES
|
||||
${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
|
||||
${VERSION_STRING_CPP}
|
||||
)
|
||||
set(SETTINGS_LIBS libcockatrice_network libcockatrice_settings)
|
||||
set(SETTINGS_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
loader_local_matching_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/card_picture_loader/card_picture_loader_local.cpp
|
||||
${SETTINGS_SOURCES}
|
||||
loader_local_matching_test.cpp
|
||||
LIBS
|
||||
${SETTINGS_LIBS}
|
||||
INCLUDE_DIRS
|
||||
${SETTINGS_INCLUDE_DIRS}
|
||||
)
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
deck_state_manager_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.cpp
|
||||
${SETTINGS_SOURCES}
|
||||
deck_state_manager_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_models
|
||||
libcockatrice_utility
|
||||
${SETTINGS_LIBS}
|
||||
INCLUDE_DIRS
|
||||
${SETTINGS_INCLUDE_DIRS}
|
||||
)
|
||||
endif()
|
||||
|
|
|
|||
92
tests/card_art_crop_test.cpp
Normal file
92
tests/card_art_crop_test.cpp
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#include "interface/widgets/cards/card_art_utils.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QTransform>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
ExactCard makeCard(bool landscape)
|
||||
{
|
||||
CardInfo::UiAttributes attributes;
|
||||
attributes.landscapeOrientation = landscape;
|
||||
return ExactCard(CardInfo::newInstance("Artwork", "", false, {}, {}, {}, {}, attributes));
|
||||
}
|
||||
|
||||
QPixmap landscapeCardWithFrame()
|
||||
{
|
||||
// Normalized landscape frame: the title reaches 14% down the upright
|
||||
// image, and the Battle type line starts at 56%. Artwork has two colors
|
||||
// so the test also detects a wrong or missing rotation.
|
||||
QImage upright(1400, 1000, QImage::Format_RGB32);
|
||||
upright.fill(Qt::red);
|
||||
QPainter painter(&upright);
|
||||
painter.fillRect(QRect(70, 140, 1260, 420), Qt::green);
|
||||
painter.fillRect(QRect(700, 140, 630, 420), Qt::blue);
|
||||
painter.end();
|
||||
QTransform storedOrientation;
|
||||
storedOrientation.rotate(-90);
|
||||
return QPixmap::fromImage(upright.transformed(storedOrientation));
|
||||
}
|
||||
|
||||
TEST(CardArtCrop, LandscapeCropExcludesNameAndRulesBoxesAndRotatesUpright)
|
||||
{
|
||||
const QImage crop = CardArtUtils::cropCardArt(landscapeCardWithFrame(), makeCard(true)).toImage();
|
||||
ASSERT_GT(crop.width(), crop.height());
|
||||
ASSERT_FALSE(crop.isNull());
|
||||
for (int y = 0; y < crop.height(); ++y) {
|
||||
for (int x = 0; x < crop.width(); ++x) {
|
||||
EXPECT_EQ(x < crop.width() / 2 ? QColor(Qt::green) : QColor(Qt::blue), crop.pixelColor(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CardArtCrop, PortraitCropRetainsItsExistingArtworkBounds)
|
||||
{
|
||||
QImage source(745, 1040, QImage::Format_RGB32);
|
||||
for (int y = 0; y < source.height(); ++y) {
|
||||
for (int x = 0; x < source.width(); ++x) {
|
||||
source.setPixelColor(x, y, QColor(x % 256, y % 256, (x + y) % 256));
|
||||
}
|
||||
}
|
||||
const QPixmap input = QPixmap::fromImage(source);
|
||||
const QImage crop = CardArtUtils::cropCardArt(input, makeCard(false)).toImage();
|
||||
EXPECT_EQ(source.copy(QRect(52, 114, 641, 458)), crop);
|
||||
}
|
||||
|
||||
TEST(CardArtCrop, DevicePixelRatioDoesNotChangeTheArtworkPixels)
|
||||
{
|
||||
QPixmap source = landscapeCardWithFrame();
|
||||
const QPixmap normal = CardArtUtils::cropCardArt(source, makeCard(true));
|
||||
source.setDevicePixelRatio(2.0);
|
||||
const QPixmap highDpi = CardArtUtils::cropCardArt(source, makeCard(true));
|
||||
EXPECT_EQ(normal.size(), highDpi.size());
|
||||
EXPECT_EQ(2.0, highDpi.devicePixelRatio());
|
||||
QImage normalPixels = normal.toImage();
|
||||
QImage highDpiPixels = highDpi.toImage();
|
||||
normalPixels.setDevicePixelRatio(1.0);
|
||||
highDpiPixels.setDevicePixelRatio(1.0);
|
||||
EXPECT_EQ(normalPixels, highDpiPixels);
|
||||
}
|
||||
|
||||
TEST(CardArtCrop, EmptyAndSmallImagesStayWithinTheirBounds)
|
||||
{
|
||||
EXPECT_TRUE(CardArtUtils::cropCardArt({}, makeCard(true)).isNull());
|
||||
QPixmap small(1, 1);
|
||||
small.fill(Qt::green);
|
||||
EXPECT_EQ(small.toImage(), CardArtUtils::cropCardArt(small, makeCard(true)).toImage());
|
||||
EXPECT_EQ(small.toImage(), CardArtUtils::cropCardArt(small, makeCard(false)).toImage());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
qputenv("QT_QPA_PLATFORM", "offscreen");
|
||||
QApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -35,6 +35,21 @@ if(WITH_ORACLE OR WITH_CLIENT)
|
|||
LIBS
|
||||
libcockatrice_filters
|
||||
)
|
||||
|
||||
# ------------------------
|
||||
# Visual Display Model Test
|
||||
# ------------------------
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
visual_display_model_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
visual_display_model_test.cpp
|
||||
mocks.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
libcockatrice_models
|
||||
)
|
||||
endif()
|
||||
|
||||
# ------------------------
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@
|
|||
#include <libcockatrice/utility/card_ref.h>
|
||||
#include <string>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <csignal>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
|
|
@ -204,6 +210,64 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
|
|||
ASSERT_EQ(printings.first().getProperty("manacost"), "1R");
|
||||
}
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
/**
|
||||
* @brief A failed write keeps the previously installed card database intact.
|
||||
*
|
||||
* This is the writer Oracle uses to install a refreshed database, so the truncation
|
||||
* bug has to be covered here and not only through OracleDatabase::saveXml.
|
||||
*/
|
||||
TEST(CardDatabaseTest, Xml4ShortWriteKeepsInstalledFile)
|
||||
{
|
||||
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", QString(1024, 'x'), false, {}, {}, {}, setsInfo, attributes);
|
||||
|
||||
SetNameMap sets;
|
||||
sets.insert("TST", set);
|
||||
CardNameMap cards;
|
||||
cards.insert("Lightning Bolt", card);
|
||||
|
||||
QTemporaryDir tempDir;
|
||||
ASSERT_TRUE(tempDir.isValid());
|
||||
const QString fileName = tempDir.filePath("cards.xml");
|
||||
|
||||
QFile installed(fileName);
|
||||
ASSERT_TRUE(installed.open(QIODevice::WriteOnly));
|
||||
ASSERT_EQ(installed.write("installed database"), 18);
|
||||
installed.close();
|
||||
|
||||
// A real filesystem write failure, isolated in the death-test child so the
|
||||
// process-wide file limit never affects other cases or the parent runner.
|
||||
ASSERT_EXIT(
|
||||
{
|
||||
std::signal(SIGXFSZ, SIG_IGN);
|
||||
struct rlimit limit;
|
||||
limit.rlim_cur = 128;
|
||||
limit.rlim_max = 128;
|
||||
if (setrlimit(RLIMIT_FSIZE, &limit) != 0) {
|
||||
_exit(2);
|
||||
}
|
||||
CockatriceXml4Parser writer(&controller);
|
||||
_exit(writer.saveToFile({}, sets, cards, fileName) ? 1 : 0);
|
||||
},
|
||||
::testing::ExitedWithCode(0), "");
|
||||
|
||||
ASSERT_TRUE(installed.open(QIODevice::ReadOnly));
|
||||
EXPECT_EQ(installed.readAll(), "installed database");
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Path provider serving a sandboxed copy of the test card database.
|
||||
*/
|
||||
|
|
|
|||
188
tests/carddatabase/visual_display_model_test.cpp
Normal file
188
tests/carddatabase/visual_display_model_test.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* @file visual_display_model_test.cpp
|
||||
* @brief Tests for the flattened model behind the visual card database grid.
|
||||
*/
|
||||
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "card_database_visual_display_model.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
#include "libcockatrice/card/database/card_database_querier.h"
|
||||
#include "libcockatrice/card/printing/exact_card.h"
|
||||
#include "libcockatrice/card/printing/printing_info.h"
|
||||
#include "libcockatrice/card/set/card_set.h"
|
||||
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
|
||||
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
|
||||
#include "test_card_database_path_provider.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDate>
|
||||
#include <QEventLoop>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
// GTest's main() creates no QCoreApplication, which QTimer needs for its event loop.
|
||||
const QCoreApplication &application()
|
||||
{
|
||||
static int argc = 1;
|
||||
static char executable[] = "visual_display_model_test";
|
||||
static char *argv[] = {executable, nullptr};
|
||||
static const QCoreApplication app(argc, argv);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Lets the rebuild timer fire, as it would in the running client.
|
||||
void waitForRebuild()
|
||||
{
|
||||
QEventLoop loop;
|
||||
QTimer::singleShot(100, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
class CardDatabaseVisualDisplayModelTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
(void)application();
|
||||
|
||||
db = std::make_unique<CardDatabase>(nullptr, new NoopCardPreferenceProvider(),
|
||||
new TestCardDatabasePathProvider(), new NoopCardSetPriorityController());
|
||||
db->loadCardDatabases();
|
||||
|
||||
cardModel = std::make_unique<CardDatabaseModel>(db.get(), false);
|
||||
displayModel = std::make_unique<CardDatabaseDisplayModel>();
|
||||
displayModel->setSourceModel(cardModel.get());
|
||||
visualModel = std::make_unique<CardDatabaseVisualDisplayModel>(displayModel.get(), db->query());
|
||||
visualModel->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Builds a card with @p printingCount printings in each named set.
|
||||
* @param printingCountPerSet Set short name to the number of printings in that set.
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr makeCard(const QString &name, const QMap<QString, int> &printingCountPerSet)
|
||||
{
|
||||
SetToPrintingsMap setsInfo;
|
||||
for (auto it = printingCountPerSet.constBegin(); it != printingCountPerSet.constEnd(); ++it) {
|
||||
CardSetPtr set = CardSet::newInstance(&setController, it.key(), it.key() + " test set", "expansion",
|
||||
QDate(), CardSet::PriorityPrimary);
|
||||
for (int printing = 0; printing < it.value(); ++printing) {
|
||||
QHash<QString, QString> properties;
|
||||
properties["uuid"] = QString("%1-%2-%3").arg(it.key(), name).arg(printing);
|
||||
setsInfo[it.key()].append(PrintingInfo(set, LazyPropertiesHash(properties)));
|
||||
}
|
||||
}
|
||||
|
||||
CardInfo::UiAttributes attributes = {.tableRow = 1};
|
||||
return CardInfo::newInstance(name, "", false, {}, {}, {}, setsInfo, attributes);
|
||||
}
|
||||
|
||||
NoopCardSetPriorityController setController;
|
||||
std::unique_ptr<CardDatabase> db;
|
||||
std::unique_ptr<CardDatabaseModel> cardModel;
|
||||
std::unique_ptr<CardDatabaseDisplayModel> displayModel;
|
||||
std::unique_ptr<CardDatabaseVisualDisplayModel> visualModel;
|
||||
};
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ExposesTheCardInfoBehindAProxyRow)
|
||||
{
|
||||
ASSERT_GT(displayModel->rowCount(), 0);
|
||||
|
||||
const QVariant info = displayModel->data(displayModel->index(0, CardDatabaseModel::NameColumn),
|
||||
CardDatabaseDisplayModel::CardInfoRole);
|
||||
ASSERT_TRUE(info.isValid());
|
||||
EXPECT_FALSE(info.value<CardInfoPtr>().isNull());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, FlattensOneRowPerProxyCardWithoutSetFilters)
|
||||
{
|
||||
ASSERT_GT(displayModel->rowCount(), 0);
|
||||
EXPECT_EQ(displayModel->rowCount(), visualModel->rowCount());
|
||||
|
||||
for (int row = 0; row < visualModel->rowCount(); ++row) {
|
||||
const ExactCard card = visualModel->cardAt(row);
|
||||
ASSERT_TRUE(card) << "row " << row;
|
||||
EXPECT_EQ(
|
||||
displayModel->data(displayModel->index(row, CardDatabaseModel::NameColumn), Qt::DisplayRole).toString(),
|
||||
card.getName())
|
||||
<< "row " << row;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ShowsOneRowPerCardWhenNoSetIsFiltered)
|
||||
{
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
|
||||
EXPECT_EQ(rowsBefore + 1, visualModel->rowCount());
|
||||
EXPECT_EQ("Multi", visualModel->cardAt(visualModel->rowCount() - 1).getName());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ShowsEveryPrintingOfAFilteredSet)
|
||||
{
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
|
||||
visualModel->setSetFilterTerms({"TST"});
|
||||
|
||||
ASSERT_EQ(2, visualModel->rowCount());
|
||||
for (int row = 0; row < visualModel->rowCount(); ++row) {
|
||||
const ExactCard card = visualModel->cardAt(row);
|
||||
ASSERT_TRUE(card) << "row " << row;
|
||||
EXPECT_EQ("Multi", card.getName());
|
||||
ASSERT_FALSE(card.getPrinting().isEmpty()) << "row " << row;
|
||||
EXPECT_EQ("TST", card.getPrinting().getSet()->getShortName()) << "row " << row;
|
||||
}
|
||||
EXPECT_NE(visualModel->cardAt(0).getPrinting(), visualModel->cardAt(1).getPrinting());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, HidesCardsWithoutAPrintingInTheFilteredSets)
|
||||
{
|
||||
visualModel->setSetFilterTerms({"NO SUCH SET"});
|
||||
|
||||
EXPECT_EQ(0, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, RestoresOneRowPerCardWhenTheSetTermsClear)
|
||||
{
|
||||
db->addCard(makeCard("Multi", {{"TST", 2}, {"OTH", 1}}));
|
||||
visualModel->refresh();
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
visualModel->setSetFilterTerms({"TST"});
|
||||
ASSERT_EQ(2, visualModel->rowCount());
|
||||
|
||||
visualModel->setSetFilterTerms({});
|
||||
EXPECT_EQ(rowsBefore, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, RebuildsByItselfWhenTheProxyGainsARow)
|
||||
{
|
||||
const int rowsBefore = visualModel->rowCount();
|
||||
|
||||
db->addCard(makeCard("Late", {{"TST", 1}}));
|
||||
waitForRebuild();
|
||||
|
||||
EXPECT_EQ(rowsBefore + 1, visualModel->rowCount());
|
||||
}
|
||||
|
||||
TEST_F(CardDatabaseVisualDisplayModelTest, ReportsTheCardNameForDisplayAndAccessibleText)
|
||||
{
|
||||
ASSERT_GT(visualModel->rowCount(), 0);
|
||||
const QString expected = visualModel->cardAt(0).getName();
|
||||
|
||||
EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::DisplayRole).toString());
|
||||
EXPECT_EQ(expected, visualModel->data(visualModel->index(0), Qt::AccessibleTextRole).toString());
|
||||
EXPECT_FALSE(visualModel->data(QModelIndex(), Qt::DisplayRole).isValid());
|
||||
}
|
||||
} // namespace
|
||||
116
tests/client_login_status_test.cpp
Normal file
116
tests/client_login_status_test.cpp
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/** @file client_login_status_test.cpp
|
||||
* @brief The login response state machine must keep the session a forced password change needs.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include "abstract_client.h"
|
||||
#include "libcockatrice/interfaces/interface_network_settings_provider.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/response_login.pb.h"
|
||||
#include "remote_client.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QMetaObject>
|
||||
#include <QString>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class StubNetworkSettingsProvider : public INetworkSettingsProvider
|
||||
{
|
||||
public:
|
||||
QString getClientID() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
[[nodiscard]] int getTimeOut() const override
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
[[nodiscard]] int getKeepAlive() const override
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
[[nodiscard]] bool getNotifyAboutUpdates() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void setKnownMissingFeatures(const QString &) override
|
||||
{
|
||||
}
|
||||
QString getKnownMissingFeatures() override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Feeds a login response to RemoteClient without a socket.
|
||||
*
|
||||
* `loginResponse` is the slot the socket thread runs for the login command's answer, so
|
||||
* invoking it directly exercises the status transition that decides whether `readData()`
|
||||
* tears the connection down right after it.
|
||||
*/
|
||||
class ClientLoginStatusTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
client = new RemoteClient(nullptr, &provider);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete client;
|
||||
client = nullptr;
|
||||
}
|
||||
|
||||
void deliverLoginResponse(Response::ResponseCode code)
|
||||
{
|
||||
Response response;
|
||||
response.set_response_code(code);
|
||||
response.MutableExtension(Response_Login::ext);
|
||||
ASSERT_TRUE(
|
||||
QMetaObject::invokeMethod(client, "loginResponse", Qt::DirectConnection, Q_ARG(Response, response)));
|
||||
}
|
||||
|
||||
StubNetworkSettingsProvider provider;
|
||||
RemoteClient *client = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A required password change is not a failed login.
|
||||
*
|
||||
* The server keeps the session open and accepts only ping and Command_AccountPassword until
|
||||
* the password is replaced. StatusDisconnecting would make readData() close the socket right
|
||||
* after this slot returns - before the dialog that collects the new password (queued to the
|
||||
* UI thread) can send anything - and the pending command would never be answered because the
|
||||
* ping timer that times it out dies with the connection. StatusLoggedIn would start the
|
||||
* normal post-login flow for a session that cannot serve it.
|
||||
*/
|
||||
TEST_F(ClientLoginStatusTest, PasswordChangeRequiredKeepsTheSessionOpen)
|
||||
{
|
||||
deliverLoginResponse(Response::RespPasswordChangeRequired);
|
||||
|
||||
EXPECT_EQ(client->getStatus(), StatusPasswordChangeRequired);
|
||||
EXPECT_NE(client->getStatus(), StatusDisconnecting);
|
||||
EXPECT_NE(client->getStatus(), StatusLoggedIn);
|
||||
}
|
||||
|
||||
/** @brief Every other login error still ends the connection, as it did before. */
|
||||
TEST_F(ClientLoginStatusTest, OtherLoginErrorsDisconnect)
|
||||
{
|
||||
deliverLoginResponse(Response::RespWrongPassword);
|
||||
|
||||
EXPECT_EQ(client->getStatus(), StatusDisconnecting);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -278,6 +278,37 @@ TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones)
|
|||
EXPECT_EQ(mapping[1].second, 1);
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, ManaGroupsUseNumericOrderWithoutChangingOtherNodes)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
const QStringList original = {"10", "2", "0.5", "1000000", "1", "unknown", "nan"};
|
||||
for (const auto &name : original) {
|
||||
new InnerDecklistNode(name, board);
|
||||
}
|
||||
auto *custom = new DecklistModelSubZoneNode("1x", board);
|
||||
new InnerDecklistNode("10", custom);
|
||||
new InnerDecklistNode("2", custom);
|
||||
root.setSortMethod(DeckSortMethod::ByName);
|
||||
|
||||
const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder, true);
|
||||
EXPECT_EQ(childNames(board), (QStringList{"0.5", "1", "2", "10", "1000000", "nan", "unknown", "1x"}));
|
||||
QStringList before = original;
|
||||
before.append("1x");
|
||||
for (const auto &move : mapping) {
|
||||
EXPECT_EQ(board->at(move.second)->getName(), before.at(move.first));
|
||||
}
|
||||
|
||||
(void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::DescendingOrder, true);
|
||||
EXPECT_EQ(childNames(board), (QStringList{"1000000", "10", "2", "1", "0.5", "unknown", "nan", "1x"}));
|
||||
|
||||
// The numeric flag only affects groups directly under a board.
|
||||
(void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, custom, Qt::AscendingOrder, true);
|
||||
EXPECT_EQ(childNames(custom), (QStringList{"10", "2"}));
|
||||
(void)DeckListModelCustomZones::sortWithCustomZonesLast(&root, board, Qt::AscendingOrder);
|
||||
EXPECT_EQ(childNames(board), (QStringList{"0.5", "1", "10", "1000000", "2", "nan", "unknown", "1x"}));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
|
|
|||
|
|
@ -393,6 +393,28 @@ TEST(DeckListZones, MaybeboardCustomZoneCardsAreExcludedFromHash)
|
|||
EXPECT_EQ(deck.getDeckHash(), expected.getDeckHash());
|
||||
}
|
||||
|
||||
TEST(DeckListZones, PlainExportPutsMainFirstWithoutReorderingSavedZones)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_SIDE, "Side Tech"), nullptr);
|
||||
tree->addCard("Side Card", 2, "Side Tech", -1);
|
||||
tree->addCard("Token Card", 1, DECK_ZONE_TOKENS, -1);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Main Card", 3, "Removal", -1);
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAYBEBOARD, "Candidates"), nullptr);
|
||||
tree->addCard("Maybe Card", 4, "Candidates", -1);
|
||||
|
||||
const QString native = deck.writeToString_Native();
|
||||
EXPECT_EQ(deck.writeToString_Plain(true, false), QString("3 Main Card\nSB: 2 Side Card\n1 Token Card\n"));
|
||||
EXPECT_EQ(deck.writeToString_Plain(false, false), QString("3 Main Card\n2 Side Card\n1 Token Card\n"));
|
||||
EXPECT_EQ(deck.writeToString_Native(), native);
|
||||
|
||||
DeckList reopened(native);
|
||||
EXPECT_EQ(reopened.getZoneNodes().first()->getName(), DECK_ZONE_SIDE);
|
||||
EXPECT_EQ(reopened.writeToString_Plain(true, false), deck.writeToString_Plain(true, false));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
|
|
|||
190
tests/deck_state_manager_test.cpp
Normal file
190
tests/deck_state_manager_test.cpp
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/** @file deck_state_manager_test.cpp
|
||||
* @brief Tests for DeckStateManager operations.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <client/settings/cache_settings.h>
|
||||
#include <deck_list_model.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <interface/widgets/deck_editor/deck_state_manager.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Assert that the DeckStateManager's deck list contains only the given zone,
|
||||
* and that that zone only contains the given number of cards, all with the given card name.
|
||||
*
|
||||
* @param manager the DeckStateManager
|
||||
* @param restrictToZones In which zones to look for the zone and cards. Empty means use the entire deck
|
||||
* @param zone The only zone that should exist in the deck
|
||||
* @param cardName The name of the only card that should exist in the deck
|
||||
* @param cardNumber The number of cards that should be in the deck
|
||||
*/
|
||||
static void containsExactly(const DeckStateManager &manager,
|
||||
const QSet<QString> &restrictToZones,
|
||||
QString zone,
|
||||
QString cardName,
|
||||
int cardNumber)
|
||||
{
|
||||
QList<const InnerDecklistNode *> zones = manager.getDeckList().getZoneNodes(restrictToZones);
|
||||
ASSERT_EQ(zones.size(), 1);
|
||||
ASSERT_EQ(zones[0]->getName(), zone);
|
||||
ASSERT_EQ(zones[0]->size(), 1);
|
||||
const DecklistCardNode *card = dynamic_cast<const DecklistCardNode *>((*zones[0])[0]);
|
||||
ASSERT_NE(card, nullptr);
|
||||
ASSERT_EQ(card->getName(), cardName);
|
||||
ASSERT_EQ(card->getNumber(), cardNumber);
|
||||
}
|
||||
|
||||
/** @brief Return CardInfo of a token with the given name. */
|
||||
static CardInfoPtr makeToken(QString name)
|
||||
{
|
||||
return CardInfo::newInstance(name, "", true, {}, {}, {}, {}, {});
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// addCard
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckStateManager, AddCards)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 1);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, AddCardZones)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Grizzly Bears")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_SIDE);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Swords to Plowshares")), DECK_ZONE_MAYBEBOARD);
|
||||
|
||||
// Check if the cards got put into the correct zones.
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 3);
|
||||
containsExactly(manager, {DECK_ZONE_MAIN}, DECK_ZONE_MAIN, "Grizzly Bears", 1);
|
||||
containsExactly(manager, {DECK_ZONE_SIDE}, DECK_ZONE_SIDE, "Lightning Bolt", 1);
|
||||
containsExactly(manager, {DECK_ZONE_MAYBEBOARD}, DECK_ZONE_MAYBEBOARD, "Swords to Plowshares", 1);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, AddTokens)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAYBEBOARD);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS);
|
||||
|
||||
// A token should always be put into the tokens zone.
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 4);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// decrementCard
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckStateManager, DecrementCards)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2);
|
||||
|
||||
// Removing from the sideboard should do nothing, as there is nothing in the side board.
|
||||
manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_SIDE);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 2);
|
||||
|
||||
// Remove the first card.
|
||||
manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
containsExactly(manager, {}, DECK_ZONE_MAIN, "Lightning Bolt", 1);
|
||||
|
||||
// Removing the second card should also remove the zone.
|
||||
manager.decrementCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, DecrementTokens)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE);
|
||||
manager.addCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS);
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 3);
|
||||
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_TOKENS);
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 2);
|
||||
|
||||
// Removing tokens from a given zone should remove them from the
|
||||
// token zone if the given zone does not contain the token.
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_SIDE);
|
||||
containsExactly(manager, {}, DECK_ZONE_TOKENS, "Beast", 1);
|
||||
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0);
|
||||
}
|
||||
|
||||
TEST(DeckStateManager, DecrementTokenMainboard)
|
||||
{
|
||||
DeckStateManager manager;
|
||||
|
||||
// Add cards to the deck list directly so tokens are not
|
||||
// automatically added to the token zone.
|
||||
manager.getDeckListShared()->addCard("Beast", DECK_ZONE_MAIN);
|
||||
manager.getDeckListShared()->addCard("Beast", DECK_ZONE_TOKENS);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 2);
|
||||
containsExactly(manager, {DECK_ZONE_MAIN}, DECK_ZONE_MAIN, "Beast", 1);
|
||||
containsExactly(manager, {DECK_ZONE_TOKENS}, DECK_ZONE_TOKENS, "Beast", 1);
|
||||
|
||||
// decrementCard uses the model to find the card, so we need to rebuild it.
|
||||
manager.getModel()->rebuildTree();
|
||||
|
||||
// Tokens should be removed from the main zone first.
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes({DECK_ZONE_MAIN}).size(), 0);
|
||||
containsExactly(manager, {DECK_ZONE_TOKENS}, DECK_ZONE_TOKENS, "Beast", 1);
|
||||
|
||||
// If the main zone is empty they can be removed from the token zone.
|
||||
manager.decrementCard(ExactCard(makeToken("Beast")), DECK_ZONE_MAIN);
|
||||
ASSERT_EQ(manager.getDeckList().getZoneNodes().size(), 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Redirect SettingsCache reads/writes (app-data location) away from the real user profile.
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("CockatriceTest"));
|
||||
QCoreApplication::setApplicationName(
|
||||
QStringLiteral("deck-state-manager-test-%1").arg(QCoreApplication::applicationPid()));
|
||||
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// Add cards to the card database
|
||||
auto *db = CardDatabaseManager::getInstance();
|
||||
db->addCard(CardInfo::newInstance("Lightning Bolt"));
|
||||
db->addCard(CardInfo::newInstance("Grizzly Bears"));
|
||||
db->addCard(CardInfo::newInstance("Swords to Plowshares"));
|
||||
db->addCard(makeToken("Beast"));
|
||||
|
||||
int result = RUN_ALL_TESTS();
|
||||
|
||||
// Clean up the settings directory in ~/.qttest if the tests ran.
|
||||
if (!::testing::GTEST_FLAG(list_tests)) {
|
||||
QDir(SettingsCache::instance().getDataPath()).removeRecursively();
|
||||
QDir(SettingsCache::instance().getCachePath()).removeRecursively();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
195
tests/font_config_test.cpp
Normal file
195
tests/font_config_test.cpp
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
#include "interface/theme_config.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QIODevice>
|
||||
#include <QString>
|
||||
#include <QStringLiteral>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTextStream>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Writes content to a file inside tmp, creating parents as needed.
|
||||
QString writeTempFile(QTemporaryDir &tmp, const QString &relativePath, const QString &content)
|
||||
{
|
||||
const QString filePath = QDir(tmp.path()).absoluteFilePath(relativePath);
|
||||
EXPECT_TRUE(QDir().mkpath(QFileInfo(filePath).absolutePath()));
|
||||
QFile file(filePath);
|
||||
EXPECT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||
QTextStream out(&file);
|
||||
out << content;
|
||||
file.close();
|
||||
return filePath;
|
||||
}
|
||||
|
||||
QFont makeFont(const QString &family, int pointSize)
|
||||
{
|
||||
QFont font(family);
|
||||
font.setPointSize(pointSize);
|
||||
return font;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(FontConfigTest, FileNameIsFontsToml)
|
||||
{
|
||||
EXPECT_EQ(FontConfig::fileName(), QStringLiteral("fonts.toml"));
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, EmptyByDefault)
|
||||
{
|
||||
FontConfig cfg;
|
||||
EXPECT_FALSE(cfg.hasFonts());
|
||||
EXPECT_TRUE(cfg.toToml().isEmpty());
|
||||
EXPECT_EQ(cfg, FontConfig());
|
||||
EXPECT_EQ(cfg, FontConfig::fromFile("/nonexistent/path/fonts.toml"));
|
||||
EXPECT_EQ(cfg, FontConfig::fromThemeDir(QString()));
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, RoundTripPreservesFamilyAndSize)
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
ASSERT_TRUE(tmp.isValid());
|
||||
|
||||
FontConfig cfg;
|
||||
cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10);
|
||||
cfg.fonts[AppFont::ChatLog] = makeFont("JetBrains Mono", 9);
|
||||
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11);
|
||||
cfg.fonts[AppFont::DeckList] = makeFont("Roboto", 12);
|
||||
ASSERT_TRUE(cfg.hasFonts());
|
||||
|
||||
const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml());
|
||||
const FontConfig loaded = FontConfig::fromFile(filePath);
|
||||
|
||||
EXPECT_EQ(loaded, cfg);
|
||||
EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].family(), QStringLiteral("Inter"));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault].pointSize(), 10);
|
||||
EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("JetBrains Mono"));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 9);
|
||||
EXPECT_EQ(loaded.fonts[AppFont::CardText].family(), QStringLiteral("Noto Serif"));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::CardText].pointSize(), 11);
|
||||
EXPECT_EQ(loaded.fonts[AppFont::DeckList].family(), QStringLiteral("Roboto"));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::DeckList].pointSize(), 12);
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, RoundTripStripsWeightAndItalic)
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
ASSERT_TRUE(tmp.isValid());
|
||||
|
||||
QFont styled("Inter");
|
||||
styled.setPointSize(10);
|
||||
styled.setBold(true);
|
||||
styled.setItalic(true);
|
||||
|
||||
FontConfig cfg;
|
||||
cfg.fonts[AppFont::ChatLog] = styled;
|
||||
|
||||
const QString filePath = writeTempFile(tmp, "fonts.toml", cfg.toToml());
|
||||
const FontConfig loaded = FontConfig::fromFile(filePath);
|
||||
|
||||
ASSERT_TRUE(loaded.fonts.contains(AppFont::ChatLog));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::ChatLog].family(), QStringLiteral("Inter"));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::ChatLog].pointSize(), 10);
|
||||
EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].bold());
|
||||
EXPECT_FALSE(loaded.fonts[AppFont::ChatLog].italic());
|
||||
EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("Inter", 10));
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, ToTomlFormat)
|
||||
{
|
||||
FontConfig cfg;
|
||||
cfg.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10);
|
||||
|
||||
const QString toml = cfg.toToml();
|
||||
EXPECT_TRUE(toml.contains(QStringLiteral("[Fonts]")));
|
||||
EXPECT_TRUE(toml.contains(QStringLiteral("InterfaceDefault")));
|
||||
EXPECT_TRUE(toml.contains(QStringLiteral("\"Inter, 10\"")));
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, MalformedLinesAreSkipped)
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
ASSERT_TRUE(tmp.isValid());
|
||||
|
||||
const QString filePath = writeTempFile(tmp, "fonts.toml",
|
||||
"# a comment\n"
|
||||
"; another comment\n"
|
||||
"\n"
|
||||
"StrayBeforeSection = \"Inter, 10\"\n"
|
||||
"\n"
|
||||
"[Other]\n"
|
||||
"ChatLog = \"ShouldBeIgnored, 10\"\n"
|
||||
"\n"
|
||||
"[Fonts]\n"
|
||||
"InterfaceDefault = \"Inter, 10\"\n"
|
||||
"ChatLog = \"JetBrains Mono\", 9 # inline comment\n"
|
||||
"CardText = Noto Serif, 11\n"
|
||||
"AppFont::DeckList = \"Roboto, 12\"\n"
|
||||
"UnknownRole = \"Inter, 10\"\n"
|
||||
"QPalette::Window = \"Inter, 10\"\n"
|
||||
"NoComma = \"Inter\"\n"
|
||||
"EmptyFamily = \", 10\"\n"
|
||||
"ZeroSize = \"Inter, 0\"\n"
|
||||
"NegativeSize = \"Inter, -3\"\n"
|
||||
"NotANumber = \"Inter, big\"\n"
|
||||
"NotAnEntry\n");
|
||||
|
||||
const FontConfig loaded = FontConfig::fromFile(filePath);
|
||||
|
||||
ASSERT_TRUE(loaded.hasFonts());
|
||||
EXPECT_EQ(loaded.fonts.size(), 4);
|
||||
EXPECT_EQ(loaded.fonts[AppFont::InterfaceDefault], makeFont("Inter", 10));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::ChatLog], makeFont("JetBrains Mono", 9));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::CardText], makeFont("Noto Serif", 11));
|
||||
EXPECT_EQ(loaded.fonts[AppFont::DeckList], makeFont("Roboto", 12));
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, FromThemeDirReadsFontsToml)
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
ASSERT_TRUE(tmp.isValid());
|
||||
|
||||
const QString themeDir = QDir(tmp.path()).absoluteFilePath("MyTheme");
|
||||
ASSERT_TRUE(QDir().mkpath(themeDir));
|
||||
|
||||
FontConfig cfg;
|
||||
cfg.fonts[AppFont::CardText] = makeFont("Noto Serif", 11);
|
||||
writeTempFile(tmp, "MyTheme/fonts.toml", cfg.toToml());
|
||||
|
||||
const FontConfig loaded = FontConfig::fromThemeDir(themeDir);
|
||||
EXPECT_EQ(loaded, cfg);
|
||||
|
||||
// A theme directory without fonts.toml loads as empty.
|
||||
const QString bareDir = QDir(tmp.path()).absoluteFilePath("BareTheme");
|
||||
ASSERT_TRUE(QDir().mkpath(bareDir));
|
||||
EXPECT_FALSE(FontConfig::fromThemeDir(bareDir).hasFonts());
|
||||
}
|
||||
|
||||
TEST(FontConfigTest, InequalityComparesRoles)
|
||||
{
|
||||
FontConfig left;
|
||||
left.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 10);
|
||||
|
||||
FontConfig right = left;
|
||||
EXPECT_EQ(left, right);
|
||||
EXPECT_FALSE(left != right);
|
||||
|
||||
right.fonts[AppFont::InterfaceDefault] = makeFont("Inter", 11);
|
||||
EXPECT_NE(left, right);
|
||||
|
||||
right = left;
|
||||
right.fonts[AppFont::ChatLog] = makeFont("Inter", 10);
|
||||
EXPECT_NE(left, right);
|
||||
}
|
||||
|
||||
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and
|
||||
// the fallback then links the bare gtest library without gtest_main.
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -1,28 +1,18 @@
|
|||
add_executable(
|
||||
game_smoke_test
|
||||
${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/client/network/update/client/release_channel.cpp
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
game_logic_test
|
||||
SOURCES
|
||||
game_test_main.cpp
|
||||
card_state_test.cpp
|
||||
card_zone_logic_test.cpp
|
||||
game_event_handler_test.cpp
|
||||
player_actions_test.cpp
|
||||
replay_test.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
game_smoke_test.cpp
|
||||
LIBS
|
||||
libcockatrice_game
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
DEFINITIONS
|
||||
GAME_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
|
||||
)
|
||||
|
||||
# SettingsCache is still reached from the game logic, but it lives in the application sources
|
||||
# rather than in libcockatrice_settings, so the client settings sources are compiled in here
|
||||
# exactly like loader_local_matching_test and theme_migration_test do.
|
||||
target_include_directories(game_smoke_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src)
|
||||
|
||||
target_link_libraries(
|
||||
game_smoke_test
|
||||
PRIVATE libcockatrice_game
|
||||
PRIVATE Threads::Threads
|
||||
PRIVATE ${GTEST_BOTH_LIBRARIES}
|
||||
PRIVATE ${TEST_QT_MODULES}
|
||||
)
|
||||
|
||||
add_test(NAME game_smoke_test COMMAND game_smoke_test)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(game_smoke_test gtest)
|
||||
endif()
|
||||
|
|
|
|||
110
tests/game/card_state_test.cpp
Normal file
110
tests/game/card_state_test.cpp
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
#include <libcockatrice/game/board/card_state.h>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
CardRef cardRefFor(const QString &name)
|
||||
{
|
||||
CardRef cardRef;
|
||||
cardRef.name = name;
|
||||
return cardRef;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CardStateTest, AttachmentGraphKeepsBothEndsConsistent)
|
||||
{
|
||||
CardState host(nullptr, nullptr, cardRefFor(QStringLiteral("Host")), 1);
|
||||
CardState guest(nullptr, nullptr, cardRefFor(QStringLiteral("Guest")), 2);
|
||||
|
||||
guest.setAttachedTo(&host);
|
||||
host.addAttachedCard(&guest);
|
||||
|
||||
EXPECT_EQ(guest.getAttachedTo(), &host);
|
||||
ASSERT_EQ(host.getAttachedCards().size(), 1);
|
||||
EXPECT_EQ(host.getAttachedCards().first(), &guest);
|
||||
|
||||
host.removeAttachedCard(&guest);
|
||||
guest.setAttachedTo(nullptr);
|
||||
|
||||
EXPECT_TRUE(host.getAttachedCards().isEmpty());
|
||||
EXPECT_EQ(guest.getAttachedTo(), nullptr);
|
||||
}
|
||||
|
||||
TEST(CardStateTest, CountersAddUpdateAndClear)
|
||||
{
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 1);
|
||||
int changes = 0;
|
||||
QObject::connect(&card, &CardState::countersChanged, [&changes]() { ++changes; });
|
||||
|
||||
card.insertCounter(1, 2);
|
||||
EXPECT_EQ(card.getCounters().value(1), 2);
|
||||
|
||||
card.setCounter(1, 3);
|
||||
EXPECT_EQ(card.getCounters().value(1), 3);
|
||||
|
||||
card.setCounter(1, 0);
|
||||
EXPECT_FALSE(card.getCounters().contains(1));
|
||||
|
||||
card.setCounter(2, 1);
|
||||
card.clearCounters();
|
||||
EXPECT_TRUE(card.getCounters().isEmpty());
|
||||
|
||||
EXPECT_EQ(changes, 5);
|
||||
}
|
||||
|
||||
TEST(CardStateTest, TapFlipAttackingAndReset)
|
||||
{
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 1);
|
||||
card.setTapped(true);
|
||||
card.setFaceDown(true);
|
||||
card.setAttacking(true);
|
||||
card.setAnnotation(QStringLiteral("note"));
|
||||
card.setPT(QStringLiteral("4/4"));
|
||||
card.setDoesntUntap(true);
|
||||
card.setDestroyOnZoneChange(true);
|
||||
card.setColor(QStringLiteral("red"));
|
||||
card.insertCounter(1, 2);
|
||||
|
||||
card.resetState(true);
|
||||
|
||||
EXPECT_FALSE(card.getTapped());
|
||||
EXPECT_FALSE(card.getAttacking());
|
||||
EXPECT_TRUE(card.getCounters().isEmpty());
|
||||
EXPECT_TRUE(card.getPT().isEmpty());
|
||||
EXPECT_FALSE(card.getDoesntUntap());
|
||||
EXPECT_EQ(card.getAnnotation(), QStringLiteral("note"));
|
||||
// resetState() deliberately leaves these three alone.
|
||||
EXPECT_TRUE(card.getFaceDown());
|
||||
EXPECT_EQ(card.getColor(), QStringLiteral("red"));
|
||||
EXPECT_TRUE(card.getDestroyOnZoneChange());
|
||||
|
||||
card.setAnnotation(QStringLiteral("other"));
|
||||
card.resetState(false);
|
||||
EXPECT_TRUE(card.getAnnotation().isEmpty());
|
||||
}
|
||||
|
||||
TEST(CardStateTest, ParsePT)
|
||||
{
|
||||
const QVariantList plain = CardState::parsePT(QStringLiteral("3/3"));
|
||||
ASSERT_EQ(plain.size(), 2);
|
||||
EXPECT_EQ(plain.at(0).toString(), QStringLiteral("3"));
|
||||
EXPECT_EQ(plain.at(1).toString(), QStringLiteral("3"));
|
||||
|
||||
const QVariantList negative = CardState::parsePT(QStringLiteral("-1/2"));
|
||||
ASSERT_EQ(negative.size(), 2);
|
||||
EXPECT_EQ(negative.at(0).typeId(), QMetaType::Int);
|
||||
EXPECT_EQ(negative.at(0).toInt(), -1);
|
||||
EXPECT_EQ(negative.at(1).toString(), QStringLiteral("2"));
|
||||
|
||||
const QVariantList star = CardState::parsePT(QStringLiteral("*"));
|
||||
ASSERT_EQ(star.size(), 1);
|
||||
EXPECT_EQ(star.at(0).toString(), QStringLiteral("*"));
|
||||
|
||||
EXPECT_TRUE(CardState::parsePT(QString()).isEmpty());
|
||||
}
|
||||
107
tests/game/card_zone_logic_test.cpp
Normal file
107
tests/game/card_zone_logic_test.cpp
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QSignalSpy>
|
||||
#include <QString>
|
||||
#include <libcockatrice/game/board/card_state.h>
|
||||
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||
#include <libcockatrice/game/zones/view_zone_logic.h>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
CardRef cardRefFor(const QString &name)
|
||||
{
|
||||
CardRef cardRef;
|
||||
cardRef.name = name;
|
||||
return cardRef;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CardZoneLogicTest, AddsAndTakesCardsWithoutAGraphicsSide)
|
||||
{
|
||||
HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true);
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7);
|
||||
|
||||
QSignalSpy cardAddedSpy(&zone, &CardZoneLogic::cardAdded);
|
||||
QSignalSpy cardCountSpy(&zone, &CardZoneLogic::cardCountChanged);
|
||||
|
||||
zone.addCard(&card, false, 0);
|
||||
|
||||
ASSERT_EQ(zone.getCards().size(), 1);
|
||||
EXPECT_EQ(zone.getCards().first(), &card);
|
||||
EXPECT_EQ(card.getZone(), &zone);
|
||||
EXPECT_EQ(cardAddedSpy.count(), 1);
|
||||
EXPECT_GE(cardCountSpy.count(), 1);
|
||||
|
||||
EXPECT_EQ(zone.takeCard(0, 7), &card);
|
||||
EXPECT_TRUE(zone.getCards().isEmpty());
|
||||
EXPECT_EQ(card.getId(), 7);
|
||||
}
|
||||
|
||||
TEST(CardZoneLogicTest, AsksTheGraphicsSideToCreateCards)
|
||||
{
|
||||
HandZoneLogic zone(nullptr, QStringLiteral("library"), false, true, true);
|
||||
// The graphics side owns the view logic and registers it on the zone it
|
||||
// shows; adding a card then goes through prepareAddCard and the request
|
||||
// signal instead of the test emitting the signal itself.
|
||||
ZoneViewZoneLogic view(nullptr, &zone, 1, true, false, false);
|
||||
zone.getViews().append(&view);
|
||||
|
||||
QSignalSpy requestSpy(&view, &CardZoneLogic::createCardRequested);
|
||||
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7);
|
||||
zone.addCard(&card, true, 0, 0);
|
||||
|
||||
ASSERT_EQ(requestSpy.count(), 1);
|
||||
const QList<QVariant> arguments = requestSpy.takeFirst();
|
||||
const ServerInfo_Card cardInfo = arguments.at(0).value<ServerInfo_Card>();
|
||||
EXPECT_EQ(cardInfo.id(), 7);
|
||||
EXPECT_EQ(cardInfo.name(), "Lightning Bolt");
|
||||
EXPECT_TRUE(arguments.at(1).toBool());
|
||||
EXPECT_EQ(card.getZone(), &zone);
|
||||
EXPECT_EQ(zone.getCards().size(), 1);
|
||||
}
|
||||
|
||||
TEST(CardZoneLogicTest, MoveBetweenZonesUpdatesCardZone)
|
||||
{
|
||||
HandZoneLogic first(nullptr, QStringLiteral("hand"), false, false, true);
|
||||
HandZoneLogic second(nullptr, QStringLiteral("grave"), false, false, true);
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7);
|
||||
|
||||
first.addCard(&card, false, 0);
|
||||
ASSERT_EQ(first.getCards().size(), 1);
|
||||
|
||||
CardState *taken = first.takeCard(0, 7);
|
||||
ASSERT_EQ(taken, &card);
|
||||
EXPECT_TRUE(first.getCards().isEmpty());
|
||||
|
||||
second.addCard(taken, false, 0);
|
||||
EXPECT_EQ(second.getCards().size(), 1);
|
||||
EXPECT_EQ(card.getZone(), &second);
|
||||
}
|
||||
|
||||
TEST(CardZoneLogicTest, GetCardFindsById)
|
||||
{
|
||||
HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true);
|
||||
CardState first(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7);
|
||||
CardState second(nullptr, nullptr, cardRefFor(QStringLiteral("Dog")), 8);
|
||||
|
||||
zone.addCard(&first, false, 0);
|
||||
zone.addCard(&second, false, 0);
|
||||
|
||||
EXPECT_EQ(zone.getCard(7), &first);
|
||||
EXPECT_EQ(zone.getCard(8), &second);
|
||||
EXPECT_EQ(zone.getCard(99), nullptr);
|
||||
}
|
||||
|
||||
TEST(CardZoneLogicTest, ReorganizeSignalFiresOnAdd)
|
||||
{
|
||||
HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true);
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Cat")), 7);
|
||||
QSignalSpy reorganizeSpy(&zone, &CardZoneLogic::reorganizeCards);
|
||||
|
||||
zone.addCard(&card, true, 0);
|
||||
|
||||
EXPECT_EQ(reorganizeSpy.count(), 1);
|
||||
}
|
||||
64
tests/game/data/cards.xml
Normal file
64
tests/game/data/cards.xml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cockatrice_carddatabase version="4">
|
||||
<cards>
|
||||
<card>
|
||||
<name>Cat</name>
|
||||
<set>CAT</set>
|
||||
<tablerow>0</tablerow>
|
||||
<text>Meow!</text>
|
||||
<prop>
|
||||
<muid>111</muid>
|
||||
<colors>G</colors>
|
||||
<manacost>2G</manacost>
|
||||
<cmc>2</cmc>
|
||||
<type>Creature — Cat</type>
|
||||
<maintype>Creature</maintype>
|
||||
<pt>3/3</pt>
|
||||
</prop>
|
||||
</card>
|
||||
<card>
|
||||
<name>Grizzly Bears</name>
|
||||
<set>LEA</set>
|
||||
<tablerow>0</tablerow>
|
||||
<cipt>1</cipt>
|
||||
<text></text>
|
||||
<prop>
|
||||
<muid>112</muid>
|
||||
<colors>G</colors>
|
||||
<manacost>1G</manacost>
|
||||
<cmc>2</cmc>
|
||||
<type>Creature — Bear</type>
|
||||
<maintype>Creature</maintype>
|
||||
<pt>2/2</pt>
|
||||
</prop>
|
||||
</card>
|
||||
<card>
|
||||
<name>Lightning Bolt</name>
|
||||
<set>LEA</set>
|
||||
<tablerow>3</tablerow>
|
||||
<text>Lightning Bolt deals 3 damage to any target.</text>
|
||||
<prop>
|
||||
<muid>113</muid>
|
||||
<colors>R</colors>
|
||||
<manacost>R</manacost>
|
||||
<cmc>1</cmc>
|
||||
<type>Instant</type>
|
||||
<maintype>Instant</maintype>
|
||||
</prop>
|
||||
</card>
|
||||
<card>
|
||||
<name>Forest</name>
|
||||
<set>LEA</set>
|
||||
<tablerow>2</tablerow>
|
||||
<text>(Add G.)</text>
|
||||
<prop>
|
||||
<muid>114</muid>
|
||||
<colors>G</colors>
|
||||
<manacost></manacost>
|
||||
<cmc>0</cmc>
|
||||
<type>Land</type>
|
||||
<maintype>Land</maintype>
|
||||
</prop>
|
||||
</card>
|
||||
</cards>
|
||||
</cockatrice_carddatabase>
|
||||
4
tests/game/data/spoilers.xml
Normal file
4
tests/game/data/spoilers.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cockatrice_carddatabase version="4">
|
||||
<cards />
|
||||
</cockatrice_carddatabase>
|
||||
21
tests/game/data/tokens.xml
Normal file
21
tests/game/data/tokens.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cockatrice_carddatabase version="4">
|
||||
<cards>
|
||||
<card>
|
||||
<name>Kitten</name>
|
||||
<set>CAT</set>
|
||||
<tablerow>0</tablerow>
|
||||
<text></text>
|
||||
<token>1</token>
|
||||
<prop>
|
||||
<muid>211</muid>
|
||||
<colors>G</colors>
|
||||
<manacost></manacost>
|
||||
<cmc></cmc>
|
||||
<type>Token</type>
|
||||
<maintype>Token</maintype>
|
||||
<pt>1/1</pt>
|
||||
</prop>
|
||||
</card>
|
||||
</cards>
|
||||
</cockatrice_carddatabase>
|
||||
566
tests/game/game_event_handler_test.cpp
Normal file
566
tests/game/game_event_handler_test.cpp
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
#include "game_test_fixture.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPoint>
|
||||
#include <QString>
|
||||
#include <libcockatrice/game/board/card_state.h>
|
||||
#include <libcockatrice/game/board/counter_state.h>
|
||||
#include <libcockatrice/game/game_event_handler.h>
|
||||
#include <libcockatrice/game/game_meta_info.h>
|
||||
#include <libcockatrice/game/game_state.h>
|
||||
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||
#include <libcockatrice/protocol/pb/context_concede.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_attach_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_change_zone_properties.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_create_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_create_token.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_del_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_destroy_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_draw_cards.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_flip_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_host_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_state_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_kicked.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_move_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_player_properties_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_context.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_counter.pb.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* Wraps one player-scoped event in the container the server would send and
|
||||
* pushes it through the real dispatcher, so the test exercises the same
|
||||
* GameEventHandler -> PlayerEventHandler path as a live client.
|
||||
*/
|
||||
template <typename EventType>
|
||||
void sendGameEvent(GameHarness &harness,
|
||||
int playerId,
|
||||
const EventType &message,
|
||||
const GameEventContext *context = nullptr)
|
||||
{
|
||||
GameEventContainer container;
|
||||
container.set_game_id(42);
|
||||
if (context != nullptr) {
|
||||
container.mutable_context()->CopyFrom(*context);
|
||||
}
|
||||
|
||||
GameEvent *event = container.add_event_list();
|
||||
event->set_player_id(playerId);
|
||||
event->MutableExtension(EventType::ext)->CopyFrom(message);
|
||||
|
||||
harness.game()->getGameEventHandler()->processGameEventContainer(container, harness.client(),
|
||||
EventProcessingOptions());
|
||||
}
|
||||
|
||||
Event_SetCardAttr cardAttrEvent(const char *zoneName, int cardId, CardAttribute attribute, const char *value)
|
||||
{
|
||||
Event_SetCardAttr event;
|
||||
event.set_zone_name(zoneName);
|
||||
event.set_card_id(cardId);
|
||||
event.set_attribute(attribute);
|
||||
event.set_attr_value(value);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(GameEventHandlerTest, MoveCardMovesBetweenZones)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
Event_MoveCard event;
|
||||
event.set_card_id(7);
|
||||
event.set_start_player_id(1);
|
||||
event.set_start_zone(ZoneNames::HAND);
|
||||
event.set_position(0);
|
||||
event.set_target_player_id(1);
|
||||
event.set_target_zone(ZoneNames::TABLE);
|
||||
event.set_x(0);
|
||||
event.set_y(TableZoneLogic::tableRowToGridY(0));
|
||||
event.set_new_card_id(7);
|
||||
event.set_face_down(false);
|
||||
|
||||
sendGameEvent(harness, 1, event);
|
||||
|
||||
EXPECT_TRUE(harness.zone(ZoneNames::HAND)->getCards().isEmpty());
|
||||
CardZoneLogic *table = harness.zone(ZoneNames::TABLE);
|
||||
ASSERT_EQ(table->getCards().size(), 1);
|
||||
EXPECT_EQ(table->getCards().first(), cat);
|
||||
EXPECT_EQ(cat->getZone(), table);
|
||||
EXPECT_EQ(cat->getId(), 7);
|
||||
EXPECT_EQ(cat->getCardRef().name, QStringLiteral("Cat"));
|
||||
EXPECT_FALSE(cat->getFaceDown());
|
||||
EXPECT_EQ(cat->getPT(), QStringLiteral("3/3"));
|
||||
EXPECT_EQ(cat->getGridPoint(), QPoint(0, TableZoneLogic::tableRowToGridY(0)));
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, MoveCardFaceDownKeepsTheCardHidden)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
Event_MoveCard event;
|
||||
event.set_card_id(7);
|
||||
event.set_start_player_id(1);
|
||||
event.set_start_zone(ZoneNames::HAND);
|
||||
event.set_position(0);
|
||||
event.set_target_player_id(1);
|
||||
event.set_target_zone(ZoneNames::TABLE);
|
||||
event.set_x(1);
|
||||
event.set_y(TableZoneLogic::tableRowToGridY(2));
|
||||
event.set_new_card_id(7);
|
||||
event.set_face_down(true);
|
||||
|
||||
sendGameEvent(harness, 1, event);
|
||||
|
||||
ASSERT_EQ(harness.zone(ZoneNames::TABLE)->getCards().size(), 1);
|
||||
EXPECT_TRUE(cat->getFaceDown());
|
||||
// A face-down card keeps whatever P/T it had instead of picking up the
|
||||
// printed one from the card database.
|
||||
EXPECT_TRUE(cat->getPT().isEmpty());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, SetCardAttrUpdatesSingleCard)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrTapped, "1"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrAttacking, "1"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrPT, "5/5"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrAnnotation, "note"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrColor, "red"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrDoesntUntap, "1"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrFaceDown, "1"));
|
||||
|
||||
EXPECT_TRUE(cat->getTapped());
|
||||
EXPECT_TRUE(cat->getAttacking());
|
||||
EXPECT_EQ(cat->getPT(), QStringLiteral("5/5"));
|
||||
EXPECT_EQ(cat->getAnnotation(), QStringLiteral("note"));
|
||||
EXPECT_EQ(cat->getColor(), QStringLiteral("red"));
|
||||
EXPECT_TRUE(cat->getDoesntUntap());
|
||||
EXPECT_TRUE(cat->getFaceDown());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, SetCardAttrWithoutCardIdTouchesEveryCardInZone)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7);
|
||||
CardState *bears = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
ASSERT_NE(bears, nullptr);
|
||||
|
||||
Event_SetCardAttr event;
|
||||
event.set_zone_name(ZoneNames::TABLE);
|
||||
event.set_attribute(AttrAnnotation);
|
||||
event.set_attr_value("marked");
|
||||
|
||||
sendGameEvent(harness, 1, event);
|
||||
|
||||
EXPECT_EQ(cat->getAnnotation(), QStringLiteral("marked"));
|
||||
EXPECT_EQ(bears->getAnnotation(), QStringLiteral("marked"));
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, UntapAllLeavesDoesntUntapCardsTapped)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7);
|
||||
CardState *bears = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
ASSERT_NE(bears, nullptr);
|
||||
cat->setDoesntUntap(true);
|
||||
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 7, AttrTapped, "1"));
|
||||
sendGameEvent(harness, 1, cardAttrEvent(ZoneNames::TABLE, 9, AttrTapped, "1"));
|
||||
|
||||
Event_SetCardAttr untapAll;
|
||||
untapAll.set_zone_name(ZoneNames::TABLE);
|
||||
untapAll.set_attribute(AttrTapped);
|
||||
untapAll.set_attr_value("0");
|
||||
|
||||
sendGameEvent(harness, 1, untapAll);
|
||||
|
||||
EXPECT_TRUE(cat->getTapped());
|
||||
EXPECT_FALSE(bears->getTapped());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, SetCardCounterAddsAndRemoves)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
Event_SetCardCounter add;
|
||||
add.set_zone_name(ZoneNames::HAND);
|
||||
add.set_card_id(7);
|
||||
add.set_counter_id(1);
|
||||
add.set_counter_value(2);
|
||||
|
||||
sendGameEvent(harness, 1, add);
|
||||
|
||||
EXPECT_EQ(cat->getCounters().value(1), 2);
|
||||
|
||||
Event_SetCardCounter remove = add;
|
||||
remove.set_counter_value(0);
|
||||
|
||||
sendGameEvent(harness, 1, remove);
|
||||
|
||||
EXPECT_FALSE(cat->getCounters().contains(1));
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, AttachCardLinksBothEndsAndUnattachClearsThem)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *host = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7);
|
||||
CardState *guest = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9);
|
||||
ASSERT_NE(host, nullptr);
|
||||
ASSERT_NE(guest, nullptr);
|
||||
|
||||
Event_AttachCard attach;
|
||||
attach.set_start_zone(ZoneNames::TABLE);
|
||||
attach.set_card_id(9);
|
||||
attach.set_target_player_id(1);
|
||||
attach.set_target_zone(ZoneNames::TABLE);
|
||||
attach.set_target_card_id(7);
|
||||
|
||||
sendGameEvent(harness, 1, attach);
|
||||
|
||||
EXPECT_EQ(guest->getAttachedTo(), host);
|
||||
ASSERT_EQ(host->getAttachedCards().size(), 1);
|
||||
EXPECT_EQ(host->getAttachedCards().first(), guest);
|
||||
|
||||
Event_AttachCard detach;
|
||||
detach.set_start_zone(ZoneNames::TABLE);
|
||||
detach.set_card_id(9);
|
||||
|
||||
sendGameEvent(harness, 1, detach);
|
||||
|
||||
EXPECT_EQ(guest->getAttachedTo(), nullptr);
|
||||
EXPECT_TRUE(host->getAttachedCards().isEmpty());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, ChangeZonePropertiesUpdatesReveal)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardZoneLogic *deck = harness.zone(ZoneNames::DECK);
|
||||
ASSERT_NE(deck, nullptr);
|
||||
|
||||
Event_ChangeZoneProperties reveal;
|
||||
reveal.set_zone_name(ZoneNames::DECK);
|
||||
reveal.set_always_reveal_top_card(true);
|
||||
|
||||
sendGameEvent(harness, 1, reveal);
|
||||
EXPECT_TRUE(deck->getAlwaysRevealTopCard());
|
||||
|
||||
reveal.set_always_reveal_top_card(false);
|
||||
sendGameEvent(harness, 1, reveal);
|
||||
EXPECT_FALSE(deck->getAlwaysRevealTopCard());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, DrawCardsMovesNamedCardsFromDeckToHand)
|
||||
{
|
||||
GameHarness harness;
|
||||
ASSERT_NE(harness.addCardToZone(ZoneNames::DECK, QStringLiteral("Cat"), 7), nullptr);
|
||||
|
||||
Event_DrawCards draw;
|
||||
draw.set_number(1);
|
||||
ServerInfo_Card *cardInfo = draw.add_cards();
|
||||
cardInfo->set_id(7);
|
||||
cardInfo->set_name("Cat");
|
||||
|
||||
sendGameEvent(harness, 1, draw);
|
||||
|
||||
EXPECT_TRUE(harness.zone(ZoneNames::DECK)->getCards().isEmpty());
|
||||
ASSERT_EQ(harness.zone(ZoneNames::HAND)->getCards().size(), 1);
|
||||
CardState *drawn = harness.zone(ZoneNames::HAND)->getCards().first();
|
||||
EXPECT_EQ(drawn->getId(), 7);
|
||||
EXPECT_EQ(drawn->getName(), QStringLiteral("Cat"));
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, FlipCardHidesAndRevealsTheCard)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
Event_FlipCard flip;
|
||||
flip.set_zone_name(ZoneNames::TABLE);
|
||||
flip.set_card_id(7);
|
||||
flip.set_face_down(true);
|
||||
|
||||
sendGameEvent(harness, 1, flip);
|
||||
|
||||
EXPECT_TRUE(cat->getFaceDown());
|
||||
// Flipping face down does not touch the card reference: the client must not
|
||||
// learn (or overwrite) the name of a card it is not allowed to see.
|
||||
EXPECT_EQ(cat->getName(), QStringLiteral("Cat"));
|
||||
|
||||
// Turning it face up carries the real printing, so the state picks it up.
|
||||
flip.set_face_down(false);
|
||||
flip.set_card_name("Grizzly Bears");
|
||||
|
||||
sendGameEvent(harness, 1, flip);
|
||||
|
||||
EXPECT_FALSE(cat->getFaceDown());
|
||||
EXPECT_EQ(cat->getName(), QStringLiteral("Grizzly Bears"));
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, DestroyCardRemovesItAndDetachesAttachedCards)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *host = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7);
|
||||
CardState *guest = harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Grizzly Bears"), 9);
|
||||
ASSERT_NE(host, nullptr);
|
||||
ASSERT_NE(guest, nullptr);
|
||||
|
||||
guest->setAttachedTo(host);
|
||||
ASSERT_EQ(guest->getAttachedTo(), host);
|
||||
ASSERT_EQ(host->getAttachedCards().size(), 1);
|
||||
|
||||
Event_DestroyCard destroy;
|
||||
destroy.set_zone_name(ZoneNames::TABLE);
|
||||
destroy.set_card_id(7);
|
||||
|
||||
sendGameEvent(harness, 1, destroy);
|
||||
|
||||
CardZoneLogic *table = harness.zone(ZoneNames::TABLE);
|
||||
ASSERT_EQ(table->getCards().size(), 1);
|
||||
EXPECT_EQ(table->getCards().first(), guest);
|
||||
EXPECT_FALSE(table->getCards().contains(host));
|
||||
|
||||
// The attached card survives the host and is left un-attached, ready to be
|
||||
// laid out again by the zone.
|
||||
EXPECT_EQ(guest->getAttachedTo(), nullptr);
|
||||
EXPECT_TRUE(host->getAttachedCards().isEmpty());
|
||||
EXPECT_EQ(guest->getZone(), table);
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, CreateTokenUsesDatabasePTUnlessFaceDown)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardZoneLogic *table = harness.zone(ZoneNames::TABLE);
|
||||
ASSERT_NE(table, nullptr);
|
||||
|
||||
QList<ServerInfo_Card> created;
|
||||
QObject::connect(table, &CardZoneLogic::createCardRequested, table,
|
||||
[&created](const ServerInfo_Card &cardInfo, bool) { created.append(cardInfo); });
|
||||
|
||||
Event_CreateToken token;
|
||||
token.set_zone_name(ZoneNames::TABLE);
|
||||
token.set_card_id(11);
|
||||
token.set_card_name("Kitten");
|
||||
token.set_x(2);
|
||||
token.set_y(1);
|
||||
token.set_color("green");
|
||||
token.set_annotation("a kitten");
|
||||
token.set_destroy_on_zone_change(true);
|
||||
|
||||
sendGameEvent(harness, 1, token);
|
||||
|
||||
ASSERT_EQ(created.size(), 1);
|
||||
// The event carries no P/T, so the token takes the printed one.
|
||||
EXPECT_EQ(created.first().pt(), "1/1");
|
||||
EXPECT_EQ(created.first().name(), "Kitten");
|
||||
EXPECT_EQ(created.first().x(), 2);
|
||||
EXPECT_EQ(created.first().y(), 1);
|
||||
EXPECT_EQ(created.first().color(), "green");
|
||||
EXPECT_EQ(created.first().annotation(), "a kitten");
|
||||
EXPECT_TRUE(created.first().destroy_on_zone_change());
|
||||
EXPECT_FALSE(created.first().face_down());
|
||||
|
||||
// An explicit P/T always wins over the database.
|
||||
Event_CreateToken explicitPt = token;
|
||||
explicitPt.set_card_id(12);
|
||||
explicitPt.set_pt("3/3");
|
||||
|
||||
sendGameEvent(harness, 1, explicitPt);
|
||||
|
||||
ASSERT_EQ(created.size(), 2);
|
||||
EXPECT_EQ(created.last().pt(), "3/3");
|
||||
|
||||
// A face-down token must not leak the printed P/T.
|
||||
Event_CreateToken faceDown = token;
|
||||
faceDown.set_card_id(13);
|
||||
faceDown.set_face_down(true);
|
||||
|
||||
sendGameEvent(harness, 1, faceDown);
|
||||
|
||||
ASSERT_EQ(created.size(), 3);
|
||||
EXPECT_TRUE(created.last().face_down());
|
||||
EXPECT_TRUE(created.last().pt().empty());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, ConcedeAndUnconcedeUpdatePlayerState)
|
||||
{
|
||||
GameHarness harness;
|
||||
ASSERT_NE(harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7), nullptr);
|
||||
EXPECT_FALSE(harness.player()->getConceded());
|
||||
|
||||
Event_PlayerPropertiesChanged properties;
|
||||
properties.mutable_player_properties()->set_player_id(1);
|
||||
properties.mutable_player_properties()->set_conceded(true);
|
||||
|
||||
GameEventContext concedeContext;
|
||||
concedeContext.MutableExtension(Context_Concede::ext);
|
||||
|
||||
sendGameEvent(harness, 1, properties, &concedeContext);
|
||||
|
||||
EXPECT_TRUE(harness.player()->getConceded());
|
||||
// Conceding tears the board down for that player.
|
||||
EXPECT_TRUE(harness.zone(ZoneNames::TABLE)->getCards().isEmpty());
|
||||
|
||||
Event_PlayerPropertiesChanged back = properties;
|
||||
back.mutable_player_properties()->set_conceded(false);
|
||||
|
||||
GameEventContext unconcedeContext;
|
||||
unconcedeContext.MutableExtension(Context_Unconcede::ext);
|
||||
|
||||
sendGameEvent(harness, 1, back, &unconcedeContext);
|
||||
|
||||
EXPECT_FALSE(harness.player()->getConceded());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, SetActivePlayerAndPhaseUpdateGameState)
|
||||
{
|
||||
GameHarness harness;
|
||||
GameState *state = harness.game()->getGameState();
|
||||
ASSERT_NE(state, nullptr);
|
||||
EXPECT_EQ(state->getActivePlayer(), -1);
|
||||
EXPECT_EQ(state->getCurrentPhase(), -1);
|
||||
|
||||
Event_SetActivePlayer setPlayer;
|
||||
setPlayer.set_active_player_id(1);
|
||||
|
||||
sendGameEvent(harness, 1, setPlayer);
|
||||
|
||||
EXPECT_EQ(state->getActivePlayer(), 1);
|
||||
|
||||
Event_SetActivePhase setPhase;
|
||||
setPhase.set_phase(3);
|
||||
|
||||
sendGameEvent(harness, 1, setPhase);
|
||||
|
||||
EXPECT_EQ(state->getCurrentPhase(), 3);
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, CreateSetAndDeletePlayerCounter)
|
||||
{
|
||||
GameHarness harness;
|
||||
|
||||
ServerInfo_Counter counter;
|
||||
counter.set_id(1);
|
||||
counter.set_name("life");
|
||||
counter.set_radius(1);
|
||||
counter.set_count(20);
|
||||
|
||||
Event_CreateCounter create;
|
||||
create.mutable_counter_info()->CopyFrom(counter);
|
||||
|
||||
sendGameEvent(harness, 1, create);
|
||||
|
||||
CounterState *state = harness.player()->getCounters().value(1);
|
||||
ASSERT_NE(state, nullptr);
|
||||
EXPECT_EQ(state->getName(), QStringLiteral("life"));
|
||||
EXPECT_EQ(state->getValue(), 20);
|
||||
EXPECT_EQ(harness.player()->getLifeCounter(), state);
|
||||
|
||||
Event_SetCounter set;
|
||||
set.set_counter_id(1);
|
||||
set.set_value(15);
|
||||
|
||||
sendGameEvent(harness, 1, set);
|
||||
|
||||
EXPECT_EQ(state->getValue(), 15);
|
||||
|
||||
// Updating a counter that does not exist is a no-op, not a crash.
|
||||
Event_SetCounter unknown = set;
|
||||
unknown.set_counter_id(99);
|
||||
sendGameEvent(harness, 1, unknown);
|
||||
|
||||
EXPECT_FALSE(harness.player()->getCounters().contains(99));
|
||||
|
||||
Event_DelCounter remove;
|
||||
remove.set_counter_id(1);
|
||||
|
||||
sendGameEvent(harness, 1, remove);
|
||||
|
||||
EXPECT_FALSE(harness.player()->getCounters().contains(1));
|
||||
EXPECT_EQ(harness.player()->getLifeCounter(), nullptr);
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, LeaveRemovesThePlayer)
|
||||
{
|
||||
GameHarness harness;
|
||||
ASSERT_NE(harness.addCardToZone(ZoneNames::TABLE, QStringLiteral("Cat"), 7), nullptr);
|
||||
ASSERT_NE(harness.game()->getPlayerManager()->getPlayer(1), nullptr);
|
||||
|
||||
Event_Leave leave;
|
||||
leave.set_reason(Event_Leave::USER_LEFT);
|
||||
|
||||
sendGameEvent(harness, 1, leave);
|
||||
|
||||
EXPECT_EQ(harness.game()->getPlayerManager()->getPlayer(1), nullptr);
|
||||
EXPECT_TRUE(harness.game()->getPlayerManager()->getPlayers().isEmpty());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, KickedSignalsGameClosed)
|
||||
{
|
||||
GameHarness harness;
|
||||
|
||||
bool closed = false;
|
||||
QObject::connect(harness.game()->getGameEventHandler(), &GameEventHandler::gameClosed,
|
||||
[&closed] { closed = true; });
|
||||
|
||||
sendGameEvent(harness, 1, Event_Kicked());
|
||||
|
||||
EXPECT_TRUE(closed);
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, GameClosedMarksTheSessionClosed)
|
||||
{
|
||||
GameHarness harness;
|
||||
ASSERT_FALSE(harness.game()->getGameState()->isGameClosed());
|
||||
|
||||
Event_GameStateChanged started;
|
||||
started.set_game_started(true);
|
||||
sendGameEvent(harness, 1, started);
|
||||
ASSERT_TRUE(harness.game()->getGameMetaInfo()->started());
|
||||
|
||||
sendGameEvent(harness, 1, Event_GameClosed());
|
||||
|
||||
EXPECT_TRUE(harness.game()->getGameState()->isGameClosed());
|
||||
EXPECT_FALSE(harness.game()->getGameMetaInfo()->started());
|
||||
}
|
||||
|
||||
TEST(GameEventHandlerTest, GameHostChangedMovesTheHostRole)
|
||||
{
|
||||
GameHarness harness;
|
||||
|
||||
ServerInfo_User bob;
|
||||
bob.set_name("Bob");
|
||||
bob.set_user_level(ServerInfo_User::IsUser);
|
||||
ASSERT_NE(harness.game()->getPlayerManager()->addPlayer(2, bob), nullptr);
|
||||
|
||||
sendGameEvent(harness, 2, Event_GameHostChanged());
|
||||
|
||||
EXPECT_EQ(harness.game()->getGameState()->getHostId(), 2);
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
/**
|
||||
* @file game_smoke_test.cpp
|
||||
*
|
||||
* Links the game logic library into a target that contains no graphics code at all, and drives
|
||||
* the two seams the graphics layer used to own: the attachment graph on CardState and the
|
||||
* card-creation request a zone makes so the graphics side can build the matching card item.
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QLoggingCategory>
|
||||
#include <QSignalSpy>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/game/board/card_state.h>
|
||||
#include <libcockatrice/game/zones/hand_zone_logic.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
CardRef cardRefFor(const QString &name)
|
||||
{
|
||||
CardRef cardRef;
|
||||
cardRef.name = name;
|
||||
return cardRef;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CardStateTest, AttachmentGraphKeepsBothEndsConsistent)
|
||||
{
|
||||
CardState host(nullptr, nullptr, cardRefFor(QStringLiteral("Host")), 1);
|
||||
CardState guest(nullptr, nullptr, cardRefFor(QStringLiteral("Guest")), 2);
|
||||
|
||||
guest.setAttachedTo(&host);
|
||||
host.addAttachedCard(&guest);
|
||||
|
||||
EXPECT_EQ(guest.getAttachedTo(), &host);
|
||||
ASSERT_EQ(host.getAttachedCards().size(), 1);
|
||||
EXPECT_EQ(host.getAttachedCards().first(), &guest);
|
||||
|
||||
host.removeAttachedCard(&guest);
|
||||
guest.setAttachedTo(nullptr);
|
||||
|
||||
EXPECT_TRUE(host.getAttachedCards().isEmpty());
|
||||
EXPECT_EQ(guest.getAttachedTo(), nullptr);
|
||||
}
|
||||
|
||||
TEST(CardZoneLogicTest, AddsAndTakesCardsWithoutAGraphicsSide)
|
||||
{
|
||||
HandZoneLogic zone(nullptr, QStringLiteral("hand"), false, false, true);
|
||||
CardState card(nullptr, nullptr, cardRefFor(QStringLiteral("Lightning Bolt")), 7);
|
||||
|
||||
QSignalSpy cardAddedSpy(&zone, &CardZoneLogic::cardAdded);
|
||||
QSignalSpy cardCountSpy(&zone, &CardZoneLogic::cardCountChanged);
|
||||
|
||||
zone.addCard(&card, false, 0);
|
||||
|
||||
ASSERT_EQ(zone.getCards().size(), 1);
|
||||
EXPECT_EQ(zone.getCards().first(), &card);
|
||||
EXPECT_EQ(card.getZone(), &zone);
|
||||
EXPECT_EQ(cardAddedSpy.count(), 1);
|
||||
EXPECT_GE(cardCountSpy.count(), 1);
|
||||
|
||||
EXPECT_EQ(zone.takeCard(0, 7), &card);
|
||||
EXPECT_TRUE(zone.getCards().isEmpty());
|
||||
EXPECT_EQ(card.getId(), 7);
|
||||
}
|
||||
|
||||
TEST(CardZoneLogicTest, AsksTheGraphicsSideToCreateCards)
|
||||
{
|
||||
HandZoneLogic zone(nullptr, QStringLiteral("library"), false, true, true);
|
||||
QSignalSpy requestSpy(&zone, &CardZoneLogic::createCardRequested);
|
||||
|
||||
ServerInfo_Card cardInfo;
|
||||
cardInfo.set_name("Lightning Bolt");
|
||||
cardInfo.set_id(7);
|
||||
cardInfo.set_x(0);
|
||||
cardInfo.set_y(1);
|
||||
|
||||
zone.createCardRequested(cardInfo, true);
|
||||
|
||||
ASSERT_EQ(requestSpy.count(), 1);
|
||||
const QList<QVariant> arguments = requestSpy.takeFirst();
|
||||
EXPECT_EQ(arguments.at(0).value<ServerInfo_Card>().id(), 7);
|
||||
EXPECT_TRUE(arguments.at(1).toBool());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Keep SettingsCache reads and writes away from the real user profile: the game logic still
|
||||
// reaches for a handful of settings, and the client settings sources linked into this test
|
||||
// would otherwise touch the developer's own configuration.
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
|
||||
QTemporaryDir home;
|
||||
if (home.isValid()) {
|
||||
qputenv("HOME", home.path().toLocal8Bit());
|
||||
}
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
QLoggingCategory::setFilterRules("settings_cache.*=false");
|
||||
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
129
tests/game/game_test_fixture.h
Normal file
129
tests/game/game_test_fixture.h
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* @file game_test_fixture.h
|
||||
*
|
||||
* Fixtures for tests that need a complete game/player/zone graph without the
|
||||
* client, a server or any graphics object. Commands are captured on a recording
|
||||
* AbstractClient, which is the same seam the real client uses.
|
||||
*/
|
||||
|
||||
#ifndef COCKATRICE_TESTS_GAME_TEST_FIXTURE_H
|
||||
#define COCKATRICE_TESTS_GAME_TEST_FIXTURE_H
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QtAlgorithms>
|
||||
#include <libcockatrice/game/board/card_state.h>
|
||||
#include <libcockatrice/game/game.h>
|
||||
#include <libcockatrice/game/player/player_logic.h>
|
||||
#include <libcockatrice/game/player/player_manager.h>
|
||||
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Captures every command the game logic sends, instead of writing it to a socket.
|
||||
*
|
||||
* AbstractClient::sendCommand uses a direct signal/slot connection in the same
|
||||
* thread, so a command is in `sent` as soon as sendCommand returns.
|
||||
*/
|
||||
class RecordingClient : public AbstractClient
|
||||
{
|
||||
public:
|
||||
QList<CommandContainer> sent;
|
||||
|
||||
protected:
|
||||
void sendCommandContainer(const CommandContainer &cont) override
|
||||
{
|
||||
sent.append(cont);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Owns a local, single-client Game with one local player, plus the cards added
|
||||
* to its zones.
|
||||
*/
|
||||
class GameHarness
|
||||
{
|
||||
public:
|
||||
explicit GameHarness(int playerId = 1) : playerId_(playerId), client_(std::make_unique<RecordingClient>())
|
||||
{
|
||||
Event_GameJoined event;
|
||||
event.set_host_id(playerId_);
|
||||
event.set_player_id(playerId_);
|
||||
event.mutable_game_info()->set_game_id(42);
|
||||
event.mutable_game_info()->set_started(true);
|
||||
|
||||
QList<AbstractClient *> clients{client_.get()};
|
||||
QMap<int, QString> roomGameTypes;
|
||||
game_ = std::make_unique<Game>(nullptr, true, clients, event, roomGameTypes);
|
||||
|
||||
ServerInfo_User user;
|
||||
user.set_name("Alice");
|
||||
user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered);
|
||||
game_->getPlayerManager()->addPlayer(playerId_, user);
|
||||
}
|
||||
|
||||
~GameHarness()
|
||||
{
|
||||
qDeleteAll(ownedCards_);
|
||||
ownedCards_.clear();
|
||||
}
|
||||
|
||||
GameHarness(const GameHarness &) = delete;
|
||||
GameHarness &operator=(const GameHarness &) = delete;
|
||||
|
||||
[[nodiscard]] Game *game() const
|
||||
{
|
||||
return game_.get();
|
||||
}
|
||||
|
||||
[[nodiscard]] RecordingClient *client() const
|
||||
{
|
||||
return client_.get();
|
||||
}
|
||||
|
||||
[[nodiscard]] PlayerLogic *player() const
|
||||
{
|
||||
return game_->getPlayerManager()->getPlayer(playerId_);
|
||||
}
|
||||
|
||||
[[nodiscard]] CardZoneLogic *zone(const QString &zoneName) const
|
||||
{
|
||||
return player()->getZones().value(zoneName);
|
||||
}
|
||||
|
||||
CardState *addCardToZone(const QString &zoneName, const QString &cardName, int cardId)
|
||||
{
|
||||
CardZoneLogic *targetZone = zone(zoneName);
|
||||
if (!targetZone) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *card = new CardState(nullptr, targetZone, CardRef{cardName, QString()}, cardId);
|
||||
// The constructor only stores the ref; this is what fills in the card
|
||||
// data (table row, P/T, ...) the actions read.
|
||||
card->refreshCardInfo();
|
||||
targetZone->addCard(card, false, 0);
|
||||
ownedCards_.append(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
[[nodiscard]] const CommandContainer &lastCommand() const
|
||||
{
|
||||
return client_->sent.last();
|
||||
}
|
||||
|
||||
private:
|
||||
int playerId_;
|
||||
std::unique_ptr<RecordingClient> client_;
|
||||
std::unique_ptr<Game> game_;
|
||||
QList<CardState *> ownedCards_;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TESTS_GAME_TEST_FIXTURE_H
|
||||
126
tests/game/game_test_main.cpp
Normal file
126
tests/game/game_test_main.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* @file game_test_main.cpp
|
||||
*
|
||||
* Test entry point for the headless game-logic suite. The game library has no
|
||||
* graphics dependency, so a QCoreApplication is enough; the only process-wide
|
||||
* setup left is the card database, which the game logic still reaches through
|
||||
* CardDatabaseManager and which therefore has to exist before the first zone is
|
||||
* constructed.
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QLoggingCategory>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryDir>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class GameTestPathProvider : public ICardDatabasePathProvider
|
||||
{
|
||||
public:
|
||||
explicit GameTestPathProvider(QString baseDir) : baseDir_(std::move(baseDir))
|
||||
{
|
||||
}
|
||||
|
||||
QString getCardDatabasePath() const override
|
||||
{
|
||||
return baseDir_ + QStringLiteral("/cards.xml");
|
||||
}
|
||||
|
||||
QString getCustomCardDatabasePath() const override
|
||||
{
|
||||
return baseDir_ + QStringLiteral("/customsets/");
|
||||
}
|
||||
|
||||
QString getTokenDatabasePath() const override
|
||||
{
|
||||
return baseDir_ + QStringLiteral("/tokens.xml");
|
||||
}
|
||||
|
||||
QString getSpoilerCardDatabasePath() const override
|
||||
{
|
||||
return baseDir_ + QStringLiteral("/spoilers.xml");
|
||||
}
|
||||
|
||||
private:
|
||||
QString baseDir_;
|
||||
};
|
||||
|
||||
bool copyFixture(const QString &fileName, const QString &targetDir)
|
||||
{
|
||||
const QString source = QString::fromUtf8(GAME_TEST_DATA_DIR) + QLatin1Char('/') + fileName;
|
||||
const QString target = targetDir + QLatin1Char('/') + fileName;
|
||||
if (QFile::copy(source, target)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::fprintf(stderr, "Failed to copy test fixture %s to %s\n", qPrintable(source), qPrintable(target));
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
||||
// gtest_discover_tests runs the binary with --gtest_list_tests while the
|
||||
// build is still in progress. No test runs there, and building the card
|
||||
// database would leave process-lifetime allocations that the sanitizer
|
||||
// leg's default leak detection reports at exit, failing discovery.
|
||||
if (::testing::GTEST_FLAG(list_tests)) {
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
|
||||
QTemporaryDir home;
|
||||
if (!home.isValid()) {
|
||||
std::fprintf(stderr, "Failed to create a temporary home directory\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
qputenv("HOME", home.path().toLocal8Bit());
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("card_database.loading=false\n"
|
||||
"card_database.loading.success_or_failure=false\n"
|
||||
"settings_cache.*=false"));
|
||||
|
||||
// Copy the fixtures so the database loader never writes its .cache file into
|
||||
// the source tree, then point the singleton at the copy.
|
||||
QTemporaryDir dataDir;
|
||||
if (!dataDir.isValid() || !QDir().mkpath(dataDir.filePath(QStringLiteral("customsets")))) {
|
||||
std::fprintf(stderr, "Failed to create a temporary card database directory\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (!copyFixture(QStringLiteral("cards.xml"), dataDir.path()) ||
|
||||
!copyFixture(QStringLiteral("tokens.xml"), dataDir.path()) ||
|
||||
!copyFixture(QStringLiteral("spoilers.xml"), dataDir.path())) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Function-local static: constructed before the CardDatabase singleton below,
|
||||
// so it is destroyed after it and the raw pointer stored in the manager stays
|
||||
// valid for the database's lifetime.
|
||||
static GameTestPathProvider pathProvider(dataDir.path());
|
||||
CardDatabaseManager::setCardDatabasePathProvider(&pathProvider);
|
||||
|
||||
CardDatabase *database = CardDatabaseManager::getInstance();
|
||||
database->loadCardDatabases();
|
||||
if (database->getLoadStatus() != Ok) {
|
||||
std::fprintf(stderr, "Failed to load the game test card database (status %d)\n",
|
||||
static_cast<int>(database->getLoadStatus()));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
119
tests/game/player_actions_test.cpp
Normal file
119
tests/game/player_actions_test.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#include "game_test_fixture.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QString>
|
||||
#include <libcockatrice/game/player/player_actions.h>
|
||||
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <string>
|
||||
|
||||
TEST(PlayerActionsTest, PlayCreatureSendsMoveToTable)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
harness.player()->getPlayerActions()->playCard(cat, false);
|
||||
|
||||
ASSERT_FALSE(harness.client()->sent.isEmpty());
|
||||
const CommandContainer &container = harness.lastCommand();
|
||||
ASSERT_EQ(container.game_command_size(), 1);
|
||||
const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext);
|
||||
|
||||
EXPECT_EQ(command.start_player_id(), 1);
|
||||
EXPECT_EQ(command.start_zone(), std::string(ZoneNames::HAND));
|
||||
EXPECT_EQ(command.target_player_id(), 1);
|
||||
EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE));
|
||||
EXPECT_EQ(command.x(), -1);
|
||||
EXPECT_EQ(command.y(), TableZoneLogic::tableRowToGridY(0));
|
||||
|
||||
ASSERT_EQ(command.cards_to_move().card_size(), 1);
|
||||
const CardToMove &card = command.cards_to_move().card(0);
|
||||
EXPECT_EQ(card.card_id(), 7);
|
||||
EXPECT_TRUE(card.has_face_down());
|
||||
EXPECT_FALSE(card.face_down());
|
||||
EXPECT_TRUE(card.has_pt());
|
||||
EXPECT_EQ(card.pt(), "3/3");
|
||||
EXPECT_TRUE(card.has_tapped());
|
||||
EXPECT_FALSE(card.tapped());
|
||||
}
|
||||
|
||||
TEST(PlayerActionsTest, PlayInstantSendsMoveToStack)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *bolt = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Lightning Bolt"), 8);
|
||||
ASSERT_NE(bolt, nullptr);
|
||||
|
||||
harness.player()->getPlayerActions()->playCard(bolt, false);
|
||||
|
||||
ASSERT_FALSE(harness.client()->sent.isEmpty());
|
||||
const CommandContainer &container = harness.lastCommand();
|
||||
ASSERT_EQ(container.game_command_size(), 1);
|
||||
const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext);
|
||||
|
||||
EXPECT_EQ(command.target_zone(), std::string(ZoneNames::STACK));
|
||||
EXPECT_EQ(command.x(), -1);
|
||||
EXPECT_EQ(command.y(), 0);
|
||||
|
||||
ASSERT_EQ(command.cards_to_move().card_size(), 1);
|
||||
const CardToMove &card = command.cards_to_move().card(0);
|
||||
EXPECT_EQ(card.card_id(), 8);
|
||||
// The stack branch deliberately leaves the other attributes unset.
|
||||
EXPECT_FALSE(card.has_face_down());
|
||||
EXPECT_FALSE(card.has_pt());
|
||||
EXPECT_FALSE(card.has_tapped());
|
||||
}
|
||||
|
||||
TEST(PlayerActionsTest, PlayFaceDownCreatureUsesFaceDownRow)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *cat = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Cat"), 7);
|
||||
ASSERT_NE(cat, nullptr);
|
||||
|
||||
harness.player()->getPlayerActions()->playCard(cat, true);
|
||||
|
||||
ASSERT_FALSE(harness.client()->sent.isEmpty());
|
||||
const CommandContainer &container = harness.lastCommand();
|
||||
ASSERT_EQ(container.game_command_size(), 1);
|
||||
const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext);
|
||||
|
||||
EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE));
|
||||
EXPECT_EQ(command.x(), -1);
|
||||
EXPECT_EQ(command.y(), TableZoneLogic::tableRowToGridY(2));
|
||||
|
||||
ASSERT_EQ(command.cards_to_move().card_size(), 1);
|
||||
const CardToMove &card = command.cards_to_move().card(0);
|
||||
EXPECT_EQ(card.card_id(), 7);
|
||||
EXPECT_TRUE(card.has_face_down());
|
||||
EXPECT_TRUE(card.face_down());
|
||||
EXPECT_FALSE(card.has_pt());
|
||||
EXPECT_TRUE(card.has_tapped());
|
||||
EXPECT_FALSE(card.tapped());
|
||||
}
|
||||
|
||||
TEST(PlayerActionsTest, PlayCiptCreatureSendsTapped)
|
||||
{
|
||||
GameHarness harness;
|
||||
CardState *bears = harness.addCardToZone(ZoneNames::HAND, QStringLiteral("Grizzly Bears"), 9);
|
||||
ASSERT_NE(bears, nullptr);
|
||||
|
||||
harness.player()->getPlayerActions()->playCard(bears, false);
|
||||
|
||||
ASSERT_FALSE(harness.client()->sent.isEmpty());
|
||||
const CommandContainer &container = harness.lastCommand();
|
||||
ASSERT_EQ(container.game_command_size(), 1);
|
||||
const Command_MoveCard command = container.game_command(0).GetExtension(Command_MoveCard::ext);
|
||||
|
||||
EXPECT_EQ(command.target_zone(), std::string(ZoneNames::TABLE));
|
||||
|
||||
ASSERT_EQ(command.cards_to_move().card_size(), 1);
|
||||
const CardToMove &card = command.cards_to_move().card(0);
|
||||
EXPECT_EQ(card.card_id(), 9);
|
||||
EXPECT_TRUE(card.has_face_down());
|
||||
EXPECT_FALSE(card.face_down());
|
||||
EXPECT_TRUE(card.has_pt());
|
||||
EXPECT_EQ(card.pt(), "2/2");
|
||||
EXPECT_TRUE(card.has_tapped());
|
||||
EXPECT_TRUE(card.tapped());
|
||||
}
|
||||
349
tests/game/replay_test.cpp
Normal file
349
tests/game/replay_test.cpp
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
/**
|
||||
* @file replay_test.cpp
|
||||
*
|
||||
* Replays a synthetic GameReplay through the real dispatcher. This is the only
|
||||
* place that exercises PlayerLogic::processPlayerInfo / processCardAttachment
|
||||
* end to end: the game state arrives as a GAME_STATE_CHANGED event and the
|
||||
* graphics side is replaced by a small materializer that mirrors what
|
||||
* CardZone::onCreateCardRequested does with the logic objects.
|
||||
*
|
||||
* The replay is built in code rather than from a .cor file: event application
|
||||
* is deterministic (the client never shuffles on its own for a replay), and a
|
||||
* checked-in binary fixture would be opaque about what it asserts.
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QtAlgorithms>
|
||||
#include <libcockatrice/game/board/card_state.h>
|
||||
#include <libcockatrice/game/game_event_handler.h>
|
||||
#include <libcockatrice/game/game_state.h>
|
||||
#include <libcockatrice/game/player/event_processing_options.h>
|
||||
#include <libcockatrice/game/player/player_logic.h>
|
||||
#include <libcockatrice/game/player/player_manager.h>
|
||||
#include <libcockatrice/game/replay.h>
|
||||
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||
#include <libcockatrice/game/zones/table_zone_logic.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_state_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_move_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_zone.pb.h>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
ServerInfo_Card makeCard(int id, const QString &name, int x = -1, int y = -1)
|
||||
{
|
||||
ServerInfo_Card card;
|
||||
card.set_id(id);
|
||||
card.set_name(name.toStdString());
|
||||
card.set_x(x);
|
||||
card.set_y(y);
|
||||
return card;
|
||||
}
|
||||
|
||||
ServerInfo_Zone makeZone(const char *name,
|
||||
ServerInfo_Zone::ZoneType type,
|
||||
bool withCoords,
|
||||
const QList<ServerInfo_Card> &cards,
|
||||
int cardCount = 0)
|
||||
{
|
||||
ServerInfo_Zone zone;
|
||||
zone.set_name(name);
|
||||
zone.set_type(type);
|
||||
zone.set_with_coords(withCoords);
|
||||
for (const auto &card : cards) {
|
||||
zone.add_card_list()->CopyFrom(card);
|
||||
}
|
||||
if (cardCount > 0) {
|
||||
zone.set_card_count(cardCount);
|
||||
}
|
||||
return zone;
|
||||
}
|
||||
|
||||
ServerInfo_Player makePlayer(int playerId, const QString &name, const QList<ServerInfo_Zone> &zones)
|
||||
{
|
||||
ServerInfo_Player player;
|
||||
ServerInfo_PlayerProperties *properties = player.mutable_properties();
|
||||
properties->set_player_id(playerId);
|
||||
properties->mutable_user_info()->set_name(name.toStdString());
|
||||
properties->mutable_user_info()->set_user_level(ServerInfo_User::IsUser);
|
||||
for (const auto &zone : zones) {
|
||||
player.add_zone_list()->CopyFrom(zone);
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
template <typename EventType> GameEventContainer makeContainer(int playerId, const EventType &message)
|
||||
{
|
||||
GameEventContainer container;
|
||||
container.set_game_id(42);
|
||||
|
||||
GameEvent *event = container.add_event_list();
|
||||
event->set_player_id(playerId);
|
||||
event->MutableExtension(EventType::ext)->CopyFrom(message);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stands in for the graphics side of a replay: when the logic asks for a card
|
||||
* item, create the CardState and add it to the zone, copying the state fields
|
||||
* the real CardItem::processCardInfo applies.
|
||||
*
|
||||
* The production view owns the visual item; the state itself is all a headless
|
||||
* test needs, and this is the smallest thing that lets processPlayerInfo build
|
||||
* a real zone.
|
||||
*/
|
||||
class HeadlessZoneView : public QObject
|
||||
{
|
||||
public:
|
||||
using QObject::QObject;
|
||||
|
||||
~HeadlessZoneView() override
|
||||
{
|
||||
qDeleteAll(cards_);
|
||||
cards_.clear();
|
||||
}
|
||||
|
||||
void attach(PlayerLogic *player)
|
||||
{
|
||||
for (CardZoneLogic *zone : player->getZones()) {
|
||||
connect(zone, &CardZoneLogic::createCardRequested, this,
|
||||
[this, zone](const ServerInfo_Card &cardInfo, bool reorganize) {
|
||||
materialize(zone, cardInfo, reorganize);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void materialize(CardZoneLogic *zone, const ServerInfo_Card &info, bool reorganize)
|
||||
{
|
||||
const CardRef cardRef{QString::fromStdString(info.name()), QString::fromStdString(info.provider_id())};
|
||||
auto *card = new CardState(nullptr, zone, cardRef, info.id());
|
||||
// The real CardItem constructor refreshes the card info before
|
||||
// processCardInfo overwrites the state fields.
|
||||
card->refreshCardInfo();
|
||||
|
||||
card->clearCounters();
|
||||
for (int i = 0; i < info.counter_list_size(); ++i) {
|
||||
card->insertCounter(info.counter_list(i).id(), info.counter_list(i).value());
|
||||
}
|
||||
card->setAttacking(info.attacking());
|
||||
card->setFaceDown(info.face_down());
|
||||
card->setPT(QString::fromStdString(info.pt()));
|
||||
card->setAnnotation(QString::fromStdString(info.annotation()));
|
||||
card->setColor(QString::fromStdString(info.color()));
|
||||
card->setTapped(info.tapped());
|
||||
card->setDestroyOnZoneChange(info.destroy_on_zone_change());
|
||||
card->setDoesntUntap(info.doesnt_untap());
|
||||
|
||||
zone->addCard(card, reorganize, info.x(), info.y());
|
||||
cards_.append(card);
|
||||
}
|
||||
|
||||
QList<CardState *> cards_;
|
||||
};
|
||||
|
||||
/** Owns a Replay built from a GameReplay and feeds its containers through it. */
|
||||
class ReplayHarness
|
||||
{
|
||||
public:
|
||||
explicit ReplayHarness(GameReplay replay)
|
||||
: replayProto_(std::move(replay)), view_(std::make_unique<HeadlessZoneView>()),
|
||||
replay_(std::make_unique<Replay>(nullptr, &replayProto_, false))
|
||||
{
|
||||
QObject::connect(replay_->getPlayerManager(), &PlayerManager::playerAdded, view_.get(),
|
||||
&HeadlessZoneView::attach);
|
||||
}
|
||||
|
||||
ReplayHarness(const ReplayHarness &) = delete;
|
||||
ReplayHarness &operator=(const ReplayHarness &) = delete;
|
||||
|
||||
void processAll()
|
||||
{
|
||||
for (const GameEventContainer &container : replayProto_.event_list()) {
|
||||
replay_->getGameEventHandler()->processGameEventContainer(container, nullptr, EventProcessingOptions());
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] Replay *replay() const
|
||||
{
|
||||
return replay_.get();
|
||||
}
|
||||
|
||||
[[nodiscard]] PlayerLogic *player(int playerId) const
|
||||
{
|
||||
return replay_->getPlayerManager()->getPlayer(playerId);
|
||||
}
|
||||
|
||||
private:
|
||||
GameReplay replayProto_;
|
||||
std::unique_ptr<HeadlessZoneView> view_;
|
||||
std::unique_ptr<Replay> replay_;
|
||||
};
|
||||
|
||||
GameReplay makeReplay(const QList<GameEventContainer> &containers)
|
||||
{
|
||||
GameReplay replay;
|
||||
replay.mutable_game_info()->set_game_id(42);
|
||||
replay.mutable_game_info()->set_started(true);
|
||||
for (const auto &container : containers) {
|
||||
replay.add_event_list()->CopyFrom(container);
|
||||
}
|
||||
return replay;
|
||||
}
|
||||
|
||||
Event_GameStateChanged initialGameState()
|
||||
{
|
||||
Event_GameStateChanged state;
|
||||
state.set_game_started(true);
|
||||
state.set_active_player_id(1);
|
||||
state.set_active_phase(0);
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ReplayTest, MaterializesGameStateZonesAndAttachments)
|
||||
{
|
||||
Event_GameStateChanged state = initialGameState();
|
||||
|
||||
ServerInfo_Card cat = makeCard(7, QStringLiteral("Cat"));
|
||||
// Cross-player attachment: Alice's card is attached to Bob's creature.
|
||||
cat.set_attach_player_id(2);
|
||||
cat.set_attach_zone(ZoneNames::TABLE);
|
||||
cat.set_attach_card_id(9);
|
||||
|
||||
const ServerInfo_Player alice = makePlayer(1, QStringLiteral("Alice"),
|
||||
{makeZone(ZoneNames::HAND, ServerInfo_Zone::PrivateZone, false, {cat}),
|
||||
makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true,
|
||||
{makeCard(9, QStringLiteral("Grizzly Bears"), 2, 1)}),
|
||||
makeZone(ZoneNames::DECK, ServerInfo_Zone::HiddenZone, false, {}, 2)});
|
||||
const ServerInfo_Player bob = makePlayer(2, QStringLiteral("Bob"),
|
||||
{makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true,
|
||||
{makeCard(9, QStringLiteral("Grizzly Bears"), 1, 0)})});
|
||||
|
||||
state.add_player_list()->CopyFrom(alice);
|
||||
state.add_player_list()->CopyFrom(bob);
|
||||
|
||||
ReplayHarness harness(makeReplay({makeContainer(-1, state)}));
|
||||
harness.processAll();
|
||||
|
||||
PlayerLogic *aliceLogic = harness.player(1);
|
||||
PlayerLogic *bobLogic = harness.player(2);
|
||||
ASSERT_NE(aliceLogic, nullptr);
|
||||
ASSERT_NE(bobLogic, nullptr);
|
||||
|
||||
CardZoneLogic *hand = aliceLogic->getZones().value(ZoneNames::HAND);
|
||||
CardZoneLogic *table = aliceLogic->getZones().value(ZoneNames::TABLE);
|
||||
CardZoneLogic *deck = aliceLogic->getZones().value(ZoneNames::DECK);
|
||||
ASSERT_NE(hand, nullptr);
|
||||
ASSERT_NE(table, nullptr);
|
||||
ASSERT_NE(deck, nullptr);
|
||||
|
||||
ASSERT_EQ(hand->getCards().size(), 1);
|
||||
CardState *catState = hand->getCards().first();
|
||||
EXPECT_EQ(catState->getId(), 7);
|
||||
EXPECT_EQ(catState->getName(), QStringLiteral("Cat"));
|
||||
|
||||
ASSERT_EQ(table->getCards().size(), 1);
|
||||
CardState *host = table->getCards().first();
|
||||
EXPECT_EQ(host->getName(), QStringLiteral("Grizzly Bears"));
|
||||
EXPECT_EQ(host->getGridPoint(), QPoint(2, 1));
|
||||
|
||||
// The hidden deck only carries a count, and the view materializes that many
|
||||
// unknown cards.
|
||||
EXPECT_EQ(deck->getCards().size(), 2);
|
||||
for (const CardState *hidden : deck->getCards()) {
|
||||
EXPECT_EQ(hidden->getId(), -1);
|
||||
}
|
||||
|
||||
// processCardAttachment links both sides after every player is materialized.
|
||||
CardZoneLogic *bobTable = bobLogic->getZones().value(ZoneNames::TABLE);
|
||||
ASSERT_NE(bobTable, nullptr);
|
||||
ASSERT_EQ(bobTable->getCards().size(), 1);
|
||||
CardState *bobHost = bobTable->getCards().first();
|
||||
EXPECT_EQ(catState->getAttachedTo(), bobHost);
|
||||
ASSERT_EQ(bobHost->getAttachedCards().size(), 1);
|
||||
EXPECT_EQ(bobHost->getAttachedCards().first(), catState);
|
||||
// The identically numbered card on Alice's own table is a different card.
|
||||
EXPECT_TRUE(host->getAttachedCards().isEmpty());
|
||||
|
||||
EXPECT_TRUE(harness.replay()->getGameState()->isGameStateKnown());
|
||||
}
|
||||
|
||||
TEST(ReplayTest, AppliesMovesAttributesAndCountersInOrder)
|
||||
{
|
||||
Event_GameStateChanged state = initialGameState();
|
||||
state.add_player_list()->CopyFrom(makePlayer(
|
||||
1, QStringLiteral("Alice"),
|
||||
{makeZone(ZoneNames::HAND, ServerInfo_Zone::PrivateZone, false, {makeCard(7, QStringLiteral("Cat"))}),
|
||||
makeZone(ZoneNames::TABLE, ServerInfo_Zone::PublicZone, true,
|
||||
{makeCard(9, QStringLiteral("Grizzly Bears"), 0, TableZoneLogic::tableRowToGridY(0))}),
|
||||
makeZone(ZoneNames::DECK, ServerInfo_Zone::HiddenZone, false, {}, 2)}));
|
||||
|
||||
Event_MoveCard move;
|
||||
move.set_card_id(7);
|
||||
move.set_start_player_id(1);
|
||||
move.set_start_zone(ZoneNames::HAND);
|
||||
move.set_position(0);
|
||||
move.set_target_player_id(1);
|
||||
move.set_target_zone(ZoneNames::TABLE);
|
||||
move.set_x(3);
|
||||
move.set_y(TableZoneLogic::tableRowToGridY(1));
|
||||
move.set_new_card_id(7);
|
||||
move.set_face_down(false);
|
||||
|
||||
Event_SetCardAttr tap;
|
||||
tap.set_zone_name(ZoneNames::TABLE);
|
||||
tap.set_card_id(7);
|
||||
tap.set_attribute(AttrTapped);
|
||||
tap.set_attr_value("1");
|
||||
|
||||
Event_SetCardCounter counter;
|
||||
counter.set_zone_name(ZoneNames::TABLE);
|
||||
counter.set_card_id(7);
|
||||
counter.set_counter_id(1);
|
||||
counter.set_counter_value(2);
|
||||
|
||||
ReplayHarness harness(makeReplay(
|
||||
{makeContainer(-1, state), makeContainer(1, move), makeContainer(1, tap), makeContainer(1, counter)}));
|
||||
harness.processAll();
|
||||
|
||||
PlayerLogic *alice = harness.player(1);
|
||||
ASSERT_NE(alice, nullptr);
|
||||
CardZoneLogic *hand = alice->getZones().value(ZoneNames::HAND);
|
||||
CardZoneLogic *table = alice->getZones().value(ZoneNames::TABLE);
|
||||
ASSERT_NE(hand, nullptr);
|
||||
ASSERT_NE(table, nullptr);
|
||||
|
||||
EXPECT_TRUE(hand->getCards().isEmpty());
|
||||
ASSERT_EQ(table->getCards().size(), 2);
|
||||
|
||||
CardState *moved = table->getCard(7);
|
||||
ASSERT_NE(moved, nullptr);
|
||||
EXPECT_EQ(moved->getGridPoint(), QPoint(3, TableZoneLogic::tableRowToGridY(1)));
|
||||
EXPECT_EQ(moved->getPT(), QStringLiteral("3/3"));
|
||||
EXPECT_TRUE(moved->getTapped());
|
||||
EXPECT_EQ(moved->getCounters().value(1), 2);
|
||||
|
||||
// The untouched creature keeps its own position and state.
|
||||
CardState *bears = table->getCard(9);
|
||||
ASSERT_NE(bears, nullptr);
|
||||
EXPECT_EQ(bears->getGridPoint(), QPoint(0, TableZoneLogic::tableRowToGridY(0)));
|
||||
EXPECT_FALSE(bears->getTapped());
|
||||
}
|
||||
3
tests/icon_assets/CMakeLists.txt
Normal file
3
tests/icon_assets/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
cockatrice_add_test(
|
||||
NAME icon_assets_test SOURCES icon_assets_test.cpp DEFINITIONS COCKATRICE_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
241
tests/icon_assets/icon_assets_test.cpp
Normal file
241
tests/icon_assets/icon_assets_test.cpp
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
#include <QColor>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QXmlStreamReader>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
const QString ICONS_RELATIVE_PATH = QStringLiteral("cockatrice/resources/icons");
|
||||
|
||||
QString iconsDir()
|
||||
{
|
||||
return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(ICONS_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
/** @brief True for content art that is allowed to carry full color. */
|
||||
bool isContentArt(const QString &relativePath)
|
||||
{
|
||||
return relativePath.startsWith(QStringLiteral("mana/"));
|
||||
}
|
||||
|
||||
QString violation(const QString &rule, const QString &relativePath)
|
||||
{
|
||||
return rule + QLatin1Char(' ') + relativePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads one SVG and returns true when the root carries a usable viewBox.
|
||||
*
|
||||
* Also reports malformed XML and editor-only metadata through the out params.
|
||||
*/
|
||||
bool inspectSvg(const QString &absolutePath,
|
||||
bool &validXml,
|
||||
bool &hasViewBox,
|
||||
bool &hasEditorMetadata,
|
||||
bool &hasPhysicalSize)
|
||||
{
|
||||
validXml = false;
|
||||
hasViewBox = false;
|
||||
hasEditorMetadata = false;
|
||||
hasPhysicalSize = false;
|
||||
|
||||
QFile file(absolutePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QXmlStreamReader reader(&file);
|
||||
bool sawRoot = false;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
reader.readNext();
|
||||
|
||||
if (reader.hasError()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!reader.isStartElement()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sawRoot) {
|
||||
sawRoot = true;
|
||||
if (reader.name() != QStringLiteral("svg")) {
|
||||
return false;
|
||||
}
|
||||
const QString viewBox = reader.attributes().value(QStringLiteral("viewBox")).toString().simplified();
|
||||
hasViewBox = viewBox == QStringLiteral("0 0 24 24");
|
||||
hasPhysicalSize = !reader.attributes().value(QStringLiteral("width")).isEmpty() ||
|
||||
!reader.attributes().value(QStringLiteral("height")).isEmpty();
|
||||
}
|
||||
|
||||
if (reader.name() == QStringLiteral("metadata") || reader.name() == QStringLiteral("namedview")) {
|
||||
hasEditorMetadata = true;
|
||||
}
|
||||
|
||||
for (const QXmlStreamAttribute &attribute : reader.attributes()) {
|
||||
const QString prefix = attribute.prefix().toString();
|
||||
if (prefix == QStringLiteral("inkscape") || prefix == QStringLiteral("sodipodi") ||
|
||||
prefix == QStringLiteral("dc") || prefix == QStringLiteral("cc") || prefix == QStringLiteral("rdf")) {
|
||||
hasEditorMetadata = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validXml = sawRoot && !reader.hasError();
|
||||
return validXml;
|
||||
}
|
||||
|
||||
bool isAllowedPaintValue(const QString &value)
|
||||
{
|
||||
const QString normalized = value.trimmed().toLower();
|
||||
return normalized.isEmpty() || normalized == QStringLiteral("none") ||
|
||||
normalized == QStringLiteral("currentcolor") || normalized == QStringLiteral("inherit") ||
|
||||
normalized == QStringLiteral("context-fill") || normalized == QStringLiteral("context-stroke") ||
|
||||
normalized.startsWith(QStringLiteral("url("));
|
||||
}
|
||||
|
||||
QRegularExpression paintValuePattern()
|
||||
{
|
||||
static const QRegularExpression pattern(
|
||||
QStringLiteral(R"re((?:fill|stroke|stop-color|color)\s*[:=]\s*["']?\s*([^"'\s;>]+))re"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
return pattern;
|
||||
}
|
||||
|
||||
bool hasHardcodedColor(const QString &absolutePath)
|
||||
{
|
||||
QFile file(absolutePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString content = QString::fromUtf8(file.readAll());
|
||||
auto matches = paintValuePattern().globalMatch(content);
|
||||
while (matches.hasNext()) {
|
||||
const QString value = matches.next().captured(1).trimmed();
|
||||
if (isAllowedPaintValue(value)) {
|
||||
continue;
|
||||
}
|
||||
const QString normalized = value.toLower();
|
||||
if (normalized.startsWith(QLatin1Char('#')) || normalized.contains(QStringLiteral("rgb(")) ||
|
||||
normalized.contains(QStringLiteral("hsl(")) || QColor(value).isValid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasCurrentColorToken(const QString &absolutePath)
|
||||
{
|
||||
QFile file(absolutePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString content = QString::fromUtf8(file.readAll());
|
||||
auto matches = paintValuePattern().globalMatch(content);
|
||||
while (matches.hasNext()) {
|
||||
if (matches.next().captured(1).trimmed().compare(QStringLiteral("currentColor"), Qt::CaseInsensitive) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isSchemeVariant(const QString &fileName)
|
||||
{
|
||||
return fileName.contains(QStringLiteral("-dark."), Qt::CaseInsensitive) ||
|
||||
fileName.contains(QStringLiteral("-light."), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool hasColorInFilename(const QString &fileName)
|
||||
{
|
||||
static const QRegularExpression colorSuffix(
|
||||
QStringLiteral(R"(_(green|blue|red|yellow|black|white|gray|grey)\.svg$)"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
return colorSuffix.match(fileName).hasMatch();
|
||||
}
|
||||
|
||||
/** @brief Collects every current violation as "rule relative/path.svg". */
|
||||
QSet<QString> collectViolations()
|
||||
{
|
||||
QSet<QString> violations;
|
||||
|
||||
QDirIterator it(iconsDir(), QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
const QFileInfo info = it.fileInfo();
|
||||
const QString relativePath = QDir(iconsDir()).relativeFilePath(info.absoluteFilePath());
|
||||
if (isContentArt(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
if (info.suffix().compare(QStringLiteral("svg"), Qt::CaseInsensitive) != 0) {
|
||||
violations.insert(violation(QStringLiteral("raster"), relativePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
bool validXml = false;
|
||||
bool hasViewBox = false;
|
||||
bool hasEditorMetadata = false;
|
||||
bool hasPhysicalSize = false;
|
||||
inspectSvg(info.absoluteFilePath(), validXml, hasViewBox, hasEditorMetadata, hasPhysicalSize);
|
||||
|
||||
if (!validXml) {
|
||||
violations.insert(violation(QStringLiteral("xml"), relativePath));
|
||||
}
|
||||
if (validXml && !hasViewBox) {
|
||||
violations.insert(violation(QStringLiteral("viewbox"), relativePath));
|
||||
}
|
||||
if (hasPhysicalSize) {
|
||||
violations.insert(violation(QStringLiteral("physical-size"), relativePath));
|
||||
}
|
||||
if (hasEditorMetadata) {
|
||||
violations.insert(violation(QStringLiteral("editor-metadata"), relativePath));
|
||||
}
|
||||
if (hasHardcodedColor(info.absoluteFilePath()) && !isSchemeVariant(info.fileName())) {
|
||||
violations.insert(violation(QStringLiteral("hardcoded-color"), relativePath));
|
||||
}
|
||||
if (!hasCurrentColorToken(info.absoluteFilePath()) && !isSchemeVariant(info.fileName())) {
|
||||
violations.insert(violation(QStringLiteral("no-token"), relativePath));
|
||||
}
|
||||
if (hasColorInFilename(info.fileName())) {
|
||||
violations.insert(violation(QStringLiteral("color-filename"), relativePath));
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
QString formatSet(const QSet<QString> &entries)
|
||||
{
|
||||
QStringList lines(entries.begin(), entries.end());
|
||||
lines.sort();
|
||||
return lines.join(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief Every UI glyph follows the palette-token rules.
|
||||
*
|
||||
* The legacy set was paid down icon by icon, so no baseline grandfathers
|
||||
* violations: a new violation fails right here. Scheme variants
|
||||
* (`*-dark.svg`/`*-light.svg`) and mana art are exempt inside collectViolations().
|
||||
*/
|
||||
TEST(IconAssets, AssetsFollowTheRules)
|
||||
{
|
||||
ASSERT_TRUE(QDir::isAbsolutePath(iconsDir()));
|
||||
ASSERT_TRUE(QDir(iconsDir()).exists()) << "Missing icon directory: " << iconsDir().toStdString();
|
||||
|
||||
const QSet<QString> violations = collectViolations();
|
||||
EXPECT_TRUE(violations.isEmpty()) << "Icon rule violations. Fix the icon; do not add exceptions:\n"
|
||||
<< formatSet(violations).toStdString();
|
||||
}
|
||||
82
tests/image_redirect_chain_test.cpp
Normal file
82
tests/image_redirect_chain_test.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#include "../cockatrice/src/interface/card_picture_loader/redirect_chain.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
using RedirectCache = QHash<QUrl, QPair<QUrl, QDateTime>>;
|
||||
|
||||
QUrl url(int index)
|
||||
{
|
||||
return QUrl(QStringLiteral("https://cards.example/%1").arg(index));
|
||||
}
|
||||
|
||||
RedirectCache chain(int hops)
|
||||
{
|
||||
RedirectCache cache;
|
||||
for (int index = 0; index < hops; ++index) {
|
||||
cache.insert(url(index), {url(index + 1), QDateTime()});
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(ImageRedirectChain, MissingHopReturnsLastKnownUrlWithoutRemovingMappings)
|
||||
{
|
||||
auto cache = chain(2);
|
||||
const auto original = cache;
|
||||
EXPECT_EQ(resolveImageRedirects(url(0), cache, 10), url(2));
|
||||
EXPECT_EQ(cache, original);
|
||||
EXPECT_EQ(resolveImageRedirects(url(9), cache, 10), url(9));
|
||||
}
|
||||
|
||||
TEST(ImageRedirectChain, ExactlyMaxHopsResolvesAndKeepsCache)
|
||||
{
|
||||
auto cache = chain(10);
|
||||
const auto original = cache;
|
||||
EXPECT_EQ(resolveImageRedirects(url(0), cache, 10), url(10));
|
||||
EXPECT_EQ(cache, original);
|
||||
}
|
||||
|
||||
TEST(ImageRedirectChain, CycleReturnsOriginalAndPurgesVisitedMappings)
|
||||
{
|
||||
auto cache = chain(2);
|
||||
cache.insert(url(2), {url(1), QDateTime()});
|
||||
cache.insert(url(9), {url(10), QDateTime()});
|
||||
EXPECT_EQ(resolveImageRedirects(url(0), cache, 10), url(0));
|
||||
EXPECT_FALSE(cache.contains(url(0)));
|
||||
EXPECT_FALSE(cache.contains(url(1)));
|
||||
EXPECT_FALSE(cache.contains(url(2)));
|
||||
EXPECT_TRUE(cache.contains(url(9)));
|
||||
}
|
||||
|
||||
TEST(ImageRedirectChain, OverlongChainReturnsOriginalAndPurgesVisitedMappings)
|
||||
{
|
||||
auto cache = chain(12);
|
||||
cache.insert(url(20), {url(21), QDateTime()});
|
||||
EXPECT_EQ(resolveImageRedirects(url(0), cache, 10), url(0));
|
||||
for (int index = 0; index < 10; ++index) {
|
||||
EXPECT_FALSE(cache.contains(url(index)));
|
||||
}
|
||||
EXPECT_TRUE(cache.contains(url(10)));
|
||||
EXPECT_TRUE(cache.contains(url(11)));
|
||||
EXPECT_TRUE(cache.contains(url(20)));
|
||||
}
|
||||
|
||||
TEST(ImageRedirectChain, EmptyTargetStopsAtItsSource)
|
||||
{
|
||||
auto cache = chain(1);
|
||||
cache.insert(url(1), {QUrl(), QDateTime()});
|
||||
const auto original = cache;
|
||||
EXPECT_EQ(resolveImageRedirects(url(0), cache, 10), url(1));
|
||||
EXPECT_EQ(cache, original);
|
||||
}
|
||||
|
||||
TEST(ImageRedirectChain, ZeroHopsDoesNotFollowOrRemoveMappings)
|
||||
{
|
||||
auto cache = chain(1);
|
||||
EXPECT_EQ(resolveImageRedirects(url(5), cache, 0), url(5));
|
||||
EXPECT_TRUE(cache.contains(url(0)));
|
||||
EXPECT_EQ(resolveImageRedirects(url(0), cache, 0), url(0));
|
||||
EXPECT_TRUE(cache.contains(url(0)));
|
||||
}
|
||||
149
tests/local_client_lifetime_test.cpp
Normal file
149
tests/local_client_lifetime_test.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* @file local_client_lifetime_test.cpp
|
||||
* @brief Tests that a LocalClient survives the death of the LocalServer it talks to.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QMessageLogContext>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/network/client/local/local_client.h>
|
||||
#include <libcockatrice/network/server/local/local_server.h>
|
||||
#include <libcockatrice/network/server/local/local_server_interface.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <memory>
|
||||
|
||||
// The server library refers to this global; the real applications define it in main.cpp.
|
||||
RNG_Abstract *rng = nullptr;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// GTest's main() creates no QCoreApplication, which QObject::deleteLater() needs.
|
||||
const QCoreApplication &application()
|
||||
{
|
||||
static int argc = 1;
|
||||
static char executable[] = "local_client_lifetime_test";
|
||||
static char *argv[] = {executable, nullptr};
|
||||
static const QCoreApplication app(argc, argv);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Collects the warnings one logging category emits while this object is alive.
|
||||
///
|
||||
/// The handler is process-global and Qt does not nest handlers, so only one capture can be
|
||||
/// installed at a time; GTest running one case at a time is what makes that safe.
|
||||
class CategoryWarningCapture
|
||||
{
|
||||
public:
|
||||
explicit CategoryWarningCapture(const char *_category) : category(_category)
|
||||
{
|
||||
previousHandler = qInstallMessageHandler(captureMessage);
|
||||
s_active = this;
|
||||
}
|
||||
|
||||
~CategoryWarningCapture()
|
||||
{
|
||||
qInstallMessageHandler(previousHandler);
|
||||
s_active = nullptr;
|
||||
}
|
||||
|
||||
CategoryWarningCapture(const CategoryWarningCapture &) = delete;
|
||||
CategoryWarningCapture &operator=(const CategoryWarningCapture &) = delete;
|
||||
|
||||
const QStringList &warnings() const
|
||||
{
|
||||
return capturedWarnings;
|
||||
}
|
||||
|
||||
private:
|
||||
static void captureMessage(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||
{
|
||||
if (s_active != nullptr && type == QtWarningMsg && context.category != nullptr &&
|
||||
std::strcmp(s_active->category, context.category) == 0) {
|
||||
s_active->capturedWarnings.append(message);
|
||||
}
|
||||
|
||||
// Forward everything else, so a failure elsewhere in the test is still visible.
|
||||
if (previousHandler != nullptr) {
|
||||
previousHandler(type, context, message);
|
||||
return;
|
||||
}
|
||||
std::fprintf(stderr, "%s\n", qPrintable(qFormatLogMessage(type, context, message)));
|
||||
}
|
||||
|
||||
const char *category;
|
||||
QStringList capturedWarnings;
|
||||
|
||||
static CategoryWarningCapture *s_active;
|
||||
static QtMessageHandler previousHandler;
|
||||
};
|
||||
|
||||
CategoryWarningCapture *CategoryWarningCapture::s_active = nullptr;
|
||||
QtMessageHandler CategoryWarningCapture::previousHandler = nullptr;
|
||||
|
||||
class LocalClientLifetimeTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
(void)application();
|
||||
|
||||
server = std::make_unique<LocalServer>();
|
||||
LocalServerInterface *lsi = server->newConnection();
|
||||
ASSERT_NE(lsi, nullptr);
|
||||
// Building the client logs in and joins room 0 through that interface, exactly as
|
||||
// MainWindow::startLocalGame() does.
|
||||
client = std::make_unique<LocalClient>(lsi, QStringLiteral("lifetime-test-player"),
|
||||
QStringLiteral("lifetime-test-client-id"));
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
// Either case may already have deleted one of these.
|
||||
client.reset();
|
||||
server.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<LocalServer> server;
|
||||
std::unique_ptr<LocalClient> client;
|
||||
};
|
||||
|
||||
TEST_F(LocalClientLifetimeTest, SendAfterServerDeletedIsDropped)
|
||||
{
|
||||
server.reset(); // also destroys the LocalServerInterface the client was built on
|
||||
|
||||
CategoryWarningCapture capture("local_client");
|
||||
// A default container stands in for a command still queued when the local game ended.
|
||||
client->sendCommandContainer(CommandContainer());
|
||||
|
||||
ASSERT_EQ(capture.warnings().size(), 1);
|
||||
EXPECT_TRUE(capture.warnings().first().contains(QStringLiteral("dropping command")))
|
||||
<< capture.warnings().first().toStdString();
|
||||
|
||||
// The surviving client is intact: reading its state must not touch the deleted interface.
|
||||
EXPECT_EQ(client->getUserName(), QStringLiteral("lifetime-test-player"));
|
||||
}
|
||||
|
||||
TEST_F(LocalClientLifetimeTest, DestroyingClientAfterServerIsSafe)
|
||||
{
|
||||
server.reset();
|
||||
|
||||
CategoryWarningCapture capture("local_client");
|
||||
client.reset(); // must neither crash nor touch the destroyed interface
|
||||
|
||||
// The destructor itself must have been silent.
|
||||
EXPECT_TRUE(capture.warnings().isEmpty());
|
||||
|
||||
// A working capture proves the emptiness above is real rather than a broken handler.
|
||||
qCWarning(LocalClientLog) << "local_client_lifetime_test capture self-check";
|
||||
EXPECT_EQ(capture.warnings().size(), 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
17
tests/model_contract/CMakeLists.txt
Normal file
17
tests/model_contract/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
cockatrice_add_test(
|
||||
NAME
|
||||
model_contract_test
|
||||
SOURCES
|
||||
${VERSION_STRING_CPP}
|
||||
model_contract_test.cpp
|
||||
LIBS
|
||||
libcockatrice_models
|
||||
libcockatrice_card
|
||||
libcockatrice_deck_list
|
||||
libcockatrice_settings
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../support
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../carddatabase
|
||||
DEFINITIONS
|
||||
CARDDB_DATADIR="${CMAKE_SOURCE_DIR}/tests/carddatabase/data/"
|
||||
)
|
||||
137
tests/model_contract/model_contract_test.cpp
Normal file
137
tests/model_contract/model_contract_test.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#include "libcockatrice/deck_list/deck_list.h"
|
||||
#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
|
||||
#include "libcockatrice/models/database/card_database_display_model.h"
|
||||
#include "libcockatrice/models/database/card_database_model.h"
|
||||
#include "libcockatrice/models/database/card_set/card_sets_model.h"
|
||||
#include "libcockatrice/models/deck_list/deck_list_model.h"
|
||||
#include "model_contract_checker.h"
|
||||
#include "test_card_database_path_provider.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <QAbstractItemModel>
|
||||
#include <QCoreApplication>
|
||||
#include <QModelIndex>
|
||||
#include <QSharedPointer>
|
||||
#include <QSignalSpy>
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <QVariant>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/game_specific_terms.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
|
||||
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
|
||||
#include <libcockatrice/settings/download_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
QSharedPointer<DeckList> makeDeck()
|
||||
{
|
||||
return QSharedPointer<DeckList>(new DeckList());
|
||||
}
|
||||
|
||||
void loadCardDatabase(CardDatabase &database)
|
||||
{
|
||||
database.loadCardDatabases();
|
||||
ASSERT_EQ(database.getLoadStatus(), Ok);
|
||||
}
|
||||
|
||||
TEST(ModelContract, DeckListModelKeepsContractWhileZonesAndCardsChange)
|
||||
{
|
||||
const QSharedPointer<DeckList> deck = makeDeck();
|
||||
DeckListModel model(nullptr, deck);
|
||||
|
||||
test_support::ModelContractChecker checker(&model);
|
||||
|
||||
ASSERT_NE(deck->getTree()->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
CardInfoPtr bolt = CardInfo::newInstance("Lightning Bolt");
|
||||
const QModelIndex added = model.addCard(ExactCard(bolt), DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
CardInfoPtr bears = CardInfo::newInstance("Grizzly Bears");
|
||||
bears->setProperty(Mtg::MainCardType, "Creature");
|
||||
const QModelIndex grouped = model.addCard(ExactCard(bears), "Removal");
|
||||
ASSERT_TRUE(grouped.isValid());
|
||||
|
||||
ASSERT_TRUE(model.removeRows(grouped.row(), 1, grouped.parent()));
|
||||
}
|
||||
|
||||
TEST(ModelContract, DeckListModelEmitsCardAddedSignal)
|
||||
{
|
||||
const QSharedPointer<DeckList> deck = makeDeck();
|
||||
DeckListModel model(nullptr, deck);
|
||||
QSignalSpy spy(&model, &DeckListModel::cardAddedAt);
|
||||
|
||||
const QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), DECK_ZONE_MAIN);
|
||||
|
||||
ASSERT_TRUE(added.isValid());
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(ModelContract, CardDatabaseModelAndDisplayProxyKeepContract)
|
||||
{
|
||||
CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(),
|
||||
new NoopCardSetPriorityController());
|
||||
ASSERT_NO_FATAL_FAILURE(loadCardDatabase(database));
|
||||
|
||||
CardDatabaseModel source(&database, false);
|
||||
test_support::ModelContractChecker sourceChecker(&source);
|
||||
|
||||
CardDatabaseDisplayModel display;
|
||||
display.setSourceModel(&source);
|
||||
test_support::ModelContractChecker displayChecker(&display);
|
||||
|
||||
ASSERT_GT(source.rowCount(), 0);
|
||||
EXPECT_EQ(display.rowCount(), source.rowCount());
|
||||
|
||||
// A card row is a leaf, so its index must not claim child columns. The
|
||||
// tester does not check this, hence the explicit assertion.
|
||||
const QModelIndex sourceCard = source.index(0, 0);
|
||||
ASSERT_TRUE(sourceCard.isValid());
|
||||
EXPECT_EQ(source.columnCount(sourceCard), 0);
|
||||
const QModelIndex displayCard = display.index(0, 0);
|
||||
ASSERT_TRUE(displayCard.isValid());
|
||||
EXPECT_EQ(display.columnCount(displayCard), 0);
|
||||
|
||||
QSignalSpy dirtySpy(&display, &CardDatabaseDisplayModel::modelDirty);
|
||||
display.setCardName("Do");
|
||||
ASSERT_TRUE(dirtySpy.wait());
|
||||
EXPECT_GT(display.rowCount(), 0);
|
||||
EXPECT_LT(display.rowCount(), source.rowCount());
|
||||
}
|
||||
|
||||
TEST(ModelContract, SetsModelKeepsContract)
|
||||
{
|
||||
CardDatabase database(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(),
|
||||
new NoopCardSetPriorityController());
|
||||
ASSERT_NO_FATAL_FAILURE(loadCardDatabase(database));
|
||||
|
||||
SetsModel model(&database);
|
||||
test_support::ModelContractChecker checker(&model);
|
||||
|
||||
ASSERT_GT(model.rowCount(), 0);
|
||||
ASSERT_GT(model.columnCount(), 0);
|
||||
}
|
||||
|
||||
TEST(ModelContract, SettingsSignalIsObservableWithQSignalSpy)
|
||||
{
|
||||
QTemporaryDir tempDir;
|
||||
ASSERT_TRUE(tempDir.isValid());
|
||||
DownloadSettings settings(tempDir.path() + "/", nullptr);
|
||||
QSignalSpy spy(&settings, &DownloadSettings::downloadSpoilerStatusChanged);
|
||||
|
||||
settings.setDownloadSpoilerStatus(true);
|
||||
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -9,3 +9,15 @@ cockatrice_add_test(
|
|||
LABELS
|
||||
server
|
||||
)
|
||||
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
player_order_test
|
||||
SOURCES
|
||||
player_order_test.cpp
|
||||
LIBS
|
||||
libcockatrice_network_server_remote
|
||||
libcockatrice_rng
|
||||
LABELS
|
||||
server
|
||||
)
|
||||
|
|
|
|||
268
tests/movecard_tests/player_order_test.cpp
Normal file
268
tests/movecard_tests/player_order_test.cpp
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#include "game/game_config.h"
|
||||
#include "game/role_info.h"
|
||||
#include "game/server_abstract_participant.h"
|
||||
#include "game/server_game.h"
|
||||
#include "server_abstractuserinterface.h"
|
||||
#include "server_response_containers.h"
|
||||
#include "server_room.h"
|
||||
#include "server_test_helpers.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/room_event.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/session_event.pb.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
|
||||
RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server
|
||||
|
||||
class ScriptedRng : public RNG_Abstract
|
||||
{
|
||||
public:
|
||||
QList<unsigned int> values;
|
||||
|
||||
unsigned int rand(int min, int max) override
|
||||
{
|
||||
const unsigned int value = values.takeFirst();
|
||||
EXPECT_GE(value, static_cast<unsigned int>(min));
|
||||
EXPECT_LE(value, static_cast<unsigned int>(max));
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
class FakeUserInterface : public Server_AbstractUserInterface
|
||||
{
|
||||
public:
|
||||
FakeUserInterface(Server *_server, const ServerInfo_User &_userInfo) : Server_AbstractUserInterface(_server)
|
||||
{
|
||||
setUserInfo(_userInfo);
|
||||
}
|
||||
|
||||
int getLastCommandTime() const override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool addSaidMessageSize(int) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void sendProtocolItem(const Response &) override
|
||||
{
|
||||
}
|
||||
|
||||
void sendProtocolItem(const SessionEvent &) override
|
||||
{
|
||||
}
|
||||
|
||||
void sendProtocolItem(const GameEventContainer &) override
|
||||
{
|
||||
}
|
||||
|
||||
void sendProtocolItem(const RoomEvent &) override
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
QStringList participantNamesInOrder(const Server_Game &game)
|
||||
{
|
||||
QStringList names;
|
||||
for (int id : game.getSeatOrder()) {
|
||||
const auto *participant = game.getParticipants().value(id, nullptr);
|
||||
if (participant) {
|
||||
names.append(QString::fromStdString(participant->getUserInfo()->name()));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
QList<int> participantIdsInOrder(const Server_Game &game)
|
||||
{
|
||||
return game.getSeatOrder();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class PlayerOrderTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
ServerInfo_User creator;
|
||||
ServerInfo_User userB;
|
||||
ServerInfo_User userC;
|
||||
|
||||
PlayerOrderTest()
|
||||
{
|
||||
creator.set_name("A");
|
||||
creator.set_user_level(ServerInfo_User::IsRegistered);
|
||||
userB.set_name("B");
|
||||
userB.set_user_level(ServerInfo_User::IsRegistered);
|
||||
userC.set_name("C");
|
||||
userC.set_user_level(ServerInfo_User::IsRegistered);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PlayerOrderTest, JoinOrderIsSeatOrder)
|
||||
{
|
||||
FakeServer server;
|
||||
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
|
||||
FakeUserInterface uiA(&server, creator);
|
||||
FakeUserInterface uiB(&server, userB);
|
||||
FakeUserInterface uiC(&server, userC);
|
||||
GameConfig config{.creatorInfo = creator,
|
||||
.gameId = 1,
|
||||
.description = QString(),
|
||||
.password = QString(),
|
||||
.maxPlayers = 3,
|
||||
.gameTypes = QList<int>(),
|
||||
.onlyBuddies = false,
|
||||
.onlyRegistered = false,
|
||||
.spectatorsAllowed = false,
|
||||
.spectatorsNeedPassword = false,
|
||||
.spectatorsCanTalk = false,
|
||||
.spectatorsSeeEverything = false,
|
||||
.startingLifeTotal = 20,
|
||||
.shareDecklistsOnLoad = false,
|
||||
.shufflePlayers = false};
|
||||
Server_Game game(config, &room);
|
||||
ResponseContainer rc(0);
|
||||
|
||||
game.addPlayer(&uiA, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, false);
|
||||
|
||||
EXPECT_EQ(participantNamesInOrder(game), QStringList({"A", "B", "C"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({0, 1, 2}));
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
EXPECT_EQ(game.getPlayerCount(), 3);
|
||||
}
|
||||
|
||||
TEST_F(PlayerOrderTest, ReorderPlayerSeats)
|
||||
{
|
||||
FakeServer server;
|
||||
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
|
||||
FakeUserInterface uiA(&server, creator);
|
||||
FakeUserInterface uiB(&server, userB);
|
||||
FakeUserInterface uiC(&server, userC);
|
||||
GameConfig config{.creatorInfo = creator,
|
||||
.gameId = 1,
|
||||
.description = QString(),
|
||||
.password = QString(),
|
||||
.maxPlayers = 3,
|
||||
.gameTypes = QList<int>(),
|
||||
.onlyBuddies = false,
|
||||
.onlyRegistered = false,
|
||||
.spectatorsAllowed = false,
|
||||
.spectatorsNeedPassword = false,
|
||||
.spectatorsCanTalk = false,
|
||||
.spectatorsSeeEverything = false,
|
||||
.startingLifeTotal = 20,
|
||||
.shareDecklistsOnLoad = false,
|
||||
.shufflePlayers = false};
|
||||
Server_Game game(config, &room);
|
||||
ResponseContainer rc(0);
|
||||
|
||||
game.addPlayer(&uiA, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, false);
|
||||
|
||||
game.reorderPlayerSeats({"C", "A", "B"});
|
||||
|
||||
// player ids are stable; only the seat order changes
|
||||
EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({2, 0, 1}));
|
||||
EXPECT_EQ(game.getParticipants().size(), 3);
|
||||
|
||||
// the host keeps its seat: creator A is still in the game, so host stays 0
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
|
||||
// no re-keying means every mapping keyed by player id stays valid
|
||||
const int gameId = game.getGameId();
|
||||
EXPECT_EQ(uiA.getGames().value(gameId).second, 0);
|
||||
EXPECT_EQ(uiB.getGames().value(gameId).second, 1);
|
||||
EXPECT_EQ(uiC.getGames().value(gameId).second, 2);
|
||||
}
|
||||
|
||||
TEST_F(PlayerOrderTest, ReorderPlayerSeatsKeepsPlayerCount)
|
||||
{
|
||||
FakeServer server;
|
||||
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
|
||||
FakeUserInterface uiA(&server, creator);
|
||||
FakeUserInterface uiB(&server, userB);
|
||||
FakeUserInterface uiC(&server, userC);
|
||||
GameConfig config{.creatorInfo = creator,
|
||||
.gameId = 1,
|
||||
.description = QString(),
|
||||
.password = QString(),
|
||||
.maxPlayers = 3,
|
||||
.gameTypes = QList<int>(),
|
||||
.onlyBuddies = false,
|
||||
.onlyRegistered = false,
|
||||
.spectatorsAllowed = false,
|
||||
.spectatorsNeedPassword = false,
|
||||
.spectatorsCanTalk = false,
|
||||
.spectatorsSeeEverything = false,
|
||||
.startingLifeTotal = 20,
|
||||
.shareDecklistsOnLoad = false,
|
||||
.shufflePlayers = false};
|
||||
Server_Game game(config, &room);
|
||||
ResponseContainer rc(0);
|
||||
|
||||
game.addPlayer(&uiA, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, false);
|
||||
|
||||
// listing only a subset appends the remaining players in their current order
|
||||
game.reorderPlayerSeats({"C"});
|
||||
|
||||
EXPECT_EQ(participantNamesInOrder(game), QStringList({"C", "A", "B"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({2, 0, 1}));
|
||||
EXPECT_EQ(game.getPlayerCount(), 3);
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
}
|
||||
|
||||
TEST_F(PlayerOrderTest, ShufflePlayerSeatsIsAPermutation)
|
||||
{
|
||||
FakeServer server;
|
||||
Server_Room room(0, 0, "", "", "", "", false, "", {}, &server);
|
||||
FakeUserInterface uiA(&server, creator);
|
||||
FakeUserInterface uiB(&server, userB);
|
||||
FakeUserInterface uiC(&server, userC);
|
||||
GameConfig config{.creatorInfo = creator,
|
||||
.gameId = 1,
|
||||
.description = QString(),
|
||||
.password = QString(),
|
||||
.maxPlayers = 3,
|
||||
.gameTypes = QList<int>(),
|
||||
.onlyBuddies = false,
|
||||
.onlyRegistered = false,
|
||||
.spectatorsAllowed = false,
|
||||
.spectatorsNeedPassword = false,
|
||||
.spectatorsCanTalk = false,
|
||||
.spectatorsSeeEverything = false,
|
||||
.startingLifeTotal = 20,
|
||||
.shareDecklistsOnLoad = false,
|
||||
.shufflePlayers = false};
|
||||
Server_Game game(config, &room);
|
||||
ResponseContainer rc(0);
|
||||
|
||||
game.addPlayer(&uiA, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiB, rc, RoleInfo{}, false);
|
||||
game.addPlayer(&uiC, rc, RoleInfo{}, false);
|
||||
|
||||
// fisher-yates on [A, B, C]: rand(0, 2) -> 1 (take B), rand(0, 1) -> 0 (take A), rand(0, 0) -> 0 (take C)
|
||||
auto *scriptedRng = new ScriptedRng;
|
||||
scriptedRng->values = {1, 0, 0};
|
||||
rng = scriptedRng;
|
||||
|
||||
game.shufflePlayerSeats();
|
||||
|
||||
EXPECT_EQ(participantNamesInOrder(game), QStringList({"B", "A", "C"}));
|
||||
EXPECT_EQ(participantIdsInOrder(game), QList<int>({1, 0, 2}));
|
||||
QSet<int> idSet(participantIdsInOrder(game).cbegin(), participantIdsInOrder(game).cend());
|
||||
EXPECT_EQ(idSet.size(), 3); // ids are unique
|
||||
// ids are stable, so the host (creator A, id 0) is unchanged
|
||||
EXPECT_EQ(game.getHostId(), 0);
|
||||
}
|
||||
84
tests/moxfield_signature_spells_test.cpp
Normal file
84
tests/moxfield_signature_spells_test.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#include "client/network/parsers/interface_json_deck_parser.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
TEST(MoxfieldImport, SignatureSpellIsAddedToMainWithoutReplacingCommanderBanner)
|
||||
{
|
||||
// Moxfield v2 board objects map IDs to entries with quantity and card.
|
||||
const auto input = QJsonDocument::fromJson(R"json({
|
||||
"name": "Oathbreaker example", "description": "Notes retained",
|
||||
"mainboard": {"land": {"quantity": 1, "card": {"name": "Island", "set": "abc", "cn": "1"}}},
|
||||
"sideboard": {"side": {"quantity": 1, "card": {"name": "Mountain", "set": "abc", "cn": "2"}}},
|
||||
"commanders": {"walker": {"quantity": 1, "card": {"name": "Jace", "set": "abc", "cn": "3", "scryfall_id": "walker-id"}}},
|
||||
"signatureSpells": {"spell": {"quantity": 1, "card": {"name": "Lightning Bolt", "set": "xyz", "cn": "4", "scryfall_id": "spell-id"}}}
|
||||
})json")
|
||||
.object();
|
||||
MoxfieldJsonParser parser;
|
||||
DeckList deck = parser.parse(input);
|
||||
int mainSpellCount = 0;
|
||||
int sideSpellCount = 0;
|
||||
int mainLands = 0;
|
||||
int sideLands = 0;
|
||||
deck.forEachCard([&](InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
if (card->getName() == "Island" && zone->getName() == DECK_ZONE_MAIN) {
|
||||
mainLands += card->getNumber();
|
||||
}
|
||||
if (card->getName() == "Mountain" && zone->getName() == DECK_ZONE_SIDE) {
|
||||
sideLands += card->getNumber();
|
||||
}
|
||||
if (card->getName() != "Lightning Bolt") {
|
||||
return;
|
||||
}
|
||||
if (zone->getName() == DECK_ZONE_MAIN) {
|
||||
mainSpellCount += card->getNumber();
|
||||
EXPECT_EQ(card->getCardSetShortName(), "XYZ");
|
||||
EXPECT_EQ(card->getCardCollectorNumber(), "4");
|
||||
EXPECT_EQ(card->getCardProviderId(), "spell-id");
|
||||
} else if (zone->getName() == DECK_ZONE_SIDE) {
|
||||
sideSpellCount += card->getNumber();
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(mainSpellCount, 1);
|
||||
EXPECT_EQ(sideSpellCount, 0);
|
||||
EXPECT_EQ(mainLands, 1);
|
||||
EXPECT_EQ(sideLands, 1);
|
||||
EXPECT_EQ(deck.getName(), "Oathbreaker example");
|
||||
EXPECT_EQ(deck.getComments(), "Notes retained");
|
||||
EXPECT_EQ(deck.getBannerCard().name, "Jace");
|
||||
EXPECT_EQ(deck.getBannerCard().providerId, "walker-id");
|
||||
}
|
||||
|
||||
TEST(MoxfieldImport, QuantityAwareDeckInsertionRetainsCountAndPrinting)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *card = deck.addCards("Island", 2, DECK_ZONE_MAIN, -1, "ABC", "4", "id");
|
||||
ASSERT_NE(card, nullptr);
|
||||
EXPECT_EQ(card->getNumber(), 2);
|
||||
EXPECT_EQ(card->getCardProviderId(), "id");
|
||||
EXPECT_EQ(deck.addCards("Island", 0, DECK_ZONE_MAIN), nullptr);
|
||||
}
|
||||
|
||||
TEST(OnlineImport, ArchidektPreservesNameDescriptionAndCards)
|
||||
{
|
||||
const auto input = QJsonDocument::fromJson(R"json({
|
||||
"name": "Online deck", "description": "Deck notes",
|
||||
"cards": [{"quantity": 2, "card": {
|
||||
"oracleCard": {"name": "Island"}, "edition": {"editioncode": "abc"}, "collectorNumber": "1"
|
||||
}}]
|
||||
})json")
|
||||
.object();
|
||||
ArchidektJsonParser parser;
|
||||
const DeckList deck = parser.parse(input);
|
||||
int count = 0;
|
||||
deck.forEachCard([&](InnerDecklistNode *zone, DecklistCardNode *card) {
|
||||
if (zone->getName() == DECK_ZONE_MAIN && card->getName() == "Island") {
|
||||
count += card->getNumber();
|
||||
}
|
||||
});
|
||||
EXPECT_EQ(count, 2);
|
||||
EXPECT_EQ(deck.getName(), "Online deck");
|
||||
EXPECT_EQ(deck.getComments(), "Deck notes");
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ cockatrice_add_test(
|
|||
../../oracle/src/oracleimporter.cpp
|
||||
../../oracle/src/parsehelpers.cpp
|
||||
../../oracle/src/raw_json_scanner.cpp
|
||||
../../oracle/src/database_writer.cpp
|
||||
oracle_importer_test.cpp
|
||||
LIBS
|
||||
libcockatrice_card
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#include "../../oracle/src/database_writer.h"
|
||||
#include "../../oracle/src/oracleimporter.h"
|
||||
#include "../../oracle/src/raw_json_scanner.h"
|
||||
#include "libcockatrice/card/card_info.h"
|
||||
|
|
@ -6,6 +7,7 @@
|
|||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QDate>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -20,6 +22,7 @@
|
|||
#include <QRegularExpressionMatch>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
|
|
@ -30,6 +33,12 @@
|
|||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <csignal>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
class OracleImporterTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
|
|
@ -1140,6 +1149,212 @@ TEST_F(OracleImporterTest, StartImportAppliesLocalizedData)
|
|||
ASSERT_EQ(result->getLocalizedText("pt"), "Raio causa 3 de dano a qualquer alvo.");
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
QByteArray refreshSource(const QString &name = "New Card")
|
||||
{
|
||||
QJsonObject card{{"name", name}, {"text", "Rules text."}, {"layout", "normal"},
|
||||
{"type", "Artifact"}, {"number", "1"}, {"rarity", "common"}};
|
||||
QJsonObject set{{"code", "TST"}, {"name", "Test Set"}, {"type", "expansion"}, {"cards", QJsonArray{card}}};
|
||||
return QJsonDocument(QJsonObject{{"data", QJsonObject{{"TST", set}}}}).toJson(QJsonDocument::Compact);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(OracleImporterTest, FailedLazyParseKeepsInstalledDatabase)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = dir.filePath("cards.xml");
|
||||
QFile original(path);
|
||||
ASSERT_TRUE(original.open(QIODevice::WriteOnly));
|
||||
ASSERT_EQ(original.write("existing database"), 17);
|
||||
original.close();
|
||||
|
||||
// The metadata scanner skips numeric values without converting them. Qt's
|
||||
// per-set parser rejects an overflowing number after selection succeeds.
|
||||
auto badSet = QJsonDocument::fromJson(refreshSource()).object().value("data").toObject().value("TST").toObject();
|
||||
auto goodSet = badSet;
|
||||
goodSet["code"] = "AAA";
|
||||
goodSet["name"] = "A successful first set";
|
||||
auto badCard = badSet.value("cards").toArray().first().toObject();
|
||||
badCard["number"] = "overflow";
|
||||
badSet["cards"] = QJsonArray{badCard};
|
||||
QByteArray data = QJsonDocument(QJsonObject{{"data", QJsonObject{{"AAA", goodSet}, {"TST", badSet}}}})
|
||||
.toJson(QJsonDocument::Compact);
|
||||
data.replace("\"number\":\"overflow\"", "\"number\":1e9999");
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(data));
|
||||
EXPECT_EQ(importer->startImport(), 0);
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
EXPECT_TRUE(importer->getImportError().contains("TST"));
|
||||
EXPECT_TRUE(importer->getCardList().isEmpty());
|
||||
EXPECT_FALSE(importer->saveToFile(path, {}, {}));
|
||||
ASSERT_TRUE(original.open(QIODevice::ReadOnly));
|
||||
EXPECT_EQ(original.readAll(), "existing database");
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, EmptyImportCannotReplaceInstalledCards)
|
||||
{
|
||||
const QByteArray source = R"({"data":{"TST":{"code":"TST","name":"Test","type":"expansion","cards":[]}}})";
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(source));
|
||||
EXPECT_EQ(importer->startImport(), 0);
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
EXPECT_FALSE(importer->getImportError().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, MissingSourceBytesCannotProduceSuccessfulImport)
|
||||
{
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource()));
|
||||
const auto selected = importer->getSets();
|
||||
importer->releaseSetData();
|
||||
importer->getSets() = selected;
|
||||
EXPECT_EQ(importer->startImport(), 0);
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
EXPECT_TRUE(importer->getImportError().contains("TST"));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CancellationDuringFinalSetCannotSavePartialDatabase)
|
||||
{
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource()));
|
||||
QObject::connect(
|
||||
importer, &OracleImporter::setIndexChanged, importer,
|
||||
[this](int, int, const QString &name) {
|
||||
if (!name.isEmpty()) {
|
||||
importer->cancelImport();
|
||||
}
|
||||
},
|
||||
Qt::DirectConnection);
|
||||
EXPECT_EQ(importer->startImport(), 0);
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
EXPECT_TRUE(importer->getCardList().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, CancellationBeforeWorkerStartsIsNotLost)
|
||||
{
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource()));
|
||||
importer->prepareImport();
|
||||
importer->cancelImport();
|
||||
EXPECT_EQ(importer->startImport(), 0);
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
importer->prepareImport();
|
||||
EXPECT_EQ(importer->startImport(), 1);
|
||||
EXPECT_TRUE(importer->hasSuccessfulImport());
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, NewImportReplacesPreviousInMemoryCards)
|
||||
{
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource("Old Card")));
|
||||
ASSERT_EQ(importer->startImport(), 1);
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource("New Card")));
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
ASSERT_EQ(importer->startImport(), 1);
|
||||
EXPECT_EQ(importer->getCardList().size(), 1);
|
||||
EXPECT_FALSE(importer->getCardList().contains("Old Card"));
|
||||
EXPECT_TRUE(importer->getCardList().contains("New Card"));
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, FailedSourceLoadCannotReusePreviousSuccessfulImport)
|
||||
{
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource()));
|
||||
ASSERT_EQ(importer->startImport(), 1);
|
||||
ASSERT_FALSE(importer->readSetsFromByteArray("invalid JSON"));
|
||||
EXPECT_FALSE(importer->hasSuccessfulImport());
|
||||
EXPECT_EQ(importer->startImport(), 0);
|
||||
}
|
||||
|
||||
TEST_F(OracleImporterTest, SuccessfulRefreshCommitsCompleteXml)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = dir.filePath("cards.xml");
|
||||
ASSERT_TRUE(importer->readSetsFromByteArray(refreshSource()));
|
||||
ASSERT_EQ(importer->startImport(), 1);
|
||||
importer->releaseSetData();
|
||||
ASSERT_TRUE(importer->saveToFile(path, "test-source", "v1"));
|
||||
QFile file(path);
|
||||
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
const QByteArray xml = file.readAll();
|
||||
EXPECT_TRUE(xml.contains("New Card"));
|
||||
EXPECT_TRUE(xml.contains("test-source"));
|
||||
EXPECT_TRUE(xml.contains("</cockatrice_carddatabase>"));
|
||||
}
|
||||
|
||||
TEST(OracleDatabaseWriter, AcceptsCompleteSupportedVersions)
|
||||
{
|
||||
EXPECT_TRUE(OracleDatabase::validateXml(
|
||||
"<?xml version=\"1.0\"?><cockatrice_carddatabase version=\"3\"><cards/></cockatrice_carddatabase>")
|
||||
.isEmpty());
|
||||
EXPECT_TRUE(OracleDatabase::validateXml("<cockatrice_carddatabase version=\"4\"><cards/></cockatrice_carddatabase>")
|
||||
.isEmpty());
|
||||
EXPECT_FALSE(
|
||||
OracleDatabase::validateXml("<cockatrice_carddatabase version=\"5\"><cards/></cockatrice_carddatabase>")
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
TEST(OracleDatabaseWriter, InvalidDownloadPreservesInstalledBytes)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = dir.filePath("tokens.xml");
|
||||
QFile original(path);
|
||||
ASSERT_TRUE(original.open(QIODevice::WriteOnly));
|
||||
ASSERT_EQ(original.write("installed tokens"), 16);
|
||||
original.close();
|
||||
for (const QByteArray &payload : {QByteArray("<html>server error</html>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><cards>"), QByteArray()}) {
|
||||
QString error;
|
||||
EXPECT_FALSE(OracleDatabase::saveXml(path, payload, &error));
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
ASSERT_TRUE(original.open(QIODevice::ReadOnly));
|
||||
EXPECT_EQ(original.readAll(), "installed tokens");
|
||||
original.close();
|
||||
}
|
||||
}
|
||||
|
||||
TEST(OracleDatabaseWriter, CompleteDownloadReplacesInstalledBytes)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = dir.filePath("cards.xml");
|
||||
const QByteArray data("<cockatrice_carddatabase version=\"4\"><cards/></cockatrice_carddatabase>");
|
||||
QString error = "previous failure";
|
||||
ASSERT_TRUE(OracleDatabase::saveXml(path, data, &error));
|
||||
EXPECT_TRUE(error.isEmpty());
|
||||
QFile file(path);
|
||||
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
EXPECT_EQ(file.readAll(), data);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
TEST(OracleDatabaseWriter, ShortWriteKeepsPreviouslyInstalledDatabase)
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = dir.filePath("cards.xml");
|
||||
QFile original(path);
|
||||
ASSERT_TRUE(original.open(QIODevice::WriteOnly));
|
||||
ASSERT_EQ(original.write("installed database"), 18);
|
||||
original.close();
|
||||
const QByteArray data = "<cockatrice_carddatabase version=\"4\"><!--" + QByteArray(16384, 'x') +
|
||||
"--><cards/></cockatrice_carddatabase>";
|
||||
// A real filesystem write failure, isolated in the death-test child so the
|
||||
// process-wide file limit never affects other cases or the parent runner.
|
||||
ASSERT_EXIT(
|
||||
{
|
||||
std::signal(SIGXFSZ, SIG_IGN);
|
||||
struct rlimit limit;
|
||||
limit.rlim_cur = 128;
|
||||
limit.rlim_max = 128;
|
||||
if (setrlimit(RLIMIT_FSIZE, &limit) != 0) {
|
||||
_exit(2);
|
||||
}
|
||||
_exit(OracleDatabase::saveXml(path, data) ? 1 : 0);
|
||||
},
|
||||
::testing::ExitedWithCode(0), "");
|
||||
ASSERT_TRUE(original.open(QIODevice::ReadOnly));
|
||||
EXPECT_EQ(original.readAll(), "installed database");
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,17 @@ TEST(PasswordHashTest, DeriveKeyMatchesKnownVector)
|
|||
ASSERT_EQ(derived.toHex(), expected.toHex());
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, DeriveKeyCoversTheWholeAcceptedRangeOfR)
|
||||
{
|
||||
// costParamsAreSane accepts r up to 32. The memory budget handed to OpenSSL has to cover
|
||||
// scrypt's XY array (256 * r bytes) on top of V and B, or derivation fails for r >= 17.
|
||||
for (const int r : {17, 32}) {
|
||||
const QByteArray derived = PasswordHasher::deriveKey("password", QByteArray("NaCl"), 1024, r, 1);
|
||||
ASSERT_FALSE(derived.isEmpty()) << "deriveKey failed for accepted r=" << r;
|
||||
ASSERT_EQ(derived.size(), SCRYPT_VERIFIER_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PasswordHashTest, PasswordVerifierRoundTrip)
|
||||
{
|
||||
const QString stored = PasswordHasher::generatePasswordVerifier("hunter2");
|
||||
|
|
|
|||
234
tests/server_forced_password_change_test.cpp
Normal file
234
tests/server_forced_password_change_test.cpp
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/** @file server_forced_password_change_test.cpp
|
||||
* @brief Tests for the restricted session a forced password change leaves behind at login.
|
||||
* @ingroup Tests
|
||||
*/
|
||||
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/server_message.pb.h"
|
||||
#include "server.h"
|
||||
#include "server_database_interface.h"
|
||||
#include "server_protocolhandler.h"
|
||||
|
||||
#include <QString>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/room_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/session_commands.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 PasswordChangeRequired;
|
||||
}
|
||||
ServerInfo_User getUserData(const QString &name, bool withId) override
|
||||
{
|
||||
ServerInfo_User user;
|
||||
user.set_name(name.toStdString());
|
||||
user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsModerator);
|
||||
if (withId) {
|
||||
user.set_id(1);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
int getNextReplayId() override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
int getNextGameId() override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
int getActiveUserCount(QString) override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
class FakeServer : public Server
|
||||
{
|
||||
public:
|
||||
FakeServer()
|
||||
{
|
||||
setDatabaseInterface(new MockDatabaseInterface());
|
||||
}
|
||||
};
|
||||
|
||||
class TestHandler : public Server_ProtocolHandler
|
||||
{
|
||||
public:
|
||||
TestHandler(Server *_server, Server_DatabaseInterface *_db) : Server_ProtocolHandler(_server, _db)
|
||||
{
|
||||
}
|
||||
|
||||
QString getAddress() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
QString getConnectionType() const override
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
Response::ResponseCode lastResponseCode = Response::RespNothing;
|
||||
int lastExtendedSessionCommand = -1;
|
||||
|
||||
protected:
|
||||
Response::ResponseCode
|
||||
processExtendedSessionCommand(int cmdType, const SessionCommand & /*cmd*/, ResponseContainer & /*rc*/) override
|
||||
{
|
||||
lastExtendedSessionCommand = cmdType;
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
void transmitProtocolItem(const ServerMessage &item) override
|
||||
{
|
||||
if (item.message_type() == ServerMessage::RESPONSE) {
|
||||
lastResponseCode = item.response().response_code();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ForcedPasswordChangeTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
FakeServer server;
|
||||
TestHandler handler{&server, server.getDatabaseInterface()};
|
||||
|
||||
void login()
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.set_cmd_id(1);
|
||||
Command_Login cmd;
|
||||
cmd.set_user_name("mod");
|
||||
cont.add_session_command()->MutableExtension(Command_Login::ext)->CopyFrom(cmd);
|
||||
handler.processCommandContainer(cont);
|
||||
}
|
||||
|
||||
Response::ResponseCode sendPing()
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.set_cmd_id(2);
|
||||
cont.add_session_command()->MutableExtension(Command_Ping::ext);
|
||||
handler.processCommandContainer(cont);
|
||||
return handler.lastResponseCode;
|
||||
}
|
||||
|
||||
Response::ResponseCode sendSessionCommand()
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.set_cmd_id(3);
|
||||
cont.add_session_command()->MutableExtension(Command_ListRooms::ext);
|
||||
handler.processCommandContainer(cont);
|
||||
return handler.lastResponseCode;
|
||||
}
|
||||
|
||||
Response::ResponseCode sendRoomCommand()
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.set_cmd_id(4);
|
||||
cont.add_room_command()->MutableExtension(Command_RoomSay::ext)->set_message("hello");
|
||||
handler.processCommandContainer(cont);
|
||||
return handler.lastResponseCode;
|
||||
}
|
||||
|
||||
Response::ResponseCode sendMixedSessionAndRoomCommand()
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.set_cmd_id(5);
|
||||
cont.add_session_command()->MutableExtension(Command_Ping::ext);
|
||||
cont.add_room_command()->MutableExtension(Command_RoomSay::ext)->set_message("hello");
|
||||
handler.processCommandContainer(cont);
|
||||
return handler.lastResponseCode;
|
||||
}
|
||||
|
||||
Response::ResponseCode sendAccountPassword()
|
||||
{
|
||||
CommandContainer cont;
|
||||
cont.set_cmd_id(6);
|
||||
Command_AccountPassword cmd;
|
||||
cmd.set_old_password("temporary-password");
|
||||
cmd.set_new_password("replacement-password");
|
||||
cont.add_session_command()->MutableExtension(Command_AccountPassword::ext)->CopyFrom(cmd);
|
||||
handler.processCommandContainer(cont);
|
||||
return handler.lastResponseCode;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ForcedPasswordChangeTest, LoginReportsTheRequiredChange)
|
||||
{
|
||||
login();
|
||||
|
||||
EXPECT_EQ(Response::RespPasswordChangeRequired, handler.lastResponseCode);
|
||||
ASSERT_NE(nullptr, handler.getUserInfo());
|
||||
EXPECT_EQ("mod", QString::fromStdString(handler.getUserInfo()->name()));
|
||||
}
|
||||
|
||||
TEST_F(ForcedPasswordChangeTest, PendingSessionIsNotRegisteredAsAUser)
|
||||
{
|
||||
login();
|
||||
// The session may change its password, but it must not take a user slot or appear in
|
||||
// the session table before the client reconnects with the new password.
|
||||
EXPECT_FALSE(handler.getUserInfo()->has_session_id());
|
||||
EXPECT_EQ(0, server.getUsersCount());
|
||||
}
|
||||
|
||||
TEST_F(ForcedPasswordChangeTest, PendingModeratorIsNotReportedOnline)
|
||||
{
|
||||
login();
|
||||
server.addClient(&handler);
|
||||
|
||||
EXPECT_TRUE(server.getOnlineModeratorList().isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(ForcedPasswordChangeTest, OnlyPingAndPasswordChangeAreServed)
|
||||
{
|
||||
login();
|
||||
|
||||
EXPECT_EQ(Response::RespOk, sendPing());
|
||||
EXPECT_EQ(Response::RespFunctionNotAllowed, sendSessionCommand());
|
||||
EXPECT_EQ(Response::RespFunctionNotAllowed, sendRoomCommand());
|
||||
// Dispatch prefers game/room over session, so a container carrying both must not slip past
|
||||
// the restriction by carrying a session command.
|
||||
EXPECT_EQ(Response::RespFunctionNotAllowed, sendMixedSessionAndRoomCommand());
|
||||
}
|
||||
|
||||
TEST_F(ForcedPasswordChangeTest, AccountPasswordReachesTheCommand)
|
||||
{
|
||||
login();
|
||||
// The forced change is useless if the gate refuses the command it needs. ACCOUNT_PASSWORD is
|
||||
// served by the extended-command hook, so it must arrive there instead of being filtered.
|
||||
EXPECT_EQ(Response::RespOk, sendAccountPassword());
|
||||
EXPECT_EQ(SessionCommand::ACCOUNT_PASSWORD, handler.lastExtendedSessionCommand);
|
||||
}
|
||||
|
||||
TEST_F(ForcedPasswordChangeTest, CompletedLoginIsStillReportedOnline)
|
||||
{
|
||||
TestHandler loggedIn{&server, server.getDatabaseInterface()};
|
||||
ServerInfo_User user;
|
||||
user.set_name("online-mod");
|
||||
user.set_user_level(ServerInfo_User::IsUser | ServerInfo_User::IsRegistered | ServerInfo_User::IsModerator);
|
||||
user.set_session_id(7);
|
||||
loggedIn.setUserInfo(user);
|
||||
server.addClient(&loggedIn);
|
||||
|
||||
const QList<QString> moderators = server.getOnlineModeratorList();
|
||||
ASSERT_EQ(1, moderators.size());
|
||||
EXPECT_EQ("online-mod", moderators.first());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
#include "game/game_config.h"
|
||||
#include "game/role_info.h"
|
||||
#include "game/server_game.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "server.h"
|
||||
|
|
@ -119,13 +120,13 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, "wrong", {}, true), 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);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", {}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {}, true), 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);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isJudge = true}, false), Response::RespWrongPassword);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "secret", {.isJudge = true}, false), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassRegisteredOnlyRestriction)
|
||||
|
|
@ -133,9 +134,9 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, false), Response::RespUserLevelTooLow);
|
||||
// An unregistered judge may enter when overriding restrictions.
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), false, true, false), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&unregisteredJudge, QString(), {}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassBuddiesOnlyRestriction)
|
||||
|
|
@ -143,10 +144,10 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {}, true), 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);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, QString(), {}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), {}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, StaffBypassSpectatorsNotAllowedRestriction)
|
||||
|
|
@ -154,11 +155,11 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isSpectator = true}, 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);
|
||||
EXPECT_EQ(game->checkJoin(&moderator, "wrong", {.isSpectator = true}, true), Response::RespOk);
|
||||
EXPECT_EQ(game->checkJoin(&judge, "wrong", {.isSpectator = true}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
TEST_F(GameJoinOverrideTest, JudgeOverrideDoesNotGrantJudgeJoinToPlainUser)
|
||||
|
|
@ -166,8 +167,8 @@ 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);
|
||||
EXPECT_EQ(game->checkJoin(&plainUser, QString(), {.isJudge = true}, true), Response::RespUserLevelTooLow);
|
||||
EXPECT_EQ(game->checkJoin(&judge, QString(), {.isJudge = true}, true), Response::RespOk);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
cockatrice_add_test(NAME settings_manager_test SOURCES settings_manager_test.cpp LIBS libcockatrice_settings)
|
||||
|
||||
cockatrice_add_test(NAME servers_settings_test SOURCES servers_settings_test.cpp LIBS libcockatrice_settings)
|
||||
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
settings_defaults_test
|
||||
|
|
|
|||
228
tests/settings/servers_settings_test.cpp
Normal file
228
tests/settings/servers_settings_test.cpp
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
class ServersSettingsTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
QTemporaryDir tempDir;
|
||||
QString settingsPath;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
settingsPath = tempDir.path() + "/";
|
||||
}
|
||||
|
||||
QString settingsFile() const
|
||||
{
|
||||
return settingsPath + "servers.ini";
|
||||
}
|
||||
|
||||
void writeLegacyServer(int index,
|
||||
const QString &saveName,
|
||||
const QString &host,
|
||||
const QString &port,
|
||||
const QString &username,
|
||||
const QString &password,
|
||||
bool savePassword,
|
||||
const QString &site)
|
||||
{
|
||||
QSettings settings(settingsFile(), QSettings::IniFormat);
|
||||
const QString prefix = QStringLiteral("server/server_details/");
|
||||
settings.setValue(prefix + QStringLiteral("saveName%1").arg(index), saveName);
|
||||
settings.setValue(prefix + QStringLiteral("server%1").arg(index), host);
|
||||
settings.setValue(prefix + QStringLiteral("port%1").arg(index), port);
|
||||
settings.setValue(prefix + QStringLiteral("username%1").arg(index), username);
|
||||
settings.setValue(prefix + QStringLiteral("password%1").arg(index), password);
|
||||
settings.setValue(prefix + QStringLiteral("savePassword%1").arg(index), savePassword);
|
||||
settings.setValue(prefix + QStringLiteral("site%1").arg(index), site);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ServersSettingsTest, AddServerStoresAllFields)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "https://alpha.example");
|
||||
|
||||
const auto servers = settings.getServers();
|
||||
ASSERT_EQ(servers.size(), 1);
|
||||
EXPECT_EQ(servers.first().saveName, QString("Alpha"));
|
||||
EXPECT_EQ(servers.first().host, QString("alpha.example"));
|
||||
EXPECT_EQ(servers.first().port, QString("4748"));
|
||||
EXPECT_EQ(servers.first().username, QString("alice"));
|
||||
EXPECT_EQ(servers.first().password, QString("secret"));
|
||||
EXPECT_TRUE(servers.first().savePassword);
|
||||
EXPECT_EQ(servers.first().site, QString("https://alpha.example"));
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, AddExistingHostAndPortUpdatesInsteadOfDuplicating)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "site-a");
|
||||
settings.addNewServer("Renamed", "alpha.example", "4748", "bob", "new-secret", true, "site-b");
|
||||
|
||||
const auto servers = settings.getServers();
|
||||
ASSERT_EQ(servers.size(), 1);
|
||||
EXPECT_EQ(servers.first().saveName, QString("Renamed"));
|
||||
EXPECT_EQ(servers.first().username, QString("bob"));
|
||||
EXPECT_EQ(servers.first().password, QString("new-secret"));
|
||||
EXPECT_EQ(servers.first().site, QString("site-b"));
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, RemoveServerRemovesOnlyMatchingHostAndKeepsOrder)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "a", true, "");
|
||||
settings.addNewServer("Beta", "beta.example", "4748", "bob", "b", true, "");
|
||||
settings.addNewServer("Gamma", "gamma.example", "4748", "carol", "c", true, "");
|
||||
|
||||
settings.removeServer("beta.example");
|
||||
|
||||
const auto servers = settings.getServers();
|
||||
ASSERT_EQ(servers.size(), 2);
|
||||
EXPECT_EQ(servers.at(0).saveName, QString("Alpha"));
|
||||
EXPECT_EQ(servers.at(1).saveName, QString("Gamma"));
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, RemovingLastServerDropsItsStaleArraySlot)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "a", true, "");
|
||||
settings.addNewServer("Beta", "beta.example", "4748", "bob", "b", true, "");
|
||||
settings.addNewServer("Gamma", "gamma.example", "4748", "carol", "gamma-secret", true, "");
|
||||
|
||||
settings.removeServer("gamma.example");
|
||||
|
||||
QSettings raw(settingsFile(), QSettings::IniFormat);
|
||||
EXPECT_FALSE(raw.contains("server/server_details/servers/3/password"));
|
||||
EXPECT_FALSE(raw.contains("server/server_details/servers/3/saveName"));
|
||||
ASSERT_EQ(settings.getServers().size(), 2);
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, UpdateWithoutLossKeepsCredentials)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "");
|
||||
|
||||
EXPECT_TRUE(settings.updateExistingServerWithoutLoss("Renamed", "alpha.example", "1234", "site"));
|
||||
|
||||
const auto server = settings.getServer(settings.findServerIndex("alpha.example", "1234"));
|
||||
ASSERT_TRUE(server.has_value());
|
||||
EXPECT_EQ(server->saveName, QString("Renamed"));
|
||||
EXPECT_EQ(server->username, QString("alice"));
|
||||
EXPECT_EQ(server->password, QString("secret"));
|
||||
EXPECT_TRUE(server->savePassword);
|
||||
EXPECT_EQ(server->site, QString("site"));
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, SetServerPasswordUpdatesOnlyTheNamedProfile)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
settings.addNewServer("Alpha", "alpha.example", "4748", "alice", "old", true, "");
|
||||
settings.addNewServer("Beta", "beta.example", "4748", "bob", "keep", true, "");
|
||||
|
||||
settings.setServerPassword("Beta", "new");
|
||||
|
||||
EXPECT_EQ(settings.getServer(0)->password, QString("old"));
|
||||
EXPECT_EQ(settings.getServer(1)->password, QString("new"));
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, LegacyProfilesAreMigrated)
|
||||
{
|
||||
{
|
||||
QSettings settings(settingsFile(), QSettings::IniFormat);
|
||||
settings.setValue("server/server_details/totalServers", 2);
|
||||
}
|
||||
writeLegacyServer(1, "Alpha", "alpha.example", "4748", "alice", "secret", true, "site-a");
|
||||
writeLegacyServer(2, "Beta", "beta.example", "4748", "bob", "other", false, "site-b");
|
||||
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
|
||||
const auto servers = settings.getServers();
|
||||
ASSERT_EQ(servers.size(), 2);
|
||||
EXPECT_EQ(servers.at(0).saveName, QString("Alpha"));
|
||||
EXPECT_EQ(servers.at(0).host, QString("alpha.example"));
|
||||
EXPECT_TRUE(servers.at(0).savePassword);
|
||||
EXPECT_EQ(servers.at(1).saveName, QString("Beta"));
|
||||
EXPECT_FALSE(servers.at(1).savePassword);
|
||||
|
||||
QSettings raw(settingsFile(), QSettings::IniFormat);
|
||||
EXPECT_FALSE(raw.contains("server/server_details/totalServers"));
|
||||
EXPECT_FALSE(raw.contains("server/server_details/saveName1"));
|
||||
EXPECT_FALSE(raw.contains("server/server_details/server1"));
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, MigrationRunsOnce)
|
||||
{
|
||||
writeLegacyServer(1, "Alpha", "alpha.example", "4748", "alice", "secret", true, "");
|
||||
{
|
||||
QSettings settings(settingsFile(), QSettings::IniFormat);
|
||||
settings.setValue("server/server_details/totalServers", 1);
|
||||
}
|
||||
|
||||
{
|
||||
ServersSettings first(settingsPath, nullptr);
|
||||
ASSERT_EQ(first.getServers().size(), 1);
|
||||
}
|
||||
|
||||
ServersSettings second(settingsPath, nullptr);
|
||||
ASSERT_EQ(second.getServers().size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, LegacyProfileAddedAfterMigrationIsMerged)
|
||||
{
|
||||
{
|
||||
ServersSettings first(settingsPath, nullptr);
|
||||
first.addNewServer("Alpha", "alpha.example", "4748", "alice", "secret", true, "");
|
||||
}
|
||||
|
||||
// An older build writes its own profile back in the legacy format after the
|
||||
// array migration already happened.
|
||||
writeLegacyServer(1, "Beta", "beta.example", "4748", "bob", "other", true, "site-b");
|
||||
// A duplicate of a profile the array already holds must not be added twice.
|
||||
writeLegacyServer(2, "Alpha duplicate", "alpha.example", "4748", "eve", "dupe", true, "");
|
||||
{
|
||||
QSettings settings(settingsFile(), QSettings::IniFormat);
|
||||
settings.setValue("server/server_details/totalServers", 2);
|
||||
}
|
||||
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
|
||||
const auto servers = settings.getServers();
|
||||
ASSERT_EQ(servers.size(), 2);
|
||||
EXPECT_EQ(servers.at(0).saveName, QString("Alpha"));
|
||||
EXPECT_EQ(servers.at(0).password, QString("secret"));
|
||||
EXPECT_EQ(servers.at(1).saveName, QString("Beta"));
|
||||
EXPECT_EQ(servers.at(1).host, QString("beta.example"));
|
||||
EXPECT_EQ(servers.at(1).username, QString("bob"));
|
||||
EXPECT_EQ(servers.at(1).password, QString("other"));
|
||||
EXPECT_TRUE(servers.at(1).savePassword);
|
||||
EXPECT_EQ(servers.at(1).site, QString("site-b"));
|
||||
|
||||
QSettings raw(settingsFile(), QSettings::IniFormat);
|
||||
EXPECT_FALSE(raw.contains("server/server_details/totalServers"));
|
||||
EXPECT_FALSE(raw.contains("server/server_details/saveName1"));
|
||||
EXPECT_FALSE(raw.contains("server/server_details/server2"));
|
||||
|
||||
// The merge is persisted, so a further start adds nothing.
|
||||
ServersSettings again(settingsPath, nullptr);
|
||||
ASSERT_EQ(again.getServers().size(), 2);
|
||||
}
|
||||
|
||||
TEST_F(ServersSettingsTest, OutOfRangeIndexReturnsNullopt)
|
||||
{
|
||||
ServersSettings settings(settingsPath, nullptr);
|
||||
EXPECT_FALSE(settings.getServer(-1).has_value());
|
||||
EXPECT_FALSE(settings.getServer(0).has_value());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
149
tests/spoiler_file_test.cpp
Normal file
149
tests/spoiler_file_test.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#include "client/network/update/card_spoiler/spoiler_file.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTemporaryDir>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <csignal>
|
||||
#include <sys/resource.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
class SpoilerFileTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
QTemporaryDir directory;
|
||||
QString path;
|
||||
QString error;
|
||||
const QByteArray original = "<cockatrice_carddatabase version=\"4\"><cards/></cockatrice_carddatabase>";
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
ASSERT_TRUE(directory.isValid());
|
||||
path = directory.filePath("spoiler.xml");
|
||||
QFile file(path);
|
||||
ASSERT_TRUE(file.open(QIODevice::WriteOnly));
|
||||
ASSERT_EQ(file.write(original), original.size());
|
||||
}
|
||||
|
||||
QByteArray read()
|
||||
{
|
||||
QFile file(path);
|
||||
EXPECT_TRUE(file.open(QIODevice::ReadOnly));
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
SpoilerFile::SaveResult save(const QByteArray &data)
|
||||
{
|
||||
return SpoilerFile::save(path, data, [] { return true; }, error);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SpoilerFileTest, InvalidDocumentsPreserveExistingFile)
|
||||
{
|
||||
for (const QByteArray &invalid :
|
||||
{QByteArray(), QByteArray("<html><cards/></html>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"2\"><cards/></cockatrice_carddatabase>"),
|
||||
QByteArray("<cockatrice_carddatabase><cards/></cockatrice_carddatabase>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><sets/></cockatrice_carddatabase>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><cards>"),
|
||||
QByteArray("<cockatrice_carddatabase version=\"4\"><cards/></cockatrice_carddatabase><extra/>")}) {
|
||||
EXPECT_EQ(save(invalid), SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, SupportedVersionsAreAccepted)
|
||||
{
|
||||
EXPECT_TRUE(SpoilerFile::validate(original).isEmpty());
|
||||
EXPECT_TRUE(
|
||||
SpoilerFile::validate("<cockatrice_carddatabase version=\"3\"><cards/></cockatrice_carddatabase>").isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, IdenticalFileIsUnchanged)
|
||||
{
|
||||
EXPECT_EQ(save(original), SpoilerFile::SaveResult::Unchanged);
|
||||
EXPECT_TRUE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, CompleteFileHashDetectsChangesAfterLargeCommonPrefix)
|
||||
{
|
||||
QByteArray changed = "<cockatrice_carddatabase version=\"4\"><!--";
|
||||
changed += QByteArray(100000, 'x');
|
||||
changed += "--><cards/></cockatrice_carddatabase>";
|
||||
ASSERT_EQ(save(changed), SpoilerFile::SaveResult::Written);
|
||||
changed.replace("<cards/>", "<cards></cards>");
|
||||
EXPECT_EQ(save(changed), SpoilerFile::SaveResult::Written);
|
||||
EXPECT_EQ(read(), changed);
|
||||
EXPECT_TRUE(error.isEmpty());
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, CancellationBeforeWritePreservesExistingFile)
|
||||
{
|
||||
EXPECT_EQ(SpoilerFile::save(path, original, [] { return false; }, error), SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, CancellationBeforeCommitPreservesExistingFile)
|
||||
{
|
||||
int checks = 0;
|
||||
EXPECT_EQ(SpoilerFile::save(
|
||||
path, original + "\n", [&checks] { return ++checks == 1; }, error),
|
||||
SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_EQ(checks, 2);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"});
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, MissingParentIsCreated)
|
||||
{
|
||||
path = directory.filePath("nested/spoiler.xml");
|
||||
EXPECT_EQ(save(original), SpoilerFile::SaveResult::Written);
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
TEST_F(SpoilerFileTest, FileInParentPathReportsFailureWithoutModifyingIt)
|
||||
{
|
||||
EXPECT_EQ(SpoilerFile::save(
|
||||
QDir(path).filePath("spoiler.xml"), original, [] { return true; }, error),
|
||||
SpoilerFile::SaveResult::Failed);
|
||||
EXPECT_FALSE(error.isEmpty());
|
||||
EXPECT_EQ(read(), original);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
TEST_F(SpoilerFileTest, ShortWriteLeavesExistingFileIntact)
|
||||
{
|
||||
// Limit only the child process: a real short write exercises QSaveFile's
|
||||
// buffered flush failure without depending on disk capacity or file ownership.
|
||||
const QByteArray changed = "<cockatrice_carddatabase version=\"4\"><!--" + QByteArray(100000, 'x') +
|
||||
"--><cards/></cockatrice_carddatabase>";
|
||||
EXPECT_EXIT(
|
||||
{
|
||||
struct rlimit limit;
|
||||
if (getrlimit(RLIMIT_FSIZE, &limit) != 0) {
|
||||
_exit(2);
|
||||
}
|
||||
limit.rlim_cur = 128;
|
||||
if (setrlimit(RLIMIT_FSIZE, &limit) != 0) {
|
||||
_exit(3);
|
||||
}
|
||||
std::signal(SIGXFSZ, SIG_IGN);
|
||||
const auto result = save(changed);
|
||||
_exit(result == SpoilerFile::SaveResult::Failed && !error.isEmpty() ? 0 : 1);
|
||||
},
|
||||
::testing::ExitedWithCode(0), "");
|
||||
EXPECT_EQ(read(), original);
|
||||
EXPECT_EQ(QDir(directory.path()).entryList(QDir::Files), QStringList{"spoiler.xml"});
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
35
tests/support/model_contract_checker.h
Normal file
35
tests/support/model_contract_checker.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H
|
||||
#define COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemModelTester>
|
||||
#include <memory>
|
||||
|
||||
namespace test_support
|
||||
{
|
||||
/**
|
||||
* @brief Keeps a QAbstractItemModelTester alive for the lifetime of a scope.
|
||||
*
|
||||
* The tester only observes the model while it exists, so construct this before
|
||||
* the mutations a test wants to validate and keep it in scope until the end.
|
||||
* Fatal reporting mode aborts on the first contract violation; a warning would
|
||||
* let the test pass while printing a line nobody reads in CI.
|
||||
*/
|
||||
class ModelContractChecker
|
||||
{
|
||||
public:
|
||||
explicit ModelContractChecker(QAbstractItemModel *model)
|
||||
: tester(
|
||||
std::make_unique<QAbstractItemModelTester>(model, QAbstractItemModelTester::FailureReportingMode::Fatal))
|
||||
{
|
||||
}
|
||||
|
||||
ModelContractChecker(const ModelContractChecker &) = delete;
|
||||
ModelContractChecker &operator=(const ModelContractChecker &) = delete;
|
||||
|
||||
private:
|
||||
std::unique_ptr<QAbstractItemModelTester> tester;
|
||||
};
|
||||
} // namespace test_support
|
||||
|
||||
#endif // COCKATRICE_TEST_MODEL_CONTRACT_CHECKER_H
|
||||
66
tests/svg_recolor_test.cpp
Normal file
66
tests/svg_recolor_test.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include "libcockatrice/utility/svg_recolor.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
TEST(SvgRecolor, ReplacesEverySentinel)
|
||||
{
|
||||
const QByteArray svg = R"(<svg><path fill="currentColor"/><path stroke="currentColor"/></svg>)";
|
||||
const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800");
|
||||
|
||||
EXPECT_EQ(result.count("#ff8800"), 2);
|
||||
EXPECT_FALSE(result.contains("currentColor"));
|
||||
}
|
||||
|
||||
TEST(SvgRecolor, ReplacesSentinelRegardlessOfCase)
|
||||
{
|
||||
const QByteArray svg = R"(<svg><path fill="CurrentColor"/><path stroke="CURRENTCOLOR"/></svg>)";
|
||||
|
||||
EXPECT_EQ(resolveSvgColorSentinel(svg, "#ff8800").count("#ff8800"), 2);
|
||||
}
|
||||
|
||||
TEST(SvgRecolor, LeavesTheRestOfTheDocumentIntact)
|
||||
{
|
||||
const QByteArray svg =
|
||||
R"(<svg viewBox="0 0 24 24"><!-- an ordinary comment --><path d="M0 0h24v24" fill="currentColor"/></svg>)";
|
||||
const QByteArray result = resolveSvgColorSentinel(svg, "#123456");
|
||||
|
||||
EXPECT_TRUE(result.contains(R"(viewBox="0 0 24 24")"));
|
||||
EXPECT_TRUE(result.contains(R"(d="M0 0h24v24")"));
|
||||
EXPECT_TRUE(result.contains("an ordinary comment"));
|
||||
}
|
||||
|
||||
TEST(SvgRecolor, DoesNotModifyTheInput)
|
||||
{
|
||||
const QByteArray svg = R"(<svg><path fill="currentColor"/></svg>)";
|
||||
const QByteArray original = svg;
|
||||
|
||||
resolveSvgColorSentinel(svg, "#000000");
|
||||
|
||||
EXPECT_EQ(svg, original);
|
||||
}
|
||||
|
||||
TEST(SvgRecolor, PreservesBytesThatAreNotValidUtf8)
|
||||
{
|
||||
QByteArray svg = "<svg>";
|
||||
svg.append("\xff\xfe");
|
||||
svg.append(R"(<path fill="currentColor"/></svg>)");
|
||||
|
||||
const QByteArray result = resolveSvgColorSentinel(svg, "#ff8800");
|
||||
|
||||
EXPECT_TRUE(result.contains("\xff\xfe"));
|
||||
EXPECT_FALSE(result.contains("currentColor"));
|
||||
EXPECT_EQ(result.count("#ff8800"), 1);
|
||||
}
|
||||
|
||||
TEST(SvgRecolor, NoSentinelIsANoOp)
|
||||
{
|
||||
const QByteArray svg = R"(<svg><path fill="#abcdef"/></svg>)";
|
||||
|
||||
EXPECT_EQ(resolveSvgColorSentinel(svg, "#ffffff"), svg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -5,12 +5,14 @@ cockatrice_add_test(
|
|||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_manager.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_config.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/pixel_map_generator.cpp
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/theme_icon.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/client/network/update/client/release_channel.cpp
|
||||
${VERSION_STRING_CPP}
|
||||
theme_migration_test.cpp
|
||||
theme_icon_render_test.cpp
|
||||
LIBS
|
||||
libcockatrice_settings
|
||||
libcockatrice_network
|
||||
|
|
@ -18,4 +20,6 @@ cockatrice_add_test(
|
|||
libcockatrice_protocol
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
DEFINITIONS
|
||||
COCKATRICE_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
|
|
|||
189
tests/theme_migration/theme_icon_render_test.cpp
Normal file
189
tests/theme_migration/theme_icon_render_test.cpp
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#include "interface/theme_icon.h"
|
||||
#include "interface/theme_manager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColor>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QImage>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QStringLiteral>
|
||||
#include <QTemporaryDir>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief Creates the GUI application QPixmap needs, using the offscreen plugin.
|
||||
*
|
||||
* The theme-migration binary is headless; creating this lazily keeps its other
|
||||
* tests free of a display dependency.
|
||||
*/
|
||||
void ensureGuiApplication()
|
||||
{
|
||||
static int argc = 1;
|
||||
static char name[] = "theme_icon_test";
|
||||
static char *argv[] = {name, nullptr};
|
||||
// Leaked on purpose: a function-local static QApplication is destroyed before Qt's
|
||||
// own library statics at exit, and the network module's destructors then run
|
||||
// against a torn-down QObject layer (SEGV under AddressSanitizer).
|
||||
static QApplication *app = new QApplication(argc, argv);
|
||||
Q_UNUSED(app);
|
||||
}
|
||||
|
||||
QString resourcesDir()
|
||||
{
|
||||
return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(QStringLiteral("cockatrice/resources"));
|
||||
}
|
||||
|
||||
int countOpaquePixels(const QPixmap &pixmap, const QColor &expected)
|
||||
{
|
||||
const QImage image = pixmap.toImage();
|
||||
int count = 0;
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
const QColor pixel = image.pixelColor(x, y);
|
||||
if (pixel.alpha() > 200 && pixel.red() == expected.red() && pixel.green() == expected.green() &&
|
||||
pixel.blue() == expected.blue()) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
QByteArray svgWith(const QByteArray &colorToken)
|
||||
{
|
||||
return QByteArrayLiteral(R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill=")") +
|
||||
colorToken + QByteArrayLiteral(R"(" d="M2 2h20v20H2z"/></svg>)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Restores the global theme search paths and palette after each test.
|
||||
*
|
||||
* The icon engine reads both process-wide values, so a failing assertion must
|
||||
* not leak a temporary theme directory or palette into the next test.
|
||||
*/
|
||||
class ThemeIconRenderTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
qputenv("QT_QPA_PLATFORM", "offscreen");
|
||||
ensureGuiApplication();
|
||||
savedSearchPaths = QDir::searchPaths(QStringLiteral("theme"));
|
||||
savedPalette = qApp->palette();
|
||||
themeManager = new ThemeManager;
|
||||
QDir::setSearchPaths(QStringLiteral("theme"), {resourcesDir()});
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
delete themeManager;
|
||||
themeManager = nullptr;
|
||||
QDir::setSearchPaths(QStringLiteral("theme"), savedSearchPaths);
|
||||
qApp->setPalette(savedPalette);
|
||||
}
|
||||
|
||||
QStringList savedSearchPaths; ///< Search paths captured before the test.
|
||||
QPalette savedPalette; ///< Application palette captured before the test.
|
||||
};
|
||||
|
||||
/** @brief A non-square request must letterbox, not squash, a 24×24 glyph. */
|
||||
TEST_F(ThemeIconRenderTest, NonSquareRequestPreservesAspectRatio)
|
||||
{
|
||||
const QIcon icon = loadThemedIcon(QStringLiteral("icons/player"));
|
||||
ASSERT_FALSE(icon.isNull());
|
||||
|
||||
EXPECT_EQ(icon.actualSize(QSize(20, 15)), QSize(15, 15));
|
||||
|
||||
const QPixmap pixmap = icon.pixmap(QSize(20, 15));
|
||||
ASSERT_FALSE(pixmap.isNull());
|
||||
EXPECT_EQ(pixmap.deviceIndependentSize(), QSizeF(15, 15));
|
||||
}
|
||||
|
||||
/** @brief The engine must follow palette flips, icon modes, and DPR. */
|
||||
TEST_F(ThemeIconRenderTest, ThemedIconFollowsPaletteModeAndDpr)
|
||||
{
|
||||
QPalette palette = qApp->palette();
|
||||
palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#123456")));
|
||||
qApp->setPalette(palette);
|
||||
|
||||
const QIcon icon = loadThemedIcon(QStringLiteral("icons/search"));
|
||||
ASSERT_FALSE(icon.isNull());
|
||||
|
||||
const QPixmap normal = icon.pixmap(QSize(24, 24), 1.0);
|
||||
ASSERT_FALSE(normal.isNull());
|
||||
EXPECT_EQ(normal.size(), QSize(24, 24));
|
||||
EXPECT_DOUBLE_EQ(normal.devicePixelRatio(), 1.0);
|
||||
EXPECT_GT(countOpaquePixels(normal, QColor(QStringLiteral("#123456"))), 0);
|
||||
|
||||
const QPixmap hidpi = icon.pixmap(QSize(24, 24), 2.0);
|
||||
ASSERT_FALSE(hidpi.isNull());
|
||||
EXPECT_DOUBLE_EQ(hidpi.devicePixelRatio(), 2.0);
|
||||
EXPECT_EQ(hidpi.deviceIndependentSize(), QSizeF(24, 24));
|
||||
|
||||
palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#abcdef")));
|
||||
qApp->setPalette(palette);
|
||||
const QPixmap flipped = icon.pixmap(QSize(24, 24), 1.0);
|
||||
EXPECT_GT(countOpaquePixels(flipped, QColor(QStringLiteral("#abcdef"))), 0);
|
||||
|
||||
QPalette selectedPalette = qApp->palette();
|
||||
selectedPalette.setColor(QPalette::HighlightedText, QColor(QStringLiteral("#00ff00")));
|
||||
qApp->setPalette(selectedPalette);
|
||||
const QPixmap selected = icon.pixmap(QSize(24, 24), 1.0, QIcon::Selected, QIcon::Off);
|
||||
EXPECT_GT(countOpaquePixels(selected, QColor(QStringLiteral("#00ff00"))), 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_F(ThemeIconRenderTest, SentinelUsesPaletteColor)
|
||||
{
|
||||
QPalette palette;
|
||||
palette.setColor(QPalette::WindowText, QColor(Qt::white));
|
||||
qApp->setPalette(palette);
|
||||
|
||||
const QPixmap pixmap = loadSentinelSvgPixmap(QStringLiteral("icons/search.svg"));
|
||||
|
||||
ASSERT_FALSE(pixmap.isNull());
|
||||
EXPECT_EQ(pixmap.size(), QSize(24, 24));
|
||||
EXPECT_GT(countOpaquePixels(pixmap, Qt::white), 0);
|
||||
}
|
||||
|
||||
TEST_F(ThemeIconRenderTest, NonSentinelSvgReturnsNull)
|
||||
{
|
||||
QTemporaryDir themeDir;
|
||||
ASSERT_TRUE(themeDir.isValid());
|
||||
QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("plain.svg")));
|
||||
ASSERT_TRUE(file.open(QIODevice::WriteOnly));
|
||||
file.write(svgWith("#000000"));
|
||||
file.close();
|
||||
QDir::addSearchPath(QStringLiteral("theme"), themeDir.path());
|
||||
|
||||
EXPECT_TRUE(loadSentinelSvgPixmap(QStringLiteral("plain.svg")).isNull());
|
||||
EXPECT_TRUE(loadSentinelSvgPixmap(QStringLiteral("icons/search.png")).isNull());
|
||||
}
|
||||
|
||||
TEST_F(ThemeIconRenderTest, LowercaseSentinelIsResolved)
|
||||
{
|
||||
QTemporaryDir themeDir;
|
||||
ASSERT_TRUE(themeDir.isValid());
|
||||
QFile file(QDir(themeDir.path()).absoluteFilePath(QStringLiteral("lower.svg")));
|
||||
ASSERT_TRUE(file.open(QIODevice::WriteOnly));
|
||||
file.write(svgWith("currentcolor"));
|
||||
file.close();
|
||||
|
||||
QDir::addSearchPath(QStringLiteral("theme"), themeDir.path());
|
||||
|
||||
QPalette palette;
|
||||
palette.setColor(QPalette::WindowText, QColor(QStringLiteral("#123456")));
|
||||
qApp->setPalette(palette);
|
||||
|
||||
const QPixmap pixmap = loadSentinelSvgPixmap(QStringLiteral("lower.svg"));
|
||||
|
||||
ASSERT_FALSE(pixmap.isNull());
|
||||
EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#123456"))), 0);
|
||||
}
|
||||
248
tests/user_games_model_test.cpp
Normal file
248
tests/user_games_model_test.cpp
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#include "interface/widgets/server/user/user_games_model.h"
|
||||
|
||||
#include <QAbstractItemModelTester>
|
||||
#include <QCoreApplication>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QSignalSpy>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/server_message.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/rng/rng_abstract.h>
|
||||
#include <memory>
|
||||
|
||||
RNG_Abstract *rng = nullptr; // required by the transitive server library; this suite sends no game commands
|
||||
|
||||
namespace
|
||||
{
|
||||
class GamesClient : public AbstractClient
|
||||
{
|
||||
public:
|
||||
QList<CommandContainer> sent;
|
||||
|
||||
GamesClient()
|
||||
{
|
||||
setStatus(StatusLoggedIn);
|
||||
}
|
||||
|
||||
~GamesClient() override
|
||||
{
|
||||
qDeleteAll(pendingCommands);
|
||||
pendingCommands.clear();
|
||||
}
|
||||
|
||||
void changeStatus(ClientStatus status)
|
||||
{
|
||||
setStatus(status);
|
||||
}
|
||||
|
||||
void answer(int request, Response response)
|
||||
{
|
||||
response.set_cmd_id(sent.at(request).cmd_id());
|
||||
ServerMessage message;
|
||||
message.set_message_type(ServerMessage::RESPONSE);
|
||||
*message.mutable_response() = response;
|
||||
processProtocolItem(message);
|
||||
}
|
||||
|
||||
protected:
|
||||
void sendCommandContainer(const CommandContainer &command) override
|
||||
{
|
||||
sent.append(command);
|
||||
}
|
||||
};
|
||||
|
||||
Response gamesResponse(std::initializer_list<int> ids)
|
||||
{
|
||||
Response response;
|
||||
response.set_response_code(Response::RespOk);
|
||||
auto *games = response.MutableExtension(Response_GetGamesOfUser::ext);
|
||||
for (int id : ids) {
|
||||
auto *game = games->add_game_list();
|
||||
game->set_game_id(id);
|
||||
game->set_room_id(7);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
using State = UserGamesModel::State;
|
||||
|
||||
TEST(UserGamesModel, OfflineUserDoesNotSendARequest)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", false);
|
||||
model.refresh();
|
||||
EXPECT_TRUE(client.sent.isEmpty());
|
||||
EXPECT_EQ(State::Offline, model.getState());
|
||||
EXPECT_FALSE(model.canRefresh());
|
||||
EXPECT_EQ(0, model.rowCount());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, PresenceLossInvalidatesTheOutstandingResponse)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", true);
|
||||
EXPECT_EQ(State::Loading, model.getState());
|
||||
EXPECT_FALSE(model.canRefresh());
|
||||
model.setUser("Alice", false);
|
||||
client.answer(0, gamesResponse({1}));
|
||||
EXPECT_EQ(State::Offline, model.getState());
|
||||
EXPECT_EQ(0, model.rowCount());
|
||||
|
||||
model.setUser("Alice", true);
|
||||
client.answer(1, gamesResponse({2}));
|
||||
EXPECT_EQ(State::Ready, model.getState());
|
||||
EXPECT_TRUE(model.canRefresh());
|
||||
EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, SwitchingBackToTheSameUserDoesNotAcceptTheirOldRequest)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", true);
|
||||
model.setUser("Bob", true);
|
||||
model.setUser("Alice", true);
|
||||
client.answer(2, gamesResponse({3}));
|
||||
client.answer(0, gamesResponse({1}));
|
||||
client.answer(1, gamesResponse({2}));
|
||||
EXPECT_EQ(1, model.rowCount());
|
||||
EXPECT_EQ(3, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, RefreshSupersedesThePreviousRequest)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", true);
|
||||
model.refresh();
|
||||
client.answer(0, gamesResponse({1}));
|
||||
EXPECT_EQ(State::Loading, model.getState());
|
||||
EXPECT_EQ(0, model.rowCount());
|
||||
client.answer(1, gamesResponse({2}));
|
||||
EXPECT_EQ(State::Ready, model.getState());
|
||||
EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, EmptySuccessAndMissingPayloadAreDifferentStates)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", true);
|
||||
client.answer(0, gamesResponse({}));
|
||||
EXPECT_EQ(State::Empty, model.getState());
|
||||
EXPECT_TRUE(model.canRefresh());
|
||||
|
||||
model.refresh();
|
||||
Response missingPayload;
|
||||
missingPayload.set_response_code(Response::RespOk);
|
||||
client.answer(1, missingPayload);
|
||||
EXPECT_EQ(State::Error, model.getState());
|
||||
EXPECT_TRUE(model.canRefresh());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, FailedRequestsCanBeRetried)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", true);
|
||||
Response error;
|
||||
error.set_response_code(Response::RespNameNotFound);
|
||||
client.answer(0, error);
|
||||
EXPECT_EQ(State::Error, model.getState());
|
||||
model.refresh();
|
||||
client.answer(1, gamesResponse({4}));
|
||||
EXPECT_EQ(State::Ready, model.getState());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, DisconnectRejectsLateRepliesAndReconnectFetchesAgain)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
model.setUser("Alice", true);
|
||||
client.changeStatus(StatusDisconnecting);
|
||||
client.answer(0, gamesResponse({1}));
|
||||
EXPECT_EQ(State::Disconnected, model.getState());
|
||||
EXPECT_FALSE(model.canRefresh());
|
||||
model.refresh();
|
||||
EXPECT_EQ(1, client.sent.size());
|
||||
client.changeStatus(StatusLoggedIn);
|
||||
client.answer(1, gamesResponse({2}));
|
||||
EXPECT_EQ(State::Ready, model.getState());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, ClientDestructionClearsGamesAndDisablesRefresh)
|
||||
{
|
||||
auto client = std::make_unique<GamesClient>();
|
||||
UserGamesModel model(client.get());
|
||||
model.setUser("Alice", true);
|
||||
client->answer(0, gamesResponse({1}));
|
||||
client.reset();
|
||||
EXPECT_EQ(State::Disconnected, model.getState());
|
||||
EXPECT_EQ(0, model.rowCount());
|
||||
EXPECT_FALSE(model.canRefresh());
|
||||
model.refresh();
|
||||
EXPECT_EQ(State::Disconnected, model.getState());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, ResetInvalidatesAnOpenGameActionAndPreservesTheModelContract)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::Fatal);
|
||||
model.setUser("Alice", true);
|
||||
client.answer(0, gamesResponse({1, 2, 3}));
|
||||
QPersistentModelIndex selected(model.index(1));
|
||||
EXPECT_TRUE(selected.isValid());
|
||||
model.refresh();
|
||||
EXPECT_FALSE(selected.isValid());
|
||||
EXPECT_EQ(0, model.rowCount());
|
||||
client.answer(1, gamesResponse({4}));
|
||||
EXPECT_EQ(1, model.rowCount());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, StateListenerCanSelectAnotherUserBeforeTheRequestIsSent)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client);
|
||||
bool switched = false;
|
||||
QObject::connect(&model, &UserGamesModel::stateChanged, &model, [&] {
|
||||
if (!switched && model.getState() == State::Loading) {
|
||||
switched = true;
|
||||
model.setUser("Bob", false);
|
||||
}
|
||||
});
|
||||
model.setUser("Alice", true);
|
||||
EXPECT_TRUE(client.sent.isEmpty());
|
||||
EXPECT_EQ(State::Offline, model.getState());
|
||||
}
|
||||
|
||||
TEST(UserGamesModel, TimedOutResponseCannotReplaceARetry)
|
||||
{
|
||||
GamesClient client;
|
||||
UserGamesModel model(&client, 50);
|
||||
model.setUser("Alice", true);
|
||||
QSignalSpy changes(&model, &UserGamesModel::stateChanged);
|
||||
ASSERT_TRUE(changes.wait(1000));
|
||||
ASSERT_EQ(State::Error, model.getState());
|
||||
EXPECT_TRUE(model.canRefresh());
|
||||
model.refresh();
|
||||
client.answer(0, gamesResponse({1}));
|
||||
EXPECT_EQ(State::Loading, model.getState());
|
||||
client.answer(1, gamesResponse({2}));
|
||||
EXPECT_EQ(State::Ready, model.getState());
|
||||
EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue