// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab #include "test/librbd/test_mock_fixture.h" #include "test/librbd/test_support.h" #include "include/rbd_types.h" #include "common/ceph_mutex.h" #include "librbd/migration/HttpClient.h" #include "gtest/gtest.h" #include "gmock/gmock.h" #include #include #include #include namespace librbd { namespace { struct MockTestImageCtx : public MockImageCtx { MockTestImageCtx(ImageCtx &image_ctx) : MockImageCtx(image_ctx) { } }; } // anonymous namespace namespace util { inline ImageCtx *get_image_ctx(MockTestImageCtx *image_ctx) { return image_ctx->image_ctx; } } // namespace util } // namespace librbd #include "librbd/migration/HttpClient.cc" using EmptyHttpRequest = boost::beast::http::request< boost::beast::http::empty_body>; using HttpResponse = boost::beast::http::response< boost::beast::http::string_body>; namespace boost { namespace beast { namespace http { template bool operator==(const boost::beast::http::request& lhs, const boost::beast::http::request& rhs) { return (lhs.method() == rhs.method() && lhs.target() == rhs.target()); } template bool operator==(const boost::beast::http::response& lhs, const boost::beast::http::response& rhs) { return (lhs.result() == rhs.result() && lhs.body() == rhs.body()); } } // namespace http } // namespace beast } // namespace boost namespace librbd { namespace migration { using ::testing::Invoke; class TestMockMigrationHttpClient : public TestMockFixture { public: typedef HttpClient MockHttpClient; void SetUp() override { TestMockFixture::SetUp(); ASSERT_EQ(0, open_image(m_image_name, &m_image_ctx)); create_acceptor(false); } void TearDown() override { m_acceptor.reset(); TestMockFixture::TearDown(); } // if we have a racing where another thread manages to bind and listen the // port picked by this acceptor, try again. static constexpr int MAX_BIND_RETRIES = 60; void create_acceptor(bool reuse) { for (int retries = 0;; retries++) { try { m_acceptor.emplace(*m_image_ctx->asio_engine, boost::asio::ip::tcp::endpoint( boost::asio::ip::tcp::v4(), m_server_port), reuse); // yay! break; } catch (const boost::system::system_error& e) { if (retries == MAX_BIND_RETRIES) { throw; } if (e.code() != boost::system::errc::address_in_use) { throw; } } // backoff a little bit sleep(1); } m_server_port = m_acceptor->local_endpoint().port(); } std::string get_local_url(UrlScheme url_scheme) { std::stringstream sstream; switch (url_scheme) { case URL_SCHEME_HTTP: sstream << "http://127.0.0.1"; break; case URL_SCHEME_HTTPS: sstream << "https://localhost"; break; default: ceph_assert(false); break; } sstream << ":" << m_server_port << "/target"; return sstream.str(); } void client_accept(boost::asio::ip::tcp::socket* socket, bool close, Context* on_connect) { m_acceptor->async_accept( boost::asio::make_strand(m_image_ctx->asio_engine->get_executor()), [socket, close, on_connect] (auto ec, boost::asio::ip::tcp::socket in_socket) { if (close) { in_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } else { ASSERT_FALSE(ec) << "Unexpected error: " << ec; *socket = std::move(in_socket); } on_connect->complete(0); }); } template void client_read_request(boost::asio::ip::tcp::socket& socket, boost::beast::http::request& expected_req) { boost::beast::http::request req; boost::beast::error_code ec; boost::beast::http::read(socket, m_buffer, req, ec); ASSERT_FALSE(ec) << "Unexpected errror: " << ec; expected_req.target("/target"); ASSERT_EQ(expected_req, req); } void client_write_response(boost::asio::ip::tcp::socket& socket, HttpResponse& expected_res) { expected_res.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); expected_res.set(boost::beast::http::field::content_type, "text/plain"); expected_res.content_length(expected_res.body().size()); expected_res.prepare_payload(); boost::beast::error_code ec; boost::beast::http::write(socket, expected_res, ec); ASSERT_FALSE(ec) << "Unexpected errror: " << ec; } template void client_ssl_handshake(Stream& stream, bool ignore_failure, Context* on_handshake) { stream.async_handshake( boost::asio::ssl::stream_base::server, [ignore_failure, on_handshake](auto ec) { ASSERT_FALSE(!ignore_failure && ec) << "Unexpected error: " << ec; on_handshake->complete(-ec.value()); }); } template void client_ssl_shutdown(Stream& stream, Context* on_shutdown) { stream.async_shutdown( [on_shutdown](auto ec) { ASSERT_FALSE(ec) << "Unexpected error: " << ec; on_shutdown->complete(-ec.value()); }); } void load_server_certificate(boost::asio::ssl::context& ctx) { ctx.set_options( boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use); ctx.use_certificate_chain( boost::asio::buffer(CERTIFICATE.data(), CERTIFICATE.size())); ctx.use_private_key( boost::asio::buffer(KEY.data(), KEY.size()), boost::asio::ssl::context::file_format::pem); ctx.use_tmp_dh( boost::asio::buffer(DH.data(), DH.size())); } // dummy self-signed cert for localhost const std::string CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" "MIIDXzCCAkegAwIBAgIUYH6rAaq66LC6yJ3XK1WEMIfmY4cwDQYJKoZIhvcNAQEL\n" "BQAwPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMQ8wDQYDVQQHDAZNY0xlYW4x\n" "EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMDExMDIyMTM5NTVaFw00ODAzMjAyMTM5\n" "NTVaMD8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTEPMA0GA1UEBwwGTWNMZWFu\n" "MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n" "AoIBAQCeRkyxjP0eNHxzj4/R+Bg/31p7kEjB5d/LYtrzBIYNe+3DN8gdReixEpR5\n" "lgTLDsl8gfk2HRz4cnAiseqYL6GKtw/cFadzLyXTbW4iavmTWiGYw/8RJlKunbhA\n" "hDjM6H99ysLf0NS6t14eK+bEJIW1PiTYRR1U5I4kSIjpCX7+nJVuwMEZ2XBpN3og\n" "nHhv2hZYTdzEkQEyZHz4V/ApfD7rlja5ecd/vJfPJeA8nudnGCh3Uo6f8I9TObAj\n" "8hJdfRiRBvnA4NnkrMrxW9UtVjScnw9Xia11FM/IGJIgMpLQ5dqBw930p6FxMYtn\n" "tRD1AF9sT+YjoCaHv0hXZvBEUEF3AgMBAAGjUzBRMB0GA1UdDgQWBBTQoIiX3+p/\n" "P4Xz2vwERz6pbjPGhzAfBgNVHSMEGDAWgBTQoIiX3+p/P4Xz2vwERz6pbjPGhzAP\n" "BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCVKoYAw+D1qqWRDSh3\n" "2KlKMnT6sySo7XmReGArj8FTKiZUprByj5CfAtaiDSdPOpcg3EazWbdasZbMmSQm\n" "+jpe5WoKnxL9b12lwwUYHrLl6RlrDHVkIVlXLNbJFY5TpfjvZfHpwVAygF3fnbgW\n" "PPuODUNAS5NDwST+t29jBZ/wwU0pyW0CS4K5d3XMGHBc13j2V/FyvmsZ5xfA4U9H\n" "oEnmZ/Qm+FFK/nR40rTAZ37cuv4ysKFtwvatNgTfHGJwaBUkKFdDbcyxt9abCi6x\n" "/K+ScoJtdIeVcfx8Fnc5PNtSpy8bHI3Zy4IEyw4kOqwwI1h37iBafZ2WdQkTxlAx\n" "JIDj\n" "-----END CERTIFICATE-----\n"; const std::string KEY = "-----BEGIN PRIVATE KEY-----\n" "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCeRkyxjP0eNHxz\n" "j4/R+Bg/31p7kEjB5d/LYtrzBIYNe+3DN8gdReixEpR5lgTLDsl8gfk2HRz4cnAi\n" "seqYL6GKtw/cFadzLyXTbW4iavmTWiGYw/8RJlKunbhAhDjM6H99ysLf0NS6t14e\n" "K+bEJIW1PiTYRR1U5I4kSIjpCX7+nJVuwMEZ2XBpN3ognHhv2hZYTdzEkQEyZHz4\n" "V/ApfD7rlja5ecd/vJfPJeA8nudnGCh3Uo6f8I9TObAj8hJdfRiRBvnA4NnkrMrx\n" "W9UtVjScnw9Xia11FM/IGJIgMpLQ5dqBw930p6FxMYtntRD1AF9sT+YjoCaHv0hX\n" "ZvBEUEF3AgMBAAECggEACCaYpoAbPOX5Dr5y6p47KXboIvrgNFQRPVke62rtOF6M\n" "dQQ3YwKJpCzPxp8qKgbd63KKEfZX2peSHMdKzIGPcSRSRcQ7tlvUN9on1M/rgGIg\n" "3swhI5H0qhdnOLNWdX73qdO6S2pmuiLdTvJ11N4IoLfNj/GnPAr1Ivs1ScL6bkQv\n" "UybaNQ/g2lB0tO7vUeVe2W/AqsIb1eQlf2g+SH7xRj2bGQkr4cWTylqfiVoL/Xic\n" "QVTCks3BWaZhYIhTFgvqVhXZpp52O9J+bxsWJItKQrrCBemxwp82xKbiW/KoI9L1\n" "wSnKvxx7Q3RUN5EvXeOpTRR8QIpBoxP3TTeoj+EOMQKBgQDQb/VfLDlLgfYJpgRC\n" "hKCLW90un9op3nA2n9Dmm9TTLYOmUyiv5ub8QDINEw/YK/NE2JsTSUk2msizqTLL\n" "Z82BFbz9kPlDbJ5MgxG5zXeLvOLurAFmZk/z5JJO+65PKjf0QVLncSAJvMCeNFuC\n" "2yZrEzbrItrjQsN6AedWdx6TTwKBgQDCZAsSI3lQgOh2q1GSxjuIzRAc7JnSGBvD\n" "nG8+SkfKAy7BWe638772Dgx8KYO7TLI4zlm8c9Tr/nkZsGWmM5S2DMI69PWOQWNa\n" "R6QzOFFwNg2JETH7ow+x8+9Q9d3WsPzROz3r5uDXgEk0glthaymVoPILFOiYpz3r\n" "heUbd6mFWQKBgQCCJBVJGhyf54IOHij0u0heGrpr/QTDNY5MnNZa1hs4y2cydyOl\n" "SH8aKp7ViPxQlYhriO6ySQS8YkJD4rXDSImIOmFo1Ja9oVjpHsD3iLFGf2YVbTHm\n" "lKUA+8raI8x+wzZyfELeHMTLL534aWpltp0zJ6kXgQi38pyIVh3x36gogwKBgQCt\n" "nba5k49VVFzLKEXqBjzD+QqMGtFjcH7TnZNJmgQ2K9OFgzIPf5atomyKNHXgQibn\n" "T32cMAQaZqR4SjDvWSBX3FtZVtE+Ja57woKn8IPj6ZL7Oa1fpwpskIbM01s31cln\n" "gjbSy9lC/+PiDw9YmeKBLkcfmKQJO021Xlf6yUxRuQKBgBWPODUO8oKjkuJXNI/w\n" "El9hNWkd+/SZDfnt93dlVyXTtTF0M5M95tlOgqvLtWKSyB/BOnoZYWqR8luMl15d\n" "bf75j5mB0lHMWtyQgvZSkFqe9Or7Zy7hfTShDlZ/w+OXK7PGesaE1F14irShXSji\n" "yn5DZYAZ5pU52xreJeYvDngO\n" "-----END PRIVATE KEY-----\n"; const std::string DH = "-----BEGIN DH PARAMETERS-----\n" "MIIBCAKCAQEA4+DA1j0gDWS71okwHpnvA65NmmR4mf+B3H39g163zY5S+cnWS2LI\n" "dvqnUDpw13naWtQ+Nu7I4rk1XoPaxOPSTu1MTbtYOxxU9M1ceBu4kQjDeHwasPVM\n" "zyEs1XXX3tsbPUxAuayX+AgW6QQAQUEjKDnv3FzVnQTFjwI49LqjnrSjbgQcoMaH\n" "EdGGUc6t1/We2vtsJZx0/dbaMkzFYO8dAbEYHL4sPKQb2mLpCPJZC3vwzpFkHFCd\n" "QSnLW2qRhy+66Mf8shdr6uvpoMcnKMOAvjKdXl9PBeJM9eJPz2lC4tnTiM3DqNzK\n" "Hn8+Pu3KkSIFL/5uBVu1fZSq+lFIEI23wwIBAg==\n" "-----END DH PARAMETERS-----\n"; librbd::ImageCtx *m_image_ctx; std::optional m_acceptor; boost::beast::flat_buffer m_buffer; uint64_t m_server_port = 0; }; TEST_F(TestMockMigrationHttpClient, OpenCloseHttp) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); C_SaferCond ctx2; http_client.close(&ctx2); ASSERT_EQ(0, ctx2.wait()); } TEST_F(TestMockMigrationHttpClient, OpenCloseHttps) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTPS)); http_client.set_ignore_self_signed_cert(true); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tlsv12}; load_server_certificate(ssl_context); boost::beast::ssl_stream ssl_stream{ std::move(socket), ssl_context}; C_SaferCond on_ssl_handshake_ctx; client_ssl_handshake(ssl_stream, false, &on_ssl_handshake_ctx); ASSERT_EQ(0, on_ssl_handshake_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); C_SaferCond ctx2; http_client.close(&ctx2); C_SaferCond on_ssl_shutdown_ctx; client_ssl_shutdown(ssl_stream, &on_ssl_shutdown_ctx); ASSERT_EQ(0, on_ssl_shutdown_ctx.wait()); ASSERT_EQ(0, ctx2.wait()); } TEST_F(TestMockMigrationHttpClient, OpenHttpsHandshakeFail) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTPS)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tlsv12}; load_server_certificate(ssl_context); boost::beast::ssl_stream ssl_stream{ std::move(socket), ssl_context}; C_SaferCond on_ssl_handshake_ctx; client_ssl_handshake(ssl_stream, true, &on_ssl_handshake_ctx); ASSERT_NE(0, on_ssl_handshake_ctx.wait()); ASSERT_NE(0, ctx1.wait()); } TEST_F(TestMockMigrationHttpClient, OpenInvalidUrl) { MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, "ftp://nope/"); C_SaferCond ctx; http_client.open(&ctx); ASSERT_EQ(-EINVAL, ctx.wait()); } TEST_F(TestMockMigrationHttpClient, OpenResolveFail) { MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, "http://foo.example"); C_SaferCond ctx; http_client.open(&ctx); ASSERT_EQ(-ENOENT, ctx.wait()); } TEST_F(TestMockMigrationHttpClient, OpenConnectFail) { MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, "http://localhost:2/"); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(-ECONNREFUSED, ctx1.wait()); } TEST_F(TestMockMigrationHttpClient, IssueHead) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); EmptyHttpRequest req; req.method(boost::beast::http::verb::head); C_SaferCond ctx2; HttpResponse res; http_client.issue(EmptyHttpRequest{req}, [&ctx2, &res](int r, HttpResponse&& response) mutable { res = std::move(response); ctx2.complete(r); }); HttpResponse expected_res; client_read_request(socket, req); client_write_response(socket, expected_res); ASSERT_EQ(0, ctx2.wait()); ASSERT_EQ(expected_res, res); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } TEST_F(TestMockMigrationHttpClient, IssueGet) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); EmptyHttpRequest req; req.method(boost::beast::http::verb::get); C_SaferCond ctx2; HttpResponse res; http_client.issue(EmptyHttpRequest{req}, [&ctx2, &res](int r, HttpResponse&& response) mutable { res = std::move(response); ctx2.complete(r); }); HttpResponse expected_res; expected_res.body() = "test"; client_read_request(socket, req); client_write_response(socket, expected_res); ASSERT_EQ(0, ctx2.wait()); ASSERT_EQ(expected_res, res); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } TEST_F(TestMockMigrationHttpClient, IssueSendFailed) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx1; client_accept(&socket, false, &on_connect_ctx1); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx1.wait()); ASSERT_EQ(0, ctx1.wait()); // close connection to client boost::system::error_code ec; socket.close(ec); C_SaferCond on_connect_ctx2; client_accept(&socket, false, &on_connect_ctx2); // send request via closed connection EmptyHttpRequest req; req.method(boost::beast::http::verb::get); C_SaferCond ctx2; http_client.issue(EmptyHttpRequest{req}, [&ctx2](int r, HttpResponse&&) mutable { ctx2.complete(r); }); // connection will be reset and request retried ASSERT_EQ(0, on_connect_ctx2.wait()); HttpResponse expected_res; expected_res.body() = "test"; client_read_request(socket, req); client_write_response(socket, expected_res); ASSERT_EQ(0, ctx2.wait()); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } TEST_F(TestMockMigrationHttpClient, IssueReceiveFailed) { boost::asio::ip::tcp::socket socket1(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx1; client_accept(&socket1, false, &on_connect_ctx1); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx1.wait()); ASSERT_EQ(0, ctx1.wait()); // send request via closed connection EmptyHttpRequest req; req.method(boost::beast::http::verb::get); C_SaferCond ctx2; http_client.issue(EmptyHttpRequest{req}, [&ctx2](int r, HttpResponse&&) mutable { ctx2.complete(r); }); // close connection to client after reading request client_read_request(socket1, req); C_SaferCond on_connect_ctx2; boost::asio::ip::tcp::socket socket2(*m_image_ctx->asio_engine); client_accept(&socket2, false, &on_connect_ctx2); boost::system::error_code ec; socket1.close(ec); ASSERT_EQ(0, on_connect_ctx2.wait()); // connection will be reset and request retried HttpResponse expected_res; expected_res.body() = "test"; client_read_request(socket2, req); client_write_response(socket2, expected_res); ASSERT_EQ(0, ctx2.wait()); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } TEST_F(TestMockMigrationHttpClient, IssueResetFailed) { m_server_port = 0; create_acceptor(true); boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx1; client_accept(&socket, false, &on_connect_ctx1); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx1.wait()); ASSERT_EQ(0, ctx1.wait()); // send requests then close connection EmptyHttpRequest req; req.method(boost::beast::http::verb::get); C_SaferCond ctx2; http_client.issue(EmptyHttpRequest{req}, [&ctx2](int r, HttpResponse&&) mutable { ctx2.complete(r); }); C_SaferCond ctx3; http_client.issue(EmptyHttpRequest{req}, [&ctx3](int r, HttpResponse&&) mutable { ctx3.complete(r); }); client_read_request(socket, req); client_read_request(socket, req); // close connection to client and verify requests are failed m_acceptor.reset(); boost::system::error_code ec; socket.close(ec); ASSERT_EQ(-ECONNREFUSED, ctx2.wait()); ASSERT_EQ(-ECONNREFUSED, ctx3.wait()); // additional request will retry the failed connection create_acceptor(true); C_SaferCond on_connect_ctx2; client_accept(&socket, false, &on_connect_ctx2); C_SaferCond ctx4; http_client.issue(EmptyHttpRequest{req}, [&ctx4](int r, HttpResponse&&) mutable { ctx4.complete(r); }); ASSERT_EQ(0, on_connect_ctx2.wait()); client_read_request(socket, req); HttpResponse expected_res; expected_res.body() = "test"; client_write_response(socket, expected_res); ASSERT_EQ(0, ctx4.wait()); C_SaferCond ctx5; http_client.close(&ctx5); ASSERT_EQ(0, ctx5.wait()); } TEST_F(TestMockMigrationHttpClient, IssuePipelined) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); // issue two pipelined (concurrent) get requests EmptyHttpRequest req1; req1.method(boost::beast::http::verb::get); C_SaferCond ctx2; HttpResponse res1; http_client.issue(EmptyHttpRequest{req1}, [&ctx2, &res1](int r, HttpResponse&& response) mutable { res1 = std::move(response); ctx2.complete(r); }); EmptyHttpRequest req2; req2.method(boost::beast::http::verb::get); C_SaferCond ctx3; HttpResponse res2; http_client.issue(EmptyHttpRequest{req2}, [&ctx3, &res2](int r, HttpResponse&& response) mutable { res2 = std::move(response); ctx3.complete(r); }); client_read_request(socket, req1); client_read_request(socket, req2); // read the responses sequentially HttpResponse expected_res1; expected_res1.body() = "test"; client_write_response(socket, expected_res1); ASSERT_EQ(0, ctx2.wait()); ASSERT_EQ(expected_res1, res1); HttpResponse expected_res2; expected_res2.body() = "test"; client_write_response(socket, expected_res2); ASSERT_EQ(0, ctx3.wait()); ASSERT_EQ(expected_res2, res2); C_SaferCond ctx4; http_client.close(&ctx4); ASSERT_EQ(0, ctx4.wait()); } TEST_F(TestMockMigrationHttpClient, IssuePipelinedRestart) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx1; client_accept(&socket, false, &on_connect_ctx1); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx1.wait()); ASSERT_EQ(0, ctx1.wait()); // issue two pipelined (concurrent) get requests EmptyHttpRequest req1; req1.keep_alive(false); req1.method(boost::beast::http::verb::get); C_SaferCond on_connect_ctx2; client_accept(&socket, false, &on_connect_ctx2); C_SaferCond ctx2; HttpResponse res1; http_client.issue(EmptyHttpRequest{req1}, [&ctx2, &res1](int r, HttpResponse&& response) mutable { res1 = std::move(response); ctx2.complete(r); }); EmptyHttpRequest req2; req2.method(boost::beast::http::verb::get); C_SaferCond ctx3; HttpResponse res2; http_client.issue(EmptyHttpRequest{req2}, [&ctx3, &res2](int r, HttpResponse&& response) mutable { res2 = std::move(response); ctx3.complete(r); }); client_read_request(socket, req1); client_read_request(socket, req2); // read the responses sequentially HttpResponse expected_res1; expected_res1.body() = "test"; expected_res1.keep_alive(false); client_write_response(socket, expected_res1); ASSERT_EQ(0, ctx2.wait()); ASSERT_EQ(expected_res1, res1); // second request will need to be re-sent due to 'need_eof' condition ASSERT_EQ(0, on_connect_ctx2.wait()); client_read_request(socket, req2); HttpResponse expected_res2; expected_res2.body() = "test"; client_write_response(socket, expected_res2); ASSERT_EQ(0, ctx3.wait()); ASSERT_EQ(expected_res2, res2); C_SaferCond ctx4; http_client.close(&ctx4); ASSERT_EQ(0, ctx4.wait()); } TEST_F(TestMockMigrationHttpClient, ShutdownInFlight) { boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); EmptyHttpRequest req; req.method(boost::beast::http::verb::get); C_SaferCond ctx2; http_client.issue(EmptyHttpRequest{req}, [&ctx2](int r, HttpResponse&&) mutable { ctx2.complete(r); }); client_read_request(socket, req); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); ASSERT_EQ(-ESHUTDOWN, ctx2.wait()); } TEST_F(TestMockMigrationHttpClient, GetSize) { MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); uint64_t size = 0; C_SaferCond ctx2; http_client.get_size(&size, &ctx2); EmptyHttpRequest expected_req; expected_req.method(boost::beast::http::verb::head); client_read_request(socket, expected_req); HttpResponse expected_res; expected_res.body() = std::string(123, '1'); client_write_response(socket, expected_res); ASSERT_EQ(0, ctx2.wait()); ASSERT_EQ(123, size); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } TEST_F(TestMockMigrationHttpClient, GetSizeError) { MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); uint64_t size = 0; C_SaferCond ctx2; http_client.get_size(&size, &ctx2); EmptyHttpRequest expected_req; expected_req.method(boost::beast::http::verb::head); client_read_request(socket, expected_req); HttpResponse expected_res; expected_res.result(boost::beast::http::status::internal_server_error); client_write_response(socket, expected_res); ASSERT_EQ(-EIO, ctx2.wait()); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } TEST_F(TestMockMigrationHttpClient, Read) { MockTestImageCtx mock_test_image_ctx(*m_image_ctx); MockHttpClient http_client(&mock_test_image_ctx, get_local_url(URL_SCHEME_HTTP)); boost::asio::ip::tcp::socket socket(*m_image_ctx->asio_engine); C_SaferCond on_connect_ctx; client_accept(&socket, false, &on_connect_ctx); C_SaferCond ctx1; http_client.open(&ctx1); ASSERT_EQ(0, on_connect_ctx.wait()); ASSERT_EQ(0, ctx1.wait()); bufferlist bl; C_SaferCond ctx2; http_client.read({{0, 128}, {256, 64}}, &bl, &ctx2); EmptyHttpRequest expected_req1; expected_req1.method(boost::beast::http::verb::get); expected_req1.set(boost::beast::http::field::range, "bytes=0-127"); client_read_request(socket, expected_req1); EmptyHttpRequest expected_req2; expected_req2.method(boost::beast::http::verb::get); expected_req2.set(boost::beast::http::field::range, "bytes=256-319"); client_read_request(socket, expected_req2); HttpResponse expected_res1; expected_res1.result(boost::beast::http::status::partial_content); expected_res1.body() = std::string(128, '1'); client_write_response(socket, expected_res1); HttpResponse expected_res2; expected_res2.result(boost::beast::http::status::partial_content); expected_res2.body() = std::string(64, '2'); client_write_response(socket, expected_res2); ASSERT_EQ(192, ctx2.wait()); bufferlist expect_bl; expect_bl.append(std::string(128, '1')); expect_bl.append(std::string(64, '2')); ASSERT_EQ(expect_bl, bl); C_SaferCond ctx3; http_client.close(&ctx3); ASSERT_EQ(0, ctx3.wait()); } } // namespace migration } // namespace librbd