/** * @file tests/unit/test_nvhttp_https.cpp * @brief Verify HTTPS callbacks retain the guard protecting server lifetime. */ // test includes #include "../tests_common.h" // standard includes #include #include #include #include #include // local includes #include #include namespace { using namespace std::chrono_literals; using boost::asio::ip::tcp; /** * @brief Run real HTTPS callbacks against an isolated loopback listener. */ struct HttpsHandlerLifetimeTest: BaseTest { /** * @brief Create credentials and start the production HTTPS accept handler. */ void SetUp() override { BaseTest::SetUp(); const auto credentials = crypto::gen_creds("Sunshine HTTPS lifetime test", 2048); certificate_file = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "https_lifetime_cert.pem"; private_key_file = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "https_lifetime_key.pem"; std::ofstream {certificate_file} << credentials.x509; std::ofstream {private_key_file} << credentials.pkey; server = nvhttp::test_support::make_https_server(certificate_file.string(), private_key_file.string(), handler_scope); server->io_service = std::make_shared(); server->config.address = "127.0.0.1"; server->config.port = 0; server->start([this](unsigned short assigned_port) { port = assigned_port; }); server->io_service->poll(); ASSERT_NE(port, 0); } /** * @brief Drain canceled callbacks and remove the generated credentials. */ void TearDown() override { if (server) { server->stop(); server->io_service->restart(); server->io_service->poll(); server.reset(); } if (stop_thread.joinable()) { stop_thread.join(); } std::error_code cleanup_error; std::filesystem::remove(certificate_file, cleanup_error); std::filesystem::remove(private_key_file, cleanup_error); BaseTest::TearDown(); } /** * @brief Check that stopping the scope runner waits until the error callback exits. * * @param callback_error Error delivered by the real accept or TLS handshake operation. */ void check_handler_guard(const boost::system::error_code &callback_error) { EXPECT_TRUE(callback_error); ++error_callbacks; std::promise stopping; auto stopping_future = stopping.get_future(); std::promise stopped; stopped_future = stopped.get_future(); stop_thread = std::jthread([this, stopping = std::move(stopping), stopped = std::move(stopped)]() mutable { stopping.set_value(); handler_scope->stop(); stopped.set_value(); }); stopping_future.wait(); EXPECT_EQ(stopped_future.wait_for(50ms), std::future_status::timeout); } std::unique_ptr> server; ///< Server running production accept and handshake callbacks. std::shared_ptr handler_scope; ///< Guard used by the server destructor. std::jthread stop_thread; ///< Concurrent attempt to stop the handler scope. std::future stopped_future; ///< Signals that all callback guards have been released. unsigned short port = 0; ///< Assigned loopback port. int error_callbacks = 0; ///< Number of errors delivered while testing a handler. std::filesystem::path certificate_file; ///< Generated server certificate. std::filesystem::path private_key_file; ///< Generated server private key. }; /** * @brief Canceling a pending accept holds the server guard through its error callback. */ TEST_F(HttpsHandlerLifetimeTest, AcceptErrorRetainsGuardUntilCallbackReturns) { server->on_error = [this](auto, const auto &callback_error) { check_handler_guard(callback_error); }; server->stop(); server->io_service->run_for(2s); ASSERT_EQ(error_callbacks, 1); ASSERT_TRUE(stopped_future.valid()); EXPECT_EQ(stopped_future.wait_for(2s), std::future_status::ready); } /** * @brief A failed TLS handshake holds the server guard through its error callback. */ TEST_F(HttpsHandlerLifetimeTest, HandshakeErrorRetainsGuardUntilCallbackReturns) { server->on_error = [this](auto, const auto &callback_error) { check_handler_guard(callback_error); }; boost::asio::io_context client_io; tcp::socket client {client_io}; client.connect({boost::asio::ip::address_v4::loopback(), port}); client.close(); server->io_service->run_for(2s); ASSERT_EQ(error_callbacks, 1); ASSERT_TRUE(stopped_future.valid()); EXPECT_EQ(stopped_future.wait_for(2s), std::future_status::ready); } /** * @brief A stopped server scope prevents a canceled accept from dispatching an error. */ TEST_F(HttpsHandlerLifetimeTest, StoppedScopeSkipsAcceptCallback) { server->on_error = [this](auto, const auto &) { ++error_callbacks; }; handler_scope->stop(); server->stop(); server->io_service->run_for(2s); EXPECT_EQ(error_callbacks, 0); } /** * @brief A stopped server scope prevents an outstanding handshake from dispatching an error. */ TEST_F(HttpsHandlerLifetimeTest, StoppedScopeSkipsHandshakeCallback) { server->on_error = [this](auto, const auto &) { ++error_callbacks; }; boost::asio::io_context client_io; tcp::socket client {client_io}; client.connect({boost::asio::ip::address_v4::loopback(), port}); server->io_service->poll(); handler_scope->stop(); client.close(); server->io_service->run_for(2s); EXPECT_EQ(error_callbacks, 0); } } // namespace