aissia/tests/modules/WebModuleTests.cpp
StillHammer 18f4f16213 feat: Add WebModule for HTTP requests via IIO
Implements WebModule that allows other modules to make HTTP requests
through IIO pub/sub messaging system.

Features:
- HTTP GET/POST support via existing HttpClient
- Request/response via IIO topics (web:request/web:response)
- Security: blocks localhost and private IPs
- Statistics tracking (total, success, failed)
- Hot-reload state preservation
- Custom headers and timeout configuration

Module architecture:
- WebModule.h/cpp: 296 lines total (within 300 line limit)
- config/web.json: Configuration file
- 10 integration tests (TI_WEB_001 to TI_WEB_010)

Tests: 120/120 passing (110 existing + 10 new)

Protocol:
- Subscribe: web:request
- Publish: web:response
- Request fields: requestId, url, method, headers, body, timeoutMs
- Response fields: requestId, success, statusCode, body, error, durationMs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 17:15:46 +08:00

307 lines
9.8 KiB
C++

/**
* @file WebModuleTests.cpp
* @brief Integration tests for WebModule (10 TI)
*/
#include <catch2/catch_test_macros.hpp>
#include "mocks/MockIO.hpp"
#include "utils/TimeSimulator.hpp"
#include "utils/TestHelpers.hpp"
#include "modules/WebModule.h"
#include <grove/JsonDataNode.h>
using namespace aissia;
using namespace aissia::tests;
// ============================================================================
// Test Fixture
// ============================================================================
class WebTestFixture {
public:
MockIO io;
TimeSimulator time;
WebModule module;
void configure(const json& config = json::object()) {
json fullConfig = {
{"enabled", true},
{"requestTimeoutMs", 5000},
{"maxConcurrentRequests", 10}
};
fullConfig.merge_patch(config);
grove::JsonDataNode configNode("config", fullConfig);
module.setConfiguration(configNode, &io, nullptr);
}
void process() {
grove::JsonDataNode input("input", time.createInput());
module.process(input);
}
};
// ============================================================================
// TI_WEB_001: Simple GET Request
// ============================================================================
TEST_CASE("TI_WEB_001_SimpleGetRequest", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send GET request (we use a public API that should work)
f.io.injectMessage("web:request", {
{"requestId", "test-001"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.process();
// Verify response published
REQUIRE(f.io.wasPublished("web:response"));
auto response = f.io.getLastPublished("web:response");
REQUIRE(response["requestId"] == "test-001");
REQUIRE(response.contains("success"));
REQUIRE(response.contains("durationMs"));
}
// ============================================================================
// TI_WEB_002: POST Request with Body
// ============================================================================
TEST_CASE("TI_WEB_002_PostRequestWithBody", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send POST request
f.io.injectMessage("web:request", {
{"requestId", "test-002"},
{"url", "https://httpbin.org/post"},
{"method", "POST"},
{"body", R"({"key": "value"})"}
});
f.process();
// Verify response
REQUIRE(f.io.wasPublished("web:response"));
auto response = f.io.getLastPublished("web:response");
REQUIRE(response["requestId"] == "test-002");
REQUIRE(response.contains("success"));
}
// ============================================================================
// TI_WEB_003: Invalid URL Handling
// ============================================================================
TEST_CASE("TI_WEB_003_InvalidUrlHandling", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send request with invalid URL
f.io.injectMessage("web:request", {
{"requestId", "test-003"},
{"url", ""},
{"method", "GET"}
});
f.process();
// Verify error response
REQUIRE(f.io.wasPublished("web:response"));
auto response = f.io.getLastPublished("web:response");
REQUIRE(response["requestId"] == "test-003");
REQUIRE(response["success"] == false);
REQUIRE(response["errorCode"] == "INVALID_URL");
}
// ============================================================================
// TI_WEB_004: Timeout Handling
// ============================================================================
TEST_CASE("TI_WEB_004_TimeoutHandling", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send request with very short timeout to a slow endpoint
f.io.injectMessage("web:request", {
{"requestId", "test-004"},
{"url", "https://httpbin.org/delay/10"},
{"method", "GET"},
{"timeoutMs", 100}
});
f.process();
// Verify response (should timeout or fail)
REQUIRE(f.io.wasPublished("web:response"));
auto response = f.io.getLastPublished("web:response");
REQUIRE(response["requestId"] == "test-004");
// Either timeout or connection error
REQUIRE(response.contains("success"));
}
// ============================================================================
// TI_WEB_005: Multiple Concurrent Requests
// ============================================================================
TEST_CASE("TI_WEB_005_MultipleConcurrentRequests", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send multiple requests
f.io.injectMessage("web:request", {
{"requestId", "test-005-a"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.io.injectMessage("web:request", {
{"requestId", "test-005-b"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.io.injectMessage("web:request", {
{"requestId", "test-005-c"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.process();
// Verify all responses
REQUIRE(f.io.countPublished("web:response") >= 3);
auto messages = f.io.getAllPublished("web:response");
REQUIRE(messages.size() >= 3);
}
// ============================================================================
// TI_WEB_006: Request ID Tracking
// ============================================================================
TEST_CASE("TI_WEB_006_RequestIdTracking", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send request with specific ID
std::string testId = "unique-request-id-12345";
f.io.injectMessage("web:request", {
{"requestId", testId},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.process();
// Verify response has same ID
REQUIRE(f.io.wasPublished("web:response"));
auto response = f.io.getLastPublished("web:response");
REQUIRE(response["requestId"] == testId);
}
// ============================================================================
// TI_WEB_007: Statistics Tracking
// ============================================================================
TEST_CASE("TI_WEB_007_StatisticsTracking", "[web][integration]") {
WebTestFixture f;
f.configure();
// Initial health check
auto health1 = f.module.getHealthStatus();
int initialTotal = health1->getInt("totalRequests", 0);
// Send successful request
f.io.injectMessage("web:request", {
{"requestId", "test-007"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.process();
// Check health status
auto health2 = f.module.getHealthStatus();
REQUIRE(health2->getInt("totalRequests", 0) == initialTotal + 1);
REQUIRE(health2->getInt("successfulRequests", 0) >= 0);
}
// ============================================================================
// TI_WEB_008: State Serialization
// ============================================================================
TEST_CASE("TI_WEB_008_StateSerialization", "[web][integration]") {
WebTestFixture f;
f.configure();
// Make some requests
f.io.injectMessage("web:request", {
{"requestId", "test-008"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.process();
// Get state
auto state = f.module.getState();
REQUIRE(state != nullptr);
int totalRequests = state->getInt("totalRequests", 0);
REQUIRE(totalRequests > 0);
// Create new module and restore state
WebModule newModule;
grove::JsonDataNode emptyConfig("config");
newModule.setConfiguration(emptyConfig, &f.io, nullptr);
newModule.setState(*state);
// Verify state restored
auto restoredHealth = newModule.getHealthStatus();
REQUIRE(restoredHealth->getInt("totalRequests", 0) == totalRequests);
}
// ============================================================================
// TI_WEB_009: Configuration Loading
// ============================================================================
TEST_CASE("TI_WEB_009_ConfigurationLoading", "[web][integration]") {
WebTestFixture f;
// Configure with custom values
f.configure({
{"enabled", false},
{"requestTimeoutMs", 15000},
{"maxConcurrentRequests", 5}
});
// Verify module respects enabled flag
f.io.injectMessage("web:request", {
{"requestId", "test-009"},
{"url", "https://api.github.com"},
{"method", "GET"}
});
f.process();
// Should not process when disabled
REQUIRE(f.io.countPublished("web:response") == 0);
}
// ============================================================================
// TI_WEB_010: Error Response Format
// ============================================================================
TEST_CASE("TI_WEB_010_ErrorResponseFormat", "[web][integration]") {
WebTestFixture f;
f.configure();
// Send request with blocked URL (localhost)
f.io.injectMessage("web:request", {
{"requestId", "test-010"},
{"url", "http://localhost:8080/test"},
{"method", "GET"}
});
f.process();
// Verify error response format
REQUIRE(f.io.wasPublished("web:response"));
auto response = f.io.getLastPublished("web:response");
REQUIRE(response["requestId"] == "test-010");
REQUIRE(response["success"] == false);
REQUIRE(response.contains("error"));
REQUIRE(response.contains("errorCode"));
REQUIRE(response["errorCode"] == "BLOCKED_URL");
}