From 6757f559ea1f838f21955aabc40544d67704ba91 Mon Sep 17 00:00:00 2001 From: Kieran Kihn <114803508+kierankihn@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:53:35 +0800 Subject: [PATCH] test(network): add `MessageSerializerTest` for serialization and deserialization coverage - Added comprehensive unit tests for `MessageSerializer` in `MessageSerializerTest.cpp`. - Included `MessageSerializerTest.cpp` in `test/CMakeLists.txt`. --- test/CMakeLists.txt | 1 + test/unit/network/MessageSerializerTest.cpp | 923 ++++++++++++++++++++ 2 files changed, 924 insertions(+) create mode 100644 test/unit/network/MessageSerializerTest.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b91744a..ec93c4c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(uno-game-test unit/game/CardTileTest.cpp unit/game/PlayerTest.cpp unit/game/GameStateTest.cpp + unit/network/MessageSerializerTest.cpp ) target_link_libraries(uno-game-test diff --git a/test/unit/network/MessageSerializerTest.cpp b/test/unit/network/MessageSerializerTest.cpp new file mode 100644 index 0000000..72b5648 --- /dev/null +++ b/test/unit/network/MessageSerializerTest.cpp @@ -0,0 +1,923 @@ +/** + * @file MessageSerializerTest.cpp + * + * @author Yuzhe Guo + * @date 2025.11.21 + */ + +#include "../../../src/network/MessageSerializer.h" + +#include +#include + +using namespace UNO::NETWORK; +using namespace UNO::GAME; + +// ========== Serialize Tests ========== + +TEST(MessageSerializerTest, SerializeJoinGameMessage) +{ + JoinGamePayload payload{"Player1"}; + Message message(MessagePayloadType::JOIN_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "JOIN_GAME"); + EXPECT_EQ(json["payload"]["name"], "Player1"); +} + +TEST(MessageSerializerTest, SerializeJoinGameMessageWithEmptyName) +{ + JoinGamePayload payload{""}; + Message message(MessagePayloadType::JOIN_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "JOIN_GAME"); + EXPECT_EQ(json["payload"]["name"], ""); +} + +TEST(MessageSerializerTest, SerializeJoinGameMessageWithSpecialCharacters) +{ + JoinGamePayload payload{"Player<>\"&\n\t"}; + Message message(MessagePayloadType::JOIN_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "JOIN_GAME"); + EXPECT_EQ(json["payload"]["name"], "Player<>\"&\n\t"); +} + +TEST(MessageSerializerTest, SerializeStartGameMessage) +{ + StartGamePayload payload{}; + Message message(MessagePayloadType::START_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "START_GAME"); + EXPECT_TRUE(json["payload"].is_null()); +} + +TEST(MessageSerializerTest, SerializeDrawCardMessage) +{ + DrawCardPayload payload{5}; + Message message(MessagePayloadType::DRAW_CARD, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "DRAW_CARD"); + EXPECT_EQ(json["payload"]["draw_count"], 5); +} + +TEST(MessageSerializerTest, SerializeDrawCardMessageWithZero) +{ + DrawCardPayload payload{0}; + Message message(MessagePayloadType::DRAW_CARD, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "DRAW_CARD"); + EXPECT_EQ(json["payload"]["draw_count"], 0); +} + +TEST(MessageSerializerTest, SerializePlayCardMessage) +{ + Card card(CardColor::RED, CardType::NUM5); + PlayCardPayload payload{card}; + Message message(MessagePayloadType::PLAY_CARD, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "PLAY_CARD"); + EXPECT_EQ(json["payload"]["card"]["card_color"], "Red"); + EXPECT_EQ(json["payload"]["card"]["card_type"], "5"); +} + +TEST(MessageSerializerTest, SerializePlayCardMessageAllColors) +{ + std::vector> colors = { + {CardColor::RED, "Red"}, {CardColor::BLUE, "Blue"}, {CardColor::GREEN, "Green"}, {CardColor::YELLOW, "Yellow"}}; + + for (const auto &[color, expected] : colors) { + Card card(color, CardType::NUM1); + PlayCardPayload payload{card}; + Message message(MessagePayloadType::PLAY_CARD, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload"]["card"]["card_color"], expected); + } +} + +TEST(MessageSerializerTest, SerializePlayCardMessageAllTypes) +{ + std::vector> types = {{CardType::NUM0, "0"}, + {CardType::NUM1, "1"}, + {CardType::NUM2, "2"}, + {CardType::NUM3, "3"}, + {CardType::NUM4, "4"}, + {CardType::NUM5, "5"}, + {CardType::NUM6, "6"}, + {CardType::NUM7, "7"}, + {CardType::NUM8, "8"}, + {CardType::NUM9, "9"}, + {CardType::SKIP, "Skip"}, + {CardType::REVERSE, "Reverse"}, + {CardType::DRAW2, "Draw 2"}, + {CardType::WILD, "Wild"}, + {CardType::WILDDRAWFOUR, "Wild Draw 4"}}; + + for (const auto &[type, expected] : types) { + Card card(CardColor::RED, type); + PlayCardPayload payload{card}; + Message message(MessagePayloadType::PLAY_CARD, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload"]["card"]["card_type"], expected); + } +} + +TEST(MessageSerializerTest, SerializeInitGameMessage) +{ + DiscardPile discardPile; + discardPile.add(Card(CardColor::RED, CardType::NUM5)); + discardPile.add(Card(CardColor::BLUE, CardType::SKIP)); + + HandCard handCard; + handCard.draw(Card(CardColor::GREEN, CardType::NUM3)); + handCard.draw(Card(CardColor::YELLOW, CardType::REVERSE)); + + InitGamePayload payload{discardPile, handCard, 2}; + Message message(MessagePayloadType::INIT_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "INIT_GAME"); + EXPECT_EQ(json["payload"]["current_player"], 2); + EXPECT_TRUE(json["payload"]["discard_pile"].is_array()); + EXPECT_TRUE(json["payload"]["hand_card"].is_array()); + EXPECT_EQ(json["payload"]["discard_pile"].size(), 2); + EXPECT_EQ(json["payload"]["hand_card"].size(), 2); +} + +TEST(MessageSerializerTest, SerializeInitGameMessageWithEmptyPiles) +{ + DiscardPile discardPile; + HandCard handCard; + + InitGamePayload payload{discardPile, handCard, 0}; + Message message(MessagePayloadType::INIT_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "INIT_GAME"); + EXPECT_EQ(json["payload"]["current_player"], 0); + EXPECT_TRUE(json["payload"]["discard_pile"].is_array()); + EXPECT_TRUE(json["payload"]["hand_card"].is_array()); + EXPECT_EQ(json["payload"]["discard_pile"].size(), 0); + EXPECT_EQ(json["payload"]["hand_card"].size(), 0); +} + +TEST(MessageSerializerTest, SerializeEndGameMessage) +{ + EndGamePayload payload{}; + Message message(MessagePayloadType::END_GAME, payload); + + std::string result = MessageSerializer::serialize(message); + nlohmann::json json = nlohmann::json::parse(result); + + EXPECT_EQ(json["payload_type"], "END_GAME"); + EXPECT_TRUE(json["payload"].is_null()); +} + +// ========== Deserialize Tests ========== + +TEST(MessageSerializerTest, DeserializeJoinGameMessage) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"Player1"}})"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::JOIN_GAME); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, "Player1"); +} + +TEST(MessageSerializerTest, DeserializeJoinGameMessageWithEmptyName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":""}})"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::JOIN_GAME); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, ""); +} + +TEST(MessageSerializerTest, DeserializeStartGameMessage) +{ + std::string json = R"({"payload_type":"START_GAME","payload":null})"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::START_GAME); +} + +TEST(MessageSerializerTest, DeserializeDrawCardMessage) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{"draw_count":5}})"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::DRAW_CARD); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.drawCount, 5); +} + +TEST(MessageSerializerTest, DeserializePlayCardMessage) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red","card_type":"5"}}})"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::PLAY_CARD); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.card.getColor(), CardColor::RED); + EXPECT_EQ(payload.card.getType(), CardType::NUM5); +} + +TEST(MessageSerializerTest, DeserializeInitGameMessage) +{ + std::string json = R"({ + "payload_type":"INIT_GAME", + "payload":{ + "discard_pile":[{"card_color":"Red","card_type":"5"}], + "hand_card":[{"card_color":"Blue","card_type":"Skip"}], + "current_player":2 + } + })"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::INIT_GAME); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.currentPlayerIndex, 2); +} + +TEST(MessageSerializerTest, DeserializeEndGameMessage) +{ + std::string json = R"({"payload_type":"END_GAME","payload":null})"; + + Message message = MessageSerializer::deserialize(json); + + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::END_GAME); +} + +// ========== Round-trip Tests ========== + +TEST(MessageSerializerTest, RoundTripJoinGameMessage) +{ + JoinGamePayload payload{"Player1"}; + Message original(MessagePayloadType::JOIN_GAME, payload); + + std::string serialized = MessageSerializer::serialize(original); + Message deserialized = MessageSerializer::deserialize(serialized); + + EXPECT_EQ(deserialized.getMessagePayloadType(), MessagePayloadType::JOIN_GAME); + auto deserializedPayload = std::get(deserialized.getMessagePayload()); + EXPECT_EQ(deserializedPayload.playerName, "Player1"); +} + +TEST(MessageSerializerTest, RoundTripAllMessageTypes) +{ + std::vector messages; + + messages.emplace_back(MessagePayloadType::JOIN_GAME, JoinGamePayload{"TestPlayer"}); + messages.emplace_back(MessagePayloadType::START_GAME, StartGamePayload{}); + messages.emplace_back(MessagePayloadType::DRAW_CARD, DrawCardPayload{3}); + messages.emplace_back(MessagePayloadType::PLAY_CARD, PlayCardPayload{Card(CardColor::RED, CardType::NUM5)}); + messages.emplace_back(MessagePayloadType::END_GAME, EndGamePayload{}); + + for (const auto &original : messages) { + std::string serialized = MessageSerializer::serialize(original); + Message deserialized = MessageSerializer::deserialize(serialized); + EXPECT_EQ(deserialized.getMessagePayloadType(), original.getMessagePayloadType()); + } +} + +// ========== Invalid Input Tests ========== + +TEST(MessageSerializerTest, DeserializeInvalidJSON) +{ + std::string json = "invalid json"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeNonObjectJSON) +{ + std::string json = R"([1,2,3])"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeNullJSON) +{ + std::string json = "null"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeStringJSON) +{ + std::string json = R"("string")"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeNumberJSON) +{ + std::string json = "123"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeBooleanJSON) +{ + std::string json = "true"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeMissingPayloadType) +{ + std::string json = R"({"payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeMissingPayload) +{ + std::string json = R"({"payload_type":"START_GAME"})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInvalidPayloadType) +{ + std::string json = R"({"payload_type":"INVALID_TYPE","payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePayloadTypeAsNumber) +{ + std::string json = R"({"payload_type":123,"payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePayloadTypeAsNull) +{ + std::string json = R"({"payload_type":null,"payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePayloadTypeAsArray) +{ + std::string json = R"({"payload_type":[],"payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePayloadTypeAsObject) +{ + std::string json = R"({"payload_type":{},"payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== JoinGamePayload Invalid Tests ========== + +TEST(MessageSerializerTest, DeserializeJoinGameWithNullPayload) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithArrayPayload) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":[]})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithNumberPayload) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":123})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithMissingName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithNumberName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":123}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithNullName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":null}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithArrayName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":[]}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithObjectName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":{}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== StartGamePayload Invalid Tests ========== + +TEST(MessageSerializerTest, DeserializeStartGameWithNonNullPayload) +{ + std::string json = R"({"payload_type":"START_GAME","payload":{}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeStartGameWithArrayPayload) +{ + std::string json = R"({"payload_type":"START_GAME","payload":[]})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeStartGameWithStringPayload) +{ + std::string json = R"({"payload_type":"START_GAME","payload":"test"})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== DrawCardPayload Invalid Tests ========== + +TEST(MessageSerializerTest, DeserializeDrawCardWithNullPayload) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeDrawCardWithArrayPayload) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":[]})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeDrawCardWithMissingDrawCount) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeDrawCardWithStringDrawCount) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{"draw_count":"5"}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeDrawCardWithNullDrawCount) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{"draw_count":null}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeDrawCardWithNegativeDrawCount) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{"draw_count":-5}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeDrawCardWithFloatDrawCount) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{"draw_count":5.5}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== PlayCardPayload Invalid Tests ========== + +TEST(MessageSerializerTest, DeserializePlayCardWithNullPayload) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithArrayPayload) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":[]})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithMissingCard) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithNullCard) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":null}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithArrayCard) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":[]}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithStringCard) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":"Red 5"}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithMissingCardColor) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_type":"5"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithMissingCardType) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithInvalidCardColor) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Purple","card_type":"5"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithInvalidCardType) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red","card_type":"Invalid"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithNumberCardColor) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":123,"card_type":"5"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithNumberCardType) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red","card_type":5}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithNullCardColor) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":null,"card_type":"5"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithNullCardType) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red","card_type":null}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithEmptyCardColor) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"","card_type":"5"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithEmptyCardType) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red","card_type":""}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithCaseSensitiveColor) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"red","card_type":"5"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializePlayCardWithCaseSensitiveType) +{ + std::string json = R"({"payload_type":"PLAY_CARD","payload":{"card":{"card_color":"Red","card_type":"skip"}}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== InitGamePayload Invalid Tests ========== + +TEST(MessageSerializerTest, DeserializeInitGameWithNullPayload) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":null})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithArrayPayload) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":[]})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithMissingDiscardPile) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"hand_card":[],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithMissingHandCard) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithMissingCurrentPlayer) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[]}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithNullDiscardPile) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":null,"hand_card":[],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithNullHandCard) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":null,"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithStringDiscardPile) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":"test","hand_card":[],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithStringHandCard) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":"test","current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithStringCurrentPlayer) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[],"current_player":"0"}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithNullCurrentPlayer) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[],"current_player":null}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithNegativeCurrentPlayer) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[],"current_player":-1}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithFloatCurrentPlayer) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[],"current_player":1.5}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithInvalidCardInDiscardPile) +{ + std::string json = + R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[{"card_color":"Invalid","card_type":"5"}],"hand_card":[],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithInvalidCardInHandCard) +{ + std::string json = + R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[{"card_color":"Red","card_type":"Invalid"}],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithNonObjectCardInDiscardPile) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":["string"],"hand_card":[],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeInitGameWithNonObjectCardInHandCard) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[123],"current_player":0}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== EndGamePayload Invalid Tests ========== + +TEST(MessageSerializerTest, DeserializeEndGameWithNonNullPayload) +{ + std::string json = R"({"payload_type":"END_GAME","payload":{}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeEndGameWithArrayPayload) +{ + std::string json = R"({"payload_type":"END_GAME","payload":[]})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeEndGameWithStringPayload) +{ + std::string json = R"({"payload_type":"END_GAME","payload":"test"})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +// ========== Edge Cases ========== + +TEST(MessageSerializerTest, DeserializeWithExtraFields) +{ + std::string json = R"({"payload_type":"START_GAME","payload":null,"extra_field":"ignored"})"; + Message message = MessageSerializer::deserialize(json); + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::START_GAME); +} + +TEST(MessageSerializerTest, DeserializeJoinGameWithExtraPayloadFields) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"Player1","extra":"ignored"}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, "Player1"); +} + +TEST(MessageSerializerTest, DeserializeVeryLongPlayerName) +{ + std::string longName(10000, 'a'); + nlohmann::json json = {{"payload_type", "JOIN_GAME"}, {"payload", {{"name", longName}}}}; + Message message = MessageSerializer::deserialize(json.dump()); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, longName); +} + +TEST(MessageSerializerTest, DeserializeLargeDrawCount) +{ + std::string json = R"({"payload_type":"DRAW_CARD","payload":{"draw_count":2147483647}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.drawCount, 2147483647); +} + +TEST(MessageSerializerTest, DeserializeLargeCurrentPlayer) +{ + std::string json = R"({"payload_type":"INIT_GAME","payload":{"discard_pile":[],"hand_card":[],"current_player":4294967295}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.currentPlayerIndex, 4294967295); +} + +TEST(MessageSerializerTest, DeserializeUnicodePlayerName) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"玩家123🎮"}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, "玩家123🎮"); +} + +TEST(MessageSerializerTest, DeserializeWithWhitespace) +{ + std::string json = R"( + { + "payload_type" : "START_GAME" , + "payload" : null + } + )"; + Message message = MessageSerializer::deserialize(json); + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::START_GAME); +} + +TEST(MessageSerializerTest, DeserializeCompactJSON) +{ + std::string json = R"({"payload_type":"START_GAME","payload":null})"; + Message message = MessageSerializer::deserialize(json); + EXPECT_EQ(message.getMessagePayloadType(), MessagePayloadType::START_GAME); +} + +TEST(MessageSerializerTest, DeserializeAllCardColors) +{ + std::vector> colors = { + {"Red", CardColor::RED}, {"Blue", CardColor::BLUE}, {"Green", CardColor::GREEN}, {"Yellow", CardColor::YELLOW}}; + + for (const auto &[colorStr, colorEnum] : colors) { + nlohmann::json json = {{"payload_type", "PLAY_CARD"}, {"payload", {{"card", {{"card_color", colorStr}, {"card_type", "5"}}}}}}; + Message message = MessageSerializer::deserialize(json.dump()); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.card.getColor(), colorEnum); + } +} + +TEST(MessageSerializerTest, DeserializeAllCardTypes) +{ + std::vector> types = {{"0", CardType::NUM0}, + {"1", CardType::NUM1}, + {"2", CardType::NUM2}, + {"3", CardType::NUM3}, + {"4", CardType::NUM4}, + {"5", CardType::NUM5}, + {"6", CardType::NUM6}, + {"7", CardType::NUM7}, + {"8", CardType::NUM8}, + {"9", CardType::NUM9}, + {"Skip", CardType::SKIP}, + {"Reverse", CardType::REVERSE}, + {"Draw 2", CardType::DRAW2}, + {"Wild", CardType::WILD}, + {"Wild Draw 4", CardType::WILDDRAWFOUR}}; + + for (const auto &[typeStr, typeEnum] : types) { + nlohmann::json json = {{"payload_type", "PLAY_CARD"}, {"payload", {{"card", {{"card_color", "Red"}, {"card_type", typeStr}}}}}}; + Message message = MessageSerializer::deserialize(json.dump()); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.card.getType(), typeEnum); + } +} + +// ========== Security Tests ========== + +TEST(MessageSerializerTest, DeserializeJSONBomb) +{ + // Test deeply nested JSON (potential DoS) + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"player"}})"; + for (int i = 0; i < 100; ++i) { + json = std::string(R"({"nested":)").append(json).append("}"); + } + EXPECT_THROW(MessageSerializer::deserialize(json), std::exception); +} + +TEST(MessageSerializerTest, DeserializeHugeArray) +{ + // Test with a very large array + nlohmann::json json; + json["payload_type"] = "INIT_GAME"; + json["payload"]["discard_pile"] = nlohmann::json::array(); + for (int i = 0; i < 10000; ++i) { + json["payload"]["discard_pile"].push_back({{"card_color", "Red"}, {"card_type", "5"}}); + } + json["payload"]["hand_card"] = nlohmann::json::array(); + json["payload"]["current_player"] = 0; + + EXPECT_NO_THROW(MessageSerializer::deserialize(json.dump())); +} + +TEST(MessageSerializerTest, DeserializeNullCharacterInString) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"Player)" + std::string(1, '\0') + R"(1"}})"; + EXPECT_THROW(MessageSerializer::deserialize(json), std::invalid_argument); +} + +TEST(MessageSerializerTest, DeserializeControlCharactersInString) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"Player\u0001\u001f"}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName.length(), 8); +} + +TEST(MessageSerializerTest, DeserializeSQLInjectionAttempt) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"'; DROP TABLE users; --"}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, "'; DROP TABLE users; --"); +} + +TEST(MessageSerializerTest, DeserializeXSSAttempt) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":""}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, ""); +} + +TEST(MessageSerializerTest, DeserializePathTraversalAttempt) +{ + std::string json = R"({"payload_type":"JOIN_GAME","payload":{"name":"../../etc/passwd"}})"; + Message message = MessageSerializer::deserialize(json); + auto payload = std::get(message.getMessagePayload()); + EXPECT_EQ(payload.playerName, "../../etc/passwd"); +} +