diff --git a/src/Stripe.net/Entities/EphemeralKeys/EphemeralKey.cs b/src/Stripe.net/Entities/EphemeralKeys/EphemeralKey.cs index b41d76bfe0..a798052311 100644 --- a/src/Stripe.net/Entities/EphemeralKeys/EphemeralKey.cs +++ b/src/Stripe.net/Entities/EphemeralKeys/EphemeralKey.cs @@ -22,7 +22,7 @@ public class EphemeralKey : StripeEntity, IHasId, IHasObject // that they'll be able to decode an object that's current according to their version. public string RawJson { - get { return this.StripeResponse?.ResponseJson; } + get { return this.StripeResponse?.Content; } } [JsonProperty("created")] diff --git a/src/Stripe.net/Infrastructure/Public/HttpClient.cs b/src/Stripe.net/Infrastructure/Public/HttpClient.cs new file mode 100644 index 0000000000..5324690bef --- /dev/null +++ b/src/Stripe.net/Infrastructure/Public/HttpClient.cs @@ -0,0 +1,32 @@ +namespace Stripe +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Abstract base class for HTTP clients used to make requests to Stripe's API. + /// + public abstract class HttpClient + { + /// Gets or sets the last request made by this client. + /// The last request made by this client. + public StripeRequest LastRequest { get; protected set; } + + /// Gets or sets the duration of the last request. + /// The duration of the last request. + public TimeSpan? LastRequestDuration { get; protected set; } + + /// Gets or sets the last response received by this client. + /// The last response received by this client. + public StripeResponse LastResponse { get; protected set; } + + /// Sends a request to Stripe's API as an asynchronous operation. + /// The parameters of the request to send. + /// The cancellation token to cancel operation. + /// The task object representing the asynchronous operation. + public abstract Task MakeRequestAsync( + StripeRequest request, + CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Stripe.net/Infrastructure/Public/IStripeClient.cs b/src/Stripe.net/Infrastructure/Public/IStripeClient.cs new file mode 100644 index 0000000000..ed37e7848f --- /dev/null +++ b/src/Stripe.net/Infrastructure/Public/IStripeClient.cs @@ -0,0 +1,28 @@ +namespace Stripe +{ + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Interface for a Stripe client. + /// + public interface IStripeClient + { + /// Sends a request to Stripe's API as an asynchronous operation. + /// Type of the Stripe entity returned by the API. + /// The HTTP method. + /// The path of the request. + /// The parameters of the request. + /// The special modifiers of the request. + /// The cancellation token to cancel operation. + /// The task object representing the asynchronous operation. + Task RequestAsync( + HttpMethod method, + string path, + BaseOptions options, + RequestOptions requestOptions, + CancellationToken cancellationToken = default(CancellationToken)) + where T : IStripeEntity; + } +} diff --git a/src/Stripe.net/Infrastructure/Public/StripeClient.cs b/src/Stripe.net/Infrastructure/Public/StripeClient.cs new file mode 100644 index 0000000000..d2f2af3401 --- /dev/null +++ b/src/Stripe.net/Infrastructure/Public/StripeClient.cs @@ -0,0 +1,92 @@ +namespace Stripe +{ + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using Newtonsoft.Json.Linq; + + /// + /// A Stripe client, used to issue requests to Stripe's API and deserialize responses. + /// + public class StripeClient : IStripeClient + { + /// Initializes a new instance of the class. + /// + /// The client to use. If null, + /// an HTTP client will be created with default parameters. + /// + public StripeClient(Stripe.HttpClient httpClient = null) + { + this.HttpClient = httpClient ?? BuildDefaultHttpClient(); + } + + /// Gets the used to send HTTP requests. + /// The used to send HTTP requests. + public Stripe.HttpClient HttpClient { get; } + + /// Sends a request to Stripe's API as an asynchronous operation. + /// Type of the Stripe entity returned by the API. + /// The HTTP method. + /// The path of the request. + /// The parameters of the request. + /// The special modifiers of the request. + /// The cancellation token to cancel operation. + /// The task object representing the asynchronous operation. + public async Task RequestAsync( + HttpMethod method, + string path, + BaseOptions options, + RequestOptions requestOptions, + CancellationToken cancellationToken = default(CancellationToken)) + where T : IStripeEntity + { + var request = new StripeRequest(method, path, options, requestOptions); + + var response = await this.HttpClient.MakeRequestAsync(request); + + return ProcessResponse(response); + } + + private static Stripe.HttpClient BuildDefaultHttpClient() + { + return new SystemNetHttpClient(); + } + + private static T ProcessResponse(StripeResponse response) + where T : IStripeEntity + { + if (response.StatusCode != HttpStatusCode.OK) + { + throw BuildStripeException(response); + } + + var obj = StripeEntity.FromJson(response.Content); + obj.StripeResponse = response; + + return obj; + } + + private static StripeException BuildStripeException(StripeResponse response) + { + // If the value of the `error` key is a string, then the error is an OAuth error + // and we instantiate the StripeError object with the entire JSON. + // Otherwise, it's a regular API error and we instantiate the StripeError object + // with just the nested hash contained in the `error` key. + var errorToken = JObject.Parse(response.Content)["error"]; + var stripeError = errorToken.Type == JTokenType.String + ? StripeError.FromJson(response.Content) + : StripeError.FromJson(errorToken.ToString()); + + stripeError.StripeResponse = response; + + return new StripeException( + response.StatusCode, + stripeError, + stripeError.Message) + { + StripeResponse = response, + }; + } + } +} diff --git a/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs b/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs index 9397a4c20d..ef73c6f062 100644 --- a/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs +++ b/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs @@ -2,7 +2,6 @@ namespace Stripe { using System; using System.Collections.Generic; - using System.Net.Http; using System.Reflection; using Newtonsoft.Json; using Stripe.Infrastructure; @@ -14,9 +13,11 @@ public static class StripeConfiguration { private static string apiKey; + private static IStripeClient stripeClient; + static StripeConfiguration() { - StripeNetVersion = new AssemblyName(typeof(Requestor).GetTypeInfo().Assembly.FullName).Version.ToString(3); + StripeNetVersion = new AssemblyName(typeof(StripeConfiguration).GetTypeInfo().Assembly.FullName).Version.ToString(3); } /// API version used by Stripe.net. @@ -71,12 +72,6 @@ public static string ApiKey /// Gets or sets the base URL for Stripe's Files API. public static string FilesBase { get; set; } = DefaultFilesBase; - /// Gets or sets a custom . - public static HttpMessageHandler HttpMessageHandler { get; set; } - - /// Gets or sets the timespan to wait before the request times out. - public static TimeSpan HttpTimeout { get; set; } = DefaultHttpTimeout; - /// /// Gets or sets the settings used for deserializing JSON objects returned by Stripe's API. /// It is highly recommended you do not change these settings, as doing so can produce @@ -87,22 +82,40 @@ public static string ApiKey /// public static JsonSerializerSettings SerializerSettings { get; set; } = DefaultSerializerSettings(); - /// Gets the version of the Stripe.net client library. - public static string StripeNetVersion { get; } - /// - /// Gets or sets the timespan to wait before the request times out. - /// This property is deprecated and will be removed in a future version, please use the - /// property instead. + /// Gets or sets a custom for sending requests to Stripe's + /// API. You can use this to use a custom message handler, set proxy parameters, etc. /// - // TODO: remove this property in a future major version - [Obsolete("Use StripeConfiguration.HttpTimeout instead.")] - public static TimeSpan? HttpTimeSpan + /// + /// To use a custom message handler: + /// + /// System.Net.Http.HttpMessageHandler messageHandler = ...; + /// var httpClient = new System.Net.HttpClient(messageHandler); + /// var stripeClient = new Stripe.StripeClient(new Stripe.SystemNetHttpClient(httpClient)); + /// Stripe.StripeConfiguration.StripeClient = stripeClient; + /// + /// + public static IStripeClient StripeClient { - get { return HttpTimeout; } - set { HttpTimeout = value ?? DefaultHttpTimeout; } + get + { + if (stripeClient == null) + { + stripeClient = new StripeClient(); + } + + return stripeClient; + } + + set + { + stripeClient = value; + } } + /// Gets the version of the Stripe.net client library. + public static string StripeNetVersion { get; } + /// /// Returns a new instance of with /// the default settings used by Stripe.net. diff --git a/src/Stripe.net/Infrastructure/Request.cs b/src/Stripe.net/Infrastructure/Public/StripeRequest.cs similarity index 87% rename from src/Stripe.net/Infrastructure/Request.cs rename to src/Stripe.net/Infrastructure/Public/StripeRequest.cs index 0e3eff5e73..7565a82284 100644 --- a/src/Stripe.net/Infrastructure/Request.cs +++ b/src/Stripe.net/Infrastructure/Public/StripeRequest.cs @@ -1,24 +1,23 @@ -namespace Stripe.Infrastructure +namespace Stripe { using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; - using Stripe.Infrastructure.Extensions; using Stripe.Infrastructure.FormEncoding; /// /// Represents a request to Stripe's API. /// - public class Request + public class StripeRequest { - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// The HTTP method. /// The path of the request. /// The parameters of the request. /// The special modifiers of the request. - public Request( + public StripeRequest( HttpMethod method, string path, BaseOptions options, @@ -62,6 +61,17 @@ public Request( /// public HttpContent Content { get; } + /// Returns a string that represents the . + /// A string that represents the . + public override string ToString() + { + return string.Format( + "<{0} Method={1} Uri={2}>", + this.GetType().FullName, + this.Method, + this.Uri.ToString()); + } + private static Uri BuildUri( HttpMethod method, string path, diff --git a/src/Stripe.net/Infrastructure/Public/StripeResponse.cs b/src/Stripe.net/Infrastructure/Public/StripeResponse.cs index 77995c51b6..88f647ec44 100644 --- a/src/Stripe.net/Infrastructure/Public/StripeResponse.cs +++ b/src/Stripe.net/Infrastructure/Public/StripeResponse.cs @@ -1,13 +1,90 @@ namespace Stripe { using System; + using System.Linq; + using System.Net; + using System.Net.Http.Headers; + /// + /// Represents a response from Stripe's API. + /// public class StripeResponse { - public string ResponseJson { get; set; } + /// Initializes a new instance of the class. + /// The HTTP status code. + /// The HTTP headers of the response. + /// The body of the response. + public StripeResponse(HttpStatusCode statusCode, HttpResponseHeaders headers, string content) + { + this.StatusCode = statusCode; + this.Headers = headers; + this.Content = content; + } - public string RequestId { get; set; } + /// Gets the HTTP status code of the response. + /// The HTTP status code of the response. + public HttpStatusCode StatusCode { get; } - public DateTime RequestDate { get; set; } + /// Gets the HTTP headers of the response. + /// The HTTP headers of the response. + public HttpResponseHeaders Headers { get; } + + /// Gets the body of the response. + /// The body of the response. + public string Content { get; } + + /// Gets the date of the request, as returned by Stripe. + /// The date of the request, as returned by Stripe. + public DateTimeOffset? Date => this.Headers?.Date; + + /// Gets the idempotency key of the request, as returned by Stripe. + /// The idempotency key of the request, as returned by Stripe. + public string IdempotencyKey => MaybeGetHeader(this.Headers, "Idempotency-Key"); + + /// Gets the ID of the request, as returned by Stripe. + /// The ID of the request, as returned by Stripe. + public string RequestId => MaybeGetHeader(this.Headers, "Request-Id"); + + /// + /// Gets the body of the response. + /// This method is deprecated and will be removed in a future version, please use the + /// property getter instead. + /// + /// The body of the response. + // TODO: remove this in a future a major version + [Obsolete("Use Content instead")] + public string ResponseJson => this.Content; + + /// + /// Gets the date of the request, as returned by Stripe. + /// This method is deprecated and will be removed in a future version, please use the + /// property getter instead. + /// + /// The date of the request, as returned by Stripe. + // TODO: remove this in a future a major version + [Obsolete("Use Date instead")] + public DateTime RequestDate => this.Date?.DateTime ?? default(DateTime); + + /// Returns a string that represents the . + /// A string that represents the . + public override string ToString() + { + return string.Format( + "<{0} status={1} Request-Id={2} Date={3}>", + this.GetType().FullName, + (int)this.StatusCode, + this.RequestId, + this.Date?.ToString("s")); + } + + private static string MaybeGetHeader(HttpHeaders headers, string name) + { + if ((headers == null) || (!headers.Contains(name))) + { + return null; + } + + return headers.GetValues(name).First(); + } } } diff --git a/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs b/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs new file mode 100644 index 0000000000..c2527824b2 --- /dev/null +++ b/src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs @@ -0,0 +1,129 @@ +namespace Stripe +{ + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + /// + /// Standard client to make requests to Stripe's API, using + /// to send HTTP requests. + /// + public class SystemNetHttpClient : HttpClient + { + private static readonly string UserAgentString + = $"Stripe/v1 .NetBindings/{StripeConfiguration.StripeNetVersion}"; + + private static readonly string StripeClientUserAgentString + = BuildStripeClientUserAgentString(); + + private readonly System.Net.Http.HttpClient httpClient; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The client to use. If null, an HTTP + /// client will be created with default parameters. + /// + public SystemNetHttpClient(System.Net.Http.HttpClient httpClient = null) + { + this.httpClient = httpClient ?? BuildDefaultSystemNetHttpClient(); + } + + /// + /// Initializes a new instance of the class + /// with default parameters. + /// + /// The new instance of the class. + public static System.Net.Http.HttpClient BuildDefaultSystemNetHttpClient() + { + // We set the User-Agent and X-Stripe-Client-User-Agent headers in each request + // message rather than through the client's DefaultRequestHeaders because we + // want these headers to be present even when a custom HTTP client is used. + return new System.Net.Http.HttpClient() + { + Timeout = StripeConfiguration.DefaultHttpTimeout, + }; + } + + /// Sends a request to Stripe's API as an asynchronous operation. + /// The parameters of the request to send. + /// The cancellation token to cancel operation. + /// The task object representing the asynchronous operation. + public override async Task MakeRequestAsync( + StripeRequest request, + CancellationToken cancellationToken = default(CancellationToken)) + { + var httpRequest = BuildRequestMessage(request); + + this.LastRequest = request; + this.LastResponse = null; + + // TODO: telemetry + // TODO: request retries + var stopwatch = Stopwatch.StartNew(); + + var response = await this.httpClient.SendAsync(httpRequest, cancellationToken) + .ConfigureAwait(false); + stopwatch.Stop(); + + this.LastRequestDuration = stopwatch.Elapsed; + + var reader = new StreamReader( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + this.LastResponse = new StripeResponse( + response.StatusCode, + response.Headers, + await reader.ReadToEndAsync().ConfigureAwait(false)); + + return this.LastResponse; + } + + private static System.Net.Http.HttpRequestMessage BuildRequestMessage(StripeRequest request) + { + var requestMessage = new System.Net.Http.HttpRequestMessage(request.Method, request.Uri); + + // Standard headers + requestMessage.Headers.UserAgent.ParseAdd(UserAgentString); + requestMessage.Headers.Authorization = request.AuthorizationHeader; + + // Custom headers + requestMessage.Headers.Add("X-Stripe-Client-User-Agent", StripeClientUserAgentString); + foreach (var header in request.StripeHeaders) + { + requestMessage.Headers.Add(header.Key, header.Value); + } + + // Request body + requestMessage.Content = request.Content; + + return requestMessage; + } + + private static string BuildStripeClientUserAgentString() + { + var values = new Dictionary + { + { "bindings_version", StripeConfiguration.StripeNetVersion }, + { "lang", ".net" }, + { "publisher", "stripe" }, + { "lang_version", RuntimeInformation.GetLanguageVersion() }, + { "os_version", RuntimeInformation.GetOSVersion() }, + }; + +#if NET45 + string monoVersion = RuntimeInformation.GetMonoVersion(); + if (!string.IsNullOrEmpty(monoVersion)) + { + values.Add("mono_version", monoVersion); + } +#endif + + return JsonConvert.SerializeObject(values, Formatting.None); + } + } +} diff --git a/src/Stripe.net/Infrastructure/Requestor.cs b/src/Stripe.net/Infrastructure/Requestor.cs deleted file mode 100644 index a349db787c..0000000000 --- a/src/Stripe.net/Infrastructure/Requestor.cs +++ /dev/null @@ -1,137 +0,0 @@ -namespace Stripe.Infrastructure -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using Stripe.Infrastructure.FormEncoding; - - internal static class Requestor - { - private static readonly string UserAgentString - = $"Stripe/v1 .NetBindings/{StripeConfiguration.StripeNetVersion}"; - - private static readonly string StripeClientUserAgentString - = BuildStripeClientUserAgentString(); - - static Requestor() - { -#if NET45 - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 - | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; -#endif - - HttpClient = - StripeConfiguration.HttpMessageHandler != null - ? new HttpClient(StripeConfiguration.HttpMessageHandler) - : new HttpClient(); - - HttpClient.Timeout = StripeConfiguration.HttpTimeout; - } - - internal static HttpClient HttpClient { get; private set; } - - internal static async Task ExecuteRequestAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default(CancellationToken)) - where T : IStripeEntity - { - var response = await HttpClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); - var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - var stripeResponse = BuildResponseData(response, responseText); - - if (response.IsSuccessStatusCode) - { - var obj = StripeEntity.FromJson(responseText); - obj.StripeResponse = stripeResponse; - return obj; - } - - throw BuildStripeException(stripeResponse, response.StatusCode, requestMessage.RequestUri.AbsoluteUri, responseText); - } - - internal static HttpRequestMessage GetRequestMessage( - HttpMethod method, - string path, - BaseOptions options, - RequestOptions requestOptions) - { - var request = new Request(method, path, options, requestOptions); - - var requestMessage = new HttpRequestMessage(request.Method, request.Uri); - - // User agent headers. These are the same for every request. - requestMessage.Headers.UserAgent.ParseAdd(UserAgentString); - requestMessage.Headers.Add("X-Stripe-Client-User-Agent", StripeClientUserAgentString); - - // Request headers - requestMessage.Headers.Authorization = request.AuthorizationHeader; - foreach (var header in request.StripeHeaders) - { - requestMessage.Headers.Add(header.Key, header.Value); - } - - // Request body - requestMessage.Content = request.Content; - - return requestMessage; - } - - private static StripeException BuildStripeException(StripeResponse response, HttpStatusCode statusCode, string requestUri, string responseContent) - { - var stripeError = requestUri.Contains("oauth") - ? StripeError.FromJson(responseContent) - : StripeError.FromJson(JObject.Parse(responseContent)["error"].ToString()); - stripeError.StripeResponse = response; - - return new StripeException(statusCode, stripeError, stripeError.Message) - { - StripeResponse = response - }; - } - - private static StripeResponse BuildResponseData(HttpResponseMessage response, string responseText) - { - var result = new StripeResponse - { - RequestId = response.Headers.Contains("Request-Id") ? - response.Headers.GetValues("Request-Id").First() : - "n/a", - RequestDate = response.Headers.Contains("Date") ? - Convert.ToDateTime(response.Headers.GetValues("Date").First(), CultureInfo.InvariantCulture) : - default(DateTime), - ResponseJson = responseText, - }; - - return result; - } - - private static string BuildStripeClientUserAgentString() - { - var values = new Dictionary - { - { "bindings_version", StripeConfiguration.StripeNetVersion }, - { "lang", ".net" }, - { "publisher", "stripe" }, - { "lang_version", RuntimeInformation.GetLanguageVersion() }, - { "os_version", RuntimeInformation.GetOSVersion() }, - }; - -#if NET45 - string monoVersion = RuntimeInformation.GetMonoVersion(); - if (!string.IsNullOrEmpty(monoVersion)) - { - values.Add("mono_version", monoVersion); - } -#endif - - return JsonConvert.SerializeObject(values, Formatting.None); - } - } -} diff --git a/src/Stripe.net/Services/_base/Service.cs b/src/Stripe.net/Services/_base/Service.cs index 717cf0fddd..14a3cbdd7f 100644 --- a/src/Stripe.net/Services/_base/Service.cs +++ b/src/Stripe.net/Services/_base/Service.cs @@ -211,12 +211,12 @@ protected async Task RequestAsync( { options = this.SetupOptions(options, IsStripeList()); requestOptions = this.SetupRequestOptions(requestOptions); - var wr = Requestor.GetRequestMessage( + return await StripeConfiguration.StripeClient.RequestAsync( method, path, options, - requestOptions); - return await Requestor.ExecuteRequestAsync(wr); + requestOptions, + cancellationToken); } protected IEnumerable ListRequestAutoPaging( diff --git a/src/StripeTests/Infrastructure/Public/StripeClientTest.cs b/src/StripeTests/Infrastructure/Public/StripeClientTest.cs new file mode 100644 index 0000000000..c55bcbde9a --- /dev/null +++ b/src/StripeTests/Infrastructure/Public/StripeClientTest.cs @@ -0,0 +1,112 @@ +namespace StripeTests +{ + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using Stripe; + using Xunit; + + public class StripeClientTest : BaseStripeTest + { + private readonly DummyHttpClient httpClient; + private readonly StripeClient stripeClient; + private readonly BaseOptions options; + private readonly RequestOptions requestOptions; + + public StripeClientTest() + { + this.httpClient = new DummyHttpClient(); + this.stripeClient = new StripeClient(this.httpClient); + this.options = new ChargeCreateOptions + { + Amount = 123, + Currency = "usd", + SourceId = "tok_visa", + }; + this.requestOptions = new RequestOptions(); + } + + [Fact] + public async Task RequestAsync_OkResponse() + { + var response = new StripeResponse(HttpStatusCode.OK, null, "{\"id\": \"ch_123\"}"); + this.httpClient.Response = response; + + var charge = await this.stripeClient.RequestAsync( + HttpMethod.Post, + "/v1/charges", + this.options, + this.requestOptions); + + Assert.NotNull(charge); + Assert.Equal("ch_123", charge.Id); + Assert.Equal(response, charge.StripeResponse); + } + + [Fact] + public async Task RequestAsync_ApiError() + { + var response = new StripeResponse( + HttpStatusCode.PaymentRequired, + null, + "{\"error\": {\"type\": \"card_error\"}}"); + this.httpClient.Response = response; + + var exception = await Assert.ThrowsAsync(async () => + await this.stripeClient.RequestAsync( + HttpMethod.Post, + "/v1/charges", + this.options, + this.requestOptions)); + + Assert.NotNull(exception); + Assert.Equal(HttpStatusCode.PaymentRequired, exception.HttpStatusCode); + Assert.Equal("card_error", exception.StripeError.ErrorType); + Assert.Equal(response, exception.StripeResponse); + } + + [Fact] + public async Task RequestAsync_OAuthError() + { + var response = new StripeResponse( + HttpStatusCode.BadRequest, + null, + "{\"error\": \"invalid_request\"}"); + this.httpClient.Response = response; + + var exception = await Assert.ThrowsAsync(async () => + await this.stripeClient.RequestAsync( + HttpMethod.Post, + "/oauth/token", + this.options, + this.requestOptions)); + + Assert.NotNull(exception); + Assert.Equal(HttpStatusCode.BadRequest, exception.HttpStatusCode); + Assert.Equal("invalid_request", exception.StripeError.Error); + Assert.Equal(response, exception.StripeResponse); + } + + private class DummyHttpClient : Stripe.HttpClient + { + public DummyHttpClient() + { + } + + public StripeResponse Response { get; set; } + + public override Task MakeRequestAsync( + StripeRequest request, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (this.Response == null) + { + throw new StripeTestException("Response is null"); + } + + return Task.FromResult(this.Response); + } + } + } +} diff --git a/src/StripeTests/Infrastructure/RequestTest.cs b/src/StripeTests/Infrastructure/Public/StripeRequestTest.cs similarity index 90% rename from src/StripeTests/Infrastructure/RequestTest.cs rename to src/StripeTests/Infrastructure/Public/StripeRequestTest.cs index 721d88813e..b503259f03 100644 --- a/src/StripeTests/Infrastructure/RequestTest.cs +++ b/src/StripeTests/Infrastructure/Public/StripeRequestTest.cs @@ -7,14 +7,14 @@ namespace StripeTests using StripeTests.Infrastructure.TestData; using Xunit; - public class RequestTest : BaseStripeTest + public class StripeRequestTest : BaseStripeTest { [Fact] public void Ctor_GetRequest() { var options = new TestOptions { String = "string!" }; var requestOptions = new RequestOptions(); - var request = new Request(HttpMethod.Get, "/get", options, requestOptions); + var request = new StripeRequest(HttpMethod.Get, "/get", options, requestOptions); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal($"{StripeConfiguration.ApiBase}/get?string=string!", request.Uri.ToString()); @@ -31,7 +31,7 @@ public async Task Ctor_PostRequest() { var options = new TestOptions { String = "string!" }; var requestOptions = new RequestOptions(); - var request = new Request(HttpMethod.Post, "/post", options, requestOptions); + var request = new StripeRequest(HttpMethod.Post, "/post", options, requestOptions); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal($"{StripeConfiguration.ApiBase}/post", request.Uri.ToString()); @@ -50,7 +50,7 @@ public void Ctor_DeleteRequest() { var options = new TestOptions { String = "string!" }; var requestOptions = new RequestOptions(); - var request = new Request(HttpMethod.Delete, "/delete", options, requestOptions); + var request = new StripeRequest(HttpMethod.Delete, "/delete", options, requestOptions); Assert.Equal(HttpMethod.Delete, request.Method); Assert.Equal($"{StripeConfiguration.ApiBase}/delete?string=string!", request.Uri.ToString()); @@ -73,7 +73,7 @@ public void Ctor_RequestOptions() BaseUrl = "https://example.com", StripeVersion = "2012-12-21", }; - var request = new Request(HttpMethod.Get, "/get", null, requestOptions); + var request = new StripeRequest(HttpMethod.Get, "/get", null, requestOptions); Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal("https://example.com/get", request.Uri.ToString()); diff --git a/src/StripeTests/Infrastructure/Public/StripeResponseTest.cs b/src/StripeTests/Infrastructure/Public/StripeResponseTest.cs new file mode 100644 index 0000000000..5369151eda --- /dev/null +++ b/src/StripeTests/Infrastructure/Public/StripeResponseTest.cs @@ -0,0 +1,50 @@ +namespace StripeTests +{ + using System.Collections.Generic; + using System.Net; + using System.Net.Http.Headers; + using Stripe; + using Xunit; + + public class StripeResponseTest : BaseStripeTest + { + /* Most of StripeResponse's methods are helpers for accessing headers. Unfortunately, + * HttpResponseHeaders is a sealed class with no public constructor, which makes it + * ~impossible to write unit tests for StripeResponse. This is why we rely on real + * requests and fetch StripeResponse instances attached to resources. + */ + + [Fact] + public void Date() + { + var response = new AccountService().GetSelf().StripeResponse; + + Assert.NotNull(response.Date); + } + + [Fact] + public void IdempotencyKey_Present() + { + var requestOptions = new RequestOptions { IdempotencyKey = "idempotency_key" }; + var response = new AccountService().GetSelf(requestOptions).StripeResponse; + + Assert.Equal("idempotency_key", response.IdempotencyKey); + } + + [Fact] + public void IdempotencyKey_Absent() + { + var response = new AccountService().GetSelf().StripeResponse; + + Assert.Null(response.IdempotencyKey); + } + + [Fact] + public void RequestId() + { + var response = new AccountService().GetSelf().StripeResponse; + + Assert.NotNull(response.RequestId); + } + } +} diff --git a/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs b/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs new file mode 100644 index 0000000000..6803d980b8 --- /dev/null +++ b/src/StripeTests/Infrastructure/Public/SystemNetHttpClientTest.cs @@ -0,0 +1,38 @@ +namespace StripeTests +{ + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using Moq; + using Moq.Protected; + using Stripe; + using Xunit; + + public class SystemNetHttpClientTest : BaseStripeTest + { + [Fact] + public async Task MakeRequestAsync() + { + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK); + responseMessage.Content = new StringContent("Hello world!"); + var mockHandler = new Mock(); + mockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Returns(Task.FromResult(responseMessage)); + var client = new SystemNetHttpClient(new System.Net.Http.HttpClient(mockHandler.Object)); + var request = new StripeRequest(HttpMethod.Post, "/foo", null, null); + + var response = await client.MakeRequestAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello world!", response.Content); + Assert.Equal(request, client.LastRequest); + Assert.Equal(response, client.LastResponse); + Assert.NotNull(client.LastRequestDuration); + } + } +} diff --git a/src/StripeTests/Infrastructure/RequestorTest.cs b/src/StripeTests/Infrastructure/RequestorTest.cs deleted file mode 100644 index 30479f7b8d..0000000000 --- a/src/StripeTests/Infrastructure/RequestorTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace StripeTests -{ - using System.Linq; - using System.Net.Http; - using Newtonsoft.Json.Linq; - using Stripe; - using Stripe.Infrastructure; - using Xunit; - - public class RequestorTest : BaseStripeTest - { - [Fact] - public void SetsHeaders() - { - RequestOptions options = new RequestOptions - { - ApiKey = "sk_key", - StripeConnectAccountId = "acct_123", - IdempotencyKey = "123", - }; - var request = Requestor.GetRequestMessage(HttpMethod.Get, string.Empty, null, options); - Assert.NotNull(request); - Assert.Equal($"Bearer {options.ApiKey}", request.Headers.GetValues("Authorization").FirstOrDefault()); - Assert.Equal(options.IdempotencyKey, request.Headers.GetValues("Idempotency-Key").FirstOrDefault()); - Assert.Equal(options.StripeConnectAccountId, request.Headers.GetValues("Stripe-Account").FirstOrDefault()); - Assert.Equal(StripeConfiguration.ApiVersion, request.Headers.GetValues("Stripe-Version").FirstOrDefault()); - - Assert.Equal( - $"Stripe/v1 .NetBindings/{StripeConfiguration.StripeNetVersion}", - request.Headers.UserAgent.ToString()); - - var json = request.Headers.GetValues("X-Stripe-Client-User-Agent").FirstOrDefault(); - Assert.NotNull(json); - var data = JObject.Parse(json); - Assert.Equal(StripeConfiguration.StripeNetVersion, data["bindings_version"]); - Assert.Equal(".net", data["lang"]); - Assert.Equal("stripe", data["publisher"]); - Assert.NotNull(data["lang_version"]); - Assert.NotNull(data["os_version"]); - } - } -} diff --git a/src/StripeTests/Infrastructure/StripeResponseTest.cs b/src/StripeTests/Infrastructure/StripeResponseTest.cs deleted file mode 100644 index 9ee16ba97b..0000000000 --- a/src/StripeTests/Infrastructure/StripeResponseTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace StripeTests -{ - using Stripe; - using Xunit; - - public class StripeResponseTest : BaseStripeTest - { - private readonly StripeList charges; - - public StripeResponseTest() - { - this.charges = new ChargeService().List(); - } - - [Fact] - public void Initializes() - { - Assert.NotNull(this.charges); - Assert.NotNull(this.charges.StripeResponse); - Assert.NotNull(this.charges.StripeResponse.RequestId); - Assert.NotNull(this.charges.StripeResponse.ResponseJson); - Assert.True(this.charges.StripeResponse.RequestDate.Year > 0); - } - } -} diff --git a/src/StripeTests/MockHttpClientFixture.cs b/src/StripeTests/MockHttpClientFixture.cs index c251087d11..07ee61f559 100644 --- a/src/StripeTests/MockHttpClientFixture.cs +++ b/src/StripeTests/MockHttpClientFixture.cs @@ -8,10 +8,11 @@ namespace StripeTests using Moq; using Moq.Protected; using Stripe; + using Stripe.Infrastructure; public class MockHttpClientFixture : IDisposable { - private readonly HttpMessageHandler origHandler; + private readonly IStripeClient origClient; public MockHttpClientFixture() { @@ -19,16 +20,18 @@ public MockHttpClientFixture() { CallBase = true }; + var httpClient = new System.Net.Http.HttpClient(this.MockHandler.Object); + var stripeClient = new StripeClient(new Stripe.SystemNetHttpClient(httpClient)); - this.origHandler = StripeConfiguration.HttpMessageHandler; - StripeConfiguration.HttpMessageHandler = this.MockHandler.Object; + this.origClient = StripeConfiguration.StripeClient; + StripeConfiguration.StripeClient = stripeClient; } public Mock MockHandler { get; } public void Dispose() { - StripeConfiguration.HttpMessageHandler = this.origHandler; + StripeConfiguration.StripeClient = this.origClient; } /// diff --git a/src/StripeTests/StripeMockFixture.cs b/src/StripeTests/StripeMockFixture.cs index 5149b997a5..c19f5ad0f9 100644 --- a/src/StripeTests/StripeMockFixture.cs +++ b/src/StripeTests/StripeMockFixture.cs @@ -70,7 +70,7 @@ public string GetFixture(string path, string[] expansions = null) url += $"?{query}"; } - using (HttpClient client = new HttpClient()) + using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient()) { client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( @@ -117,7 +117,7 @@ private void EnsureStripeMockMinimumVersion() { string url = $"http://localhost:{this.port}"; - using (HttpClient client = new HttpClient()) + using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient()) { HttpResponseMessage response;