#include "../buildactions/buildaction.h" #include "../serversetup.h" #include "../webapi/params.h" #include "../webapi/routes.h" #include "../webapi/server.h" #include "../webapi/session.h" #include "../webclient/session.h" #include "../../libpkg/data/config.h" #include "resources/config.h" #include #include #include using CppUtilities::operator<<; // must be visible prior to the call site #include #include #include #include #include #include #include using namespace std; using namespace CPPUNIT_NS; using namespace CppUtilities; using namespace CppUtilities::Literals; using namespace LibRepoMgr; using namespace LibRepoMgr::WebAPI; class WebAPITests : public TestFixture { CPPUNIT_TEST_SUITE(WebAPITests); CPPUNIT_TEST(testBasicNetworking); CPPUNIT_TEST(testPostingBuildAction); CPPUNIT_TEST(testPostingBuildActionsFromTask); CPPUNIT_TEST_SUITE_END(); public: WebAPITests(); void setUp() override; void tearDown() override; void testRoutes(const std::list> &routes); void testBasicNetworking(); std::shared_ptr invokeRouteHandler( void (*handler)(const Params ¶ms, ResponseHandler &&handler), std::vector> &&queryParams); void testPostingBuildAction(); void testPostingBuildActionsFromTask(); private: std::string m_configDbFile, m_buildingDbFile; ServiceSetup m_setup; boost::beast::error_code m_lastError; string m_body; }; CPPUNIT_TEST_SUITE_REGISTRATION(WebAPITests); static unsigned short randomPort() { random_device dev; default_random_engine engine(dev()); uniform_int_distribution distri(5000, 25000); return distri(engine); } WebAPITests::WebAPITests() { } void WebAPITests::setUp() { applicationInfo.version = APP_VERSION; m_configDbFile = workingCopyPath("test-webapi-config.db", WorkingCopyMode::Cleanup); m_buildingDbFile = workingCopyPath("test-webapi-building.db", WorkingCopyMode::Cleanup); m_setup.webServer.port = randomPort(); m_setup.config.initStorage(m_configDbFile.data()); m_setup.building.initStorage(m_buildingDbFile.data()); } void WebAPITests::tearDown() { } /*! * \brief Runs the Boost.Asio/Beast server and client to simulte accessing the specified \a routes. */ void WebAPITests::testRoutes(const std::list> &routes) { // get first route auto currentRoute = routes.begin(); if (currentRoute == routes.end()) { return; } // start server auto server = std::make_shared(m_setup); server->run(); cout << "Test server running under http://" << m_setup.webServer.address << ':' << m_setup.webServer.port << endl; // define function to stop server const auto stopServer = [&] { if (server->m_acceptor.is_open()) { server->m_acceptor.cancel(); } m_setup.webServer.ioContext.stop(); }; // define function to request the next route to test WebClient::Session::Handler handleResponse; const auto testNextRoute = [&] { std::make_shared(m_setup.webServer.ioContext, handleResponse) ->run(m_setup.webServer.address.to_string().data(), numberToString(m_setup.webServer.port).data(), boost::beast::http::verb::get, currentRoute->first.data(), 11); }; // define function to respond handleResponse = [&](WebClient::Session &session, const WebClient::HttpClientError &error) { currentRoute->second(session, error); if (++currentRoute == routes.end()) { boost::asio::post(server->m_acceptor.get_executor(), stopServer); return; } testNextRoute(); }; // start to actually run the tests testNextRoute(); m_setup.webServer.ioContext.run(); } /*! * \brief Checks a few basic routes using the Boost.Beast-based HTTP server and client to test whether basic * networking and HTTP processing works. */ void WebAPITests::testBasicNetworking() { testRoutes({ { "/", [](const WebClient::Session &session, const WebClient::HttpClientError &error) { const auto &response = get(session.response); CPPUNIT_ASSERT(!error); CPPUNIT_ASSERT(!response.body().empty()); } }, { "/foo", [](const WebClient::Session &session, const WebClient::HttpClientError &error) { const auto &response = get(session.response); CPPUNIT_ASSERT(!error); CPPUNIT_ASSERT_EQUAL("text/plain"s, response[boost::beast::http::field::content_type].to_string()); CPPUNIT_ASSERT_EQUAL("The resource 'route \"GET /foo\"' was not found."s, response.body()); } }, { "/api/v0/version", [](const WebClient::Session &session, const WebClient::HttpClientError &error) { const auto &response = get(session.response); CPPUNIT_ASSERT(!error); CPPUNIT_ASSERT_EQUAL("text/plain"s, response[boost::beast::http::field::content_type].to_string()); CPPUNIT_ASSERT_EQUAL(string(APP_VERSION), response.body()); } }, { "/api/v0/status", [](const WebClient::Session &session, const WebClient::HttpClientError &error) { const auto &response = get(session.response); CPPUNIT_ASSERT(!error); CPPUNIT_ASSERT(!response.body().empty()); CPPUNIT_ASSERT_EQUAL("application/json"s, response[boost::beast::http::field::content_type].to_string()); } }, }); } /*! * \brief Invokes the specified route \a handler with the specified \a queryParams and returns the response. */ std::shared_ptr WebAPITests::invokeRouteHandler( void (*handler)(const Params &, ResponseHandler &&), std::vector> &&queryParams) { auto &ioc = m_setup.webServer.ioContext; auto session = std::make_shared(boost::asio::ip::tcp::socket(ioc), m_setup); auto params = WebAPI::Params(m_setup, *session, WebAPI::Url(std::string_view(), std::string_view(), std::move(queryParams))); auto response = std::shared_ptr(); session->assignEmptyRequest(); std::invoke(handler, params, [&response](std::shared_ptr &&r) { response = r; }); return response; } /*! * \brief Parses the specified \a json as build action storing results in \a buildAction. */ static void parseBuildAction(BuildAction &buildAction, std::string_view json) { const auto doc = ReflectiveRapidJSON::JsonReflector::parseJsonDocFromString(json.data(), json.size()); if (!doc.IsObject()) { CPPUNIT_FAIL("json document is no object"); } auto errors = ReflectiveRapidJSON::JsonDeserializationErrors(); errors.throwOn = ReflectiveRapidJSON::JsonDeserializationErrors::ThrowOn::All; ReflectiveRapidJSON::JsonReflector::pull(buildAction, doc.GetObject(), &errors); } /*! * \brief Parses the specified \a json as build actions storing results in \a buildActions. */ static auto parseBuildActions(std::string_view json) { auto errors = ReflectiveRapidJSON::JsonDeserializationErrors(); errors.throwOn = ReflectiveRapidJSON::JsonDeserializationErrors::ThrowOn::All; return ReflectiveRapidJSON::JsonReflector::fromJson>(json.data(), json.size(), &errors); } /*! * \brief Tests the handler to post a build action. * \remarks Only covers a very basic use so far; tasks are handled in the next test function. */ void WebAPITests::testPostingBuildAction() { { const auto response = invokeRouteHandler(&WebAPI::Routes::postBuildAction, {}); CPPUNIT_ASSERT_MESSAGE("got response", response); CPPUNIT_ASSERT_EQUAL_MESSAGE("response body", "need exactly either one type or one task parameter"s, response->body()); CPPUNIT_ASSERT_EQUAL_MESSAGE("response ok", boost::beast::http::status::bad_request, response->result()); } { const auto response = invokeRouteHandler(&WebAPI::Routes::postBuildAction, { { "type"sv, "prepare-build"sv }, { "start-condition"sv, "manually"sv }, }); CPPUNIT_ASSERT_MESSAGE("got response", response); auto buildAction = BuildAction(); parseBuildAction(buildAction, response->body()); CPPUNIT_ASSERT_EQUAL_MESSAGE("expected build action type returned", BuildActionType::PrepareBuild, buildAction.type); const auto createdBuildAction = m_setup.building.getBuildAction(buildAction.id); CPPUNIT_ASSERT_MESSAGE("build action actually created", createdBuildAction); CPPUNIT_ASSERT_EQUAL_MESSAGE("build action not started yet", BuildActionStatus::Created, createdBuildAction->status); CPPUNIT_ASSERT_EQUAL_MESSAGE("build action has no result yet", BuildActionResult::None, createdBuildAction->result); CPPUNIT_ASSERT_EQUAL_MESSAGE("response ok", boost::beast::http::status::ok, response->result()); } } /*! * \brief Tests the handler to post build actions from a pre-defined task. */ void WebAPITests::testPostingBuildActionsFromTask() { auto &building = m_setup.building; building.presets = decltype(building.presets)::fromJson(readFile(testFilePath("test-config/presets.json"))); CPPUNIT_ASSERT_MESSAGE("templates parsed from JSON", !building.presets.templates.empty()); CPPUNIT_ASSERT_MESSAGE("task parsed from JSON", building.presets.tasks.contains("foobarbaz")); CPPUNIT_ASSERT_EQUAL_MESSAGE("no build actions present before", 0_st, building.buildActionCount()); CPPUNIT_ASSERT_EQUAL_MESSAGE("no build actions running before", 0_st, building.runningBuildActionCount()); const auto response = invokeRouteHandler(&WebAPI::Routes::postBuildAction, { { "task"sv, "foobarbaz"sv }, { "start-condition"sv, "manually"sv }, }); CPPUNIT_ASSERT_MESSAGE("got response", response); const auto buildActions = parseBuildActions(response->body()); CPPUNIT_ASSERT_EQUAL_MESSAGE("expected number of build actions created", 5_st, buildActions.size()); CPPUNIT_ASSERT_EQUAL_MESSAGE("build actions actually present", 5_st, building.buildActionCount()); CPPUNIT_ASSERT_EQUAL_MESSAGE("build actions not started yet", 0_st, building.runningBuildActionCount()); building.forEachBuildAction([](std::size_t count) { CPPUNIT_ASSERT_EQUAL_MESSAGE("for-each loop returns correct size", 5_st, count); }, [](LibPkg::StorageID id, BuildActionBase &&action) { CPPUNIT_ASSERT_EQUAL_MESSAGE(argsToString("build action ", action.id, " not started yet"), BuildActionStatus::Created, action.status); CPPUNIT_ASSERT_EQUAL_MESSAGE(argsToString("build action ", action.id, " has no result yet"), BuildActionResult::None, action.result); CPPUNIT_ASSERT_EQUAL_MESSAGE(argsToString("build action ", action.id, " has task name assigned"), "foobarbaz"s, action.taskName); switch (id) { case 1: CPPUNIT_ASSERT_EQUAL_MESSAGE("foo is 1st action", "foo"s, action.templateName); CPPUNIT_ASSERT_EQUAL_MESSAGE("foo has dir assigned", "foo"s, action.directory); CPPUNIT_ASSERT_EQUAL_MESSAGE("foo has correct deps", std::vector{}, action.startAfter); break; case 2: CPPUNIT_ASSERT_EQUAL_MESSAGE("bar-1 is 2nd action", "bar-1"s, action.templateName); CPPUNIT_ASSERT_EQUAL_MESSAGE("bar-1 has dir assigned", "bar"s, action.directory); CPPUNIT_ASSERT_EQUAL_MESSAGE("bar-1 has correct deps", std::vector{ 1 }, action.startAfter); break; case 3: CPPUNIT_ASSERT_EQUAL_MESSAGE("bar-2 is 3rd action", "bar-2"s, action.templateName); CPPUNIT_ASSERT_EQUAL_MESSAGE("bar-2 has dir assigned", "bar"s, action.directory); CPPUNIT_ASSERT_EQUAL_MESSAGE("bar-2 has correct deps", std::vector{ 2 }, action.startAfter); break; case 4: CPPUNIT_ASSERT_EQUAL_MESSAGE("baz is 4th action", "baz"s, action.templateName); CPPUNIT_ASSERT_EQUAL_MESSAGE("baz has dir assigned", "baz"s, action.directory); CPPUNIT_ASSERT_EQUAL_MESSAGE("baz has correct deps", std::vector{ 1 }, action.startAfter); break; case 5: CPPUNIT_ASSERT_EQUAL_MESSAGE("buz is 5th action", "buz"s, action.templateName); CPPUNIT_ASSERT_EQUAL_MESSAGE("buz has dir assigned", "buz"s, action.directory); CPPUNIT_ASSERT_EQUAL_MESSAGE( "buz has correct deps", std::vector{ 3 CPP_UTILITIES_PP_COMMA 4 }, action.startAfter); break; default: CPPUNIT_FAIL(argsToString("build action with unexpected ID \"", id, "\" present")); } return false; }, 20, 0); CPPUNIT_ASSERT_EQUAL_MESSAGE("response ok", boost::beast::http::status::ok, response->result()); }