From a10674fbd372d2151171afc0593e2cb4baa6c305 Mon Sep 17 00:00:00 2001 From: Kieran Kihn <114803508+kierankihn@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:52:08 +0800 Subject: [PATCH] test(network): add unit tests for `NetworkClient` and `NetworkServer` - Introduced comprehensive test cases for `NetworkClient` and `NetworkServer` functionality. - Added tests for constructor, `connect`, `send`, `read`, and round-trip scenarios in `NetworkClientTest`. - Added tests for player management, message sending, and concurrent access in `NetworkServerTest`. - Updated `CMakeLists.txt` to include `NetworkClientTest` and `NetworkServerTest` in the build configuration. --- test/CMakeLists.txt | 2 + test/unit/network/NetworkClientTest.cpp | 557 +++++++++++++++++++++ test/unit/network/NetworkServerTest.cpp | 636 ++++++++++++++++++++++++ 3 files changed, 1195 insertions(+) create mode 100644 test/unit/network/NetworkClientTest.cpp create mode 100644 test/unit/network/NetworkServerTest.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ec93c4c..eb2aac0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,6 +11,8 @@ add_executable(uno-game-test unit/game/PlayerTest.cpp unit/game/GameStateTest.cpp unit/network/MessageSerializerTest.cpp + unit/network/NetworkServerTest.cpp + unit/network/NetworkClientTest.cpp ) target_link_libraries(uno-game-test diff --git a/test/unit/network/NetworkClientTest.cpp b/test/unit/network/NetworkClientTest.cpp new file mode 100644 index 0000000..5e41f39 --- /dev/null +++ b/test/unit/network/NetworkClientTest.cpp @@ -0,0 +1,557 @@ +/** + * @file NetworkClientTest.cpp + * + * @author Yuzhe Guo + * @date 2025.11.28 + */ + +#include "../../../src/network/NetworkClient.h" + +#include +#include +#include +#include +#include + +using namespace UNO::NETWORK; + +// Helper class to create a simple echo server for testing +class SimpleTestServer { +private: + asio::io_context io_context_; + asio::ip::tcp::acceptor acceptor_; + std::thread server_thread_; + std::atomic running_{true}; + +public: + SimpleTestServer(uint16_t port) : acceptor_(io_context_, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)) + { + server_thread_ = std::thread([this]() { runServer(); }); + } + + ~SimpleTestServer() + { + running_ = false; + asio::error_code ec; + acceptor_.close(ec); + io_context_.stop(); + if (server_thread_.joinable()) { + server_thread_.join(); + } + } + +private: + void runServer() + { + while (running_) { + try { + asio::ip::tcp::socket socket(io_context_); + acceptor_.accept(socket); + + // Echo server: read message and send it back + while (socket.is_open() && running_) { + try { + size_t length; + // Use async read with timeout simulation via non-blocking approach + asio::error_code ec; + + // Try to read the length with error handling + asio::read(socket, asio::buffer(&length, sizeof(length)), ec); + if (ec) { + break; // Connection closed or error + } + + if (length <= 10 * 1024 * 1024) { + std::vector buffer(length); + asio::read(socket, asio::buffer(buffer), ec); + if (ec) { + break; // Connection closed or error + } + + // Echo back + asio::write(socket, asio::buffer(&length, sizeof(length)), ec); + if (ec) { + break; + } + asio::write(socket, asio::buffer(buffer), ec); + if (ec) { + break; + } + } + } + catch (...) { + break; + } + } + + // Ensure socket is closed when exiting the loop + asio::error_code ec; + socket.close(ec); + } + catch (...) { + if (!running_) { + break; + } + } + } + } +}; + +// ========== NetworkClient Constructor Tests ========== + +TEST(NetworkClientTest, ConstructorWithCallback) +{ + auto callback = [](std::string message) {}; + + EXPECT_NO_THROW({ NetworkClient client(callback); }); +} + +TEST(NetworkClientTest, ConstructorWithEmptyCallback) +{ + auto callback = [](std::string message) {}; + + EXPECT_NO_THROW({ NetworkClient client(callback); }); +} + +// ========== NetworkClient Connect Tests ========== + +TEST(NetworkClientTest, ConnectToValidServer) +{ + SimpleTestServer server(30001); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + EXPECT_NO_THROW({ client.connect("127.0.0.1", 30001); }); +} + +TEST(NetworkClientTest, ConnectToLocalhost) +{ + SimpleTestServer server(30002); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + EXPECT_NO_THROW({ client.connect("localhost", 30002); }); +} + +TEST(NetworkClientTest, ConnectToInvalidHost) +{ + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + // Should throw when trying to resolve invalid host + EXPECT_THROW({ client.connect("invalid.host.that.does.not.exist.12345", 30003); }, std::exception); +} + +TEST(NetworkClientTest, ConnectToUnreachablePort) +{ + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + // Should throw when trying to connect to a port with no server + EXPECT_THROW({ client.connect("127.0.0.1", 30004); }, std::exception); +} + +// ========== NetworkClient Send Tests ========== + +TEST(NetworkClientTest, SendSimpleMessage) +{ + SimpleTestServer server(30005); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30005); + + EXPECT_NO_THROW({ client.send("Hello, Server!"); }); +} + +TEST(NetworkClientTest, SendEmptyMessage) +{ + SimpleTestServer server(30006); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30006); + + EXPECT_NO_THROW({ client.send(""); }); +} + +TEST(NetworkClientTest, SendLargeMessage) +{ + SimpleTestServer server(30007); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30007); + + std::string large_message(1024 * 1024, 'X'); // 1MB message + EXPECT_NO_THROW({ client.send(large_message); }); +} + +TEST(NetworkClientTest, SendMessageWithSpecialCharacters) +{ + SimpleTestServer server(30008); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30008); + + EXPECT_NO_THROW({ client.send("Special: \n\t\r\"'\\"); }); +} + +TEST(NetworkClientTest, SendMessageWithUnicode) +{ + SimpleTestServer server(30009); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30009); + + EXPECT_NO_THROW({ client.send("Unicode: 你好世界 🎮🃏"); }); +} + +TEST(NetworkClientTest, SendMultipleMessages) +{ + SimpleTestServer server(30010); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30010); + + for (int i = 0; i < 10; ++i) { + EXPECT_NO_THROW({ client.send("Message " + std::to_string(i)); }); + } +} + +TEST(NetworkClientTest, SendJSONMessage) +{ + SimpleTestServer server(30011); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30011); + + std::string json_message = R"({"status":"OK","type":"TEST","data":"value"})"; + EXPECT_NO_THROW({ client.send(json_message); }); +} + +// ========== NetworkClient Read Tests ========== + +TEST(NetworkClientTest, ReadMessageFromServer) +{ + SimpleTestServer server(30012); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30012); + + std::string test_message = "Test Message"; + client.send(test_message); + + std::string received; + EXPECT_NO_THROW({ received = client.read(); }); + + EXPECT_EQ(received, test_message); +} + +TEST(NetworkClientTest, ReadEmptyMessage) +{ + SimpleTestServer server(30013); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30013); + + client.send(""); + + std::string received; + EXPECT_NO_THROW({ received = client.read(); }); + + EXPECT_EQ(received, ""); +} + +TEST(NetworkClientTest, ReadLargeMessage) +{ + SimpleTestServer server(30014); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30014); + + std::string large_message(100 * 1024, 'L'); // 100KB + client.send(large_message); + + std::string received; + EXPECT_NO_THROW({ received = client.read(); }); + + EXPECT_EQ(received, large_message); +} + +TEST(NetworkClientTest, ReadMessageWithSpecialCharacters) +{ + SimpleTestServer server(30015); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30015); + + std::string special_message = "Special: \n\t\r\"'\\"; + client.send(special_message); + + std::string received = client.read(); + EXPECT_EQ(received, special_message); +} + +TEST(NetworkClientTest, ReadMessageWithUnicode) +{ + SimpleTestServer server(30016); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30016); + + std::string unicode_message = "Unicode: 你好世界 🎮"; + client.send(unicode_message); + + std::string received = client.read(); + EXPECT_EQ(received, unicode_message); +} + +TEST(NetworkClientTest, ReadMultipleMessages) +{ + SimpleTestServer server(30017); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30017); + + for (int i = 0; i < 5; ++i) { + std::string test_message = "Message " + std::to_string(i); + client.send(test_message); + + std::string received = client.read(); + EXPECT_EQ(received, test_message); + } +} + +// ========== Round-trip Tests ========== + +TEST(NetworkClientTest, RoundTripSimpleMessage) +{ + SimpleTestServer server(30018); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30018); + + std::string original = "Round trip test"; + client.send(original); + std::string received = client.read(); + + EXPECT_EQ(original, received); +} + +TEST(NetworkClientTest, RoundTripJSONMessage) +{ + SimpleTestServer server(30019); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30019); + + std::string json = R"({"player":"Alice","action":"DRAW_CARD","count":3})"; + client.send(json); + std::string received = client.read(); + + EXPECT_EQ(json, received); +} + +TEST(NetworkClientTest, RoundTripMultipleMessages) +{ + SimpleTestServer server(30020); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30020); + + std::vector messages = {"First message", "Second message with 123", "Third: special chars \n\t", "Fourth: 你好", ""}; + + for (const auto &msg : messages) { + client.send(msg); + std::string received = client.read(); + EXPECT_EQ(msg, received); + } +} + +TEST(NetworkClientTest, RoundTripLargeMessages) +{ + SimpleTestServer server(30021); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30021); + + for (size_t size : {1024, 10240, 102400}) { + std::string large_msg(size, 'Z'); + client.send(large_msg); + std::string received = client.read(); + EXPECT_EQ(large_msg.size(), received.size()); + EXPECT_EQ(large_msg, received); + } +} + +// ========== Edge Cases ========== + +TEST(NetworkClientTest, SendBeforeConnect) +{ + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + // Should throw because not connected + EXPECT_THROW({ client.send("Message before connect"); }, std::exception); +} + +TEST(NetworkClientTest, ReadBeforeConnect) +{ + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + // Should throw because not connected + EXPECT_THROW({ client.read(); }, std::exception); +} + +TEST(NetworkClientTest, MultipleConnectAttempts) +{ + SimpleTestServer server1(30022); + SimpleTestServer server2(30023); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + + // First connection should succeed + EXPECT_NO_THROW({ client.connect("127.0.0.1", 30022); }); + + // Second connection attempt should also work (replaces first connection) + EXPECT_NO_THROW({ client.connect("127.0.0.1", 30023); }); +} + +TEST(NetworkClientTest, RapidSendAndRead) +{ + SimpleTestServer server(30025); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30025); + + // Rapidly send and read 100 messages + for (int i = 0; i < 100; ++i) { + std::string msg = std::to_string(i); + client.send(msg); + std::string received = client.read(); + EXPECT_EQ(msg, received); + } +} + +TEST(NetworkClientTest, VeryLongMessageContent) +{ + SimpleTestServer server(30026); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30026); + + // Create a complex message with various characters + std::string complex_message; + for (int i = 0; i < 1000; ++i) { + complex_message += "Line " + std::to_string(i) + ": Hello World! 你好世界 🎮\n"; + } + + client.send(complex_message); + std::string received = client.read(); + EXPECT_EQ(complex_message, received); +} + +TEST(NetworkClientTest, SendBinaryData) +{ + SimpleTestServer server(30027); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30027); + + // Create binary data with all byte values + std::string binary_data; + for (int i = 1; i < 256; ++i) { // Skip 0 to avoid null terminator issues + binary_data += static_cast(i); + } + + client.send(binary_data); + std::string received = client.read(); + EXPECT_EQ(binary_data.size(), received.size()); + EXPECT_EQ(binary_data, received); +} + +TEST(NetworkClientTest, AlternatingReadAndWrite) +{ + SimpleTestServer server(30028); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30028); + + for (int i = 0; i < 20; ++i) { + if (i % 2 == 0) { + client.send("Message " + std::to_string(i)); + } + else { + std::string received = client.read(); + EXPECT_FALSE(received.empty()); + } + } +} + +// ========== Protocol Compliance Tests ========== + +TEST(NetworkClientTest, MessageLengthPrefixCorrect) +{ + SimpleTestServer server(30029); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + auto callback = [](std::string message) {}; + NetworkClient client(callback); + client.connect("127.0.0.1", 30029); + + // Test various message lengths + for (size_t len : {0, 1, 10, 100, 1000, 10000}) { + std::string msg(len, 'A'); + client.send(msg); + std::string received = client.read(); + EXPECT_EQ(len, received.size()); + EXPECT_EQ(msg, received); + } +} diff --git a/test/unit/network/NetworkServerTest.cpp b/test/unit/network/NetworkServerTest.cpp new file mode 100644 index 0000000..6d0e4ba --- /dev/null +++ b/test/unit/network/NetworkServerTest.cpp @@ -0,0 +1,636 @@ +/** + * @file NetworkServerTest.cpp + * + * @author Yuzhe Guo + * @date 2025.11.28 + */ + +#include "../../../src/network/NetworkServer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace UNO::NETWORK; + +// ========== NetworkServer Constructor Tests ========== + +TEST(NetworkServerTest, ConstructorWithValidPort) +{ + auto callback = [](size_t player_id, std::string message) {}; + + EXPECT_NO_THROW({ NetworkServer server(20001, callback); }); +} + +TEST(NetworkServerTest, ConstructorWithZeroPort) +{ + auto callback = [](size_t player_id, std::string message) {}; + + // Port 0 should let OS assign a port + EXPECT_NO_THROW({ NetworkServer server(0, callback); }); +} + +TEST(NetworkServerTest, ConstructorWithHighPort) +{ + auto callback = [](size_t player_id, std::string message) {}; + + EXPECT_NO_THROW({ NetworkServer server(65535, callback); }); +} + +// ========== NetworkServer AddPlayer Tests ========== + +TEST(NetworkServerTest, AddPlayerSinglePlayer) +{ + std::atomic callback_called{false}; + std::atomic received_player_id{999}; + std::string received_message; + std::mutex msg_mutex; + + auto callback = [&callback_called, &received_player_id, &received_message, &msg_mutex](size_t player_id, std::string message) { + callback_called = true; + received_player_id = player_id; + std::lock_guard lock(msg_mutex); + received_message = std::move(message); + }; + + NetworkServer server(20002, callback); + + // Start server in background thread + std::thread server_thread([&server]() { server.run(); }); + + // Give server time to start + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Connect a client + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20002"); + asio::connect(socket, endpoints); + + // Give time for connection to be established + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Send a message to trigger callback + std::string test_message = "Hello Server"; + size_t length = test_message.size(); + asio::write(socket, asio::buffer(&length, sizeof(length))); + asio::write(socket, asio::buffer(test_message)); + + // Give time for message to be received + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Verify callback was called + EXPECT_TRUE(callback_called); + EXPECT_EQ(received_player_id, 0); + { + std::lock_guard lock(msg_mutex); + EXPECT_EQ(received_message, "Hello Server"); + } + + socket.close(); + io_context.stop(); + + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, AddPlayerMultiplePlayers) +{ + std::mutex mutex; + size_t message_count{0}; + std::set received_player_ids; + + auto callback = [&message_count, &received_player_ids, &mutex](size_t player_id, std::string message) { + std::lock_guard lock(mutex); + message_count++; + received_player_ids.insert(player_id); + }; + + NetworkServer server(20003, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Connect multiple clients + std::vector> contexts; + std::vector> sockets; + + for (int i = 0; i < 3; ++i) { + contexts.push_back(std::make_unique()); + sockets.push_back(std::make_unique(*contexts.back())); + + asio::ip::tcp::resolver resolver(*contexts.back()); + auto endpoints = resolver.resolve("127.0.0.1", "20003"); + asio::connect(*sockets.back(), endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Each client sends a message + for (int i = 0; i < 3; ++i) { + std::string msg = "Message from client " + std::to_string(i); + size_t length = msg.size(); + asio::write(*sockets[i], asio::buffer(&length, sizeof(length))); + asio::write(*sockets[i], asio::buffer(msg)); + } + + // Give time for messages to be received + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // Verify all callbacks were called + EXPECT_EQ(message_count, 3); + { + std::lock_guard lock(mutex); + EXPECT_EQ(received_player_ids.size(), 3); + EXPECT_TRUE(received_player_ids.contains(0)); + EXPECT_TRUE(received_player_ids.contains(1)); + EXPECT_TRUE(received_player_ids.contains(2)); + } + + // Cleanup + for (auto &socket : sockets) { + socket->close(); + } + for (auto &context : contexts) { + context->stop(); + } + + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +// ========== NetworkServer Send Tests ========== + +TEST(NetworkServerTest, SendMessageToValidPlayer) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20004, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Connect a client + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20004"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Send message to player 0 + EXPECT_NO_THROW({ server.send(0, "test message"); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Read the message from client side to verify it was sent + std::atomic message_received{false}; + std::string received_msg; + std::thread read_thread([&socket, &message_received, &received_msg]() { + try { + size_t length; + asio::read(socket, asio::buffer(&length, sizeof(length))); + std::vector buffer(length); + asio::read(socket, asio::buffer(buffer)); + received_msg = std::string(buffer.begin(), buffer.end()); + if (received_msg == "test message") { + message_received = true; + } + } + catch (...) { + } + }); + + read_thread.join(); + EXPECT_TRUE(message_received); + EXPECT_EQ(received_msg, "test message"); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, SendMessageToInvalidPlayer) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20005, callback); + + // Try to send to non-existent player + EXPECT_THROW({ server.send(999, "test message"); }, std::invalid_argument); +} + +TEST(NetworkServerTest, SendEmptyMessage) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20006, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20006"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_NO_THROW({ server.send(0, ""); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify empty message was received + std::atomic message_received{false}; + std::thread read_thread([&socket, &message_received]() { + try { + size_t length; + asio::read(socket, asio::buffer(&length, sizeof(length))); + if (length == 0) { + message_received = true; + } + } + catch (...) { + } + }); + + read_thread.join(); + EXPECT_TRUE(message_received); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, SendLargeMessage) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20007, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20007"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + std::string large_message(1024 * 1024, 'A'); // 1MB message + EXPECT_NO_THROW({ server.send(0, large_message); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Verify large message was received + std::atomic message_received{false}; + std::thread read_thread([&socket, &message_received, &large_message]() { + try { + size_t length; + asio::read(socket, asio::buffer(&length, sizeof(length))); + std::vector buffer(length); + asio::read(socket, asio::buffer(buffer)); + std::string received(buffer.begin(), buffer.end()); + if (received == large_message) { + message_received = true; + } + } + catch (...) { + } + }); + + read_thread.join(); + EXPECT_TRUE(message_received); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, SendMessageWithSpecialCharacters) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20008, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20008"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + std::string special_msg = "Special chars: \n\t\r\"'\\"; + EXPECT_NO_THROW({ server.send(0, special_msg); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify message with special characters was received + std::atomic message_received{false}; + std::thread read_thread([&socket, &message_received, &special_msg]() { + try { + size_t length; + asio::read(socket, asio::buffer(&length, sizeof(length))); + std::vector buffer(length); + asio::read(socket, asio::buffer(buffer)); + std::string received(buffer.begin(), buffer.end()); + if (received == special_msg) { + message_received = true; + } + } + catch (...) { + } + }); + + read_thread.join(); + EXPECT_TRUE(message_received); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, SendMessageWithUnicode) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20009, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20009"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + std::string unicode_msg = "Unicode: 你好世界 🎮"; + EXPECT_NO_THROW({ server.send(0, unicode_msg); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify Unicode message was received + std::atomic message_received{false}; + std::thread read_thread([&socket, &message_received, &unicode_msg]() { + try { + size_t length; + asio::read(socket, asio::buffer(&length, sizeof(length))); + std::vector buffer(length); + asio::read(socket, asio::buffer(buffer)); + std::string received(buffer.begin(), buffer.end()); + if (received == unicode_msg) { + message_received = true; + } + } + catch (...) { + } + }); + + read_thread.join(); + EXPECT_TRUE(message_received); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +// ========== Session Tests ========== + +TEST(SessionTest, SessionCreation) +{ + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + + EXPECT_NO_THROW({ Session session(0, std::move(socket)); }); +} + +TEST(SessionTest, SessionStartWithCallback) +{ + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + + auto session = std::make_shared(0, std::move(socket)); + + auto callback = [](size_t player_id, std::string message) {}; + + EXPECT_NO_THROW({ session->start(callback); }); +} + +// ========== Concurrent Access Tests ========== + +TEST(NetworkServerTest, ConcurrentSendToSamePlayer) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20010, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20010"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Multiple threads sending to same player + std::vector threads; + for (int i = 0; i < 10; ++i) { + threads.emplace_back([&server, i]() { server.send(0, "Message " + std::to_string(i)); }); + } + + for (auto &t : threads) { + t.join(); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, ConcurrentSendToDifferentPlayers) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20011, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Connect multiple clients + std::vector> contexts; + std::vector> sockets; + + for (int i = 0; i < 3; ++i) { + contexts.push_back(std::make_unique()); + sockets.push_back(std::make_unique(*contexts.back())); + + asio::ip::tcp::resolver resolver(*contexts.back()); + auto endpoints = resolver.resolve("127.0.0.1", "20011"); + asio::connect(*sockets.back(), endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Multiple threads sending to different players + std::vector threads; + for (int i = 0; i < 3; ++i) { + threads.emplace_back([&server, i]() { server.send(i, "Message to player " + std::to_string(i)); }); + } + + for (auto &t : threads) { + t.join(); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Cleanup + for (auto &socket : sockets) { + socket->close(); + } + for (auto &context : contexts) { + context->stop(); + } + + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +// ========== Edge Cases ========== + +TEST(NetworkServerTest, MultipleMessagesToSamePlayer) +{ + std::atomic callback_count{0}; + auto callback = [&callback_count](size_t player_id, std::string message) { callback_count++; }; + + NetworkServer server(20012, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20012"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Send multiple messages from server + for (int i = 0; i < 10; ++i) { + EXPECT_NO_THROW({ server.send(0, "Message " + std::to_string(i)); }); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Client sends messages back to trigger callbacks + for (int i = 0; i < 5; ++i) { + std::string msg = "Client message " + std::to_string(i); + size_t length = msg.size(); + asio::write(socket, asio::buffer(&length, sizeof(length))); + asio::write(socket, asio::buffer(msg)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Verify callbacks were called + EXPECT_EQ(callback_count, 5); + + socket.close(); + io_context.stop(); + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +} + +TEST(NetworkServerTest, SendAfterPlayerDisconnect) +{ + auto callback = [](size_t player_id, std::string message) {}; + + NetworkServer server(20013, callback); + + std::thread server_thread([&server]() { server.run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + { + asio::io_context io_context; + asio::ip::tcp::socket socket(io_context); + asio::ip::tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve("127.0.0.1", "20013"); + asio::connect(socket, endpoints); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Socket goes out of scope and closes + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Player 0 should still exist in the session map even after disconnect + EXPECT_NO_THROW({ server.send(0, "Message after disconnect"); }); + + server.stop(); + if (server_thread.joinable()) { + server_thread.join(); + } +}