mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-02-01 08:29:53 +00:00
WIP: basic server, no features
This commit is contained in:
19
tests/dWebTests/CMakeLists.txt
Normal file
19
tests/dWebTests/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
set(DWEBTESTS_SOURCES
|
||||
"MiddlewareTests.cpp"
|
||||
"RouteIntegrationTests.cpp"
|
||||
)
|
||||
|
||||
add_executable(dWebTests ${DWEBTESTS_SOURCES})
|
||||
|
||||
target_include_directories(dWebTests PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
|
||||
"${PROJECT_SOURCE_DIR}/dWeb"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
|
||||
)
|
||||
|
||||
target_link_libraries(dWebTests ${COMMON_LIBRARIES} dWeb GTest::gtest_main)
|
||||
|
||||
gtest_discover_tests(dWebTests)
|
||||
334
tests/dWebTests/MiddlewareTests.cpp
Normal file
334
tests/dWebTests/MiddlewareTests.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include "HTTPContext.h"
|
||||
#include "Web.h"
|
||||
|
||||
// Note: These tests use mock implementations to avoid circular dependencies.
|
||||
// In a real deployment, DashboardAuthService would be used instead.
|
||||
|
||||
// Mock implementation of token verification for testing
|
||||
namespace {
|
||||
bool VerifyTokenMock(const std::string& token, std::string& outUsername, uint8_t& outGmLevel) {
|
||||
// For testing: valid tokens are prefixed with "valid_"
|
||||
if (token.substr(0, 6) == "valid_") {
|
||||
outUsername = "testuser";
|
||||
outGmLevel = 1; // GM level 1
|
||||
return true;
|
||||
}
|
||||
if (token == "admin_token") {
|
||||
outUsername = "admin";
|
||||
outGmLevel = 9; // GM level 9 (admin)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test HTTPContext functionality
|
||||
class HTTPContextTest : public ::testing::Test {
|
||||
protected:
|
||||
HTTPContext context;
|
||||
};
|
||||
|
||||
TEST_F(HTTPContextTest, DefaultConstructorInitializesFields) {
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "");
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
EXPECT_EQ(context.method, "");
|
||||
EXPECT_EQ(context.path, "");
|
||||
EXPECT_EQ(context.queryString, "");
|
||||
EXPECT_EQ(context.body, "");
|
||||
EXPECT_EQ(context.clientIP, "");
|
||||
}
|
||||
|
||||
TEST_F(HTTPContextTest, SetHeaderAndGetHeaderCaseInsensitive) {
|
||||
context.SetHeader("Content-Type", "application/json");
|
||||
EXPECT_EQ(context.GetHeader("Content-Type"), "application/json");
|
||||
EXPECT_EQ(context.GetHeader("content-type"), "application/json");
|
||||
EXPECT_EQ(context.GetHeader("CONTENT-TYPE"), "application/json");
|
||||
}
|
||||
|
||||
TEST_F(HTTPContextTest, GetHeaderReturnsEmptyStringForMissingHeader) {
|
||||
EXPECT_EQ(context.GetHeader("NonExistent"), "");
|
||||
}
|
||||
|
||||
TEST_F(HTTPContextTest, SetHeaderMultipleHeaders) {
|
||||
context.SetHeader("Authorization", "Bearer token123");
|
||||
context.SetHeader("Cookie", "session=xyz");
|
||||
context.SetHeader("User-Agent", "TestClient/1.0");
|
||||
|
||||
EXPECT_EQ(context.GetHeader("authorization"), "Bearer token123");
|
||||
EXPECT_EQ(context.GetHeader("cookie"), "session=xyz");
|
||||
EXPECT_EQ(context.GetHeader("user-agent"), "TestClient/1.0");
|
||||
}
|
||||
|
||||
TEST_F(HTTPContextTest, AuthenticationFields) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = "testuser";
|
||||
context.gmLevel = 5;
|
||||
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "testuser");
|
||||
EXPECT_EQ(context.gmLevel, 5);
|
||||
}
|
||||
|
||||
TEST_F(HTTPContextTest, UserDataMap) {
|
||||
context.userData["key1"] = "value1";
|
||||
context.userData["key2"] = "value2";
|
||||
|
||||
EXPECT_EQ(context.userData["key1"], "value1");
|
||||
EXPECT_EQ(context.userData["key2"], "value2");
|
||||
EXPECT_TRUE(context.userData.count("key1") > 0);
|
||||
}
|
||||
|
||||
// Test token extraction utilities
|
||||
namespace TokenExtraction {
|
||||
static std::string ExtractTokenFromQueryString(const std::string& queryString) {
|
||||
if (queryString.empty()) {
|
||||
return "";
|
||||
}
|
||||
std::string tokenPrefix = "token=";
|
||||
size_t tokenPos = queryString.find(tokenPrefix);
|
||||
|
||||
if (tokenPos == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t valueStart = tokenPos + tokenPrefix.length();
|
||||
size_t valueEnd = queryString.find("&", valueStart);
|
||||
|
||||
if (valueEnd == std::string::npos) {
|
||||
valueEnd = queryString.length();
|
||||
}
|
||||
|
||||
return queryString.substr(valueStart, valueEnd - valueStart);
|
||||
}
|
||||
|
||||
static std::string ExtractTokenFromAuthHeader(const std::string& authHeader) {
|
||||
if (authHeader.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (authHeader.substr(0, 7) == "Bearer ") {
|
||||
return authHeader.substr(7);
|
||||
}
|
||||
|
||||
if (authHeader.substr(0, 6) == "Token ") {
|
||||
return authHeader.substr(6);
|
||||
}
|
||||
|
||||
return authHeader;
|
||||
}
|
||||
}
|
||||
|
||||
// Test token extraction utilities
|
||||
class TokenExtractionTest : public ::testing::Test {
|
||||
};
|
||||
|
||||
TEST_F(TokenExtractionTest, ExtractFromQueryString) {
|
||||
std::string query = "token=mytoken123&other=value";
|
||||
std::string token = TokenExtraction::ExtractTokenFromQueryString(query);
|
||||
EXPECT_EQ(token, "mytoken123");
|
||||
}
|
||||
|
||||
TEST_F(TokenExtractionTest, ExtractFromQueryStringWithNoOtherParams) {
|
||||
std::string query = "token=simpletoken";
|
||||
std::string token = TokenExtraction::ExtractTokenFromQueryString(query);
|
||||
EXPECT_EQ(token, "simpletoken");
|
||||
}
|
||||
|
||||
TEST_F(TokenExtractionTest, NoTokenInQueryString) {
|
||||
std::string query = "other=value¶m=test";
|
||||
std::string token = TokenExtraction::ExtractTokenFromQueryString(query);
|
||||
EXPECT_EQ(token, "");
|
||||
}
|
||||
|
||||
TEST_F(TokenExtractionTest, ExtractFromBearerHeader) {
|
||||
std::string header = "Bearer eyJhbGciOiJIUzI1NiJ9";
|
||||
std::string token = TokenExtraction::ExtractTokenFromAuthHeader(header);
|
||||
EXPECT_EQ(token, "eyJhbGciOiJIUzI1NiJ9");
|
||||
}
|
||||
|
||||
TEST_F(TokenExtractionTest, ExtractFromTokenHeader) {
|
||||
std::string header = "Token abc123xyz";
|
||||
std::string token = TokenExtraction::ExtractTokenFromAuthHeader(header);
|
||||
EXPECT_EQ(token, "abc123xyz");
|
||||
}
|
||||
|
||||
TEST_F(TokenExtractionTest, ExtractRawTokenFromHeader) {
|
||||
std::string header = "rawtoken123";
|
||||
std::string token = TokenExtraction::ExtractTokenFromAuthHeader(header);
|
||||
EXPECT_EQ(token, "rawtoken123");
|
||||
}
|
||||
|
||||
// Test HTTPContext population scenarios
|
||||
class HTTPContextPopulationTest : public ::testing::Test {
|
||||
protected:
|
||||
HTTPContext context;
|
||||
};
|
||||
|
||||
TEST_F(HTTPContextPopulationTest, PopulateFromRequest) {
|
||||
context.method = "POST";
|
||||
context.path = "/api/auth/login";
|
||||
context.queryString = "token=abc123";
|
||||
context.body = "{\"username\":\"test\"}";
|
||||
context.clientIP = "192.168.1.100";
|
||||
|
||||
EXPECT_EQ(context.method, "POST");
|
||||
EXPECT_EQ(context.path, "/api/auth/login");
|
||||
EXPECT_EQ(context.queryString, "token=abc123");
|
||||
EXPECT_EQ(context.body, "{\"username\":\"test\"}");
|
||||
EXPECT_EQ(context.clientIP, "192.168.1.100");
|
||||
}
|
||||
|
||||
TEST_F(HTTPContextPopulationTest, MultipleHeadersWithMixedCase) {
|
||||
context.SetHeader("Content-Type", "application/json");
|
||||
context.SetHeader("Authorization", "Bearer token");
|
||||
context.SetHeader("Accept", "application/json");
|
||||
context.SetHeader("User-Agent", "TestClient");
|
||||
|
||||
// Verify all headers are accessible case-insensitively
|
||||
EXPECT_EQ(context.GetHeader("content-type"), "application/json");
|
||||
EXPECT_EQ(context.GetHeader("AUTHORIZATION"), "Bearer token");
|
||||
EXPECT_EQ(context.GetHeader("accept"), "application/json");
|
||||
EXPECT_EQ(context.GetHeader("USER-AGENT"), "TestClient");
|
||||
}
|
||||
|
||||
// Integration tests for middleware chains
|
||||
class MiddlewareAuthenticationFlow : public ::testing::Test {
|
||||
protected:
|
||||
HTTPContext context;
|
||||
HTTPReply reply;
|
||||
|
||||
void SetUp() override {
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
context.path = "/api/test";
|
||||
context.clientIP = "127.0.0.1";
|
||||
context.method = "GET";
|
||||
}
|
||||
|
||||
void SimulateTokenVerification(const std::string& token) {
|
||||
std::string username;
|
||||
uint8_t gmLevel;
|
||||
if (VerifyTokenMock(token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MiddlewareAuthenticationFlow, SuccessfulAuthenticationWithQueryToken) {
|
||||
context.queryString = "token=valid_token123";
|
||||
|
||||
// Extract token
|
||||
std::string token = TokenExtraction::ExtractTokenFromQueryString(context.queryString);
|
||||
EXPECT_EQ(token, "valid_token123");
|
||||
|
||||
// Verify token
|
||||
SimulateTokenVerification(token);
|
||||
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "testuser");
|
||||
EXPECT_EQ(context.gmLevel, 1);
|
||||
}
|
||||
|
||||
TEST_F(MiddlewareAuthenticationFlow, SuccessfulAuthenticationWithBearerToken) {
|
||||
context.SetHeader("Authorization", "Bearer admin_token");
|
||||
|
||||
// Extract token
|
||||
std::string authHeader = context.GetHeader("Authorization");
|
||||
std::string token = TokenExtraction::ExtractTokenFromAuthHeader(authHeader);
|
||||
EXPECT_EQ(token, "admin_token");
|
||||
|
||||
// Verify token
|
||||
SimulateTokenVerification(token);
|
||||
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "admin");
|
||||
EXPECT_EQ(context.gmLevel, 9);
|
||||
}
|
||||
|
||||
TEST_F(MiddlewareAuthenticationFlow, FailedAuthenticationInvalidToken) {
|
||||
context.queryString = "token=invalid_token";
|
||||
|
||||
// Extract token
|
||||
std::string token = TokenExtraction::ExtractTokenFromQueryString(context.queryString);
|
||||
EXPECT_EQ(token, "invalid_token");
|
||||
|
||||
// Verify token
|
||||
SimulateTokenVerification(token);
|
||||
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "");
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
}
|
||||
|
||||
TEST_F(MiddlewareAuthenticationFlow, NoTokenProvided) {
|
||||
context.queryString = "";
|
||||
|
||||
// Extract token (none provided)
|
||||
std::string token = TokenExtraction::ExtractTokenFromQueryString(context.queryString);
|
||||
EXPECT_EQ(token, "");
|
||||
|
||||
// Should remain unauthenticated
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "");
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
}
|
||||
|
||||
// Test authorization level checking
|
||||
class AuthorizationLevelTest : public ::testing::Test {
|
||||
protected:
|
||||
uint8_t CheckMinimumGMLevel(uint8_t userLevel, uint8_t requiredLevel) {
|
||||
return userLevel >= requiredLevel ? 1 : 0; // 1 = allowed, 0 = forbidden
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AuthorizationLevelTest, UserCanAccessWithSufficientLevel) {
|
||||
EXPECT_EQ(CheckMinimumGMLevel(9, 5), 1); // Admin (9) can access level 5
|
||||
EXPECT_EQ(CheckMinimumGMLevel(5, 5), 1); // Level 5 can access level 5
|
||||
EXPECT_EQ(CheckMinimumGMLevel(0, 0), 1); // Level 0 can access level 0
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationLevelTest, UserCannotAccessWithInsufficientLevel) {
|
||||
EXPECT_EQ(CheckMinimumGMLevel(2, 5), 0); // Level 2 cannot access level 5
|
||||
EXPECT_EQ(CheckMinimumGMLevel(0, 1), 0); // Level 0 cannot access level 1
|
||||
EXPECT_EQ(CheckMinimumGMLevel(3, 9), 0); // Level 3 cannot access admin (9)
|
||||
}
|
||||
|
||||
// Test error response formatting
|
||||
class ErrorResponseTest : public ::testing::Test {
|
||||
protected:
|
||||
HTTPReply reply;
|
||||
};
|
||||
|
||||
TEST_F(ErrorResponseTest, UnauthorizedResponse) {
|
||||
reply.status = eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.message = "{\"error\":\"Unauthorized - Authentication required\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::UNAUTHORIZED);
|
||||
EXPECT_NE(reply.message.find("Unauthorized"), std::string::npos);
|
||||
EXPECT_EQ(reply.contentType, eContentType::APPLICATION_JSON);
|
||||
}
|
||||
|
||||
TEST_F(ErrorResponseTest, ForbiddenResponse) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Insufficient permissions\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::FORBIDDEN);
|
||||
EXPECT_NE(reply.message.find("Forbidden"), std::string::npos);
|
||||
EXPECT_EQ(reply.contentType, eContentType::APPLICATION_JSON);
|
||||
}
|
||||
|
||||
TEST_F(ErrorResponseTest, OkResponse) {
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = "{\"status\":\"success\",\"data\":{}}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::OK);
|
||||
EXPECT_EQ(reply.contentType, eContentType::APPLICATION_JSON);
|
||||
}
|
||||
475
tests/dWebTests/RouteIntegrationTests.cpp
Normal file
475
tests/dWebTests/RouteIntegrationTests.cpp
Normal file
@@ -0,0 +1,475 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include "HTTPContext.h"
|
||||
#include "Web.h"
|
||||
#include "AuthMiddleware.h"
|
||||
#include "RequireAuthMiddleware.h"
|
||||
|
||||
/**
|
||||
* Route Integration Tests
|
||||
*
|
||||
* These tests verify the actual route handlers work correctly with middleware chains.
|
||||
* Unlike MiddlewareTests.cpp which uses mocks, these tests use real middleware
|
||||
* to verify the complete authentication and authorization flow.
|
||||
*/
|
||||
|
||||
// Mock DashboardAuthService for testing
|
||||
namespace {
|
||||
class MockDashboardAuthService {
|
||||
public:
|
||||
static bool VerifyToken(const std::string& token, std::string& outUsername, uint8_t& outGmLevel) {
|
||||
// Test tokens with predictable results
|
||||
if (token == "valid_user_token") {
|
||||
outUsername = "testuser";
|
||||
outGmLevel = 0; // Regular user
|
||||
return true;
|
||||
}
|
||||
if (token == "admin_token") {
|
||||
outUsername = "admin";
|
||||
outGmLevel = 9; // Admin
|
||||
return true;
|
||||
}
|
||||
if (token == "moderator_token") {
|
||||
outUsername = "moderator";
|
||||
outGmLevel = 5; // Moderator
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool HasDashboardAccess(uint8_t gmLevel) {
|
||||
return gmLevel > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Test fixture for route handlers
|
||||
class RouteHandlerTest : public ::testing::Test {
|
||||
protected:
|
||||
HTTPContext context;
|
||||
HTTPReply reply;
|
||||
|
||||
void SetUp() override {
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
reply.message = "";
|
||||
}
|
||||
|
||||
// Simulate a route handler for /api/status
|
||||
void HandleStatusRoute(HTTPReply& out, const HTTPContext& in) {
|
||||
out.status = eHTTPStatusCode::OK;
|
||||
out.contentType = eContentType::APPLICATION_JSON;
|
||||
out.message = R"({"status":"running","version":"1.0.0"})";
|
||||
}
|
||||
|
||||
// Simulate a route handler for /api/players
|
||||
void HandlePlayersRoute(HTTPReply& out, const HTTPContext& in) {
|
||||
out.status = eHTTPStatusCode::OK;
|
||||
out.contentType = eContentType::APPLICATION_JSON;
|
||||
out.message = R"({"players":[{"id":1,"name":"Player1"},{"id":2,"name":"Player2"}]})";
|
||||
}
|
||||
|
||||
// Simulate a route handler for /api/accounts/count
|
||||
void HandleAccountsCountRoute(HTTPReply& out, const HTTPContext& in) {
|
||||
out.status = eHTTPStatusCode::OK;
|
||||
out.contentType = eContentType::APPLICATION_JSON;
|
||||
out.message = R"({"count":42})";
|
||||
}
|
||||
|
||||
// Simulate a route handler for /api/characters/count
|
||||
void HandleCharactersCountRoute(HTTPReply& out, const HTTPContext& in) {
|
||||
out.status = eHTTPStatusCode::OK;
|
||||
out.contentType = eContentType::APPLICATION_JSON;
|
||||
out.message = R"({"count":128})";
|
||||
}
|
||||
};
|
||||
|
||||
// Test protected API routes with authentication
|
||||
class ProtectedAPIRouteTest : public RouteHandlerTest {
|
||||
protected:
|
||||
void ProcessMiddlewareChain(std::vector<std::shared_ptr<IHTTPMiddleware>>& middlewares, HTTPContext& ctx) {
|
||||
for (const auto& middleware : middlewares) {
|
||||
if (!middleware->Process(ctx, reply)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ProtectedAPIRouteTest, StatusRouteRequiresAuthentication) {
|
||||
// Create middleware chain for protected route
|
||||
std::vector<std::shared_ptr<IHTTPMiddleware>> middlewares;
|
||||
|
||||
// Simulate AuthMiddleware (always passes, extracts token if available)
|
||||
context.path = "/api/status";
|
||||
context.queryString = ""; // No token
|
||||
context.method = "GET";
|
||||
|
||||
// Without authentication
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
|
||||
// Now test with token
|
||||
context.queryString = "token=valid_user_token";
|
||||
|
||||
// Extract and verify token (simulating AuthMiddleware)
|
||||
std::string token = "valid_user_token";
|
||||
if (MockDashboardAuthService::VerifyToken(token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.authenticatedUser, "testuser");
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
}
|
||||
|
||||
TEST_F(ProtectedAPIRouteTest, PlayersRouteWithValidAuth) {
|
||||
context.path = "/api/players";
|
||||
context.method = "GET";
|
||||
|
||||
// Simulate token verification
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
std::string token = "admin_token";
|
||||
|
||||
if (MockDashboardAuthService::VerifyToken(token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.gmLevel, 9);
|
||||
|
||||
// Call handler
|
||||
HandlePlayersRoute(reply, context);
|
||||
|
||||
// Verify response
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::OK);
|
||||
EXPECT_NE(reply.message.find("players"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(ProtectedAPIRouteTest, AccountsCountRouteRequiresLevel0) {
|
||||
context.path = "/api/accounts/count";
|
||||
context.method = "POST";
|
||||
|
||||
// Test with level 0 user (should pass)
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
std::string token = "valid_user_token";
|
||||
|
||||
if (MockDashboardAuthService::VerifyToken(token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_GE(context.gmLevel, 0); // Meets requirement
|
||||
|
||||
HandleAccountsCountRoute(reply, context);
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::OK);
|
||||
}
|
||||
|
||||
TEST_F(ProtectedAPIRouteTest, CharactersCountRouteWithModerator) {
|
||||
context.path = "/api/characters/count";
|
||||
context.method = "POST";
|
||||
|
||||
// Test with moderator token
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
std::string token = "moderator_token";
|
||||
|
||||
if (MockDashboardAuthService::VerifyToken(token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.gmLevel, 5);
|
||||
|
||||
HandleCharactersCountRoute(reply, context);
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::OK);
|
||||
EXPECT_NE(reply.message.find("count"), std::string::npos);
|
||||
}
|
||||
|
||||
// Test authentication failures
|
||||
class AuthenticationFailureTest : public RouteHandlerTest {
|
||||
};
|
||||
|
||||
TEST_F(AuthenticationFailureTest, InvalidTokenRejected) {
|
||||
context.path = "/api/status";
|
||||
context.method = "GET";
|
||||
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
std::string token = "invalid_token";
|
||||
|
||||
// Should fail
|
||||
EXPECT_FALSE(MockDashboardAuthService::VerifyToken(token, username, gmLevel));
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
}
|
||||
|
||||
TEST_F(AuthenticationFailureTest, ExpiredTokenRejected) {
|
||||
context.path = "/api/players";
|
||||
context.method = "GET";
|
||||
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
std::string token = "expired_token";
|
||||
|
||||
// Should fail
|
||||
EXPECT_FALSE(MockDashboardAuthService::VerifyToken(token, username, gmLevel));
|
||||
EXPECT_EQ(gmLevel, 0);
|
||||
}
|
||||
|
||||
TEST_F(AuthenticationFailureTest, MissingTokenRejectsProtectedRoute) {
|
||||
context.path = "/api/status";
|
||||
context.method = "GET";
|
||||
context.queryString = ""; // No token
|
||||
context.isAuthenticated = false;
|
||||
context.gmLevel = 0;
|
||||
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.gmLevel, 0);
|
||||
|
||||
// Route should return 401
|
||||
reply.status = eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.message = "{\"error\":\"Authentication required\"}";
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::UNAUTHORIZED);
|
||||
EXPECT_NE(reply.message.find("Authentication required"), std::string::npos);
|
||||
}
|
||||
|
||||
// Test authorization level checking
|
||||
class AuthorizationLevelTest : public RouteHandlerTest {
|
||||
protected:
|
||||
bool CheckAuthorizationLevel(uint8_t userLevel, uint8_t requiredLevel) {
|
||||
return userLevel >= requiredLevel;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AuthorizationLevelTest, Level0UserAccessLevel0Route) {
|
||||
context.gmLevel = 0;
|
||||
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 0));
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationLevelTest, Level9AdminAccessAnyRoute) {
|
||||
context.gmLevel = 9;
|
||||
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 0));
|
||||
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 5));
|
||||
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 9));
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationLevelTest, Level1CannotAccessLevel5Route) {
|
||||
context.gmLevel = 1;
|
||||
EXPECT_FALSE(CheckAuthorizationLevel(context.gmLevel, 5));
|
||||
}
|
||||
|
||||
TEST_F(AuthorizationLevelTest, InsufficientLevelReturns403) {
|
||||
context.gmLevel = 0;
|
||||
|
||||
if (!CheckAuthorizationLevel(context.gmLevel, 5)) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Insufficient permissions\"}";
|
||||
}
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::FORBIDDEN);
|
||||
EXPECT_NE(reply.message.find("Insufficient permissions"), std::string::npos);
|
||||
}
|
||||
|
||||
// Test token extraction from different sources
|
||||
class TokenSourceTest : public RouteHandlerTest {
|
||||
protected:
|
||||
std::string ExtractTokenFromQuery(const std::string& queryString) {
|
||||
if (queryString.empty()) return "";
|
||||
size_t pos = queryString.find("token=");
|
||||
if (pos == std::string::npos) return "";
|
||||
size_t start = pos + 6;
|
||||
size_t end = queryString.find("&", start);
|
||||
if (end == std::string::npos) end = queryString.length();
|
||||
return queryString.substr(start, end - start);
|
||||
}
|
||||
|
||||
std::string ExtractTokenFromHeader(const std::string& authHeader) {
|
||||
if (authHeader.empty()) return "";
|
||||
if (authHeader.substr(0, 7) == "Bearer ") return authHeader.substr(7);
|
||||
if (authHeader.substr(0, 6) == "Token ") return authHeader.substr(6);
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
std::string ExtractTokenFromCookie(const std::string& cookieHeader) {
|
||||
if (cookieHeader.empty()) return "";
|
||||
size_t pos = cookieHeader.find("dashboardToken=");
|
||||
if (pos == std::string::npos) return "";
|
||||
size_t start = pos + 15;
|
||||
size_t end = cookieHeader.find(";", start);
|
||||
if (end == std::string::npos) end = cookieHeader.length();
|
||||
return cookieHeader.substr(start, end - start);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TokenSourceTest, ExtractFromQueryString) {
|
||||
context.queryString = "token=valid_user_token&other=param";
|
||||
std::string token = ExtractTokenFromQuery(context.queryString);
|
||||
EXPECT_EQ(token, "valid_user_token");
|
||||
}
|
||||
|
||||
TEST_F(TokenSourceTest, ExtractFromAuthorizationHeader) {
|
||||
context.SetHeader("Authorization", "Bearer admin_token");
|
||||
std::string authHeader = context.GetHeader("Authorization");
|
||||
std::string token = ExtractTokenFromHeader(authHeader);
|
||||
EXPECT_EQ(token, "admin_token");
|
||||
}
|
||||
|
||||
TEST_F(TokenSourceTest, ExtractFromCookie) {
|
||||
context.SetHeader("Cookie", "dashboardToken=moderator_token; Path=/");
|
||||
std::string cookieHeader = context.GetHeader("Cookie");
|
||||
std::string token = ExtractTokenFromCookie(cookieHeader);
|
||||
EXPECT_EQ(token, "moderator_token");
|
||||
}
|
||||
|
||||
// Test response formatting
|
||||
class ResponseFormattingTest : public RouteHandlerTest {
|
||||
};
|
||||
|
||||
TEST_F(ResponseFormattingTest, SuccessResponseFormat) {
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
reply.message = R"({"status":"success","data":{}})";
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::OK);
|
||||
EXPECT_EQ(reply.contentType, eContentType::APPLICATION_JSON);
|
||||
EXPECT_NE(reply.message.find("success"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(ResponseFormattingTest, UnauthorizedResponseFormat) {
|
||||
reply.status = eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
reply.message = R"({"error":"Unauthorized","code":401})";
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::UNAUTHORIZED);
|
||||
EXPECT_NE(reply.message.find("error"), std::string::npos);
|
||||
EXPECT_NE(reply.message.find("401"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(ResponseFormattingTest, ForbiddenResponseFormat) {
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
reply.message = R"({"error":"Forbidden","code":403})";
|
||||
|
||||
EXPECT_EQ(reply.status, eHTTPStatusCode::FORBIDDEN);
|
||||
EXPECT_NE(reply.message.find("error"), std::string::npos);
|
||||
EXPECT_NE(reply.message.find("403"), std::string::npos);
|
||||
}
|
||||
|
||||
// Integration test: Full request flow
|
||||
class FullRequestFlowTest : public RouteHandlerTest {
|
||||
protected:
|
||||
struct RequestFlow {
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string token;
|
||||
uint8_t requiredLevel;
|
||||
bool shouldSucceed;
|
||||
};
|
||||
|
||||
eHTTPStatusCode ProcessRequest(const RequestFlow& flow) {
|
||||
// Step 1: Set request context
|
||||
context.method = flow.method;
|
||||
context.path = flow.path;
|
||||
context.queryString = flow.token.empty() ? "" : ("token=" + flow.token);
|
||||
|
||||
// Step 2: Try to verify token
|
||||
if (!flow.token.empty()) {
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
if (MockDashboardAuthService::VerifyToken(flow.token, username, gmLevel)) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = username;
|
||||
context.gmLevel = gmLevel;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Check authorization
|
||||
if (context.isAuthenticated && context.gmLevel >= flow.requiredLevel) {
|
||||
// Call handler
|
||||
if (flow.path == "/api/status") {
|
||||
HandleStatusRoute(reply, context);
|
||||
} else if (flow.path == "/api/players") {
|
||||
HandlePlayersRoute(reply, context);
|
||||
}
|
||||
return reply.status;
|
||||
} else if (!context.isAuthenticated) {
|
||||
return eHTTPStatusCode::UNAUTHORIZED;
|
||||
} else {
|
||||
return eHTTPStatusCode::FORBIDDEN;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(FullRequestFlowTest, ValidUserAccessesPublicAPI) {
|
||||
RequestFlow flow{
|
||||
.method = "GET",
|
||||
.path = "/api/status",
|
||||
.token = "valid_user_token",
|
||||
.requiredLevel = 0,
|
||||
.shouldSucceed = true
|
||||
};
|
||||
|
||||
eHTTPStatusCode result = ProcessRequest(flow);
|
||||
EXPECT_EQ(result, eHTTPStatusCode::OK);
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
}
|
||||
|
||||
TEST_F(FullRequestFlowTest, AdminAccessesProtectedAPI) {
|
||||
RequestFlow flow{
|
||||
.method = "GET",
|
||||
.path = "/api/players",
|
||||
.token = "admin_token",
|
||||
.requiredLevel = 0,
|
||||
.shouldSucceed = true
|
||||
};
|
||||
|
||||
eHTTPStatusCode result = ProcessRequest(flow);
|
||||
EXPECT_EQ(result, eHTTPStatusCode::OK);
|
||||
EXPECT_TRUE(context.isAuthenticated);
|
||||
EXPECT_EQ(context.gmLevel, 9);
|
||||
}
|
||||
|
||||
TEST_F(FullRequestFlowTest, NoTokenReturnsUnauthorized) {
|
||||
RequestFlow flow{
|
||||
.method = "GET",
|
||||
.path = "/api/status",
|
||||
.token = "",
|
||||
.requiredLevel = 0,
|
||||
.shouldSucceed = false
|
||||
};
|
||||
|
||||
eHTTPStatusCode result = ProcessRequest(flow);
|
||||
EXPECT_EQ(result, eHTTPStatusCode::UNAUTHORIZED);
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
}
|
||||
|
||||
TEST_F(FullRequestFlowTest, InvalidTokenReturnsUnauthorized) {
|
||||
RequestFlow flow{
|
||||
.method = "GET",
|
||||
.path = "/api/players",
|
||||
.token = "invalid_token",
|
||||
.requiredLevel = 0,
|
||||
.shouldSucceed = false
|
||||
};
|
||||
|
||||
eHTTPStatusCode result = ProcessRequest(flow);
|
||||
EXPECT_EQ(result, eHTTPStatusCode::UNAUTHORIZED);
|
||||
EXPECT_FALSE(context.isAuthenticated);
|
||||
}
|
||||
Reference in New Issue
Block a user