From 8f4fe46761bd701e89e9509b6b23a690c825c193 Mon Sep 17 00:00:00 2001 From: Todd Date: Fri, 3 Nov 2023 11:45:10 -0500 Subject: [PATCH] #773 refactor IHttpSettingsContainer --- .../Configuration/FlurlClientBuilder.cs | 27 +-- src/Flurl.Http/FlurlClient.cs | 27 ++- src/Flurl.Http/FlurlRequest.cs | 12 +- src/Flurl.Http/HeaderExtensions.cs | 76 ------- src/Flurl.Http/IHeadersContainer.cs | 84 +++++++ src/Flurl.Http/IHttpSettingsContainer.cs | 23 -- ...ngsExtensions.cs => ISettingsContainer.cs} | 96 ++++---- src/Flurl.Http/Testing/HttpTest.cs | 13 +- src/Flurl.Http/Testing/HttpTestSetup.cs | 2 +- .../Http/FlurlClientBuilderTests.cs | 7 - test/Flurl.Test/Http/FlurlClientTests.cs | 7 + test/Flurl.Test/Http/HeadersTests.cs | 158 +++++++++++++ .../Http/SettingsExtensionsTests.cs | 210 ------------------ test/Flurl.Test/Http/SettingsTests.cs | 209 +++++++++++------ 14 files changed, 480 insertions(+), 471 deletions(-) delete mode 100644 src/Flurl.Http/HeaderExtensions.cs create mode 100644 src/Flurl.Http/IHeadersContainer.cs delete mode 100644 src/Flurl.Http/IHttpSettingsContainer.cs rename src/Flurl.Http/{SettingsExtensions.cs => ISettingsContainer.cs} (69%) create mode 100644 test/Flurl.Test/Http/HeadersTests.cs delete mode 100644 test/Flurl.Test/Http/SettingsExtensionsTests.cs diff --git a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs index 8c05ed54..aaced9ea 100644 --- a/src/Flurl.Http/Configuration/FlurlClientBuilder.cs +++ b/src/Flurl.Http/Configuration/FlurlClientBuilder.cs @@ -3,19 +3,15 @@ using System.Linq; using System.Net.Http; using System.Runtime.Versioning; +using Flurl.Util; namespace Flurl.Http.Configuration { /// /// A builder for configuring IFlurlClient instances. /// - public interface IFlurlClientBuilder + public interface IFlurlClientBuilder : ISettingsContainer, IHeadersContainer { - /// - /// Configure the IFlurlClient's Settings. - /// - IFlurlClientBuilder WithSettings(Action configure); - /// /// Configure the HttpClient wrapped by this IFlurlClient. /// @@ -57,10 +53,15 @@ public class FlurlClientBuilder : IFlurlClientBuilder private readonly string _baseUrl; private readonly List> _addMiddleware = new(); - private readonly List> _settingsConfigs = new(); private readonly List> _clientConfigs = new(); private readonly List> _handlerConfigs = new(); + /// + public FlurlHttpSettings Settings { get; } = new(); + + /// + public INameValueList Headers { get; } = new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 + /// /// Creates a new FlurlClientBuilder. /// @@ -68,12 +69,6 @@ public FlurlClientBuilder(string baseUrl = null) { _baseUrl = baseUrl; } - /// - public IFlurlClientBuilder WithSettings(Action configure) { - _settingsConfigs.Add(configure); - return this; - } - /// public IFlurlClientBuilder AddMiddleware(Func create) { _addMiddleware.Add(create); @@ -128,11 +123,7 @@ public IFlurlClient Build() { foreach (var config in _clientConfigs) config(httpCli); - var flurlCli = new FlurlClient(httpCli, _baseUrl); - foreach (var config in _settingsConfigs) - config(flurlCli.Settings); - - return flurlCli; + return new FlurlClient(httpCli, _baseUrl, Settings, Headers); } } } diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index d2b2843f..89e56e83 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -12,7 +12,7 @@ namespace Flurl.Http /// /// Interface defining FlurlClient's contract (useful for mocking and DI) /// - public interface IFlurlClient : IHttpSettingsContainer, IDisposable { + public interface IFlurlClient : ISettingsContainer, IHeadersContainer, IDisposable { /// /// Gets the HttpClient that this IFlurlClient wraps. /// @@ -67,26 +67,32 @@ public FlurlClient(string baseUrl = null) : this(_defaultFactory.Value.CreateHtt /// Flurl's re-implementation of those features may not work properly. /// /// The instantiated HttpClient instance. - /// The base URL associated with this client. - public FlurlClient(HttpClient httpClient, string baseUrl = null) { + /// Optional. The base URL associated with this client. + /// Optional. A pre-initialized collection of settings. + /// Optional. A pre-initialized collection of default request headers. + public FlurlClient(HttpClient httpClient, string baseUrl = null, FlurlHttpSettings settings = null, INameValueList headers = null) { HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); BaseUrl = baseUrl ?? httpClient.BaseAddress?.ToString(); - foreach (var header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value, (kv, v) => (kv.Key, v))) - Headers.Add(header); - Settings.Timeout = httpClient.Timeout; + Settings = settings ?? new FlurlHttpSettings { Timeout = httpClient.Timeout }; // Timeout can be overridden per request, so don't constrain it by the underlying HttpClient httpClient.Timeout = Timeout.InfiniteTimeSpan; + + Headers = headers ?? new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 + foreach (var header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value, (kv, v) => (kv.Key, v))) { + if (!Headers.Contains(header.Key)) + Headers.Add(header); + } } /// public string BaseUrl { get; set; } /// - public FlurlHttpSettings Settings { get; } = new(); + public FlurlHttpSettings Settings { get; } /// - public INameValueList Headers { get; } = new NameValueList(false); // header names are case-insensitive https://stackoverflow.com/a/5259004/62600 + public INameValueList Headers { get; } /// public HttpClient HttpClient { get; } @@ -153,10 +159,7 @@ public async Task SendAsync(IFlurlRequest request, HttpCompletio private void SyncHeaders(IFlurlRequest req, HttpRequestMessage reqMsg) { // copy any client-level (default) headers to FlurlRequest - foreach (var header in this.Headers.ToList()) { - if (!req.Headers.Contains(header.Name)) - req.Headers.Add(header.Name, header.Value); - } + FlurlRequest.SyncHeaders(this, req); // copy headers from FlurlRequest to HttpRequestMessage foreach (var header in req.Headers) diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 6d52f2e1..ce11b73a 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -13,7 +13,7 @@ namespace Flurl.Http /// Represents an HTTP request. Can be created explicitly via new FlurlRequest(), fluently via Url.Request(), /// or implicitly when a call is made via methods like Url.GetAsync(). /// - public interface IFlurlRequest : IHttpSettingsContainer + public interface IFlurlRequest : ISettingsContainer, IHeadersContainer { /// /// Gets or sets the IFlurlClient to use when sending the request. @@ -106,6 +106,7 @@ public IFlurlClient Client { set { _client = value; Settings.Parent = _client?.Settings; + SyncHeaders(_client, this); } } @@ -142,6 +143,15 @@ public Task SendAsync(HttpMethod verb, HttpContent content = nul return Client.SendAsync(this, completionOption, cancellationToken); } + internal static void SyncHeaders(IFlurlClient client, IFlurlRequest request) { + if (client == null || request == null) return; + + foreach (var header in client.Headers.ToList()) { + if (!request.Headers.Contains(header.Name)) + request.Headers.Add(header.Name, header.Value); + } + } + private void ApplyCookieJar(CookieJar jar) { _jar = jar; if (jar == null) diff --git a/src/Flurl.Http/HeaderExtensions.cs b/src/Flurl.Http/HeaderExtensions.cs deleted file mode 100644 index b6762246..00000000 --- a/src/Flurl.Http/HeaderExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Flurl.Util; - -namespace Flurl.Http -{ - /// - /// Fluent extension methods for working with HTTP request headers. - /// - public static class HeaderExtensions - { - /// - /// Sets an HTTP header to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// HTTP header name. - /// HTTP header value. - /// This IFlurlClient or IFlurlRequest. - public static T WithHeader(this T clientOrRequest, string name, object value) where T : IHttpSettingsContainer { - if (value == null) - clientOrRequest.Headers.Remove(name); - else - clientOrRequest.Headers.AddOrReplace(name, value.ToInvariantString().Trim()); - return clientOrRequest; - } - - /// - /// Sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. - /// If true, underscores in property names will be replaced by hyphens. Default is true. - /// This IFlurlClient or IFlurlRequest. - public static T WithHeaders(this T clientOrRequest, object headers, bool replaceUnderscoreWithHyphen = true) where T : IHttpSettingsContainer { - if (headers == null) - return clientOrRequest; - - // underscore replacement only applies when object properties are parsed to kv pairs - replaceUnderscoreWithHyphen = replaceUnderscoreWithHyphen && !(headers is string) && !(headers is IEnumerable); - - foreach (var kv in headers.ToKeyValuePairs()) { - var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; - clientOrRequest.WithHeader(key, kv.Value); - } - - return clientOrRequest; - } - - /// - /// Sets HTTP authorization header according to Basic Authentication protocol to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// Username of authenticating user. - /// Password of authenticating user. - /// This IFlurlClient or IFlurlRequest. - public static T WithBasicAuth(this T clientOrRequest, string username, string password) where T : IHttpSettingsContainer { - // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient - var encodedCreds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); - return clientOrRequest.WithHeader("Authorization", $"Basic {encodedCreds}"); - } - - /// - /// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification to be sent with this IFlurlRequest or all requests made with this IFlurlClient. - /// - /// The IFlurlClient or IFlurlRequest. - /// The acquired bearer token to pass. - /// This IFlurlClient or IFlurlRequest. - public static T WithOAuthBearerToken(this T clientOrRequest, string token) where T : IHttpSettingsContainer { - return clientOrRequest.WithHeader("Authorization", $"Bearer {token}"); - } - } -} diff --git a/src/Flurl.Http/IHeadersContainer.cs b/src/Flurl.Http/IHeadersContainer.cs new file mode 100644 index 00000000..366f1b3f --- /dev/null +++ b/src/Flurl.Http/IHeadersContainer.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Text; +using Flurl.Util; + +namespace Flurl.Http +{ + /// + /// A common interface for Flurl.Http objects that contain a collection of request headers. + /// + public interface IHeadersContainer + { + /// + /// A collection of request headers. + /// + INameValueList Headers { get; } + } + + /// + /// Fluent extension methods for working with HTTP request headers. + /// + public static class HeaderExtensions + { + /// + /// Sets an HTTP header associated with this request or client. + /// + /// Object containing request headers. + /// HTTP header name. + /// HTTP header value. + /// This headers container. + public static T WithHeader(this T obj, string name, object value) where T : IHeadersContainer { + if (value == null) + obj.Headers.Remove(name); + else + obj.Headers.AddOrReplace(name, value.ToInvariantString().Trim()); + return obj; + } + + /// + /// Sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, associated with this request or client. + /// + /// Object containing request headers. + /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. + /// If true, underscores in property names will be replaced by hyphens. Default is true. + /// This headers container. + public static T WithHeaders(this T obj, object headers, bool replaceUnderscoreWithHyphen = true) where T : IHeadersContainer { + if (headers == null) + return obj; + + // underscore replacement only applies when object properties are parsed to kv pairs + replaceUnderscoreWithHyphen = replaceUnderscoreWithHyphen && !(headers is string) && !(headers is IEnumerable); + + foreach (var kv in headers.ToKeyValuePairs()) { + var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; + obj.WithHeader(key, kv.Value); + } + + return obj; + } + + /// + /// Sets HTTP authorization header according to Basic Authentication protocol associated with this request or client. + /// + /// Object containing request headers. + /// Username of authenticating user. + /// Password of authenticating user. + /// This headers container. + public static T WithBasicAuth(this T obj, string username, string password) where T : IHeadersContainer { + // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient + var encodedCreds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + return obj.WithHeader("Authorization", $"Basic {encodedCreds}"); + } + + /// + /// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification associated with this request or client. + /// + /// Object containing request headers. + /// The acquired bearer token to pass. + /// This headers container. + public static T WithOAuthBearerToken(this T obj, string token) where T : IHeadersContainer { + return obj.WithHeader("Authorization", $"Bearer {token}"); + } + } +} diff --git a/src/Flurl.Http/IHttpSettingsContainer.cs b/src/Flurl.Http/IHttpSettingsContainer.cs deleted file mode 100644 index 50696c41..00000000 --- a/src/Flurl.Http/IHttpSettingsContainer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using Flurl.Http.Configuration; -using Flurl.Util; - -namespace Flurl.Http -{ - /// - /// Defines stateful aspects (headers, cookies, etc) common to both IFlurlClient and IFlurlRequest - /// - public interface IHttpSettingsContainer - { - /// - /// Gets the FlurlHttpSettings object used by this client or request. - /// - FlurlHttpSettings Settings { get; } - - /// - /// Collection of headers sent on this request or all requests using this client. - /// - INameValueList Headers { get; } - } -} diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/ISettingsContainer.cs similarity index 69% rename from src/Flurl.Http/SettingsExtensions.cs rename to src/Flurl.Http/ISettingsContainer.cs index b5c3a06f..2c8a6d0b 100644 --- a/src/Flurl.Http/SettingsExtensions.cs +++ b/src/Flurl.Http/ISettingsContainer.cs @@ -7,50 +7,50 @@ namespace Flurl.Http { /// - /// Fluent extension methods for tweaking FlurlHttpSettings + /// A common interface for Flurl.Http objects that contain a collection of request settings. /// - public static class SettingsExtensions + public interface ISettingsContainer { /// - /// Change FlurlHttpSettings for this IFlurlClient. + /// A collection request settings. /// - /// The IFlurlClient. - /// Action defining the settings changes. - /// The IFlurlClient with the modified Settings - public static IFlurlClient WithSettings(this IFlurlClient client, Action action) { - action(client.Settings); - return client; - } + FlurlHttpSettings Settings { get; } + } + /// + /// Fluent extension methods for tweaking FlurlHttpSettings + /// + public static class SettingsExtensions + { /// - /// Change FlurlHttpSettings for this IFlurlRequest. + /// Change FlurlHttpSettings for this request, client, or test context. /// - /// The IFlurlRequest. + /// Object containing settings. /// Action defining the settings changes. - /// The IFlurlRequest with the modified Settings - public static IFlurlRequest WithSettings(this IFlurlRequest request, Action action) { - action(request.Settings); - return request; + /// This settings container. + public static T WithSettings(this T obj, Action action) where T : ISettingsContainer { + action(obj.Settings); + return obj; } /// - /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. + /// Sets the timeout for this request, client, or test context. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Time to wait before the request times out. - /// This IFlurlClient or IFlurlRequest. - public static T WithTimeout(this T obj, TimeSpan timespan) where T : IHttpSettingsContainer { + /// This settings container. + public static T WithTimeout(this T obj, TimeSpan timespan) where T : ISettingsContainer { obj.Settings.Timeout = timespan; return obj; } /// - /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. + /// Sets the timeout for this request, client, or test context. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Seconds to wait before the request times out. - /// This IFlurlClient or IFlurlRequest. - public static T WithTimeout(this T obj, int seconds) where T : IHttpSettingsContainer { + /// This settings container. + public static T WithTimeout(this T obj, int seconds) where T : ISettingsContainer { obj.Settings.Timeout = TimeSpan.FromSeconds(seconds); return obj; } @@ -58,10 +58,10 @@ public static T WithTimeout(this T obj, int seconds) where T : IHttpSettingsC /// /// Adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Examples: "3xx", "100,300,600", "100-299,6xx" - /// This IFlurlClient or IFlurlRequest. - public static T AllowHttpStatus(this T obj, string pattern) where T : IHttpSettingsContainer { + /// This settings container. + public static T AllowHttpStatus(this T obj, string pattern) where T : ISettingsContainer { if (!string.IsNullOrWhiteSpace(pattern)) { var current = obj.Settings.AllowedHttpStatusRange; if (string.IsNullOrWhiteSpace(current)) @@ -75,10 +75,10 @@ public static T AllowHttpStatus(this T obj, string pattern) where T : IHttpSe /// /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// Examples: HttpStatusCode.NotFound - /// This IFlurlClient or IFlurlRequest. - public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCodes) where T : IHttpSettingsContainer { + /// This settings container. + public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCodes) where T : ISettingsContainer { var pattern = string.Join(",", statusCodes.Select(c => (int)c)); return AllowHttpStatus(obj, pattern); } @@ -86,9 +86,9 @@ public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCod /// /// Prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. /// - /// The IFlurlClient or IFlurlRequest. - /// This IFlurlClient or IFlurlRequest. - public static T AllowAnyHttpStatus(this T obj) where T : IHttpSettingsContainer { + /// Object containing settings. + /// This settings container. + public static T AllowAnyHttpStatus(this T obj) where T : ISettingsContainer { obj.Settings.AllowedHttpStatusRange = "*"; return obj; } @@ -96,10 +96,10 @@ public static T AllowAnyHttpStatus(this T obj) where T : IHttpSettingsContain /// /// Configures whether redirects are automatically followed. /// - /// The IFlurlClient or IFlurlRequest. + /// Object containing settings. /// true if Flurl should automatically send a new request to the redirect URL, false if it should not. - /// This IFlurlClient or IFlurlRequest. - public static T WithAutoRedirect(this T obj, bool enabled) where T : IHttpSettingsContainer { + /// This settings container. + public static T WithAutoRedirect(this T obj, bool enabled) where T : ISettingsContainer { obj.Settings.Redirects.Enabled = enabled; return obj; } @@ -107,7 +107,8 @@ public static T WithAutoRedirect(this T obj, bool enabled) where T : IHttpSet /// /// Sets a callback that is invoked immediately before every HTTP request is sent. /// - public static T BeforeCall(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T BeforeCall(this T obj, Action act) where T : ISettingsContainer { obj.Settings.BeforeCall = act; return obj; } @@ -115,7 +116,8 @@ public static T BeforeCall(this T obj, Action act) where T : IHttp /// /// Sets a callback that is invoked asynchronously immediately before every HTTP request is sent. /// - public static T BeforeCall(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T BeforeCall(this T obj, Func act) where T : ISettingsContainer { obj.Settings.BeforeCallAsync = act; return obj; } @@ -123,7 +125,8 @@ public static T BeforeCall(this T obj, Func act) where T : I /// /// Sets a callback that is invoked immediately after every HTTP response is received. /// - public static T AfterCall(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T AfterCall(this T obj, Action act) where T : ISettingsContainer { obj.Settings.AfterCall = act; return obj; } @@ -131,7 +134,8 @@ public static T AfterCall(this T obj, Action act) where T : IHttpS /// /// Sets a callback that is invoked asynchronously immediately after every HTTP response is received. /// - public static T AfterCall(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T AfterCall(this T obj, Func act) where T : ISettingsContainer { obj.Settings.AfterCallAsync = act; return obj; } @@ -140,7 +144,8 @@ public static T AfterCall(this T obj, Func act) where T : IH /// Sets a callback that is invoked when an error occurs during any HTTP call, including when any non-success /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. /// - public static T OnError(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnError(this T obj, Action act) where T : ISettingsContainer { obj.Settings.OnError = act; return obj; } @@ -149,7 +154,8 @@ public static T OnError(this T obj, Action act) where T : IHttpSet /// Sets a callback that is invoked asynchronously when an error occurs during any HTTP call, including when any non-success /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. /// - public static T OnError(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnError(this T obj, Func act) where T : ISettingsContainer { obj.Settings.OnErrorAsync = act; return obj; } @@ -159,7 +165,8 @@ public static T OnError(this T obj, Func act) where T : IHtt /// You can inspect/manipulate the call.Redirect object to determine what will happen next. /// An auto-redirect will only happen if call.Redirect.Follow is true upon exiting the callback. /// - public static T OnRedirect(this T obj, Action act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnRedirect(this T obj, Action act) where T : ISettingsContainer { obj.Settings.OnRedirect = act; return obj; } @@ -169,7 +176,8 @@ public static T OnRedirect(this T obj, Action act) where T : IHttp /// You can inspect/manipulate the call.Redirect object to determine what will happen next. /// An auto-redirect will only happen if call.Redirect.Follow is true upon exiting the callback. /// - public static T OnRedirect(this T obj, Func act) where T : IHttpSettingsContainer { + /// This settings container. + public static T OnRedirect(this T obj, Func act) where T : ISettingsContainer { obj.Settings.OnRedirectAsync = act; return obj; } diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 43a0c0a6..cf221944 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Net.Http; using Flurl.Http.Configuration; namespace Flurl.Http.Testing @@ -13,7 +12,7 @@ namespace Flurl.Http.Testing /// queue, call log, and assertion helpers for use in Arrange/Act/Assert style tests. /// [Serializable] - public class HttpTest : HttpTestSetup, IDisposable + public class HttpTest : HttpTestSetup, ISettingsContainer, IDisposable { private readonly ConcurrentQueue _calls = new(); private readonly List _filteredSetups = new(); @@ -38,16 +37,6 @@ public HttpTest() : base(new FlurlHttpSettings()) { /// public IReadOnlyList CallLog => new ReadOnlyCollection(_calls.ToList()); - /// - /// Change FlurlHttpSettings for the scope of this HttpTest. - /// - /// Action defining the settings changes. - /// This HttpTest - public HttpTest WithSettings(Action action) { - action(Settings); - return this; - } - /// /// Fluently creates and returns a new request-specific test setup. /// diff --git a/src/Flurl.Http/Testing/HttpTestSetup.cs b/src/Flurl.Http/Testing/HttpTestSetup.cs index 4439ce30..698339b0 100644 --- a/src/Flurl.Http/Testing/HttpTestSetup.cs +++ b/src/Flurl.Http/Testing/HttpTestSetup.cs @@ -16,7 +16,7 @@ namespace Flurl.Http.Testing /// public abstract class HttpTestSetup { - private readonly List> _responses = new List>(); + private readonly List> _responses = new(); private int _respIndex = 0; private bool _allowRealHttp = false; diff --git a/test/Flurl.Test/Http/FlurlClientBuilderTests.cs b/test/Flurl.Test/Http/FlurlClientBuilderTests.cs index fba59818..efdcae5f 100644 --- a/test/Flurl.Test/Http/FlurlClientBuilderTests.cs +++ b/test/Flurl.Test/Http/FlurlClientBuilderTests.cs @@ -12,13 +12,6 @@ namespace Flurl.Test.Http [TestFixture] public class FlurlClientBuilderTests { - [Test] - public void can_configure_settings() { - var builder = new FlurlClientBuilder(); - var cli = builder.WithSettings(s => s.HttpVersion = "3.0").Build(); - Assert.AreEqual("3.0", cli.Settings.HttpVersion); - } - [Test] public void can_configure_HttpClient() { var builder = new FlurlClientBuilder(); diff --git a/test/Flurl.Test/Http/FlurlClientTests.cs b/test/Flurl.Test/Http/FlurlClientTests.cs index 87dddc34..2936b08b 100644 --- a/test/Flurl.Test/Http/FlurlClientTests.cs +++ b/test/Flurl.Test/Http/FlurlClientTests.cs @@ -70,6 +70,13 @@ public void can_create_request_with_base_url_and_no_segments() { Assert.AreEqual("http://myapi.com", req.Url.ToString()); } + [Test] + public void can_create_request_with_Uri() { + var uri = new System.Uri("http://www.mysite.com/foo?x=1"); + var req = new FlurlClient().Request(uri); + Assert.AreEqual(uri.ToString(), req.Url.ToString()); + } + [Test] public void cannot_send_invalid_request() { var cli = new FlurlClient(); diff --git a/test/Flurl.Test/Http/HeadersTests.cs b/test/Flurl.Test/Http/HeadersTests.cs new file mode 100644 index 00000000..684ddd40 --- /dev/null +++ b/test/Flurl.Test/Http/HeadersTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Http.Configuration; +using Flurl.Http.Testing; +using NUnit.Framework; + +namespace Flurl.Test.Http +{ + /// + /// A Headers collection is available on IFlurlRequest, IFlurlClient, and IFlurlBuilder. + /// This abstract class allows the same tests to be run against all 3. + /// + public abstract class HeadersTestsBase where T : IHeadersContainer + { + protected abstract T CreateContainer(); + protected abstract IFlurlRequest GetRequest(T container); + + [Test] + public void can_set_header() { + var c = CreateContainer(); + c.WithHeader("a", 1); + Assert.AreEqual(("a", "1"), GetRequest(c).Headers.Single()); + } + + [Test] + public void can_set_headers_from_anon_object() { + var c = CreateContainer(); + // null values shouldn't be added + c.WithHeaders(new { a = "b", one = 2, three = (object)null }); + + var req = GetRequest(c); + Assert.AreEqual(2, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("a", "b")); + Assert.IsTrue(req.Headers.Contains("one", "2")); + } + + [Test] + public void can_remove_header_by_setting_null() { + var c = CreateContainer(); + c.WithHeaders(new { a = 1, b = 2 }); + Assert.AreEqual(2, GetRequest(c).Headers.Count); + + c.WithHeader("b", null); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.IsFalse(req.Headers.Contains("b")); + } + + [Test] + public void can_set_headers_from_dictionary() { + var c = CreateContainer(); + c.WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); + + var req = GetRequest(c); + Assert.AreEqual(2, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("a", "b")); + Assert.IsTrue(req.Headers.Contains("one", "2")); + } + + [Test] + public void underscores_in_properties_convert_to_hyphens_in_header_names() { + var c = CreateContainer(); + c.WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" }); + + var req = GetRequest(c); + Assert.IsTrue(req.Headers.Contains("User-Agent")); + Assert.IsTrue(req.Headers.Contains("Cache-Control")); + + // make sure we can disable the behavior + c.WithHeaders(new { no_i_really_want_underscores = "foo" }, false); + Assert.IsTrue(GetRequest(c).Headers.Contains("no_i_really_want_underscores")); + + // dictionaries don't get this behavior since you can use hyphens explicitly + c.WithHeaders(new Dictionary { { "exclude_dictionaries", "bar" } }); + Assert.IsTrue(GetRequest(c).Headers.Contains("exclude_dictionaries")); + + // same with strings + c.WithHeaders("exclude_strings=123"); + Assert.IsTrue(GetRequest(c).Headers.Contains("exclude_strings")); + } + + [Test] + public void header_names_are_case_insensitive() { + var c = CreateContainer(); + c.WithHeader("a", 1).WithHeader("A", 2); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.AreEqual("A", req.Headers.Single().Name); + Assert.AreEqual("2", req.Headers.Single().Value); + } + + [Test] // #623 + public async Task header_values_are_trimmed() { + var c = CreateContainer(); + c.WithHeader("a", " 1 \t\r\n"); + c.Headers.Add("b", " 2 "); + + var req = GetRequest(c); + Assert.AreEqual(2, req.Headers.Count); + Assert.AreEqual("1", req.Headers[0].Value); + // Not trimmed when added directly to Headers collection (implementation seemed like overkill), + // but below we'll make sure it happens on HttpRequestMessage when request is sent. + Assert.AreEqual(" 2 ", req.Headers[1].Value); + + using var test = new HttpTest(); + await GetRequest(c).GetAsync(); + var sentHeaders = test.CallLog[0].HttpRequestMessage.Headers; + Assert.AreEqual("1", sentHeaders.GetValues("a").Single()); + Assert.AreEqual("2", sentHeaders.GetValues("b").Single()); + } + + [Test] + public void can_setup_oauth_bearer_token() { + var c = CreateContainer(); + c.WithOAuthBearerToken("mytoken"); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("Authorization", "Bearer mytoken")); + } + + [Test] + public void can_setup_basic_auth() { + var c = CreateContainer(); + c.WithBasicAuth("user", "pass"); + + var req = GetRequest(c); + Assert.AreEqual(1, req.Headers.Count); + Assert.IsTrue(req.Headers.Contains("Authorization", "Basic dXNlcjpwYXNz")); + } + } + + [TestFixture] + public class RequestHeadersTests : HeadersTestsBase + { + protected override IFlurlRequest CreateContainer() => new FlurlRequest("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; + } + + [TestFixture] + public class ClientHeadersTests : HeadersTestsBase + { + protected override IFlurlClient CreateContainer() => new FlurlClient(); + protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + } + + [TestFixture] + public class ClientBuilderHeadersTests : HeadersTestsBase + { + protected override IFlurlClientBuilder CreateContainer() => new FlurlClientBuilder(); + protected override IFlurlRequest GetRequest(IFlurlClientBuilder builder) => builder.Build().Request("http://api.com"); + } +} diff --git a/test/Flurl.Test/Http/SettingsExtensionsTests.cs b/test/Flurl.Test/Http/SettingsExtensionsTests.cs deleted file mode 100644 index 96cb170e..00000000 --- a/test/Flurl.Test/Http/SettingsExtensionsTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Flurl.Http; -using Flurl.Http.Testing; -using NUnit.Framework; - -namespace Flurl.Test.Http -{ - // IFlurlClient and IFlurlRequest both implement IHttpSettingsContainer, which defines a number - // of settings-related extension methods. This abstract test class allows those methods to be - // tested against both both client-level and request-level implementations. - public abstract class SettingsExtensionsTests where T : IHttpSettingsContainer - { - protected abstract T GetSettingsContainer(); - protected abstract IFlurlRequest GetRequest(T sc); - - [Test] - public void can_set_timeout() { - var sc = GetSettingsContainer().WithTimeout(TimeSpan.FromSeconds(15)); - Assert.AreEqual(TimeSpan.FromSeconds(15), sc.Settings.Timeout); - } - - [Test] - public void can_set_timeout_in_seconds() { - var sc = GetSettingsContainer().WithTimeout(15); - Assert.AreEqual(sc.Settings.Timeout, TimeSpan.FromSeconds(15)); - } - - [Test] - public void can_set_header() { - var sc = GetSettingsContainer().WithHeader("a", 1); - Assert.AreEqual(("a", "1"), sc.Headers.Single()); - } - - [Test] - public void can_set_headers_from_anon_object() { - // null values shouldn't be added - var sc = GetSettingsContainer().WithHeaders(new { a = "b", one = 2, three = (object)null }); - Assert.AreEqual(2, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("a", "b")); - Assert.IsTrue(sc.Headers.Contains("one", "2")); - } - - [Test] - public void can_remove_header_by_setting_null() { - var sc = GetSettingsContainer().WithHeaders(new { a = 1, b = 2 }); - Assert.AreEqual(2, sc.Headers.Count); - sc.WithHeader("b", null); - Assert.AreEqual(1, sc.Headers.Count); - Assert.IsFalse(sc.Headers.Contains("b")); - } - - [Test] - public void can_set_headers_from_dictionary() { - var sc = GetSettingsContainer().WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); - Assert.AreEqual(2, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("a", "b")); - Assert.IsTrue(sc.Headers.Contains("one", "2")); - } - - [Test] - public void underscores_in_properties_convert_to_hyphens_in_header_names() { - var sc = GetSettingsContainer().WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" }); - Assert.IsTrue(sc.Headers.Contains("User-Agent")); - Assert.IsTrue(sc.Headers.Contains("Cache-Control")); - - // make sure we can disable the behavior - sc.WithHeaders(new { no_i_really_want_underscores = "foo" }, false); - Assert.IsTrue(sc.Headers.Contains("no_i_really_want_underscores")); - - // dictionaries don't get this behavior since you can use hyphens explicitly - sc.WithHeaders(new Dictionary { { "exclude_dictionaries", "bar" } }); - Assert.IsTrue(sc.Headers.Contains("exclude_dictionaries")); - - // same with strings - sc.WithHeaders("exclude_strings=123"); - Assert.IsTrue(sc.Headers.Contains("exclude_strings")); - } - - [Test] - public void header_names_are_case_insensitive() { - var sc = GetSettingsContainer().WithHeader("a", 1).WithHeader("A", 2); - Assert.AreEqual(1, sc.Headers.Count); - Assert.AreEqual("A", sc.Headers.Single().Name); - Assert.AreEqual("2", sc.Headers.Single().Value); - } - - [Test] // #623 - public async Task header_values_are_trimmed() { - var sc = GetSettingsContainer().WithHeader("a", " 1 \t\r\n"); - sc.Headers.Add("b", " 2 "); - - Assert.AreEqual(2, sc.Headers.Count); - Assert.AreEqual("1", sc.Headers[0].Value); - // Not trimmed when added directly to Headers collection (implementation seemed like overkill), - // but below we'll make sure it happens on HttpRequestMessage when request is sent. - Assert.AreEqual(" 2 ", sc.Headers[1].Value); - - using (var test = new HttpTest()) { - await GetRequest(sc).GetAsync(); - var sentHeaders = test.CallLog[0].HttpRequestMessage.Headers; - Assert.AreEqual("1", sentHeaders.GetValues("a").Single()); - Assert.AreEqual("2", sentHeaders.GetValues("b").Single()); - } - } - - [Test] - public void can_setup_oauth_bearer_token() { - var sc = GetSettingsContainer().WithOAuthBearerToken("mytoken"); - Assert.AreEqual(1, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("Authorization", "Bearer mytoken")); - } - - [Test] - public void can_setup_basic_auth() { - var sc = GetSettingsContainer().WithBasicAuth("user", "pass"); - Assert.AreEqual(1, sc.Headers.Count); - Assert.IsTrue(sc.Headers.Contains("Authorization", "Basic dXNlcjpwYXNz")); - } - - [Test] - public async Task can_allow_specific_http_status() { - using var test = new HttpTest(); - test.RespondWith("Nothing to see here", 404); - var sc = GetSettingsContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); - await GetRequest(sc).DeleteAsync(); // no exception = pass - } - - [Test] - public async Task allow_specific_http_status_also_allows_2xx() { - using var test = new HttpTest(); - test.RespondWith("I'm just an innocent 2xx, I should never fail!", 201); - var sc = GetSettingsContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); - await GetRequest(sc).GetAsync(); // no exception = pass - } - - [Test] - public void can_clear_non_success_status() { - using var test = new HttpTest(); - test.RespondWith("I'm a teapot", 418); - // allow 4xx - var sc = GetSettingsContainer().AllowHttpStatus("4xx"); - // but then disallow it - sc.Settings.AllowedHttpStatusRange = null; - Assert.ThrowsAsync(async () => await GetRequest(sc).GetAsync()); - } - - [Test] - public async Task can_allow_any_http_status() { - using var test = new HttpTest(); - test.RespondWith("epic fail", 500); - try { - var sc = GetSettingsContainer().AllowAnyHttpStatus(); - var result = await GetRequest(sc).GetAsync(); - Assert.AreEqual(500, result.StatusCode); - } - catch (Exception) { - Assert.Fail("Exception should not have been thrown."); - } - } - } - - [TestFixture, Parallelizable] - public class ClientSettingsExtensionsTests : SettingsExtensionsTests - { - protected override IFlurlClient GetSettingsContainer() => new FlurlClient(); - protected override IFlurlRequest GetRequest(IFlurlClient client) => client.Request("http://api.com"); - - [Test] - public void WithUrl_shares_client_but_not_Url() { - var cli = new FlurlClient().WithHeader("myheader", "123"); - var req1 = cli.Request("http://www.api.com/for-req1"); - var req2 = cli.Request("http://www.api.com/for-req2"); - var req3 = cli.Request("http://www.api.com/for-req3"); - - CollectionAssert.AreEquivalent(req1.Headers, req2.Headers); - CollectionAssert.AreEquivalent(req1.Headers, req3.Headers); - var urls = new[] { req1, req2, req3 }.Select(c => c.Url.ToString()); - CollectionAssert.AllItemsAreUnique(urls); - } - - [Test] - public void can_use_uri_with_WithUrl() { - var uri = new System.Uri("http://www.mysite.com/foo?x=1"); - var req = new FlurlClient().Request(uri); - Assert.AreEqual(uri.ToString(), req.Url.ToString()); - } - - [Test] - public void can_override_settings_fluently() { - using (var test = new HttpTest()) { - var cli = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*"); - test.RespondWith("epic fail", 500); - var req = "http://www.api.com".WithSettings(c => c.AllowedHttpStatusRange = "2xx"); - req.Client = cli; // client-level settings shouldn't win - Assert.ThrowsAsync(async () => await req.GetAsync()); - } - } - } - - [TestFixture, Parallelizable] - public class RequestSettingsExtensionsTests : SettingsExtensionsTests - { - protected override IFlurlRequest GetSettingsContainer() => new FlurlRequest("http://api.com"); - protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; - } -} \ No newline at end of file diff --git a/test/Flurl.Test/Http/SettingsTests.cs b/test/Flurl.Test/Http/SettingsTests.cs index 64e2f7cd..9ccbe4da 100644 --- a/test/Flurl.Test/Http/SettingsTests.cs +++ b/test/Flurl.Test/Http/SettingsTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Net; using System.Threading.Tasks; using Flurl.Http; using Flurl.Http.Configuration; @@ -9,22 +10,23 @@ namespace Flurl.Test.Http { /// - /// FlurlHttpSettings are available at the test, client, and request level. This abstract class - /// allows the same tests to be run against settings at all 4 levels. + /// A Settings collection is available on IFlurlRequest, IFlurlClient, IFlurlBuilder, and HttpTest. + /// This abstract class allows the same tests to be run against all 4. /// - public abstract class SettingsTestsBase + public abstract class SettingsTestsBase where T : ISettingsContainer { - protected abstract FlurlHttpSettings GetSettings(); - protected abstract IFlurlRequest GetRequest(); + protected abstract T CreateContainer(); + protected abstract IFlurlRequest GetRequest(T container); [Test] public async Task can_set_http_version() { - Assert.AreEqual("1.1", GetSettings().HttpVersion); // default - using var test = new HttpTest(); - GetSettings().HttpVersion = "2.0"; - var req = GetRequest(); + var c = CreateContainer(); + Assert.AreEqual("1.1", c.Settings.HttpVersion); // default + + c.Settings.HttpVersion = "2.0"; + var req = GetRequest(c); Assert.AreEqual("2.0", req.Settings.HttpVersion); Version versionUsed = null; @@ -37,17 +39,18 @@ await req [Test] public void cannot_set_invalid_http_version() { - Assert.Throws(() => GetSettings().HttpVersion = "foo"); + Assert.Throws(() => CreateContainer().Settings.HttpVersion = "foo"); } [Test] public async Task can_allow_non_success_status() { using var test = new HttpTest(); - GetSettings().AllowedHttpStatusRange = "4xx"; + var c = CreateContainer(); + c.Settings.AllowedHttpStatusRange = "4xx"; test.RespondWith("I'm a teapot", 418); try { - var result = await GetRequest().GetAsync(); + var result = await GetRequest(c).GetAsync(); Assert.AreEqual(418, result.StatusCode); } catch (Exception) { @@ -61,12 +64,13 @@ public async Task can_set_pre_callback() { using var test = new HttpTest(); test.RespondWith("ok"); - GetSettings().BeforeCall = call => { + var c = CreateContainer(); + c.Settings.BeforeCall = call => { Assert.Null(call.Response); // verifies that callback is running before HTTP call is made callbackCalled = true; }; Assert.IsFalse(callbackCalled); - await GetRequest().GetAsync(); + await GetRequest(c).GetAsync(); Assert.IsTrue(callbackCalled); } @@ -76,12 +80,13 @@ public async Task can_set_post_callback() { using var test = new HttpTest(); test.RespondWith("ok"); - GetSettings().AfterCall = call => { + var c = CreateContainer(); + c.Settings.AfterCall = call => { Assert.NotNull(call.Response); // verifies that callback is running after HTTP call is made callbackCalled = true; }; Assert.IsFalse(callbackCalled); - await GetRequest().GetAsync(); + await GetRequest(c).GetAsync(); Assert.IsTrue(callbackCalled); } @@ -92,14 +97,15 @@ public async Task can_set_error_callback(bool markExceptionHandled) { using var test = new HttpTest(); test.RespondWith("server error", 500); - GetSettings().OnError = call => { + var c = CreateContainer(); + c.Settings.OnError = call => { Assert.NotNull(call.Response); // verifies that callback is running after HTTP call is made callbackCalled = true; call.ExceptionHandled = markExceptionHandled; }; Assert.IsFalse(callbackCalled); try { - await GetRequest().GetAsync(); + await GetRequest(c).GetAsync(); Assert.IsTrue(callbackCalled, "OnError was never called"); Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); } @@ -113,12 +119,13 @@ public async Task can_set_error_callback(bool markExceptionHandled) { public async Task can_disable_exception_behavior() { using var test = new HttpTest(); - GetSettings().OnError = call => { + var c = CreateContainer(); + c.Settings.OnError = call => { call.ExceptionHandled = true; }; test.RespondWith("server error", 500); try { - var result = await GetRequest().GetAsync(); + var result = await GetRequest(c).GetAsync(); Assert.AreEqual(500, result.StatusCode); } catch (FlurlHttpException) { @@ -128,22 +135,27 @@ public async Task can_disable_exception_behavior() { [Test] public void can_reset_defaults() { - GetSettings().JsonSerializer = null; - GetSettings().Redirects.Enabled = false; - GetSettings().BeforeCall = (call) => Console.WriteLine("Before!"); - GetSettings().Redirects.MaxAutoRedirects = 5; + var c = CreateContainer(); + + c.Settings.JsonSerializer = null; + c.Settings.Redirects.Enabled = false; + c.Settings.BeforeCall = (call) => Console.WriteLine("Before!"); + c.Settings.Redirects.MaxAutoRedirects = 5; - Assert.IsNull(GetSettings().JsonSerializer); - Assert.IsFalse(GetSettings().Redirects.Enabled); - Assert.IsNotNull(GetSettings().BeforeCall); - Assert.AreEqual(5, GetSettings().Redirects.MaxAutoRedirects); + var req = GetRequest(c); - GetSettings().ResetDefaults(); + Assert.IsNull(req.Settings.JsonSerializer); + Assert.IsFalse(req.Settings.Redirects.Enabled); + Assert.IsNotNull(req.Settings.BeforeCall); + Assert.AreEqual(5, req.Settings.Redirects.MaxAutoRedirects); - Assert.That(GetSettings().JsonSerializer is DefaultJsonSerializer); - Assert.IsTrue(GetSettings().Redirects.Enabled); - Assert.IsNull(GetSettings().BeforeCall); - Assert.AreEqual(10, GetSettings().Redirects.MaxAutoRedirects); + c.Settings.ResetDefaults(); + req = GetRequest(c); + + Assert.That(req.Settings.JsonSerializer is DefaultJsonSerializer); + Assert.IsTrue(req.Settings.Redirects.Enabled); + Assert.IsNull(req.Settings.BeforeCall); + Assert.AreEqual(10, req.Settings.Redirects.MaxAutoRedirects); } [Test] // #256 @@ -160,21 +172,110 @@ public async Task explicit_content_type_header_is_not_overridden() { Assert.AreEqual(new[] { "application/json-patch+json; utf-8" }, h.GetValues("Content-Type")); Assert.AreEqual(new[] { "10" }, h.GetValues("Content-Length")); } + + [Test] + public void can_set_timeout() { + var c = CreateContainer().WithTimeout(TimeSpan.FromSeconds(15)); + var req = GetRequest(c); + Assert.AreEqual(TimeSpan.FromSeconds(15), req.Settings.Timeout); + } + + [Test] + public void can_set_timeout_in_seconds() { + var c = CreateContainer().WithTimeout(15); + var req = GetRequest(c); + Assert.AreEqual(req.Settings.Timeout, TimeSpan.FromSeconds(15)); + } + + [Test] + public async Task can_allow_specific_http_status() { + using var test = new HttpTest(); + test.RespondWith("Nothing to see here", 404); + var c = CreateContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); + await GetRequest(c).DeleteAsync(); // no exception = pass + } + + [Test] + public async Task allow_specific_http_status_also_allows_2xx() { + using var test = new HttpTest(); + test.RespondWith("I'm just an innocent 2xx, I should never fail!", 201); + var c = CreateContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); + await GetRequest(c).GetAsync(); // no exception = pass + } + + [Test] + public void can_clear_non_success_status() { + using var test = new HttpTest(); + test.RespondWith("I'm a teapot", 418); + // allow 4xx + var c = CreateContainer().AllowHttpStatus("4xx"); + // but then disallow it + c.Settings.AllowedHttpStatusRange = null; + Assert.ThrowsAsync(async () => await GetRequest(c).GetAsync()); + } + + [Test] + public async Task can_allow_any_http_status() { + using var test = new HttpTest(); + test.RespondWith("epic fail", 500); + try { + var c = CreateContainer().AllowAnyHttpStatus(); + var result = await GetRequest(c).GetAsync(); + Assert.AreEqual(500, result.StatusCode); + } + catch (Exception) { + Assert.Fail("Exception should not have been thrown."); + } + } } [TestFixture] - public class HttpTestSettingsTests : SettingsTestsBase + public class RequestSettingsTests : SettingsTestsBase { - private HttpTest _test; + protected override IFlurlRequest CreateContainer() => new FlurlRequest("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; - [SetUp] - public void CreateTest() => _test = new HttpTest(); + [Test] + public void request_gets_default_settings_when_no_client() { + var req = new FlurlRequest(); + Assert.IsNull(req.Client); + Assert.IsNull(req.Url); + Assert.IsInstanceOf(req.Settings.JsonSerializer); + } - [TearDown] - public void DisposeTest() => _test.Dispose(); + [Test] + public void can_override_settings_fluently() { + using var test = new HttpTest(); + var cli = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*"); + test.RespondWith("epic fail", 500); + var req = "http://www.api.com".WithSettings(c => c.AllowedHttpStatusRange = "2xx"); + req.Client = cli; // client-level settings shouldn't win + Assert.ThrowsAsync(async () => await req.GetAsync()); + } + } - protected override FlurlHttpSettings GetSettings() => HttpTest.Current.Settings; - protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com"); + [TestFixture] + public class ClientSettingsTests : SettingsTestsBase + { + protected override IFlurlClient CreateContainer() => new FlurlClient(); + protected override IFlurlRequest GetRequest(IFlurlClient cli) => cli.Request("http://api.com"); + } + + [TestFixture] + public class ClientBuilderSettingsTests : SettingsTestsBase + { + protected override IFlurlClientBuilder CreateContainer() => new FlurlClientBuilder(); + protected override IFlurlRequest GetRequest(IFlurlClientBuilder builder) => builder.Build().Request("http://api.com"); + } + + [TestFixture] + public class HttpTestSettingsTests : SettingsTestsBase + { + protected override HttpTest CreateContainer() => HttpTest.Current ?? new HttpTest(); + protected override IFlurlRequest GetRequest(HttpTest container) => new FlurlRequest("http://api.com"); + + [TearDown] + public void DisposeTest() => HttpTest.Current?.Dispose(); [Test] // #246 public void test_settings_dont_override_request_settings_when_not_set_explicitily() { @@ -201,30 +302,4 @@ private class FakeSerializer : ISerializer public T Deserialize(Stream stream) => default; } } - - [TestFixture] - public class ClientSettingsTests : SettingsTestsBase - { - private readonly Lazy _client = new Lazy(() => new FlurlClient()); - - protected override FlurlHttpSettings GetSettings() => _client.Value.Settings; - protected override IFlurlRequest GetRequest() => _client.Value.Request("http://api.com"); - } - - [TestFixture] - public class RequestSettingsTests : SettingsTestsBase - { - private readonly Lazy _req = new Lazy(() => new FlurlRequest("http://api.com")); - - protected override FlurlHttpSettings GetSettings() => _req.Value.Settings; - protected override IFlurlRequest GetRequest() => _req.Value; - - [Test] - public void request_gets_default_settings_when_no_client() { - var req = new FlurlRequest(); - Assert.IsNull(req.Client); - Assert.IsNull(req.Url); - Assert.IsInstanceOf(req.Settings.JsonSerializer); - } - } }