fix: Fix bugs in new webserver (#1780)

Fixes #919.

Fixes bugs for new webserver:
- Unhandled exception when closing already closed websocket
- No pings for plain websocket connection
- Server drops websocket connection when client responds to pings but
doesn't send anything

Also changing API of ng connections. Now timeout is set by a separate
method instead of providing it for each call.
This commit is contained in:
Sergey Kuznetsov
2024-12-19 15:14:04 +00:00
committed by GitHub
parent c4b87d2a0a
commit 7d4e3619b0
13 changed files with 238 additions and 204 deletions

View File

@@ -65,7 +65,7 @@ TEST_F(ng_SubscriptionContextTests, Send)
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto, auto) {
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;
});
@@ -84,13 +84,13 @@ TEST_F(ng_SubscriptionContextTests, SendOrder)
testing::Sequence const sequence;
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message1](boost::asio::const_buffer buffer, auto, auto) {
.WillOnce([&message1](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message1);
return std::nullopt;
});
EXPECT_CALL(connection_, sendBuffer)
.InSequence(sequence)
.WillOnce([&message2](boost::asio::const_buffer buffer, auto, auto) {
.WillOnce([&message2](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message2);
return std::nullopt;
});
@@ -107,7 +107,7 @@ TEST_F(ng_SubscriptionContextTests, SendFailed)
auto subscriptionContext = makeSubscriptionContext(yield);
auto const message = std::make_shared<std::string>("some message");
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto, auto) {
EXPECT_CALL(connection_, sendBuffer).WillOnce([&message](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return boost::system::errc::make_error_code(boost::system::errc::not_supported);
});
@@ -125,7 +125,7 @@ TEST_F(ng_SubscriptionContextTests, SendTooManySubscriptions)
auto const message = std::make_shared<std::string>("message1");
EXPECT_CALL(connection_, sendBuffer)
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield, auto) {
.WillOnce([&message](boost::asio::const_buffer buffer, boost::asio::yield_context innerYield) {
boost::asio::post(innerYield); // simulate send is slow by switching to another coroutine
EXPECT_EQ(boost::beast::buffers_to_string(buffer), *message);
return std::nullopt;

View File

@@ -159,7 +159,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_NoHandler_Send)
.WillOnce(Return(makeRequest("some_request", headers_)))
.WillOnce(Return(makeError(websocket::error::closed)));
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&) {
EXPECT_EQ(response.message(), "WebSocket is not supported by this server");
return std::nullopt;
});
@@ -183,7 +183,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadTarget_Send)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::get, target, 11, requestMessage})))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&) {
EXPECT_EQ(response.message(), "Bad target");
auto const httpResponse = std::move(response).intoHttpResponse();
EXPECT_EQ(httpResponse.result(), http::status::bad_request);
@@ -207,7 +207,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_BadMethod_Send)
.WillOnce(Return(makeRequest(http::request<http::string_body>{http::verb::acl, "/", 11})))
.WillOnce(Return(makeError(http::error::end_of_stream)));
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([](Response response, auto&&) {
EXPECT_EQ(response.message(), "Unsupported http method");
return std::nullopt;
});
@@ -241,7 +241,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send)
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
@@ -279,7 +279,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, SendSubscriptionMessage)
EXPECT_CALL(*mockWsConnection_, send).WillOnce(Return(std::nullopt));
EXPECT_CALL(*mockWsConnection_, sendBuffer)
.WillOnce([&subscriptionMessage](boost::asio::const_buffer buffer, auto&&, auto&&) {
.WillOnce([&subscriptionMessage](boost::asio::const_buffer buffer, auto&&) {
EXPECT_EQ(boost::beast::buffers_to_string(buffer), subscriptionMessage);
return std::nullopt;
});
@@ -353,7 +353,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, SubscriptionContextIsNullForHt
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
@@ -395,12 +395,10 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_Send_Loop)
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockHttpConnection_, send)
.Times(3)
.WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockHttpConnection_, send).Times(3).WillRepeatedly([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockHttpConnection_, close);
@@ -434,7 +432,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Receive_Handle_SendError)
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockHttpConnection_, send).WillOnce([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return makeError(http::error::end_of_stream).error();
});
@@ -460,14 +458,12 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
bool connectionClosed = false;
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
.Times(4)
.WillRepeatedly([&](auto&&, auto&&) -> std::expected<Request, Error> {
if (connectionClosed) {
return makeError(websocket::error::closed);
}
return makeRequest(requestMessage, headers_);
});
EXPECT_CALL(*mockWsConnection_, receive).Times(4).WillRepeatedly([&](auto&&) -> std::expected<Request, Error> {
if (connectionClosed) {
return makeError(websocket::error::closed);
}
return makeRequest(requestMessage, headers_);
});
EXPECT_CALL(wsHandlerMock, Call).Times(3).WillRepeatedly([&](Request const& request, auto&&, auto&&, auto&&) {
EXPECT_EQ(request.message(), requestMessage);
@@ -475,7 +471,7 @@ TEST_F(ConnectionHandlerSequentialProcessingTest, Stop)
});
size_t numCalls = 0;
EXPECT_CALL(*mockWsConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockWsConnection_, send).Times(3).WillRepeatedly([&](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
++numCalls;
@@ -550,7 +546,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send)
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_CALL(*mockWsConnection_, send).WillOnce([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
@@ -574,7 +570,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, headers_); };
auto const returnRequest = [&](auto&&) { return makeRequest(requestMessage, headers_); };
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
EXPECT_CALL(*mockWsConnection_, receive)
@@ -587,12 +583,10 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop)
return Response(http::status::ok, responseMessage, request);
});
EXPECT_CALL(*mockWsConnection_, send)
.Times(2)
.WillRepeatedly([&responseMessage](Response response, auto&&, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(*mockWsConnection_, send).Times(2).WillRepeatedly([&responseMessage](Response response, auto&&) {
EXPECT_EQ(response.message(), responseMessage);
return std::nullopt;
});
EXPECT_CALL(onDisconnectMock_, Call).WillOnce([connectionPtr = mockWsConnection_.get()](Connection const& c) {
EXPECT_EQ(&c, connectionPtr);
@@ -613,7 +607,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
std::string const requestMessage = "some message";
std::string const responseMessage = "some response";
auto const returnRequest = [&](auto&&, auto&&) { return makeRequest(requestMessage, headers_); };
auto const returnRequest = [&](auto&&) { return makeRequest(requestMessage, headers_); };
testing::Sequence const sequence;
EXPECT_CALL(*mockWsConnection_, wasUpgraded).WillOnce(Return(true));
@@ -635,11 +629,7 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
EXPECT_CALL(
*mockWsConnection_,
send(
testing::ResultOf([](Response response) { return response.message(); }, responseMessage),
testing::_,
testing::_
)
send(testing::ResultOf([](Response response) { return response.message(); }, responseMessage), testing::_)
)
.Times(3)
.WillRepeatedly(Return(std::nullopt));
@@ -650,7 +640,6 @@ TEST_F(ConnectionHandlerParallelProcessingTest, Receive_Handle_Send_Loop_TooMany
testing::ResultOf(
[](Response response) { return response.message(); }, "Too many requests for one connection"
),
testing::_,
testing::_
)
)

View File

@@ -66,9 +66,11 @@ struct HttpConnectionTests : SyncAsioContextTest {
auto expectedSocket = httpServer_.accept(yield);
[&]() { ASSERT_TRUE(expectedSocket.has_value()) << expectedSocket.error().message(); }();
auto ip = expectedSocket->remote_endpoint().address().to_string();
return PlainHttpConnection{
PlainHttpConnection connection{
std::move(expectedSocket).value(), std::move(ip), boost::beast::flat_buffer{}, tagDecoratorFactory_
};
connection.setTimeout(std::chrono::milliseconds{100});
return connection;
}
};
@@ -100,7 +102,7 @@ TEST_F(HttpConnectionTests, Receive)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{100});
auto expectedRequest = connection.receive(yield);
ASSERT_TRUE(expectedRequest.has_value()) << expectedRequest.error().message();
ASSERT_TRUE(expectedRequest->isHttp());
@@ -124,7 +126,8 @@ TEST_F(HttpConnectionTests, ReceiveTimeout)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1});
connection.setTimeout(std::chrono::milliseconds{1});
auto expectedRequest = connection.receive(yield);
EXPECT_FALSE(expectedRequest.has_value());
});
}
@@ -139,7 +142,8 @@ TEST_F(HttpConnectionTests, ReceiveClientDisconnected)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto expectedRequest = connection.receive(yield, std::chrono::milliseconds{1});
connection.setTimeout(std::chrono::milliseconds{1});
auto expectedRequest = connection.receive(yield);
EXPECT_FALSE(expectedRequest.has_value());
});
}
@@ -166,7 +170,7 @@ TEST_F(HttpConnectionTests, Send)
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100});
auto maybeError = connection.send(response, yield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
});
}
@@ -197,7 +201,7 @@ TEST_F(HttpConnectionTests, SendMultipleTimes)
auto connection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{100});
auto maybeError = connection.send(response, yield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError->message(); }();
}
});
@@ -213,11 +217,12 @@ TEST_F(HttpConnectionTests, SendClientDisconnected)
});
runSpawn([this, &response](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto maybeError = connection.send(response, yield, std::chrono::milliseconds{1});
connection.setTimeout(std::chrono::milliseconds{1});
auto maybeError = connection.send(response, yield);
size_t counter{1};
while (not maybeError.has_value() and counter < 100) {
++counter;
maybeError = connection.send(response, yield, std::chrono::milliseconds{1});
maybeError = connection.send(response, yield);
}
EXPECT_TRUE(maybeError.has_value());
EXPECT_LT(counter, 100);
@@ -241,7 +246,8 @@ TEST_F(HttpConnectionTests, Close)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
connection.close(yield, std::chrono::milliseconds{1});
connection.setTimeout(std::chrono::milliseconds{1});
connection.close(yield);
});
}
@@ -257,7 +263,7 @@ TEST_F(HttpConnectionTests, IsUpgradeRequested_GotHttpRequest)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100});
auto result = connection.isUpgradeRequested(yield);
[&]() { ASSERT_TRUE(result.has_value()) << result.error().message(); }();
EXPECT_FALSE(result.value());
});
@@ -272,7 +278,8 @@ TEST_F(HttpConnectionTests, IsUpgradeRequested_FailedToFetch)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto result = connection.isUpgradeRequested(yield, std::chrono::milliseconds{1});
connection.setTimeout(std::chrono::milliseconds{1});
auto result = connection.isUpgradeRequested(yield);
EXPECT_FALSE(result.has_value());
});
}
@@ -288,7 +295,7 @@ TEST_F(HttpConnectionTests, Upgrade)
runSpawn([this](boost::asio::yield_context yield) {
auto connection = acceptConnection(yield);
auto const expectedResult = connection.isUpgradeRequested(yield, std::chrono::milliseconds{100});
auto const expectedResult = connection.isUpgradeRequested(yield);
[&]() { ASSERT_TRUE(expectedResult.has_value()) << expectedResult.error().message(); }();
[&]() { ASSERT_TRUE(expectedResult.value()); }();

View File

@@ -30,9 +30,13 @@
#include "web/ng/impl/HttpConnection.hpp"
#include "web/ng/impl/WsConnection.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/status.hpp>
#include <gmock/gmock.h>
@@ -43,6 +47,7 @@
#include <memory>
#include <optional>
#include <ranges>
#include <string>
#include <utility>
using namespace web::ng::impl;
@@ -81,6 +86,7 @@ struct web_WsConnectionTests : SyncAsioContextTest {
auto connection = std::move(expectedWsConnection).value();
auto wsConnectionPtr = dynamic_cast<PlainWsConnection*>(connection.release());
[&]() { ASSERT_NE(wsConnectionPtr, nullptr) << "Expected PlainWsConnection"; }();
wsConnectionPtr->setTimeout(std::chrono::milliseconds{100});
return std::unique_ptr<PlainWsConnection>{wsConnectionPtr};
}
};
@@ -97,6 +103,36 @@ TEST_F(web_WsConnectionTests, WasUpgraded)
});
}
TEST_F(web_WsConnectionTests, DisconnectClientOnInactivity)
{
boost::asio::io_context clientCtx;
auto work = boost::asio::make_work_guard(clientCtx);
std::thread clientThread{[&clientCtx]() { clientCtx.run(); }};
boost::asio::spawn(clientCtx, [&work, this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
boost::asio::steady_timer timer{yield.get_executor(), std::chrono::milliseconds{5}};
timer.async_wait(yield);
work.reset();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
wsConnection->setTimeout(std::chrono::milliseconds{1});
// Client will not respond to pings because there is no reading operation scheduled for it.
auto const start = std::chrono::steady_clock::now();
auto const receivedMessage = wsConnection->receive(yield);
auto const end = std::chrono::steady_clock::now();
EXPECT_LT(end - start, std::chrono::milliseconds{4}); // Should be 2 ms, double it in case of slow CI.
EXPECT_FALSE(receivedMessage.has_value());
EXPECT_EQ(receivedMessage.error().value(), boost::asio::error::no_permission);
});
clientThread.join();
}
TEST_F(web_WsConnectionTests, Send)
{
Response const response{boost::beast::http::status::ok, "some response", request_};
@@ -111,7 +147,7 @@ TEST_F(web_WsConnectionTests, Send)
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100});
auto maybeError = wsConnection->send(response, yield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
});
}
@@ -135,7 +171,7 @@ TEST_F(web_WsConnectionTests, MultipleSend)
auto wsConnection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{100});
auto maybeError = wsConnection->send(response, yield);
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
}
});
@@ -153,10 +189,11 @@ TEST_F(web_WsConnectionTests, SendFailed)
runSpawn([this, &response](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
wsConnection->setTimeout(std::chrono::milliseconds{1});
std::optional<Error> maybeError;
size_t counter = 0;
while (not maybeError.has_value() and counter < 100) {
maybeError = wsConnection->send(response, yield, std::chrono::milliseconds{1});
maybeError = wsConnection->send(response, yield);
++counter;
}
EXPECT_TRUE(maybeError.has_value());
@@ -177,7 +214,7 @@ TEST_F(web_WsConnectionTests, Receive)
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
auto maybeRequest = wsConnection->receive(yield);
[&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }();
EXPECT_EQ(maybeRequest->message(), request_.message());
});
@@ -199,7 +236,7 @@ TEST_F(web_WsConnectionTests, MultipleReceive)
auto wsConnection = acceptConnection(yield);
for ([[maybe_unused]] auto _i : std::ranges::iota_view{0, 3}) {
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
auto maybeRequest = wsConnection->receive(yield);
[&]() { ASSERT_TRUE(maybeRequest.has_value()) << maybeRequest.error().message(); }();
EXPECT_EQ(maybeRequest->message(), request_.message());
}
@@ -215,9 +252,10 @@ TEST_F(web_WsConnectionTests, ReceiveTimeout)
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{1});
wsConnection->setTimeout(std::chrono::milliseconds{2});
auto maybeRequest = wsConnection->receive(yield);
EXPECT_FALSE(maybeRequest.has_value());
EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::timed_out);
EXPECT_EQ(maybeRequest.error().value(), boost::system::errc::operation_not_permitted);
});
}
@@ -231,7 +269,7 @@ TEST_F(web_WsConnectionTests, ReceiveFailed)
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
auto maybeRequest = wsConnection->receive(yield, std::chrono::milliseconds{100});
auto maybeRequest = wsConnection->receive(yield);
EXPECT_FALSE(maybeRequest.has_value());
EXPECT_EQ(maybeRequest.error().value(), boost::asio::error::eof);
});
@@ -249,6 +287,22 @@ TEST_F(web_WsConnectionTests, Close)
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
wsConnection->close(yield, std::chrono::milliseconds{100});
wsConnection->close(yield);
});
}
TEST_F(web_WsConnectionTests, CloseWhenConnectionIsAlreadyClosed)
{
boost::asio::spawn(ctx, [this](boost::asio::yield_context yield) {
auto maybeError = wsClient_.connect("localhost", httpServer_.port(), yield, std::chrono::milliseconds{100});
[&]() { ASSERT_FALSE(maybeError.has_value()) << maybeError.value().message(); }();
wsClient_.close();
});
runSpawn([this](boost::asio::yield_context yield) {
auto wsConnection = acceptConnection(yield);
boost::asio::post(yield);
wsConnection->close(yield);
wsConnection->close(yield);
});
}