/*
* tests/http/HttpUTest.cxxtest
*
* Copyright (C) 2025 BrainyBlaze Dynamics Inc.
* All Rights Reserved
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License v3 as
* published by the Free Software Foundation and including the exceptions
* at http://opencog.org/wiki/Licenses
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses/
*/
#include <thread>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/atoms/atom_types/atom_names.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atoms/value/VoidValue.h>
#include <opencog/cogserver/atoms/CogServerNode.h>
using namespace opencog;
class HttpUTest : public CxxTest::TestSuite
{
private:
AtomSpacePtr _asp;
CogServerNodePtr _cogserver;
// Simple TCP client connection
int connect_to_server(int port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
TS_FAIL("ERROR opening socket");
return -1;
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
close(sockfd);
TS_FAIL("ERROR connecting to server");
return -1;
}
return sockfd;
}
// Send data and receive response
std::string send_and_receive(int sockfd, const std::string& data)
{
// Send the request
if (send(sockfd, data.c_str(), data.length(), 0) < 0) {
TS_FAIL("ERROR writing to socket");
return "";
}
// Read the response
char buffer[4096];
memset(buffer, 0, sizeof(buffer));
int n = recv(sockfd, buffer, sizeof(buffer)-1, 0);
if (n < 0) {
TS_FAIL("ERROR reading from socket");
return "";
}
return std::string(buffer, n);
}
public:
HttpUTest()
{
// logger().set_level(Logger::DEBUG);
logger().set_level(Logger::INFO);
logger().set_print_to_stdout_flag(true);
// Create the AtomSpace and CogServerNode
_asp = createAtomSpace();
Handle hcsn = _asp->add_node(COG_SERVER_NODE, "test-cogserver");
_cogserver = CogServerNodeCast(hcsn);
// Set ports and start the server (web only)
_cogserver->setValue(_asp->add_atom(Predicate("*-telnet-port-*")),
createFloatValue(0.0));
_cogserver->setValue(_asp->add_atom(Predicate("*-web-port-*")),
createFloatValue(18181.0));
_cogserver->setValue(_asp->add_atom(Predicate("*-mcp-port-*")),
createFloatValue(0.0));
_cogserver->setValue(_asp->add_atom(Predicate("*-start-*")),
createVoidValue());
}
~HttpUTest()
{
// Stop the server
_cogserver->setValue(_asp->add_atom(Predicate("*-stop-*")),
createVoidValue());
_cogserver = nullptr;
_asp = nullptr;
// erase the log file if no assertions failed
if (!CxxTest::TestTracker::tracker().suiteFailed())
std::remove(logger().get_filename().c_str());
}
void setUp()
{
}
void tearDown()
{
}
// Test basic HTTP GET request to root
void test_http_get_root()
{
int sockfd = connect_to_server(18181);
TS_ASSERT_LESS_THAN(0, sockfd);
// Send HTTP GET request for root
std::string request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
std::string response = send_and_receive(sockfd, request);
close(sockfd);
// Check that we got a response
TS_ASSERT(!response.empty());
// Check for HTTP/1.1 404 Not Found (since root doesn't exist)
TS_ASSERT(response.find("HTTP/1.1 404 Not Found") != std::string::npos);
// Check for proper headers
TS_ASSERT(response.find("Content-Type: text/html") != std::string::npos);
TS_ASSERT(response.find("Server: CogServer") != std::string::npos);
// Check that there's a body after the headers
size_t body_start = response.find("\r\n\r\n");
TS_ASSERT(body_start != std::string::npos);
// Check that the body looks like HTML
std::string body = response.substr(body_start + 4);
TS_ASSERT(!body.empty());
TS_ASSERT(body.find("<html>") != std::string::npos ||
body.find("<HTML>") != std::string::npos ||
body.find("<!DOCTYPE") != std::string::npos);
}
// Test HTTP GET request for /stats
void test_http_get_stats()
{
int sockfd = connect_to_server(18181);
TS_ASSERT_LESS_THAN(0, sockfd);
// Send HTTP GET request for /stats
std::string request = "GET /stats HTTP/1.1\r\nHost: localhost\r\n\r\n";
std::string response = send_and_receive(sockfd, request);
close(sockfd);
// Check that we got a response
TS_ASSERT(!response.empty());
// Check for HTTP/1.1 200 OK
TS_ASSERT(response.find("HTTP/1.1 200 OK") != std::string::npos);
// Check for proper headers
TS_ASSERT(response.find("Content-Type: text/html") != std::string::npos);
TS_ASSERT(response.find("Server: CogServer") != std::string::npos);
// Check that there's a body after the headers
size_t body_start = response.find("\r\n\r\n");
TS_ASSERT(body_start != std::string::npos);
// Check that the body looks like HTML
std::string body = response.substr(body_start + 4);
TS_ASSERT(!body.empty());
TS_ASSERT(body.find("<html>") != std::string::npos ||
body.find("<!DOCTYPE html>") != std::string::npos);
// Stats page should contain network statistics
TS_ASSERT(body.find("Network Activity") != std::string::npos ||
body.find("CogServer Stats") != std::string::npos ||
body.find("socket") != std::string::npos);
}
// Test HTTP POST request (should return 501 Not Implemented)
void test_http_post()
{
int sockfd = connect_to_server(18181);
TS_ASSERT_LESS_THAN(0, sockfd);
// Send HTTP POST request
std::string request = "POST /test HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n";
std::string response = send_and_receive(sockfd, request);
close(sockfd);
// Check that we got a response
TS_ASSERT(!response.empty());
// WebServer uses GET/POST to distinguish requests, so this should work
// It should return 404 for unknown URL
TS_ASSERT(response.find("HTTP/1.1 404 Not Found") != std::string::npos ||
response.find("HTTP/1.1 501 Not Implemented") != std::string::npos);
}
// Test malformed HTTP request
void test_http_malformed()
{
int sockfd = connect_to_server(18181);
TS_ASSERT_LESS_THAN(0, sockfd);
// Send malformed request (not HTTP)
std::string request = "INVALID REQUEST\r\n\r\n";
std::string response = send_and_receive(sockfd, request);
close(sockfd);
// Should get HTTP 501 Not Implemented
TS_ASSERT(response.find("HTTP/1.1 501 Not Implemented") != std::string::npos);
}
};