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>
307 lines
9.8 KiB
C++
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");
|
|
}
|