diff --git a/net/http/CMakeLists.txt b/net/http/CMakeLists.txt index 3121f376cf253..f05d568a01177 100644 --- a/net/http/CMakeLists.txt +++ b/net/http/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (C) 1995-2025, Rene Brun and Fons Rademakers. +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. # All rights reserved. # # For the licensing terms see $ROOTSYS/LICENSE. @@ -117,3 +117,5 @@ if(FASTCGI_FOUND) else() target_compile_definitions(RHTTP PUBLIC -DHTTP_WITHOUT_FASTCGI) endif() + +ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/net/http/test/CMakeLists.txt b/net/http/test/CMakeLists.txt new file mode 100644 index 0000000000000..62d10242e72b2 --- /dev/null +++ b/net/http/test/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +############################################################################ +# CMakeLists.txt file for building ROOT net/http package +# @author Sergey Linev, GSI +############################################################################ + +find_program(CURL_EXECUTABLE curl) + +# http server does not work on default Windows configuration +# so skip testing there +if(CURL_EXECUTABLE AND NOT WIN32) + ROOT_ADD_GTEST(testHttpServer test_server.cxx LIBRARIES RHTTP RHTTPSniff Hist) +endif() diff --git a/net/http/test/test_server.cxx b/net/http/test/test_server.cxx new file mode 100644 index 0000000000000..6f3152676d33b --- /dev/null +++ b/net/http/test/test_server.cxx @@ -0,0 +1,157 @@ +#include "gtest/gtest.h" + +#include +#include + +#include "THttpServer.h" +#include "TROOT.h" + +#include "TSystem.h" +#include "TNamed.h" +#include "TRandom.h" + +#include "ROOT/TestSupport.hxx" + +Int_t httpport = 0; +TString server_url; + +// main http server +std::string execute_request(const char *url, const char *post = nullptr) +{ + TString fname = TString::Format("http_server_%d.output", httpport), + pname, exec; + + if (post) { + pname = TString::Format("http_server_%d.post", httpport); + std::ofstream f(pname.Data()); + f << post; + exec = TString::Format("curl -sS -X POST '%s%s' --data-binary @%s -o %s", server_url.Data(), url, pname.Data(), fname.Data()); + } else { + exec = TString::Format("curl -sS '%s%s' -o %s", server_url.Data(), url, fname.Data()); + } + + printf("Execute %s\n", exec.Data()); + + std::string res; + + if (gSystem->Exec(exec) != 0) + res = ""; + else + res = THttpServer::ReadFileContent(fname.Data()); + + gSystem->Unlink(fname); + if (!pname.IsNull()) + gSystem->Unlink(pname); + + return res; +} + +// main http server +TEST(THttpServer, main) +{ + THttpServer serv(""); + + gRandom->SetSeed(0); + + for(int ntry = 0; ntry < 100; ++ntry) { + Int_t port = (Int_t) (25000 + gRandom->Rndm() * 1000); + // only two threads, bind to loopback address only + TString arg = TString::Format("http:%d?loopback&thrds=2", port); + if (serv.CreateEngine(arg)) { + httpport = port; + break; + } + } + + EXPECT_NE(httpport, 0); + + if (!httpport) + return; + + server_url = TString::Format("http:/localhost:%d", httpport); + + TNamed obj1("obj1", "title1"); + TNamed obj2("obj2", "title2"); + + serv.Register("/", &obj1); + serv.Register("/", &obj2); + + // let process requests in separate thread + serv.CreateServerThread(); + + // check JSON representation for the object + std::string res = execute_request("/obj1/root.json"); + EXPECT_EQ(res, "{\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj1\",\n" + " \"fTitle\" : \"title1\"\n" + "}"); + + // check XML representation for the object + res = execute_request("/obj1/root.xml"); + EXPECT_EQ(res, "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"); + + + // check BINARY representation for the object + res = execute_request("/obj1/root.bin"); + // keep minimal margin for binary format change + EXPECT_NEAR(res.length(), 28, 4); + + // check ROOT file creation + res = execute_request("/obj1/file.root"); + // TODO: anlyze why so big size for small object + EXPECT_NEAR(res.length(), 2097152, 10000); + + // check item request hierarchy request + res = execute_request("/obj1/item.json"); + + EXPECT_EQ(res, "{\n" + " \"_name\" : \"obj1\",\n" + " \"_root_version\" : " + std::to_string(gROOT->GetVersionCode()) + ",\n" + " \"_kind\" : \"ROOT.TNamed\",\n" + " \"_title\" : \"title1\"\n" + "}"); + + + // check multi request to several objects + res = execute_request("/multi.json?number=2", "/obj1/root.json\n/obj2/root.json\n"); + EXPECT_EQ(res, "[{\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj1\",\n" + " \"fTitle\" : \"title1\"\n" + "}, {\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj2\",\n" + " \"fTitle\" : \"title2\"\n" + "}]"); + + + // by default methods execution is not allowed + res = execute_request("/obj1/exe.json?method=GetTitle"); + EXPECT_EQ(res, ""); + + + // disable readonly to get extra functionality + serv.SetReadOnly(kFALSE); + + // only now one can execute method + res = execute_request("/obj1/exe.json?method=GetTitle"); + EXPECT_EQ(res, "\"title1\""); + + res = execute_request("/obj1/exe.json?method=SetTitle&title=NewTitle"); + EXPECT_EQ(res, "null"); + EXPECT_EQ(std::string("NewTitle"), obj1.GetTitle()); + obj1.SetTitle("title1"); +}