From b3cfe0148eb9709d3519eace62f071ef45915965 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Thu, 12 Aug 2021 10:58:21 -0700 Subject: [PATCH] Playback test impl (#2737) * checkpoint * playback working * formant --- .../azure/core/test/interceptor_manager.hpp | 18 +++++ .../azure/core/test/playback_http_client.hpp | 37 +++++++++++ .../src/interceptor_manager.cpp | 65 ++++++++++++++++++- .../src/playback_http_transport.cpp | 59 +++++++++++++++-- .../azure-core-test/src/record_policy.cpp | 19 +----- sdk/core/azure-core-test/src/test_base.cpp | 14 ++++ .../test/ut/key_client_create_test_live.cpp | 4 +- .../KeyVaultClientTest.CreateKey.json | 57 ++++++++++++++++ 8 files changed, 245 insertions(+), 28 deletions(-) create mode 100644 sdk/keyvault/azure-security-keyvault-keys/test/ut/recordings/KeyVaultClientTest.CreateKey.json diff --git a/sdk/core/azure-core-test/inc/azure/core/test/interceptor_manager.hpp b/sdk/core/azure-core-test/inc/azure/core/test/interceptor_manager.hpp index 46b2a780c..1f9bbd24c 100644 --- a/sdk/core/azure-core-test/inc/azure/core/test/interceptor_manager.hpp +++ b/sdk/core/azure-core-test/inc/azure/core/test/interceptor_manager.hpp @@ -20,6 +20,7 @@ #pragma once +#include #include #include #include @@ -97,6 +98,23 @@ namespace Azure { namespace Core { namespace Test { * @return TestMode */ static TestMode GetTestMode(); + + /** + * @brief This function is expected to be called by the playback transport adapter. + * + * @remark The name of the test is known and set when the test is actually started. That's why + * the recorded data can't be loaded until the test is already running (Can't load on SetUp). + * + */ + void LoadTestData(); + + /** + * @brief Removes sensitive info from a request Url. + * + * @param url The request Url. + * @return Azure::Core::Url + */ + Azure::Core::Url RedactUrl(Azure::Core::Url const& url); }; }}} // namespace Azure::Core::Test diff --git a/sdk/core/azure-core-test/inc/azure/core/test/playback_http_client.hpp b/sdk/core/azure-core-test/inc/azure/core/test/playback_http_client.hpp index 1e564457c..51b567741 100644 --- a/sdk/core/azure-core-test/inc/azure/core/test/playback_http_client.hpp +++ b/sdk/core/azure-core-test/inc/azure/core/test/playback_http_client.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "azure/core/test/network_models.hpp" @@ -21,6 +22,42 @@ namespace Azure { namespace Core { namespace Test { // Partial class. Required to reference the Interceptor that is defined in the implementation. class InterceptorManager; + /** + * @brief A body stream which holds the memory inside. + * + * @remark The playback http uses this body stream to be returned as part of the raw response so + * the transport policy can read from it. + * + */ + class WithMemoryBodyStream : public Azure::Core::IO::BodyStream { + private: + std::vector m_memory; + Azure::Core::IO::MemoryBodyStream m_streamer; + + size_t OnRead(uint8_t* buffer, size_t count, Azure::Core::Context const& context) override + { + return m_streamer.Read(buffer, count, context); + }; + + public: + // Forbid constructor for rval so we don't end up storing dangling ptr + WithMemoryBodyStream(std::vector const&&) = delete; + + /** + * @brief Construct using vector of bytes. + * + * @param buffer Vector of bytes with the contents to provide the data from to the readers. + */ + WithMemoryBodyStream(std::vector const& buffer) + : m_memory(buffer), m_streamer(m_memory) + { + } + + int64_t Length() const override { return m_streamer.Length(); } + + void Rewind() override { m_streamer.Rewind(); } + }; + /** * @brief Creates an HTTP Transport adapter that answer to requests using recorded data. * diff --git a/sdk/core/azure-core-test/src/interceptor_manager.cpp b/sdk/core/azure-core-test/src/interceptor_manager.cpp index c0c2f269a..1fdbb55d1 100644 --- a/sdk/core/azure-core-test/src/interceptor_manager.cpp +++ b/sdk/core/azure-core-test/src/interceptor_manager.cpp @@ -5,14 +5,75 @@ #define _CRT_SECURE_NO_WARNINGS // for std::getenv() #endif +#include #include #include "azure/core/test/interceptor_manager.hpp" #include "private/environment.hpp" +#include +#include #include +#include -Azure::Core::Test::TestMode Azure::Core::Test::InterceptorManager::GetTestMode() +using namespace Azure::Core::Test; +using namespace Azure::Core::Json::_internal; +using namespace Azure::Core; + +TestMode InterceptorManager::GetTestMode() { return _detail::Environment::GetTestMode(); } + +void InterceptorManager::LoadTestData() { - return Azure::Core::Test::_detail::Environment::GetTestMode(); + if (m_recordedData.NetworkCallRecords.size() > 0) + { + // TestData was loaded it before. + return; + } + + std::string const recordingName( + m_testContext.RecordingPath + "/" + m_testContext.GetTestPlaybackRecordingName() + ".json"); + std::ifstream readFile(recordingName); + if (!readFile.is_open()) + { + throw std::runtime_error("Can't open recording: " + recordingName); + } + + std::string recordingContent( + (std::istreambuf_iterator(readFile)), std::istreambuf_iterator()); + + auto const jsonRecord = json::parse(recordingContent); + auto const networkRecords = jsonRecord["networkCallRecords"]; + for (auto const& record : networkRecords) + { + NetworkCallRecord modelRecord; + modelRecord.Method = record["Method"]; + modelRecord.Url = record["Url"]; + modelRecord.Headers = record["Headers"].get>(); + modelRecord.Response = record["Response"].get>(); + m_recordedData.NetworkCallRecords.push_back(modelRecord); + } + readFile.close(); +} + +Url InterceptorManager::RedactUrl(Url const& url) +{ + Azure::Core::Url redactedUrl; + redactedUrl.SetScheme(url.GetScheme()); + auto host = url.GetHost(); + auto hostWithNoAccount = std::find(host.begin(), host.end(), '.'); + redactedUrl.SetHost("REDACTED" + std::string(hostWithNoAccount, host.end())); + redactedUrl.SetPath(url.GetPath()); + // Query parameters + for (auto const& qp : url.GetQueryParameters()) + { + if (qp.first == "sig") + { + redactedUrl.AppendQueryParameter("sig", "REDACTED"); + } + else + { + redactedUrl.AppendQueryParameter(qp.first, qp.second); + } + } + return redactedUrl; } diff --git a/sdk/core/azure-core-test/src/playback_http_transport.cpp b/sdk/core/azure-core-test/src/playback_http_transport.cpp index aad75c961..fc0f3bed7 100644 --- a/sdk/core/azure-core-test/src/playback_http_transport.cpp +++ b/sdk/core/azure-core-test/src/playback_http_transport.cpp @@ -6,15 +6,62 @@ #include "azure/core/test/interceptor_manager.hpp" #include "azure/core/test/playback_http_client.hpp" +#include +#include #include +#include -std::unique_ptr Azure::Core::Test::PlaybackClient::Send( - Azure::Core::Http::Request& request, +using namespace Azure::Core::Http; +using namespace Azure::Core::Test; + +std::unique_ptr PlaybackClient::Send( + Request& request, Azure::Core::Context const& context) { - (void)(context); - (void)(request); - (void)m_interceptorManager->GetTestContext().GetTestPlaybackRecordingName(); + context.ThrowIfCancelled(); - throw; + // The test name can't be known before the test is started. That's why the test data is loaded up + // to this point instead of loading it on test SetUp. + // The test data will be loaded just one time. + m_interceptorManager->LoadTestData(); + + auto& recordedData = m_interceptorManager->GetRecordedData(); + Azure::Core::Url const redactedUrl = m_interceptorManager->RedactUrl(request.GetUrl()); + + for (auto record = recordedData.NetworkCallRecords.begin(); + record != recordedData.NetworkCallRecords.end();) + { + auto url = redactedUrl.GetAbsoluteUrl(); + auto m = request.GetMethod().ToString(); + // Use the first occurrence and remove it from the recording. + if (m == record->Method && url == record->Url) + { + // StatusCode + auto const statusCode + = HttpStatusCode(std::stoi(record->Response.find("STATUS_CODE")->second)); + auto response = std::make_unique(1, 1, statusCode, "recorded response"); + + // Headers + for (auto const& header : record->Response) + { + if (header.first != "STATUS_CODE" && header.first != "BODY") + { + response->SetHeader(header.first, header.second); + } + } + + // Body + auto body = record->Response.find("BODY")->second; + std::vector bodyVector(body.begin(), body.end()); + response->SetBodyStream(std::make_unique(bodyVector)); + + // take the record out of the recording + record = recordedData.NetworkCallRecords.erase(record); + + return response; + } + ++record; + } + + throw std::runtime_error("Did not found a response for the request in the recordings."); } diff --git a/sdk/core/azure-core-test/src/record_policy.cpp b/sdk/core/azure-core-test/src/record_policy.cpp index 1df88a7ff..132aaf390 100644 --- a/sdk/core/azure-core-test/src/record_policy.cpp +++ b/sdk/core/azure-core-test/src/record_policy.cpp @@ -58,24 +58,7 @@ std::unique_ptr RecordNetworkCallPolicy::Send( // Remove sensitive information such as SAS token signatures from the recording. { - auto const& url = request.GetUrl(); - Azure::Core::Url redactedUrl; - redactedUrl.SetScheme(url.GetScheme()); - auto host = url.GetHost(); - auto hostWithNoAccount = std::find(host.begin(), host.end(), '.'); - redactedUrl.SetHost("REDACTED" + std::string(hostWithNoAccount, host.end())); - // Query parameters - for (auto const& qp : url.GetQueryParameters()) - { - if (qp.first == "sig") - { - redactedUrl.AppendQueryParameter("sig", "REDACTED"); - } - else - { - redactedUrl.AppendQueryParameter(qp.first, qp.second); - } - } + Azure::Core::Url const redactedUrl = m_interceptorManager->RedactUrl(request.GetUrl()); record.Url = redactedUrl.GetAbsoluteUrl(); } diff --git a/sdk/core/azure-core-test/src/test_base.cpp b/sdk/core/azure-core-test/src/test_base.cpp index cd0afc0e6..07aee5610 100644 --- a/sdk/core/azure-core-test/src/test_base.cpp +++ b/sdk/core/azure-core-test/src/test_base.cpp @@ -14,9 +14,23 @@ using namespace Azure::Core::Json::_internal; void Azure::Core::Test::TestBase::TearDown() { + + if (m_testContext.IsLiveMode() || m_testContext.IsPlaybackMode()) + { + // Don't want to record here + return; + } + json root; json records; auto const& recordData = m_interceptor->GetRecordedData(); + + if (recordData.NetworkCallRecords.size() == 0) + { + // Don't make empty recordings + return; + } + for (auto const& record : recordData.NetworkCallRecords) { json recordJson; diff --git a/sdk/keyvault/azure-security-keyvault-keys/test/ut/key_client_create_test_live.cpp b/sdk/keyvault/azure-security-keyvault-keys/test/ut/key_client_create_test_live.cpp index 3bf7d75a6..f46ac1a04 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/test/ut/key_client_create_test_live.cpp +++ b/sdk/keyvault/azure-security-keyvault-keys/test/ut/key_client_create_test_live.cpp @@ -16,9 +16,9 @@ using namespace Azure::Security::KeyVault::Keys::Test; -TEST_F(KeyVaultClientTest, CreateKey123) +TEST_F(KeyVaultClientTest, CreateKey) { - auto keyName = GetUniqueName(); + auto keyName = "CreateKeyWithThisName"; auto const& client = GetClientForTest(::testing::UnitTest::GetInstance()->current_test_info()->name()); diff --git a/sdk/keyvault/azure-security-keyvault-keys/test/ut/recordings/KeyVaultClientTest.CreateKey.json b/sdk/keyvault/azure-security-keyvault-keys/test/ut/recordings/KeyVaultClientTest.CreateKey.json new file mode 100644 index 000000000..462ebc921 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-keys/test/ut/recordings/KeyVaultClientTest.CreateKey.json @@ -0,0 +1,57 @@ +{ + "networkCallRecords": [ + { + "Headers": { + "content-type": "application/json", + "user-agent": "azsdk-cpp-keyvault-keys/7.2 (Linux 5.4.0-1048-azure x86_64 #50~18.04.1-Ubuntu SMP Fri May 14 15:30:12 UTC 2021)", + "x-ms-client-request-id": "91c8f74c-4a6b-4a4a-5c6f-084d8620c1e3" + }, + "Method": "POST", + "Response": { + "BODY": "{\"key\":{\"kid\":\"https://doesNotMatter.vault.azure.net/keys/CreateKeyWithThisName/4abbde2456dd4aa5b1cd82d2212cb67c\",\"kty\":\"EC\",\"key_ops\":[\"sign\",\"verify\"],\"crv\":\"P-256\",\"x\":\"sJDOZ-TgRQISPgI8cog7r1GPDr5_5SfCNHW1es3jPTY\",\"y\":\"C2-3YQh16DDe-uw92l40p6lJUI9ZTIMxqIG2wUDqXr0\"},\"attributes\":{\"enabled\":true,\"created\":1628713611,\"updated\":1628713611,\"recoveryLevel\":\"Recoverable+Purgeable\",\"recoverableDays\":90}}", + "STATUS_CODE": "200", + "cache-control": "no-cache", + "content-length": "398", + "content-type": "application/json; charset=utf-8", + "date": "Wed, 11 Aug 2021 20:26:51 GMT", + "expires": "-1", + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-client-request-id": "91c8f74c-4a6b-4a4a-5c6f-084d8620c1e3", + "x-ms-keyvault-network-info": "conn_type=Ipv4;addr=20.49.4.206;act_addr_fam=InterNetwork;", + "x-ms-keyvault-region": "westus2", + "x-ms-keyvault-service-version": "1.9.48.0", + "x-ms-request-id": "e43b0631-91d0-4c8d-887b-9e08cb8b7eaa", + "x-powered-by": "ASP.NET" + }, + "Url": "https://REDACTED.vault.azure.net/keys/CreateKeyWithThisName/create?api-version=7.2" + }, + { + "Headers": { + "user-agent": "azsdk-cpp-keyvault-keys/7.2 (Linux 5.4.0-1048-azure x86_64 #50~18.04.1-Ubuntu SMP Fri May 14 15:30:12 UTC 2021)", + "x-ms-client-request-id": "3737836e-02b0-4077-5841-1959854cdb45" + }, + "Method": "GET", + "Response": { + "BODY": "{\"key\":{\"kid\":\"https://doesNotMatter.vault.azure.net/keys/CreateKeyWithThisName/4abbde2456dd4aa5b1cd82d2212cb67c\",\"kty\":\"EC\",\"key_ops\":[\"sign\",\"verify\"],\"crv\":\"P-256\",\"x\":\"sJDOZ-TgRQISPgI8cog7r1GPDr5_5SfCNHW1es3jPTY\",\"y\":\"C2-3YQh16DDe-uw92l40p6lJUI9ZTIMxqIG2wUDqXr0\"},\"attributes\":{\"enabled\":true,\"created\":1628713611,\"updated\":1628713611,\"recoveryLevel\":\"Recoverable+Purgeable\",\"recoverableDays\":90}}", + "STATUS_CODE": "200", + "cache-control": "no-cache", + "content-length": "398", + "content-type": "application/json; charset=utf-8", + "date": "Wed, 11 Aug 2021 20:26:51 GMT", + "expires": "-1", + "pragma": "no-cache", + "strict-transport-security": "max-age=31536000;includeSubDomains", + "x-content-type-options": "nosniff", + "x-ms-client-request-id": "3737836e-02b0-4077-5841-1959854cdb45", + "x-ms-keyvault-network-info": "conn_type=Ipv4;addr=20.49.4.206;act_addr_fam=InterNetwork;", + "x-ms-keyvault-region": "westus2", + "x-ms-keyvault-service-version": "1.9.48.0", + "x-ms-request-id": "cdd8fe71-5202-46ab-8001-aa5e7c94f8f4", + "x-powered-by": "ASP.NET" + }, + "Url": "https://REDACTED.vault.azure.net/keys/CreateKeyWithThisName?api-version=7.2" + } + ] +}