mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
476 lines
14 KiB
C++
476 lines
14 KiB
C++
#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 RouteAuthorizationLevelTest : public RouteHandlerTest {
|
|
protected:
|
|
bool CheckAuthorizationLevel(uint8_t userLevel, uint8_t requiredLevel) {
|
|
return userLevel >= requiredLevel;
|
|
}
|
|
};
|
|
|
|
TEST_F(RouteAuthorizationLevelTest, Level0UserAccessLevel0Route) {
|
|
context.gmLevel = 0;
|
|
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 0));
|
|
}
|
|
|
|
TEST_F(RouteAuthorizationLevelTest, Level9AdminAccessAnyRoute) {
|
|
context.gmLevel = 9;
|
|
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 0));
|
|
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 5));
|
|
EXPECT_TRUE(CheckAuthorizationLevel(context.gmLevel, 9));
|
|
}
|
|
|
|
TEST_F(RouteAuthorizationLevelTest, Level1CannotAccessLevel5Route) {
|
|
context.gmLevel = 1;
|
|
EXPECT_FALSE(CheckAuthorizationLevel(context.gmLevel, 5));
|
|
}
|
|
|
|
TEST_F(RouteAuthorizationLevelTest, 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);
|
|
}
|