\n GitHub is a development platform inspired by the way you work. From open source to business, you can host and review code, manage projects, and build software alongside millions of other developers.\n
\n We worry about your administrative and security needs so you don’t have to. From flexible hosting to authentication options, GitHub can help you meet your team’s requirements.\n
\n Prevent problems before they happen. Protected branches, signed commits, and required status checks protect your work and help you maintain a high standard for your code.\n
\n\n
Access controlled
\n
\n Encourage teams to work together while limiting access to those who need it with granular permissions and authentication through SAML/SSO and LDAP.\n
\n
\n
\n\n
\n
\n \n\n
\n
\n
Hosted where you need it
\n
Securely and reliably host your work on GitHub.com. Or, deploy GitHub Enterprise on your own servers or in a private cloud using Amazon Web Services, Azure or Google Cloud Platform.
\n Customize your process with GitHub apps and an intuitive API. Integrate the tools you already use or discover new favorites to create a happier, more efficient way of working.\n
\n Get started for free — join the millions of developers already using GitHub to share their code, work together, and build amazing things.\n
\n
\n
\n
\n
\n\n\n \n\n
\n\n \n\n\n\n\n
\n \n \n You can't perform that action at this time.\n
\n\n\n \n \n \n \n \n \n \n \n
\n \n You signed in with another tab or window. Reload to refresh your session.\n You signed out in another tab or window. Reload to refresh your session.\n
\n GitHub is a development platform inspired by the way you work. From open source to business, you can host and review code, manage projects, and build software alongside millions of other developers.\n
\n We worry about your administrative and security needs so you don’t have to. From flexible hosting to authentication options, GitHub can help you meet your team’s requirements.\n
\n Prevent problems before they happen. Protected branches, signed commits, and required status checks protect your work and help you maintain a high standard for your code.\n
\n\n
Access controlled
\n
\n Encourage teams to work together while limiting access to those who need it with granular permissions and authentication through SAML/SSO and LDAP.\n
\n
\n
\n\n
\n
\n \n\n
\n
\n
Hosted where you need it
\n
Securely and reliably host your work on GitHub.com. Or, deploy GitHub Enterprise on your own servers or in a private cloud using Amazon Web Services, Azure or Google Cloud Platform.
\n Customize your process with GitHub apps and an intuitive API. Integrate the tools you already use or discover new favorites to create a happier, more efficient way of working.\n
\n Get started for free — join the millions of developers already using GitHub to share their code, work together, and build amazing things.\n
\n
\n
\n
\n
\n\n\n \n\n
\n\n \n\n\n\n\n
\n \n \n You can't perform that action at this time.\n
\n\n\n \n \n \n \n \n \n \n \n
\n \n You signed in with another tab or window. Reload to refresh your session.\n You signed out in another tab or window. Reload to refresh your session.\n
\n
\n
\n
\n
\n \n
\n
\n\n \n\n \n\n\n",
+ "IsEncoded": false
+ },
"ResponseHeaders": {
"Transfer-Encoding": [
"chunked"
@@ -71,6 +74,42 @@
]
},
"ResponseStatusCode": 200
+ },
+ {
+ "Method": "GET",
+ "Url": "http://localhost/iisstart.png",
+ "RequestCookieContainer": null,
+ "RequestHeaders": {},
+ "RequestPayload": "",
+ "ResponseBody": {
+ "SerializedStream": "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",
+ "IsEncoded": true
+ },
+ "ResponseHeaders": {
+ "Accept-Ranges": [
+ "bytes"
+ ],
+ "Content-Length": [
+ "98757"
+ ],
+ "Content-Type": [
+ "image/png"
+ ],
+ "Date": [
+ "Sat, 03 Mar 2018 13:44:11 GMT"
+ ],
+ "ETag": [
+ "\"371efb4eeb2d31:0\""
+ ],
+ "Last-Modified": [
+ "Sat, 03 Mar 2018 12:49:07 GMT"
+ ],
+ "Server": [
+ "Microsoft-IIS/10.0"
+ ]
+ },
+ "ResponseStatusCode": 200,
+ "ResponseException": null
}
]
}
diff --git a/src/HttpWebRequestWrapper.Tests/RecordingSessionInterceptorRequestBuilderTests.cs b/src/HttpWebRequestWrapper.Tests/RecordingSessionInterceptorRequestBuilderTests.cs
index 27f7a81..c4a3a38 100644
--- a/src/HttpWebRequestWrapper.Tests/RecordingSessionInterceptorRequestBuilderTests.cs
+++ b/src/HttpWebRequestWrapper.Tests/RecordingSessionInterceptorRequestBuilderTests.cs
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using System.Net;
+using System.Text;
+using HttpWebRequestWrapper.Recording;
using HttpWebRequestWrapper.Tests.Properties;
using Newtonsoft.Json;
using Should;
@@ -108,10 +111,10 @@ public void CanPlaybackFromMultipleRecordingSessions()
response2.ShouldNotBeNull();
using (var sr = new StreamReader(response1.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response2.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody.SerializedStream);
}
[Fact]
@@ -220,7 +223,7 @@ public void DefaultNotFoundBehaviorReturns404()
}
// WARNING!! Makes live request
- [Fact]
+ [Fact(Timeout = 10000)]
public void CanChangeDefaultNotFoundBehaviorToPassThrough()
{
// ARRANGE
@@ -329,7 +332,7 @@ public void CanCustomizeMatchingAlgorithm()
response.ShouldNotBeNull();
using (var sr = new StreamReader(response.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody.SerializedStream);
}
[Fact]
@@ -446,13 +449,13 @@ public void CanSetRecordedRequestsToOnlyMatchOnce()
response2b.ShouldNotBeNull();
using (var sr = new StreamReader(response1a.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response1b.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response2a.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody.SerializedStream);
response1c.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
response2b.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
@@ -497,10 +500,10 @@ public void MatchesOnUniqueUrl()
response2.ShouldNotBeNull();
using (var sr = new StreamReader(response1.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response2.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody.SerializedStream);
}
[Fact]
@@ -544,10 +547,10 @@ public void MatchesOnUniqueMethod()
response2.ShouldNotBeNull();
using (var sr = new StreamReader(response1.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response2.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody.SerializedStream);
}
[Fact]
@@ -559,14 +562,19 @@ public void MatchesOnUniquePayload()
Url = "http://fakeSite.fake",
Method = "POST",
RequestPayload = "Request 1",
+ RequestHeaders = new RecordedHeaders
+ {
+ {"Content-Type", new []{"text/plain" }}
+ },
ResponseBody = "Response 1"
};
var recordedRequest2 = new RecordedRequest
{
Url = recordedRequest1.Url,
- Method = "POST",
+ Method = recordedRequest1.Method,
RequestPayload = "Request 2",
+ RequestHeaders = recordedRequest1.RequestHeaders,
ResponseBody = "Response 2"
};
@@ -581,13 +589,15 @@ public void MatchesOnUniquePayload()
var request1 = creator.Create(new Uri(recordedRequest1.Url));
request1.Method = "POST";
- using (var sw = new StreamWriter(request1.GetRequestStream()))
- sw.Write(recordedRequest1.RequestPayload);
+ request1.ContentType = "text/plain";
+
+ recordedRequest1.RequestPayload.ToStream().CopyTo(request1.GetRequestStream());
var request2 = creator.Create(new Uri(recordedRequest2.Url));
request2.Method = "POST";
- using (var sw = new StreamWriter(request2.GetRequestStream()))
- sw.Write(recordedRequest2.RequestPayload);
+ request2.ContentType = "text/plain";
+
+ recordedRequest2.RequestPayload.ToStream().CopyTo(request2.GetRequestStream());
// ACT
var response1 = request1.GetResponse();
@@ -598,10 +608,10 @@ public void MatchesOnUniquePayload()
response2.ShouldNotBeNull();
using (var sr = new StreamReader(response1.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response2.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody.SerializedStream);
}
[Fact]
@@ -649,10 +659,145 @@ public void MatchesOnUniqueRequestHeaders()
response2.ShouldNotBeNull();
using (var sr = new StreamReader(response1.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest1.ResponseBody.SerializedStream);
using (var sr = new StreamReader(response2.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest2.ResponseBody.SerializedStream);
+ }
+
+ [Fact]
+ public void CanPlaybackZippedResponse()
+ {
+ // ARRANGE
+ var recordedRequest = new RecordedRequest
+ {
+ Url = "http://fakeSite.fake",
+ Method = "GET",
+ ResponseBody = new RecordedStream
+ {
+ SerializedStream = "Response 1",
+ IsGzippedCompressed = true
+ },
+ ResponseHeaders = new RecordedHeaders
+ {
+ {"Content-Encoding", new []{"gzip"} }
+ }
+ };
+
+ var recordingSession = new RecordingSession
+ {
+ RecordedRequests = new List { recordedRequest }
+ };
+
+ var requestBuilder = new RecordingSessionInterceptorRequestBuilder(recordingSession);
+
+ IWebRequestCreate creator = new HttpWebRequestWrapperInterceptorCreator(requestBuilder);
+
+ var request = (HttpWebRequest)creator.Create(new Uri(recordedRequest.Url));
+ request.AutomaticDecompression = DecompressionMethods.GZip;
+
+ // ACT
+ var response = request.GetResponse();
+
+ // ASSERT
+ response.ShouldNotBeNull();
+
+ using (var sr = new StreamReader(response.GetResponseStream()))
+ sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody.SerializedStream);
+ }
+
+ [Fact]
+ public void CanPlaybackDeflatedResponse()
+ {
+ // ARRANGE
+ var recordedRequest = new RecordedRequest
+ {
+ Url = "http://fakeSite.fake",
+ Method = "GET",
+ ResponseBody = new RecordedStream
+ {
+ SerializedStream = "Response 1",
+ IsDefalteCompressed = true
+ },
+ ResponseHeaders = new RecordedHeaders
+ {
+ {"Content-Encoding", new []{"deflate"} }
+ }
+ };
+
+ var recordingSession = new RecordingSession
+ {
+ RecordedRequests = new List { recordedRequest }
+ };
+
+ var requestBuilder = new RecordingSessionInterceptorRequestBuilder(recordingSession);
+
+ IWebRequestCreate creator = new HttpWebRequestWrapperInterceptorCreator(requestBuilder);
+
+ var request = (HttpWebRequest)creator.Create(new Uri(recordedRequest.Url));
+ request.AutomaticDecompression = DecompressionMethods.Deflate;
+
+ // ACT
+ var response = request.GetResponse();
+
+ // ASSERT
+ response.ShouldNotBeNull();
+
+ using (var sr = new StreamReader(response.GetResponseStream()))
+ sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody.SerializedStream);
+ }
+
+ [Fact]
+ public void MatchesOnZippedPayload()
+ {
+ // ARRANGE
+ var recordedRequest = new RecordedRequest
+ {
+ Url = "http://fakeSite.fake",
+ Method = "POST",
+ RequestPayload = new RecordedStream
+ {
+ SerializedStream = "Request 1",
+ IsGzippedCompressed = true
+ },
+ RequestHeaders = new RecordedHeaders
+ {
+ {"Content-Type", new []{"text/plain" }}
+ },
+ ResponseBody = "Response 1"
+ };
+
+ var recordingSession = new RecordingSession
+ {
+ RecordedRequests = new List { recordedRequest }
+ };
+
+ var requestBuilder = new RecordingSessionInterceptorRequestBuilder(recordingSession);
+
+ IWebRequestCreate creator = new HttpWebRequestWrapperInterceptorCreator(requestBuilder);
+
+ var request = creator.Create(new Uri(recordedRequest.Url));
+ request.Method = "POST";
+ request.ContentType = "text/plain";
+
+ using (var input = new MemoryStream(Encoding.UTF8.GetBytes(recordedRequest.RequestPayload.SerializedStream)))
+ using (var compressed = new MemoryStream())
+ using (var zip = new GZipStream(compressed, CompressionMode.Compress, leaveOpen: true))
+ {
+ input.CopyTo(zip);
+ zip.Close();
+ compressed.Seek(0, SeekOrigin.Begin);
+ compressed.CopyTo(request.GetRequestStream());
+ }
+
+ // ACT
+ var response = request.GetResponse();
+
+ // ASSERT
+ response.ShouldNotBeNull();
+
+ using (var sr = new StreamReader(response.GetResponseStream()))
+ sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody.SerializedStream);
}
[Fact]
@@ -681,7 +826,7 @@ public void BuilderSetsResponseBody()
response.ShouldNotBeNull();
using (var sr = new StreamReader(response.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody.SerializedStream);
}
[Fact]
@@ -818,7 +963,60 @@ public void BuilderSetsWebExceptionWithResponse()
webExceptionResponse.ContentLength.ShouldBeGreaterThan(0);
using (var sr = new StreamReader(webExceptionResponse.GetResponseStream()))
- sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody);
+ sr.ReadToEnd().ShouldEqual(recordedRequest.ResponseBody.SerializedStream);
+ }
+
+ ///
+ /// From documentation
+ /// https://msdn.microsoft.com/en-us/library/system.net.webexception.response(v=vs.110).aspx
+ /// Response should always be set if is
+ ///
+ ///
+ [Fact]
+ public void BuilderAlwaysSetsWebExcpetionResponseWhenStatusIsProtocolError()
+ {
+ // ARRANGE
+ var recordedRequest = new RecordedRequest
+ {
+ Url = "http://fakeSite.fake",
+ Method = "GET",
+ ResponseException = new RecordedResponseException
+ {
+ Message = "Test Exception Message",
+ Type = typeof(WebException),
+ WebExceptionStatus = WebExceptionStatus.ProtocolError,
+ },
+ ResponseHeaders = new RecordedHeaders
+ {
+ {"header1", new[] {"value1"}}
+ },
+ ResponseStatusCode = HttpStatusCode.Unauthorized
+ //intentionally leave ResponseBody null
+ };
+
+ var recordingSession = new RecordingSession { RecordedRequests = new List { recordedRequest } };
+
+ var requestBuilder = new RecordingSessionInterceptorRequestBuilder(recordingSession);
+
+ IWebRequestCreate creator = new HttpWebRequestWrapperInterceptorCreator(requestBuilder);
+
+ var request = creator.Create(new Uri(recordedRequest.Url));
+
+ // ACT
+ var exception = Record.Exception(() => request.GetResponse());
+ var webException = exception as WebException;
+ var webExceptionResponse = webException.Response as HttpWebResponse;
+
+ // ASSERT
+ webException.ShouldNotBeNull();
+ webException.Message.ShouldEqual(recordedRequest.ResponseException.Message);
+ webException.Status.ShouldEqual(recordedRequest.ResponseException.WebExceptionStatus.Value);
+
+ webExceptionResponse.ShouldNotBeNull();
+ Assert.Equal(recordedRequest.ResponseHeaders, (RecordedHeaders)webExceptionResponse.Headers);
+ webExceptionResponse.StatusCode.ShouldEqual(recordedRequest.ResponseStatusCode);
+ // no response content in recordedResponse, so content length should be 0
+ webExceptionResponse.ContentLength.ShouldEqual(0);
}
[Fact]
diff --git a/src/HttpWebRequestWrapper/Extensions/RecordedRequestExtensions.cs b/src/HttpWebRequestWrapper/Extensions/RecordedRequestExtensions.cs
index 546fadb..8037fe3 100644
--- a/src/HttpWebRequestWrapper/Extensions/RecordedRequestExtensions.cs
+++ b/src/HttpWebRequestWrapper/Extensions/RecordedRequestExtensions.cs
@@ -1,5 +1,7 @@
using System;
+using System.IO;
using System.Net;
+using HttpWebRequestWrapper.Recording;
namespace HttpWebRequestWrapper.Extensions
{
@@ -56,7 +58,11 @@ public static bool TryGetResponseException(this RecordedRequest request, out Exc
return true;
}
- if (null == request.ResponseBody)
+ // can we return a WebException without a Response?
+ if (string.IsNullOrEmpty(request?.ResponseBody?.SerializedStream) &&
+ // always need to return a response if WebExceptionStatus is ProtocolError
+ //https://msdn.microsoft.com/en-us/library/system.net.webexception.response(v=vs.110).aspx
+ request.ResponseException.WebExceptionStatus != WebExceptionStatus.ProtocolError)
{
recordedException = new WebException(
request.ResponseException.Message,
@@ -76,7 +82,7 @@ public static bool TryGetResponseException(this RecordedRequest request, out Exc
new Uri(request.Url),
request.Method,
request.ResponseStatusCode,
- request.ResponseBody,
+ request.ResponseBody?.ToStream() ?? new MemoryStream(),
request.ResponseHeaders));
return true;
diff --git a/src/HttpWebRequestWrapper/Extensions/StreamExtensions.cs b/src/HttpWebRequestWrapper/Extensions/StreamExtensions.cs
new file mode 100644
index 0000000..ad90caf
--- /dev/null
+++ b/src/HttpWebRequestWrapper/Extensions/StreamExtensions.cs
@@ -0,0 +1,22 @@
+using System.IO;
+
+namespace HttpWebRequestWrapper.Extensions
+{
+ internal static class StreamExtensions
+ {
+ public static void CopyTo(this Stream source, Stream destinaton)
+ {
+ var buffer = new byte[1024];
+
+ while (true)
+ {
+ var read = source.Read(buffer, 0, buffer.Length);
+
+ destinaton.Write(buffer, 0, read);
+
+ if (read != buffer.Length)
+ break;
+ }
+ }
+ }
+}
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapper.csproj b/src/HttpWebRequestWrapper/HttpWebRequestWrapper.csproj
index d44d311..2c0dc71 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapper.csproj
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapper.csproj
@@ -49,6 +49,7 @@
+
@@ -59,9 +60,11 @@
-
-
-
+
+
+
+
+
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapperDelegateCreator.cs b/src/HttpWebRequestWrapper/HttpWebRequestWrapperDelegateCreator.cs
index 5c27a39..e65ff1d 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapperDelegateCreator.cs
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapperDelegateCreator.cs
@@ -1,5 +1,6 @@
using System;
using System.Net;
+using HttpWebRequestWrapper.Recording;
namespace HttpWebRequestWrapper
{
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptor.cs b/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptor.cs
index 6190574..f138b84 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptor.cs
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptor.cs
@@ -3,7 +3,7 @@
using System.Net;
using System.Reflection;
using System.Threading;
-using HttpWebRequestWrapper.IO;
+using HttpWebRequestWrapper.Recording;
namespace HttpWebRequestWrapper
{
@@ -35,7 +35,27 @@ public HttpWebRequestWrapperInterceptor(Uri uri, Func
+ ///
+ /// This override is very important. It greatly
+ /// speeds up execution during interception when an async
+ /// caller (ie HttpClient) wants to GetRequestStream.
+ ///
+ /// Enabling this override was also found to be the solution for
+ /// https://github.com/ppittle/HttpWebRequestWrapper/issues/21
+ /// where the 3rd HttpClient.PostAsync call would stall here.
+ ///
+ public override IAsyncResult BeginGetRequestStream(AsyncCallback callback, object state)
+ {
+ var asyncResult = new DummyAsyncResult(new ManualResetEvent(true), state);
+
+ callback?.Invoke(asyncResult);
+
+ return asyncResult;
+ }
+
///
public override Stream GetRequestStream()
{
@@ -54,9 +74,12 @@ public override WebResponse GetResponse()
HttpWebResponse passThroughShadowCopy = null;
var interceptedRequest = new InterceptedRequest
{
- RequestPayload = _requestStream.ReadToEnd(),
+ RequestPayload =
+ new RecordedStream(
+ _requestStream.ToArray(),
+ this),
HttpWebRequest = this,
- HttpWebResponseCreator = new HttpWebResponseInterceptorCreator(RequestUri, Method),
+ HttpWebResponseCreator = new HttpWebResponseInterceptorCreator(RequestUri, Method, AutomaticDecompression),
PassThroughResponse = () =>
{
// if we are going to pass through - we need to use the base.GetRequest
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptorCreator.cs b/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptorCreator.cs
index 53a2c3f..e79f98a 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptorCreator.cs
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapperInterceptorCreator.cs
@@ -1,5 +1,6 @@
using System;
using System.Net;
+using HttpWebRequestWrapper.Recording;
namespace HttpWebRequestWrapper
{
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorder.cs b/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorder.cs
index 2721f69..25c695e 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorder.cs
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorder.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Net;
using HttpWebRequestWrapper.IO;
+using HttpWebRequestWrapper.Recording;
// Justification: Improves readability
// ReSharper disable ConvertIfStatementToNullCoalescingExpression
@@ -96,7 +97,12 @@ private HttpWebResponse RecordRequestAndResponse(Func getRespon
Method = Method,
RequestCookieContainer = CookieContainer,
RequestHeaders = Headers,
- RequestPayload = _shadowCopyRequestStream.ReadToEnd()
+ RequestPayload =
+ null == _shadowCopyRequestStream
+ ? new RecordedStream()
+ : new RecordedStream(
+ _shadowCopyRequestStream.ShadowCopy.ToArray(),
+ this)
};
RecordedRequests.Add(recordedRequest);
@@ -157,11 +163,10 @@ private void RecordResponse(HttpWebResponse response, RecordedRequest recordedRe
// seek to beginning so we can read the memory stream
memoryStream.Seek(0, SeekOrigin.Begin);
- using (var sr = new StreamReader(memoryStream))
- recordedRequest.ResponseBody = sr.ReadToEnd();
-
- // reset the stream - stream reader closes the first one
- memoryStream = new MemoryStream(memoryStream.ToArray());
+ recordedRequest.ResponseBody =
+ new RecordedStream(
+ memoryStream.ToArray(),
+ response);
// replace the default stream in response with the copy
ReflectionExtensions.SetField(response, "m_ConnectStream", memoryStream);
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorderCreator.cs b/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorderCreator.cs
index b3aeb33..1b71883 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorderCreator.cs
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapperRecorderCreator.cs
@@ -1,5 +1,6 @@
using System;
using System.Net;
+using HttpWebRequestWrapper.Recording;
// Justification: Public Api
// ReSharper disable MemberCanBePrivate.Global
diff --git a/src/HttpWebRequestWrapper/HttpWebRequestWrapperSession.cs b/src/HttpWebRequestWrapper/HttpWebRequestWrapperSession.cs
index dd3fc05..ac5d9b3 100644
--- a/src/HttpWebRequestWrapper/HttpWebRequestWrapperSession.cs
+++ b/src/HttpWebRequestWrapper/HttpWebRequestWrapperSession.cs
@@ -65,7 +65,7 @@ public virtual void Dispose()
#region WebRequest Prefix helpers
- private static PropertyInfo WebRequestPrefixListProperty =
+ private static readonly PropertyInfo _webRequestPrefixListProperty =
typeof(WebRequest)
.GetProperty(
"PrefixList",
@@ -73,13 +73,13 @@ public virtual void Dispose()
private static ArrayList GetWebRequestPrefixList()
{
- var prefixList = (ArrayList)WebRequestPrefixListProperty.GetValue(null, new object[0]);
+ var prefixList = (ArrayList)_webRequestPrefixListProperty.GetValue(null, new object[0]);
return (ArrayList) prefixList.Clone();
}
private static void SetWebRequestPrefixList(ArrayList prefixList)
{
- WebRequestPrefixListProperty.SetValue(null, prefixList, new object[0]);
+ _webRequestPrefixListProperty.SetValue(null, prefixList, new object[0]);
}
#endregion
diff --git a/src/HttpWebRequestWrapper/HttpWebResponseCreator.cs b/src/HttpWebRequestWrapper/HttpWebResponseCreator.cs
index ae7b6b7..abe1c41 100644
--- a/src/HttpWebRequestWrapper/HttpWebResponseCreator.cs
+++ b/src/HttpWebRequestWrapper/HttpWebResponseCreator.cs
@@ -19,8 +19,8 @@ namespace HttpWebRequestWrapper
{
///
/// Helper on top of that
- /// pre-populates and when
- /// building .
+ /// pre-populates ,
+ /// and when building .
///
/// Use these methods to build a real functioning
/// without having to deal with the reflection head-aches of doing it manually.
@@ -29,6 +29,7 @@ public class HttpWebResponseInterceptorCreator
{
private readonly Uri _responseUri;
private readonly string _method;
+ private readonly DecompressionMethods _automaticDecompression;
///
/// Creates a new
@@ -38,10 +39,12 @@ public class HttpWebResponseInterceptorCreator
///
public HttpWebResponseInterceptorCreator(
Uri responseUri,
- string method)
+ string method,
+ DecompressionMethods automaticDecompression)
{
_responseUri = responseUri;
_method = method;
+ _automaticDecompression = automaticDecompression;
}
///
@@ -72,7 +75,8 @@ public HttpWebResponse Create(
_method,
statusCode,
responseBody,
- responseHeaders);
+ responseHeaders,
+ _automaticDecompression);
}
///
@@ -108,7 +112,7 @@ public HttpWebResponse Create(
Stream responseStream,
HttpStatusCode statusCode = HttpStatusCode.OK,
WebHeaderCollection responseHeaders = null,
- DecompressionMethods decompressionMethod = DecompressionMethods.None,
+ DecompressionMethods? decompressionMethod = null,
long? contentLength = null)
{
return HttpWebResponseCreator.Create(
@@ -117,7 +121,7 @@ public HttpWebResponse Create(
statusCode,
responseStream,
responseHeaders ?? new WebHeaderCollection(),
- decompressionMethod,
+ decompressionMethod ?? _automaticDecompression,
contentLength: contentLength);
}
@@ -181,7 +185,7 @@ public HttpWebResponse Create(
HttpStatusCode statusCode,
Stream responseStream,
WebHeaderCollection responseHeaders,
- DecompressionMethods decompressionMethod = DecompressionMethods.None,
+ DecompressionMethods? decompressionMethod = null,
string mediaType = null,
long? contentLength = null,
string statusDescription = null,
@@ -196,7 +200,7 @@ public HttpWebResponse Create(
statusCode,
responseStream,
responseHeaders,
- decompressionMethod,
+ decompressionMethod ?? _automaticDecompression,
mediaType,
contentLength,
statusDescription,
@@ -237,12 +241,18 @@ public static class HttpWebResponseCreator
///
/// Use this to also set Cookies via
///
+ ///
+ /// OPTIONAL: Controls if will decompress
+ /// in its constructor.
+ /// Default is
+ ///
public static HttpWebResponse Create(
Uri responseUri,
string method,
HttpStatusCode statusCode,
string responseBody,
- WebHeaderCollection responseHeaders = null)
+ WebHeaderCollection responseHeaders = null,
+ DecompressionMethods decompressionMethod = DecompressionMethods.None)
{
// allow responseBody to be null - but change to empty string
responseBody = responseBody ?? string.Empty;
@@ -255,7 +265,8 @@ public static HttpWebResponse Create(
method,
statusCode,
responseStream,
- responseHeaders);
+ responseHeaders,
+ decompressionMethod);
}
///
diff --git a/src/HttpWebRequestWrapper/IO/MemoryStreamExtensions.cs b/src/HttpWebRequestWrapper/IO/MemoryStreamExtensions.cs
deleted file mode 100644
index ec89d1f..0000000
--- a/src/HttpWebRequestWrapper/IO/MemoryStreamExtensions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.IO;
-
-namespace HttpWebRequestWrapper.IO
-{
- internal static class MemoryStreamExtensions
- {
- public static string ReadToEnd(this MemoryStream stream)
- {
- if (null == stream)
- return string.Empty;
-
- // read even if stream is closed
- var copy = new MemoryStream(stream.ToArray());
-
- using (var sr = new StreamReader(copy))
- return sr.ReadToEnd();
- }
- }
-}
diff --git a/src/HttpWebRequestWrapper/IO/ShadowCopyStream.cs b/src/HttpWebRequestWrapper/IO/ShadowCopyStream.cs
index 3ac34c5..00603ba 100644
--- a/src/HttpWebRequestWrapper/IO/ShadowCopyStream.cs
+++ b/src/HttpWebRequestWrapper/IO/ShadowCopyStream.cs
@@ -1,7 +1,5 @@
-using System;
-using System.IO;
+using System.IO;
using System.Net;
-using System.Text;
namespace HttpWebRequestWrapper.IO
{
@@ -72,32 +70,4 @@ public override long Position
set => _primaryStream.Position = value;
}
}
-
- ///
- /// Add-ons for
- ///
- internal static class ShadowCopySteamExtensions
- {
- internal static string ReadToEnd(this ShadowCopyStream shadowCopyStream)
- {
- if (null == shadowCopyStream)
- return string.Empty;
-
- if (shadowCopyStream.ShadowCopy.Length == 0)
- return string.Empty;
-
- try
- {
- shadowCopyStream.ShadowCopy.Seek(0, SeekOrigin.Begin);
-
- using (var sr = new StreamReader(shadowCopyStream.ShadowCopy, Encoding.UTF8))
- return sr.ReadToEnd();
- }
- catch (Exception e)
- {
- // suppress exception, but update history
- return $"ERROR: {e.Message}\r\n{e.StackTrace}";
- }
- }
- }
}
\ No newline at end of file
diff --git a/src/HttpWebRequestWrapper/InterceptedRequest.cs b/src/HttpWebRequestWrapper/InterceptedRequest.cs
index db360d4..6094b81 100644
--- a/src/HttpWebRequestWrapper/InterceptedRequest.cs
+++ b/src/HttpWebRequestWrapper/InterceptedRequest.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Net;
+using HttpWebRequestWrapper.Recording;
namespace HttpWebRequestWrapper
{
@@ -22,7 +23,7 @@ public class InterceptedRequest
/// Don't try and read this from , the request stream
/// has probably already been closed.
///
- public string RequestPayload { get; set; }
+ public RecordedStream RequestPayload { get; set; }
///
/// The that has been intercepted.
/// Use this to read , etc
diff --git a/src/HttpWebRequestWrapper/RecordedRequest.cs b/src/HttpWebRequestWrapper/RecordedRequest.cs
deleted file mode 100644
index 967c49f..0000000
--- a/src/HttpWebRequestWrapper/RecordedRequest.cs
+++ /dev/null
@@ -1,207 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Net;
-using HttpWebRequestWrapper.Extensions;
-
-namespace HttpWebRequestWrapper
-{
- ///
- /// Request / Response data recorded by . Can be played back
- /// using and .
- ///
- /// Supports serialization to JSON! Perfect for saving as an embedded resource in your test projects!
- ///
- /// See for more information.
- ///
- [DebuggerDisplay("{Method} {Url}")]
- public class RecordedRequest
- {
- ///
- /// Recorded
- ///
- public string Method { get;set; }
- ///
- /// Recorded
- ///
- public string Url { get; set; }
- ///
- /// Recorded
- ///
- /// This is mostly exposed for convenience. This data will also
- /// be contained in .
- ///
- public CookieContainer RequestCookieContainer { get; set; }
- ///
- /// Recorded
- ///
- /// NOTE: From MS Documentation:
- /// https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.headers%28v=vs.110%29.aspx
- /// You should not assume that the header values will remain unchanged,
- /// because Web servers and caches may change or add headers to a Web request.
- ///
- /// are recorded *before*
- /// is called, so this might not be the same as
- /// after calling
- ///
- public RecordedHeaders RequestHeaders { get; set; } = new RecordedHeaders();
- ///
- /// Recorded
- ///
- public string RequestPayload { get; set; }
- ///
- /// Recorded
- ///
- public string ResponseBody { get; set; }
- ///
- /// Recorded
- ///
- public RecordedHeaders ResponseHeaders { get; set; } = new RecordedHeaders();
- ///
- /// Recorded
- ///
- public HttpStatusCode ResponseStatusCode { get; set; }
- ///
- /// Recorded information captured
- /// during .
- ///
- /// If no exception was thrown, this will be null.
- ///
- /// Use
- /// to convert this to a strongly typed exception instance.
- ///
- public RecordedResponseException ResponseException { get; set; }
- }
-
- ///
- /// Helper class for dealing with -
- /// primarily here to support json serialization as
- /// objects don't serialize correctly.
- ///
- /// Supports two implicit conversions to/from .
- ///
- /// Also has some equality methods that were useful when unit testing the
- /// library.
- ///
- public class RecordedHeaders : Dictionary,
- IEquatable,
- IEquatable
- {
- ///
- /// Implicit conversion from a to a
- /// .
- ///
- public static implicit operator WebHeaderCollection(RecordedHeaders headers)
- {
- if (null == headers)
- return null;
-
- var webHeaders = new WebHeaderCollection();
-
- foreach (var kvp in headers)
- foreach(var value in kvp.Value)
- {
- webHeaders.Add(kvp.Key, value);
- }
-
- return webHeaders;
- }
-
- ///
- /// Implicit conversion from a to a
- /// .
- ///
- public static implicit operator RecordedHeaders(WebHeaderCollection webHeader)
- {
- if (null == webHeader)
- return null;
-
- var recordedHeaders = new RecordedHeaders();
-
- foreach (var key in webHeader.AllKeys)
- {
- var values = webHeader.GetValues(key);
-
- recordedHeaders.Add(key, values ?? new string[0]);
- }
-
- return recordedHeaders;
- }
-
- ///
- /// Performs an equality comparison with an external
- /// .
- ///
- /// Don't care about ordering, just make sure both dictionaries
- /// contain every key, and they have the same array of strings for every
- /// key. All string comparisons are case sensitive.
- ///
- public bool Equals(RecordedHeaders other)
- {
- if (null == other)
- return false;
-
- // make sure we have the same number of keys
- // and every key in this dictionary exists in
- // other and the other dictionary has the same string[]
- // associated with key. string comparisons are default (case-sensitive)
- // but order doesn't matter.
- return
- Count == other.Count &&
- this.All(kvp =>
- other.Any(otherKvp =>
- string.Equals(kvp.Key, otherKvp.Key) &&
- kvp.Value.Length == otherKvp.Value.Length &&
- kvp.Value.All(v => otherKvp.Value.Contains(v))
- ));
- }
-
- ///
- /// Performs an equality comparison with an external
- /// by casting
- /// to a and then using
- ///
- ///
- public bool Equals(WebHeaderCollection other)
- {
- return Equals((RecordedHeaders) other);
- }
- }
-
- ///
- /// A specialized container for collection s
- /// recorded during a .
- ///
- /// This collection is optimized for serialization, as unfortunately
- /// objects don't reliably support xml serialization.
- ///
- /// NOTE: Currently this object only supports capturing
- /// for all exceptions and for .
- /// All other exception properties will be discarded.
- ///
- /// See
- /// for information on how this object is consumer and converted back into
- /// an exception.
- ///
- [DebuggerDisplay("{Type.Name}: {Message}")]
- public class RecordedResponseException
- {
- ///
- ///
- ///
- public string Message { get; set; }
- ///
- /// . This is captured
- /// so the correctly typed exception can be built from
- /// this .
- ///
- public Type Type { get; set; }
- ///
- /// .
- /// This will be null if is not
- ///
- ///
- public WebExceptionStatus? WebExceptionStatus { get; set; }
- }
-}
diff --git a/src/HttpWebRequestWrapper/Recording/RecordedHeaders.cs b/src/HttpWebRequestWrapper/Recording/RecordedHeaders.cs
new file mode 100644
index 0000000..8aca6ad
--- /dev/null
+++ b/src/HttpWebRequestWrapper/Recording/RecordedHeaders.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+
+namespace HttpWebRequestWrapper.Recording
+{
+ ///
+ /// Helper class for dealing with -
+ /// primarily here to support json serialization as
+ /// objects don't serialize correctly.
+ ///
+ /// Supports two implicit conversions to/from .
+ ///
+ /// Also has some equality methods that were useful when unit testing the
+ /// library.
+ ///
+ public class RecordedHeaders : Dictionary,
+ IEquatable,
+ IEquatable
+ {
+ ///
+ /// Implicit conversion from a to a
+ /// .
+ ///
+ public static implicit operator WebHeaderCollection(RecordedHeaders headers)
+ {
+ if (null == headers)
+ return null;
+
+ var webHeaders = new WebHeaderCollection();
+
+ foreach (var kvp in headers)
+ foreach(var value in kvp.Value)
+ {
+ webHeaders.Add(kvp.Key, value);
+ }
+
+ return webHeaders;
+ }
+
+ ///
+ /// Implicit conversion from a to a
+ /// .
+ ///
+ public static implicit operator RecordedHeaders(WebHeaderCollection webHeader)
+ {
+ if (null == webHeader)
+ return null;
+
+ var recordedHeaders = new RecordedHeaders();
+
+ foreach (var key in webHeader.AllKeys)
+ {
+ var values = webHeader.GetValues(key);
+
+ recordedHeaders.Add(key, values ?? new string[0]);
+ }
+
+ return recordedHeaders;
+ }
+
+ ///
+ /// Performs an equality comparison with an external
+ /// .
+ ///
+ /// Don't care about ordering, just make sure both dictionaries
+ /// contain every key, and they have the same array of strings for every
+ /// key. All string comparisons are case sensitive.
+ ///
+ public bool Equals(RecordedHeaders other)
+ {
+ if (null == other)
+ return false;
+
+ // make sure we have the same number of keys
+ // and every key in this dictionary exists in
+ // other and the other dictionary has the same string[]
+ // associated with key. string comparisons are default (case-sensitive)
+ // but order doesn't matter.
+ return
+ Count == other.Count &&
+ this.All(kvp =>
+ other.Any(otherKvp =>
+ string.Equals(kvp.Key, otherKvp.Key) &&
+ kvp.Value.Length == otherKvp.Value.Length &&
+ kvp.Value.All(v => otherKvp.Value.Contains(v))
+ ));
+ }
+
+ ///
+ /// Performs an equality comparison with an external
+ /// by casting
+ /// to a and then using
+ ///
+ ///
+ public bool Equals(WebHeaderCollection other)
+ {
+ return Equals((RecordedHeaders) other);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HttpWebRequestWrapper/Recording/RecordedRequest.cs b/src/HttpWebRequestWrapper/Recording/RecordedRequest.cs
new file mode 100644
index 0000000..26b407b
--- /dev/null
+++ b/src/HttpWebRequestWrapper/Recording/RecordedRequest.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Diagnostics;
+using System.Net;
+using HttpWebRequestWrapper.Extensions;
+
+// Justification: Can't use nameof in attriutes (ie DebuggerDisplay)
+// ReSharper disable UseNameofExpression
+
+namespace HttpWebRequestWrapper.Recording
+{
+ ///
+ /// Request / Response data recorded by . Can be played back
+ /// using and .
+ ///
+ /// Supports serialization to JSON! Perfect for saving as an embedded resource in your test projects!
+ ///
+ /// See for more information.
+ ///
+ [DebuggerDisplay("{Method} {Url}")]
+ public class RecordedRequest
+ {
+ ///
+ /// Recorded
+ ///
+ public string Method { get;set; }
+ ///
+ /// Recorded
+ ///
+ public string Url { get; set; }
+ ///
+ /// Recorded
+ ///
+ /// This is mostly exposed for convenience. This data will also
+ /// be contained in .
+ ///
+ public CookieContainer RequestCookieContainer { get; set; }
+ ///
+ /// Recorded
+ ///
+ /// NOTE: From MS Documentation:
+ /// https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.headers%28v=vs.110%29.aspx
+ /// You should not assume that the header values will remain unchanged,
+ /// because Web servers and caches may change or add headers to a Web request.
+ ///
+ /// are recorded *before*
+ /// is called, so this might not be the same as
+ /// after calling
+ ///
+ public RecordedHeaders RequestHeaders { get; set; } = new RecordedHeaders();
+ ///
+ /// Recorded
+ ///
+ public RecordedStream RequestPayload { get; set; } = new RecordedStream();
+ ///
+ /// Recorded
+ ///
+ public RecordedStream ResponseBody { get; set; } = new RecordedStream();
+ ///
+ /// Recorded
+ ///
+ public RecordedHeaders ResponseHeaders { get; set; } = new RecordedHeaders();
+ ///
+ /// Recorded
+ ///
+ public HttpStatusCode ResponseStatusCode { get; set; }
+ ///
+ /// Recorded information captured
+ /// during .
+ ///
+ /// If no exception was thrown, this will be null.
+ ///
+ /// Use
+ /// to convert this to a strongly typed exception instance.
+ ///
+ public RecordedResponseException ResponseException { get; set; }
+ }
+}
diff --git a/src/HttpWebRequestWrapper/Recording/RecordedResponseException.cs b/src/HttpWebRequestWrapper/Recording/RecordedResponseException.cs
new file mode 100644
index 0000000..8380fa8
--- /dev/null
+++ b/src/HttpWebRequestWrapper/Recording/RecordedResponseException.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Diagnostics;
+using System.Net;
+using HttpWebRequestWrapper.Extensions;
+
+namespace HttpWebRequestWrapper.Recording
+{
+ ///
+ /// A specialized container for collection s
+ /// recorded during a .
+ ///
+ /// This collection is optimized for serialization, as unfortunately
+ /// objects don't reliably support xml serialization.
+ ///
+ /// NOTE: Currently this object only supports capturing
+ /// for all exceptions and for .
+ /// All other exception properties will be discarded.
+ ///
+ /// See
+ /// for information on how this object is consumer and converted back into
+ /// an exception.
+ ///
+ [DebuggerDisplay("{Type.Name}: {Message}")]
+ public class RecordedResponseException
+ {
+ ///
+ ///
+ ///
+ public string Message { get; set; }
+ ///
+ /// . This is captured
+ /// so the correctly typed exception can be built from
+ /// this .
+ ///
+ public Type Type { get; set; }
+ ///
+ /// .
+ /// This will be null if is not
+ ///
+ ///
+ public WebExceptionStatus? WebExceptionStatus { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/HttpWebRequestWrapper/Recording/RecordedStream.cs b/src/HttpWebRequestWrapper/Recording/RecordedStream.cs
new file mode 100644
index 0000000..efe7e68
--- /dev/null
+++ b/src/HttpWebRequestWrapper/Recording/RecordedStream.cs
@@ -0,0 +1,313 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Text;
+using HttpWebRequestWrapper.Extensions;
+
+// Justification: Can't use nameof in attriutes (ie DebuggerDisplay)
+// ReSharper disable UseNameofExpression
+
+// Justificaton: Public Api
+// ReSharper disable MemberCanBePrivate.Global
+
+// Justification: Prefer instance methods
+// ReSharper disable MemberCanBeMadeStatic.Local
+
+namespace HttpWebRequestWrapper.Recording
+{
+ ///
+ /// Specialized container for recording
+ /// and . Examines content type/encoding
+ /// and if content reports to be text, stream is stored plain-text, otherwise, content
+ /// is stored base64. This way binary request/responses can be recorded and serialized.
+ ///
+ /// The effort is made to store plain text content as plain-text, as opposed to
+ /// storing everything base64, so that when this class is serialized, it's easier
+ /// to read / modify recorded content.
+ ///
+ [DebuggerDisplay("{SerializedStream}")]
+ public class RecordedStream : IEquatable
+ {
+ ///
+ /// Serialized stream. If is true,
+ /// this is stored as a Base64 string, otherwise
+ /// stored plain text.
+ ///
+ /// If you want to get the string content of this
+ /// it's
+ /// recommended to use rather than
+ /// using directly.
+ ///
+ public string SerializedStream { get; set; }
+
+ ///
+ /// Indicates if is encoded.
+ ///
+ public bool IsEncoded { get; set; }
+
+ ///
+ /// Indicates should be GZip
+ /// compressed when is called.
+ ///
+ public bool IsGzippedCompressed { get; set; }
+
+ ///
+ /// Indicates should
+ /// be compressed with the Deflate aglorithm when
+ /// is called.
+ ///
+ public bool IsDefalteCompressed { get; set; }
+
+ ///
+ /// Creates an empty .
+ /// is intiailized to
+ ///
+ public RecordedStream()
+ {
+ SerializedStream = string.Empty;
+ IsEncoded = false;
+ }
+
+ ///
+ /// Creates a new around
+ /// .
+ ///
+ /// If 's
+ /// is empty or can be inferred to represent plain text then
+ /// is stored in
+ /// via .
+ /// Otherwise, is stored as base64 string.
+ ///
+ public RecordedStream(
+ byte[] streamBytes,
+ HttpWebRequest request)
+ {
+ if (streamBytes.Length == 0)
+ {
+ SerializedStream = string.Empty;
+ return;
+ }
+
+ streamBytes = TryAndUnzipStream(streamBytes);
+
+ if (ContentTypeIsForPlainText(request.ContentType))
+ {
+ SerializedStream = Encoding.UTF8.GetString(streamBytes);
+ }
+ else
+ {
+ SerializedStream = Convert.ToBase64String(streamBytes);
+ IsEncoded = true;
+ }
+ }
+
+ ///
+ /// Creates a new around
+ /// .
+ ///
+ /// If 's
+ /// is empty or can be inferred to represent plain text OR
+ /// is "utf-8"
+ /// is stored in
+ /// via .
+ /// Otherwise, is stored as base64 string.
+ ///
+ public RecordedStream(
+ byte[] streamBytes,
+ HttpWebResponse response)
+ {
+ if (streamBytes.Length == 0)
+ {
+ SerializedStream = string.Empty;
+ return;
+ }
+
+ if (response.ContentEncoding.ToLower().Contains("gzip"))
+ streamBytes = TryAndUnzipStream(streamBytes);
+
+ if (response.ContentEncoding.ToLower().Contains("deflate"))
+ streamBytes = TryAndDeflateStream(streamBytes);
+
+ if (
+ response.CharacterSet?.ToLower() == "utf-8" ||
+ ContentTypeIsForPlainText(response.ContentType))
+ {
+ SerializedStream = Encoding.UTF8.GetString(streamBytes);
+ }
+ else
+ {
+ SerializedStream = Convert.ToBase64String(streamBytes);
+ IsEncoded = true;
+ }
+ }
+
+ private byte[] TryAndUnzipStream(byte[] streamBytes)
+ {
+ // check if streamBytes starts with gzip header
+ // https://stackoverflow.com/questions/4662821/is-there-a-way-to-know-if-the-byte-has-been-compressed-by-gzipstream
+
+ if (streamBytes.Length < 3)
+ return streamBytes;
+
+ var gzipHeader = new byte[] {0x1f, 0x8b, 8};
+
+ if (!streamBytes.Take(3).SequenceEqual(gzipHeader))
+ return streamBytes;
+
+ // at this point streamBytes is probably compressed, only way to know for sure
+ // is to try and decompress it
+ try
+ {
+ using (var compressedStream = new MemoryStream(streamBytes))
+ using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
+ using (var decompressed = new MemoryStream())
+ {
+ zipStream.CopyTo(decompressed);
+
+ IsGzippedCompressed = true;
+ return decompressed.ToArray();
+ }
+ }
+ catch
+ {
+ return streamBytes;
+ }
+ }
+
+ private byte[] TryAndDeflateStream(byte[] streamBytes)
+ {
+ if (streamBytes.Length == 0)
+ return streamBytes;
+
+ // don't know of a way to pre-emptively guess if stream is compressed with deflate
+ // have to try to deflate in a try/catch
+ try
+ {
+ using (var compressedStream = new MemoryStream(streamBytes))
+ using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
+ using (var decompressed = new MemoryStream())
+ {
+ deflateStream.CopyTo(decompressed);
+
+ IsDefalteCompressed = true;
+ return decompressed.ToArray();
+ }
+ }
+ catch
+ {
+ return streamBytes;
+ }
+ }
+
+ private bool ContentTypeIsForPlainText(string contentType)
+ {
+ return
+ // assume if contenttype are empty that
+ // we do **not** need to encode streamBytes
+ string.IsNullOrEmpty(contentType) ||
+ contentType.ToLower().Contains("text") ||
+ contentType.ToLower().Contains("xml") ||
+ contentType.ToLower().Contains("json") ||
+ contentType.ToLower().Contains("application/x-www-form-urlencoded");
+ }
+
+ ///
+ /// Builds a new correclty
+ /// populated with the content of
+ ///
+ public Stream ToStream()
+ {
+ var baseStream = new MemoryStream(
+ IsEncoded
+ ? Convert.FromBase64String(SerializedStream ?? "")
+ : Encoding.UTF8.GetBytes(SerializedStream));
+
+ if (IsGzippedCompressed)
+ {
+ var compressed = new MemoryStream();
+
+ using (var zip = new GZipStream(compressed, CompressionMode.Compress, leaveOpen: true))
+ baseStream.CopyTo(zip);
+
+ compressed.Seek(0, SeekOrigin.Begin);
+ return compressed;
+ }
+ else if (IsDefalteCompressed)
+ {
+ var compressed = new MemoryStream();
+
+ using (var deflate = new DeflateStream(compressed, CompressionMode.Compress, leaveOpen: true))
+ baseStream.CopyTo(deflate);
+
+ compressed.Seek(0, SeekOrigin.Begin);
+ return compressed;
+ }
+ else
+ {
+ return baseStream;
+ }
+ }
+
+ ///
+ /// Returns the Stream content as to as close as a useable
+ /// string as possible.
+ ///
+ /// Returns un-encoded and
+ /// un-compressed. If represents
+ /// binanry conent, this will not be useful. However, if for some
+ /// reason is string content but has
+ /// been marked , this will return a usable string.
+ ///
+ /// This is the perferred way of getting the Stream as a string. It
+ /// is unadvisable to inspect directly.
+ ///
+ ///
+ public override string ToString()
+ {
+ if (string.IsNullOrEmpty(SerializedStream))
+ return string.Empty;
+
+ var baseStream = new MemoryStream(
+ IsEncoded
+ ? Convert.FromBase64String(SerializedStream ?? "")
+ : Encoding.UTF8.GetBytes(SerializedStream));
+
+ using (var sr = new StreamReader(baseStream))
+ return sr.ReadToEnd();
+ }
+
+ ///
+ /// Builds a new from ,
+ /// storing as plain text in .
+ ///
+ /// This makes it very easy to assign string text directly to .
+ ///
+ public static implicit operator RecordedStream(string textResponse)
+ {
+ return new RecordedStream
+ {
+ SerializedStream = textResponse,
+ IsEncoded = false
+ };
+ }
+
+ ///
+ /// Determines equality betweeen and this
+ /// . This allows comparing s
+ /// easier for things like
+ /// as well as tests.
+ ///
+ public bool Equals(RecordedStream other)
+ {
+ if (null == other)
+ return string.IsNullOrEmpty(SerializedStream);
+
+ return
+ IsEncoded == other.IsEncoded &&
+ SerializedStream == other.SerializedStream;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HttpWebRequestWrapper/RecordingSession.cs b/src/HttpWebRequestWrapper/Recording/RecordingSession.cs
similarity index 96%
rename from src/HttpWebRequestWrapper/RecordingSession.cs
rename to src/HttpWebRequestWrapper/Recording/RecordingSession.cs
index f7b184a..284724f 100644
--- a/src/HttpWebRequestWrapper/RecordingSession.cs
+++ b/src/HttpWebRequestWrapper/Recording/RecordingSession.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
-namespace HttpWebRequestWrapper
+namespace HttpWebRequestWrapper.Recording
{
///
/// Collection of s.
diff --git a/src/HttpWebRequestWrapper/RecordingSessionInterceptorRequestBuilder.cs b/src/HttpWebRequestWrapper/RecordingSessionInterceptorRequestBuilder.cs
index 255f8a7..dce4b4e 100644
--- a/src/HttpWebRequestWrapper/RecordingSessionInterceptorRequestBuilder.cs
+++ b/src/HttpWebRequestWrapper/RecordingSessionInterceptorRequestBuilder.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Net;
using HttpWebRequestWrapper.Extensions;
+using HttpWebRequestWrapper.Recording;
// Justification: Public Api
// ReSharper disable MemberCanBePrivate.Global
@@ -215,10 +216,7 @@ private bool DefaultMatchingAlgorithm(
StringComparison.InvariantCultureIgnoreCase);
var requestPayloadMatches =
- string.Equals(
- interceptedRequest.RequestPayload ?? "",
- recordedRequest.RequestPayload ?? "",
- StringComparison.InvariantCultureIgnoreCase);
+ true == interceptedRequest?.RequestPayload.Equals(recordedRequest.RequestPayload);
var requestHeadersMatch =
recordedRequest.RequestHeaders.Equals(interceptedRequest.HttpWebRequest.Headers);
@@ -244,7 +242,7 @@ private HttpWebResponse DefaultRecordedResultResponseBuilder(
throw recordedException;
return interceptedRequest.HttpWebResponseCreator.Create(
- recordedRequest.ResponseBody,
+ recordedRequest.ResponseBody.ToStream(),
recordedRequest.ResponseStatusCode,
headers);
}