Playback test impl (#2737)

* checkpoint

* playback working

* formant
This commit is contained in:
Victor Vazquez 2021-08-12 10:58:21 -07:00 committed by GitHub
parent 33e56e1bbf
commit b3cfe0148e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 28 deletions

View File

@ -20,6 +20,7 @@
#pragma once
#include <azure/core/http/http.hpp>
#include <azure/core/http/policies/policy.hpp>
#include <memory>
#include <string>
@ -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

View File

@ -12,6 +12,7 @@
#include <azure/core/context.hpp>
#include <azure/core/http/http.hpp>
#include <azure/core/http/policies/policy.hpp>
#include <azure/core/io/body_stream.hpp>
#include <azure/core/response.hpp>
#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<uint8_t> 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<uint8_t> 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<uint8_t> 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.
*

View File

@ -5,14 +5,75 @@
#define _CRT_SECURE_NO_WARNINGS // for std::getenv()
#endif
#include <azure/core/internal/json/json.hpp>
#include <azure/core/internal/strings.hpp>
#include "azure/core/test/interceptor_manager.hpp"
#include "private/environment.hpp"
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
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<char>(readFile)), std::istreambuf_iterator<char>());
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<std::map<std::string, std::string>>();
modelRecord.Response = record["Response"].get<std::map<std::string, std::string>>();
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;
}

View File

@ -6,15 +6,62 @@
#include "azure/core/test/interceptor_manager.hpp"
#include "azure/core/test/playback_http_client.hpp"
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <vector>
std::unique_ptr<Azure::Core::Http::RawResponse> Azure::Core::Test::PlaybackClient::Send(
Azure::Core::Http::Request& request,
using namespace Azure::Core::Http;
using namespace Azure::Core::Test;
std::unique_ptr<RawResponse> 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<RawResponse>(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<uint8_t> bodyVector(body.begin(), body.end());
response->SetBodyStream(std::make_unique<WithMemoryBodyStream>(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.");
}

View File

@ -58,24 +58,7 @@ std::unique_ptr<RawResponse> 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();
}

View File

@ -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;

View File

@ -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());

View File

@ -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"
}
]
}