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);
- }
- }
}