diff --git a/src/Stripe.net/Infrastructure/Public/ApiRequestorAdapter.cs b/src/Stripe.net/Infrastructure/Public/ApiRequestorAdapter.cs index e8b80f217f..c2a366da5d 100644 --- a/src/Stripe.net/Infrastructure/Public/ApiRequestorAdapter.cs +++ b/src/Stripe.net/Infrastructure/Public/ApiRequestorAdapter.cs @@ -35,6 +35,11 @@ internal static ApiRequestor Adapt(IStripeClient client) return stripeClient.Requestor; } + if (client is DefaultStripeClient defaultStripeClient) + { + return defaultStripeClient.Requestor; + } + return new ApiRequestorAdapter(client); } diff --git a/src/Stripe.net/Infrastructure/Public/DefaultStripeClient.cs b/src/Stripe.net/Infrastructure/Public/DefaultStripeClient.cs new file mode 100644 index 0000000000..449a4b9c45 --- /dev/null +++ b/src/Stripe.net/Infrastructure/Public/DefaultStripeClient.cs @@ -0,0 +1,80 @@ +namespace Stripe +{ + using System.Collections.Generic; + using System.IO; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + /// + /// A default implementation of the interface. This is used by + /// StripeConfiguration to provide a default client, which is used in conjuction with Service + /// instances when no client is provided. + /// + internal class DefaultStripeClient : IStripeClient + { + public DefaultStripeClient(string apiKey, string clientId, IHttpClient httpClient) + { + this.Requestor = new LiveApiRequestor( + new StripeClientOptions + { + ApiKey = apiKey, + ClientId = clientId, + HttpClient = httpClient, + }, new List()); + } + + /// Gets the base URL for Stripe's API. + /// The base URL for Stripe's API. + public string ApiBase => this.Requestor?.ApiBase; + + /// Gets the API key used by the client to send requests. + /// The API key used by the client to send requests. + public string ApiKey => this.Requestor?.ApiKey; + + /// Gets the client ID used by the client in OAuth requests. + /// The client ID used by the client in OAuth requests. + public string ClientId => this.Requestor?.ClientId; + + /// Gets the base URL for Stripe's OAuth API. + /// The base URL for Stripe's OAuth API. + public string ConnectBase => this.Requestor?.ConnectBase; + + /// Gets the base URL for Stripe's Files API. + /// The base URL for Stripe's Files API. + public string FilesBase => this.Requestor?.FilesBase; + + /// Gets the base URL for Stripe's Meter Events API. + /// The base URL for Stripe's Meter Events API. + public string MeterEventsBase => this.Requestor?.MeterEventsBase; + + /// Gets the used to send HTTP requests. + /// The used to send HTTP requests. + public IHttpClient HttpClient => this.Requestor?.HttpClient; + + internal ApiRequestor Requestor { get; } + + /// + public async Task RequestAsync( + HttpMethod method, + string path, + BaseOptions options, + RequestOptions requestOptions, + CancellationToken cancellationToken = default) + where T : IStripeEntity + { + return await this.Requestor.RequestAsync(BaseAddress.Api, method, path, options, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task RequestStreamingAsync( + HttpMethod method, + string path, + BaseOptions options, + RequestOptions requestOptions, + CancellationToken cancellationToken = default) + { + return await this.Requestor.RequestStreamingAsync(BaseAddress.Api, method, path, options, requestOptions, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Stripe.net/Infrastructure/Public/LiveApiRequestor.cs b/src/Stripe.net/Infrastructure/Public/LiveApiRequestor.cs index d877c8fe16..652b563171 100644 --- a/src/Stripe.net/Infrastructure/Public/LiveApiRequestor.cs +++ b/src/Stripe.net/Infrastructure/Public/LiveApiRequestor.cs @@ -3,6 +3,7 @@ namespace Stripe using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -17,8 +18,9 @@ internal class LiveApiRequestor : ApiRequestor internal static readonly List RawRequestUsage = new List { "raw_request" }; private JsonSerializerSettings jsonSerializerSettings; private StripeClientOptions clientOptions; + private List defaultUsage; - public LiveApiRequestor(StripeClientOptions options) + public LiveApiRequestor(StripeClientOptions options, List defaultUsage = null) { // Clone the object passed in, or use an empty option object if it is null options = options?.Clone() ?? new StripeClientOptions(); @@ -41,6 +43,7 @@ public LiveApiRequestor(StripeClientOptions options) this.ConnectBase = options.ConnectBase ?? DefaultConnectBase; this.FilesBase = options.FilesBase ?? DefaultFilesBase; this.MeterEventsBase = options.MeterEventsBase ?? DefaultMeterEventsBase; + this.defaultUsage = defaultUsage ?? new List(); this.jsonSerializerSettings = StripeConfiguration.DefaultSerializerSettings(this); } @@ -216,6 +219,17 @@ private StripeRequest MakeStripeRequest( RequestOptions requestOptions, ApiMode apiMode) { + if (this.defaultUsage.Count > 0) + { + var usage = this.defaultUsage; + if (requestOptions?.Usage?.Count > 0) + { + usage = usage.Concat(requestOptions.Usage).ToList(); + } + + requestOptions = requestOptions.WithUsage(usage); + } + var uri = StripeRequest.BuildUri( requestOptions?.BaseUrl ?? this.GetBaseUrl(baseAddress), method, @@ -297,7 +311,7 @@ public override async Task RawRequestAsync( throw new InvalidOperationException("content is not allowed for non-POST requests."); } - requestOptions = requestOptions.WithUsage(RawRequestUsage); + requestOptions = requestOptions.WithUsage(this.defaultUsage.Concat(RawRequestUsage).ToList()); var apiMode = ApiModeUtils.GetApiMode(path); var uri = StripeRequest.BuildUri( requestOptions?.BaseUrl ?? this.GetBaseUrl(BaseAddress.Api), diff --git a/src/Stripe.net/Infrastructure/Public/StripeClient.cs b/src/Stripe.net/Infrastructure/Public/StripeClient.cs index 4251488e9e..9919800c8f 100644 --- a/src/Stripe.net/Infrastructure/Public/StripeClient.cs +++ b/src/Stripe.net/Infrastructure/Public/StripeClient.cs @@ -1,6 +1,7 @@ namespace Stripe { using System; + using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Threading; @@ -13,6 +14,8 @@ namespace Stripe /// public class StripeClient : IStripeClient { + internal static readonly List StripeClientUsage = new List { "stripe_client" }; + private JsonSerializerSettings jsonSerializerSettings; // Fields: The beginning of the section generated from our OpenAPI spec @@ -71,7 +74,7 @@ public StripeClient( } public StripeClient(StripeClientOptions options) - : this(new LiveApiRequestor(options)) + : this(new LiveApiRequestor(options, StripeClientUsage)) { } diff --git a/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs b/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs index 033b06ee56..0ac501f181 100644 --- a/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs +++ b/src/Stripe.net/Infrastructure/Public/StripeConfiguration.cs @@ -3,7 +3,6 @@ namespace Stripe using System; using System.Collections.Generic; using System.Configuration; - using System.Reflection; using System.Runtime.Serialization; using Newtonsoft.Json; using Stripe.Infrastructure; @@ -243,7 +242,7 @@ public static void SetApiKey(string newApiKey) ApiKey = newApiKey; } - private static StripeClient BuildDefaultStripeClient() + private static IStripeClient BuildDefaultStripeClient() { if (ApiKey != null && ApiKey.Length == 0) { @@ -268,7 +267,7 @@ private static StripeClient BuildDefaultStripeClient() maxNetworkRetries: MaxNetworkRetries, appInfo: AppInfo, enableTelemetry: EnableTelemetry); - return new StripeClient(ApiKey, ClientId, httpClient: httpClient); + return new DefaultStripeClient(ApiKey, ClientId, httpClient); } } } diff --git a/src/StripeTests/BaseStripeTest.cs b/src/StripeTests/BaseStripeTest.cs index 6eccea6a96..1c12a69cbc 100644 --- a/src/StripeTests/BaseStripeTest.cs +++ b/src/StripeTests/BaseStripeTest.cs @@ -69,9 +69,10 @@ public BaseStripeTest( if ((this.StripeMockFixture != null) && (this.MockHttpClientFixture != null)) { // Set up StripeClient to use stripe-mock with the mock HTTP client - var requestor = this.StripeMockFixture.BuildApiRequestor(this.MockHttpClientFixture.MockHandler.Object); - this.StripeClient = new StripeClient(requestor); - this.Requestor = requestor; + var clientOptions = this.StripeMockFixture.BuildStripeClientOptions(this.MockHttpClientFixture.MockHandler.Object); + var client = new StripeClient(clientOptions); + this.StripeClient = client; + this.Requestor = client.Requestor; // Reset the mock before each test this.MockHttpClientFixture.Reset(); @@ -79,22 +80,24 @@ public BaseStripeTest( else if (this.StripeMockFixture != null) { // Set up StripeClient to use stripe-mock - var requestor = this.StripeMockFixture.BuildApiRequestor(); - this.StripeClient = new StripeClient(requestor); - this.Requestor = requestor; + var clientOptions = this.StripeMockFixture.BuildStripeClientOptions(); + var client = new StripeClient(clientOptions); + this.StripeClient = client; + this.Requestor = client.Requestor; } else if (this.MockHttpClientFixture != null) { // Set up StripeClient with the mock HTTP client var httpClient = new SystemNetHttpClient( new HttpClient(this.MockHttpClientFixture.MockHandler.Object)); - var requestor = new LiveApiRequestor(new StripeClientOptions + var clientOptions = new StripeClientOptions { ApiKey = "sk_test_123", HttpClient = httpClient, - }); - this.StripeClient = new StripeClient(requestor); - this.Requestor = requestor; + }; + var client = new StripeClient(clientOptions); + this.StripeClient = client; + this.Requestor = client.Requestor; // Reset the mock before each test this.MockHttpClientFixture.Reset(); @@ -102,9 +105,10 @@ public BaseStripeTest( else { // Use the default StripeClient - var requestor = new LiveApiRequestor(new StripeClientOptions { ApiKey = "sk_test_123" }); - this.StripeClient = new StripeClient(requestor); - this.Requestor = requestor; + var clientOptions = new StripeClientOptions { ApiKey = "sk_test_123" }; + var client = new StripeClient(clientOptions); + this.StripeClient = client; + this.Requestor = client.Requestor; } } diff --git a/src/StripeTests/Functional/TelemetryTest.cs b/src/StripeTests/Functional/TelemetryTest.cs index 2a82cb5c66..b6c3ef014d 100644 --- a/src/StripeTests/Functional/TelemetryTest.cs +++ b/src/StripeTests/Functional/TelemetryTest.cs @@ -15,6 +15,8 @@ namespace StripeTests using Stripe; using Xunit; + using static TelemetryTestUtils; + public class TelemetryTest : BaseStripeTest { public TelemetryTest(MockHttpClientFixture mockHttpClientFixture) @@ -95,7 +97,7 @@ public void TelemetryIncludesUsage() m.Headers, (_) => true, (_) => true, - (t) => t != null && t.Count == 2 && t.Contains("llama") && t.Contains("bufo"))), + (t) => t != null && t.Count >= 2 && t.Contains("llama") && t.Contains("bufo"))), ItExpr.IsAny()); } @@ -175,33 +177,6 @@ public void NoTelemetryWhenDisabled() ItExpr.IsAny()); } - private static bool TelemetryHeaderMatcher( - HttpHeaders headers, - Func requestIdMatcher, - Func durationMatcher, - Func, bool> usageMatcher) - { - if (!headers.Contains("X-Stripe-Client-Telemetry")) - { - return false; - } - - var payload = headers.GetValues("X-Stripe-Client-Telemetry").First(); - - var deserialized = JToken.Parse(payload); - var requestId = (string)deserialized["last_request_metrics"]["request_id"]; - var duration = (long)deserialized["last_request_metrics"]["request_duration_ms"]; - var usageRaw = deserialized["last_request_metrics"]["usage"]; - - List usage = null; - if (usageRaw != null) - { - usage = usageRaw.Select(x => (string)x).ToList(); - } - - return requestIdMatcher(requestId) && durationMatcher(duration) && usageMatcher(usage); - } - private class TestEntity : StripeEntity { } diff --git a/src/StripeTests/Functional/TelemetryTestUtils.cs b/src/StripeTests/Functional/TelemetryTestUtils.cs new file mode 100644 index 0000000000..3bc3e98456 --- /dev/null +++ b/src/StripeTests/Functional/TelemetryTestUtils.cs @@ -0,0 +1,47 @@ +namespace StripeTests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Moq; + using Moq.Protected; + using Newtonsoft.Json.Linq; + using Stripe; + using Xunit; + + internal class TelemetryTestUtils + { + public static bool TelemetryHeaderMatcher( + HttpHeaders headers, + Func requestIdMatcher, + Func durationMatcher, + Func, bool> usageMatcher) + { + if (!headers.Contains("X-Stripe-Client-Telemetry")) + { + return false; + } + + var payload = headers.GetValues("X-Stripe-Client-Telemetry").First(); + + var deserialized = JToken.Parse(payload); + var requestId = (string)deserialized["last_request_metrics"]["request_id"]; + var duration = (long)deserialized["last_request_metrics"]["request_duration_ms"]; + var usageRaw = deserialized["last_request_metrics"]["usage"]; + + List usage = null; + if (usageRaw != null) + { + usage = usageRaw.Select(x => (string)x).ToList(); + } + + return requestIdMatcher(requestId) && durationMatcher(duration) && usageMatcher(usage); + } + } +} diff --git a/src/StripeTests/Infrastructure/Public/StripeClientTest.cs b/src/StripeTests/Infrastructure/Public/StripeClientTest.cs index 6b252add8d..a02d3c64e4 100644 --- a/src/StripeTests/Infrastructure/Public/StripeClientTest.cs +++ b/src/StripeTests/Infrastructure/Public/StripeClientTest.cs @@ -1,16 +1,20 @@ namespace StripeTests { using System; + using System.Collections.Generic; using System.Linq; using System.Net.Http; + using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Moq; using Moq.Protected; + using Newtonsoft.Json.Linq; using Stripe; - using StripeTests.V2; using Xunit; + using static TelemetryTestUtils; + public class StripeClientTest : BaseStripeTest { private StripeClient stripeClient; @@ -73,6 +77,58 @@ public void Ctr_StripeClientOptions_ChangesAfterConstruction() Assert.Equal(goodApiKey, client.ApiKey); } + [Fact] + public void StripeClientRequestorIncludesCorrectUsage() + { + var client = this.StripeClient as StripeClient; + client.V1.Customers.Get("cus_123"); + client.V1.Customers.Get("cus_123"); + + this.MockHttpClientFixture.MockHandler.Protected() + .Verify( + "SendAsync", + Times.Once(), + ItExpr.Is(m => + TelemetryHeaderMatcher( + m.Headers, + (_) => true, + (_) => true, + (t) => t != null && t.Count == 1 && t.Contains(Stripe.StripeClient.StripeClientUsage[0]))), + ItExpr.IsAny()); + } + + [Fact] + public void DefaultStripeClientRequestorDoesNotIncludeUsage() + { + var stripeClient = this.StripeClient as StripeClient; + + // This mimics StripeConfiguration.BuildDefaultStripeClient, which is used + // by StripeConfiguration.StripeClient when a client is not set + StripeConfiguration.StripeClient = new DefaultStripeClient( + stripeClient.ApiKey, stripeClient.ClientId, stripeClient.HttpClient); + + var customers = new CustomerService(); + customers.Get("cus_123"); + customers.Get("cus_123"); + + this.MockHttpClientFixture.MockHandler.Protected() + .Verify( + "SendAsync", + Times.Once(), + ItExpr.Is(m => + TelemetryHeaderMatcher( + m.Headers, + (_) => true, + (_) => true, + (t) => t != null && t.Count == 0)), + ItExpr.IsAny()); + + // unfortunately we cannot check at the top of the test because + // StripeConfiguration.StripeClient always returns a value; but + // we should assume it was not set + StripeConfiguration.StripeClient = null; + } + [Fact] public async Task RawRequestAsync_Json() { @@ -108,6 +164,38 @@ public async Task RawRequestAsync_Json() Assert.Equal("mes_123", obj.Id); } + [Fact] + public async Task RawRequestAsyncIncludesCorrectUsage() + { + await this.stripeClient.RawRequestAsync( + HttpMethod.Get, + "/v1/customers/cus_123", + null, + new RawRequestOptions + { + }); + + await this.stripeClient.RawRequestAsync( + HttpMethod.Get, + "/v1/customers/cus_123", + null, + new RawRequestOptions + { + }); + + this.MockHttpClientFixture.MockHandler.Protected() + .Verify( + "SendAsync", + Times.Once(), + ItExpr.Is(m => + TelemetryHeaderMatcher( + m.Headers, + (_) => true, + (_) => true, + (t) => t != null && t.Count == 2 && t.Contains(Stripe.StripeClient.StripeClientUsage[0]) && t.Contains(LiveApiRequestor.RawRequestUsage[0]))), + ItExpr.IsAny()); + } + [Fact] public void ConstructThinEvent() { diff --git a/src/StripeTests/Infrastructure/Public/StripeConfigurationTest.cs b/src/StripeTests/Infrastructure/Public/StripeConfigurationTest.cs index 3d66619557..54d6720059 100644 --- a/src/StripeTests/Infrastructure/Public/StripeConfigurationTest.cs +++ b/src/StripeTests/Infrastructure/Public/StripeConfigurationTest.cs @@ -24,6 +24,7 @@ public void StripeClient_Getter_CreatesNewStripeClientIfNullAndApiKeyIsSet() var client = StripeConfiguration.StripeClient; Assert.NotNull(client); + Assert.IsType(client); Assert.Equal(StripeConfiguration.ApiKey, client.ApiKey); Assert.Equal(StripeConfiguration.ClientId, client.ClientId); } @@ -46,6 +47,7 @@ public void StripeClient_Getter_CreatesNewStripeClientIfNullAndApiKeyIsNull() var client = StripeConfiguration.StripeClient; Assert.NotNull(client); + Assert.IsType(client); Assert.Null(client.ApiKey); } finally diff --git a/src/StripeTests/StripeMockFixture.cs b/src/StripeTests/StripeMockFixture.cs index 244dd867dc..a521ac1dc7 100644 --- a/src/StripeTests/StripeMockFixture.cs +++ b/src/StripeTests/StripeMockFixture.cs @@ -43,14 +43,14 @@ public void Dispose() /// created with default parameters. /// /// The new instance. - internal ApiRequestor BuildApiRequestor(HttpClientHandler innerHandler = null) + internal StripeClientOptions BuildStripeClientOptions(HttpClientHandler innerHandler = null) { - return new LiveApiRequestor(new StripeClientOptions + return new StripeClientOptions { ApiKey = "sk_test_123", ClientId = "ca_123", HttpClient = new SystemNetHttpClient(new ForwardingHttpClient(innerHandler, this.port)), - }); + }; } ///