mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 11:03:25 -08:00
Merge branch 'master' into master
This commit is contained in:
commit
ba22736def
114 changed files with 4835 additions and 1044 deletions
|
|
@ -328,6 +328,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
|
||||
|
|
@ -494,6 +528,7 @@ add_subdirectory(carddatabase)
|
|||
add_subdirectory(deck_list_model)
|
||||
add_subdirectory(deck_list_zones)
|
||||
add_subdirectory(loading_from_clipboard)
|
||||
add_subdirectory(model_contract)
|
||||
add_subdirectory(movecard_tests)
|
||||
add_subdirectory(oracle)
|
||||
add_subdirectory(settings)
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ cockatrice_add_test(
|
|||
)
|
||||
|
||||
# ------------------------
|
||||
# Filter String and Tree Test
|
||||
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)
|
||||
# Guard to match condition for libcockatrice_filters in the root CMakeLists.txt
|
||||
# ------------------------
|
||||
if(WITH_ORACLE OR WITH_CLIENT)
|
||||
# ------------------------
|
||||
# Filter String Test
|
||||
# ------------------------
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
filter_string_test
|
||||
|
|
@ -35,6 +37,10 @@ if(WITH_ORACLE OR WITH_CLIENT)
|
|||
LIBS
|
||||
libcockatrice_filters
|
||||
)
|
||||
|
||||
# ------------------------
|
||||
# Filter Tree Test
|
||||
# ------------------------
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
filter_tree_test
|
||||
|
|
@ -45,6 +51,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()
|
||||
|
||||
# ------------------------
|
||||
|
|
|
|||
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();
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue