not-so-minor overhaul of cookies

This commit is contained in:
Todd Menier 2016-06-24 16:04:35 -05:00
parent 66515e9906
commit c7bc34600f
10 changed files with 182 additions and 85 deletions

View File

@ -94,7 +94,7 @@ namespace Flurl.Test.Http
{
using (var test = new HttpTest())
{
test.RespondWith(404, "Nothing to see here");
test.RespondWith("Nothing to see here", 404);
// no exception = pass
await "http://www.api.com"
.AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound)
@ -107,7 +107,7 @@ namespace Flurl.Test.Http
{
using (var test = new HttpTest())
{
test.RespondWith(418, "I'm a teapot");
test.RespondWith("I'm a teapot", 418);
// allow 4xx
var client = "http://www.api.com".AllowHttpStatus("4xx");
// but then disallow it
@ -121,7 +121,7 @@ namespace Flurl.Test.Http
{
using (var test = new HttpTest())
{
test.RespondWith(500, "epic fail");
test.RespondWith("epic fail", 500);
try
{
var result = await "http://www.api.com".AllowAnyHttpStatus().GetAsync();
@ -140,7 +140,7 @@ namespace Flurl.Test.Http
using (var test = new HttpTest())
{
FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "*";
test.RespondWith(500, "epic fail");
test.RespondWith("epic fail", 500);
Assert.ThrowsAsync<FlurlHttpException>(async () => await "http://www.api.com".ConfigureClient(c => c.AllowedHttpStatusRange = "2xx").GetAsync());
}
}
@ -153,9 +153,9 @@ namespace Flurl.Test.Http
var client3 = client1.WithUrl("http://www.api.com/for-client3");
var client4 = client2.WithUrl("http://www.api.com/for-client4");
CollectionAssert.AreEquivalent(client1.GetCookies(), client2.GetCookies());
CollectionAssert.AreEquivalent(client1.GetCookies(), client3.GetCookies());
CollectionAssert.AreEquivalent(client1.GetCookies(), client4.GetCookies());
CollectionAssert.AreEquivalent(client1.Cookies, client2.Cookies);
CollectionAssert.AreEquivalent(client1.Cookies, client3.Cookies);
CollectionAssert.AreEquivalent(client1.Cookies, client4.Cookies);
var urls = new[] { client1, client2, client3, client4 }.Select(c => c.Url.ToString());
CollectionAssert.AllItemsAreUnique(urls);
}
@ -171,11 +171,11 @@ namespace Flurl.Test.Http
client2.Dispose();
client3.Dispose();
CollectionAssert.IsEmpty(client2.GetCookies());
CollectionAssert.IsEmpty(client3.GetCookies());
CollectionAssert.IsEmpty(client2.Cookies);
CollectionAssert.IsEmpty(client3.Cookies);
CollectionAssert.IsNotEmpty(client1.GetCookies());
CollectionAssert.IsNotEmpty(client4.GetCookies());
CollectionAssert.IsNotEmpty(client1.Cookies);
CollectionAssert.IsNotEmpty(client4.Cookies);
}
[Test]
@ -186,9 +186,9 @@ namespace Flurl.Test.Http
var client3 = "http://www.api.com/for-client3".WithClient(client1);
var client4 = "http://www.api.com/for-client4".WithClient(client1);
CollectionAssert.AreEquivalent(client1.GetCookies(), client2.GetCookies());
CollectionAssert.AreEquivalent(client1.GetCookies(), client3.GetCookies());
CollectionAssert.AreEquivalent(client1.GetCookies(), client4.GetCookies());
CollectionAssert.AreEquivalent(client1.Cookies, client2.Cookies);
CollectionAssert.AreEquivalent(client1.Cookies, client3.Cookies);
CollectionAssert.AreEquivalent(client1.Cookies, client4.Cookies);
var urls = new[] { client1, client2, client3, client4 }.Select(c => c.Url.ToString());
CollectionAssert.AllItemsAreUnique(urls);
}
@ -204,11 +204,11 @@ namespace Flurl.Test.Http
client2.Dispose();
client3.Dispose();
CollectionAssert.IsEmpty(client2.GetCookies());
CollectionAssert.IsEmpty(client3.GetCookies());
CollectionAssert.IsEmpty(client2.Cookies);
CollectionAssert.IsEmpty(client3.Cookies);
CollectionAssert.IsNotEmpty(client1.GetCookies());
CollectionAssert.IsNotEmpty(client4.GetCookies());
CollectionAssert.IsNotEmpty(client1.Cookies);
CollectionAssert.IsNotEmpty(client4.Cookies);
}
[Test]

View File

@ -14,7 +14,7 @@ namespace Flurl.Test.Http
var fcExts = ReflectionHelper.GetAllExtensionMethods<FlurlClient>(typeof(FlurlClient).GetTypeInfo().Assembly);
var urlExts = ReflectionHelper.GetAllExtensionMethods<Url>(typeof(FlurlClient).GetTypeInfo().Assembly);
var stringExts = ReflectionHelper.GetAllExtensionMethods<string>(typeof(FlurlClient).GetTypeInfo().Assembly);
var whitelist = new[] { "GetCookies", "WithUrl" }; // cases where Url method of the same name was excluded intentionally
var whitelist = new[] { "WithUrl" }; // cases where Url method of the same name was excluded intentionally
foreach (var method in fcExts) {
if (whitelist.Contains(method.Name))

View File

@ -85,7 +85,7 @@ namespace Flurl.Test.Http
[Test]
public async Task failure_throws_detailed_exception() {
HttpTest.RespondWith(500, "bad job");
HttpTest.RespondWith("bad job", status: 500);
try {
await "http://api.com".GetStringAsync();
@ -101,7 +101,7 @@ namespace Flurl.Test.Http
[Test]
public async Task can_get_error_json_typed() {
HttpTest.RespondWithJson(500, new { code = 999, message = "our server crashed" });
HttpTest.RespondWithJson(new { code = 999, message = "our server crashed" }, 500);
try {
await "http://api.com".GetStringAsync();
@ -116,7 +116,7 @@ namespace Flurl.Test.Http
[Test]
public async Task can_get_error_json_untyped() {
HttpTest.RespondWithJson(500, new { code = 999, message = "our server crashed" });
HttpTest.RespondWithJson(new { code = 999, message = "our server crashed" }, 500);
try {
await "http://api.com".GetStringAsync();

View File

@ -1,5 +1,4 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Flurl.Http;
@ -41,7 +40,7 @@ namespace Flurl.Test.Http
public async Task can_allow_non_success_status() {
using (var test = new HttpTest()) {
GetSettings().AllowedHttpStatusRange = "4xx";
test.RespondWith(418, "I'm a teapot");
test.RespondWith("I'm a teapot", 418);
try {
var result = await GetClient().GetAsync();
Assert.IsFalse(result.IsSuccessStatusCode);
@ -87,7 +86,7 @@ namespace Flurl.Test.Http
public async Task can_set_error_callback(bool markExceptionHandled) {
var callbackCalled = false;
using (var test = new HttpTest()) {
test.RespondWith(500, "server error");
test.RespondWith("server error", 500);
GetSettings().OnError = call => {
CollectionAssert.IsEmpty(test.ResponseQueue); // verifies that callback is running after HTTP call is made
callbackCalled = true;
@ -112,7 +111,7 @@ namespace Flurl.Test.Http
GetSettings().OnError = call => {
call.ExceptionHandled = true;
};
test.RespondWith(500, "server error");
test.RespondWith("server error", 500);
try {
var result = await GetClient().GetAsync();
Assert.IsFalse(result.IsSuccessStatusCode);

View File

@ -27,7 +27,7 @@ namespace Flurl.Test.Http
}
[Test]
public async Task can_set_cookies() {
public async Task can_set_request_cookies() {
var resp = await "http://httpbin.org/cookies".WithCookies(new { x = 1, y = 2 }).GetJsonAsync();
// httpbin returns json representation of cookies that were set on the server.
@ -35,15 +35,29 @@ namespace Flurl.Test.Http
Assert.AreEqual("2", resp.cookies.y);
}
[Test]
public async Task can_set_cookies_before_setting_url() {
var fc = new FlurlClient().WithCookie("z", "999");
var resp = await fc.WithUrl("http://httpbin.org/cookies").GetJsonAsync();
Assert.AreEqual("999", resp.cookies.z);
}
[Test]
public async Task can_get_response_cookies() {
var fc = new FlurlClient().EnableCookies();
await fc.WithUrl("https://httpbin.org/cookies/set?z=999").HeadAsync();
Assert.AreEqual("999", fc.Cookies["z"].Value);
}
[Test]
public async Task cant_persist_cookies_without_resuing_client() {
var fc = "http://httpbin.org/cookies".WithCookie("z", 999);
// cookie should be set
Assert.AreEqual("999", fc.GetCookies()["z"].Value);
Assert.AreEqual("999", fc.Cookies["z"].Value);
await fc.HeadAsync();
// FlurlClient was auto-disposed, so cookie should be gone
Assert.IsFalse(fc.GetCookies().ContainsKey("z"));
CollectionAssert.IsEmpty(fc.Cookies);
// httpbin returns json representation of cookies that were set on the server.
var resp = await "http://httpbin.org/cookies".GetJsonAsync();
@ -55,13 +69,13 @@ namespace Flurl.Test.Http
using (var fc = new FlurlClient()) {
var fc2 = "http://httpbin.org/cookies".WithClient(fc).WithCookie("z", 999);
// cookie should be set
Assert.AreEqual("999", fc.GetCookies()["z"].Value);
Assert.AreEqual("999", fc2.GetCookies()["z"].Value);
Assert.AreEqual("999", fc.Cookies["z"].Value);
Assert.AreEqual("999", fc2.Cookies["z"].Value);
await fc2.HeadAsync();
// FlurlClient should be re-used, so cookie should stick
Assert.AreEqual("999", fc.GetCookies()["z"].Value);
Assert.AreEqual("999", fc2.GetCookies()["z"].Value);
Assert.AreEqual("999", fc.Cookies["z"].Value);
Assert.AreEqual("999", fc2.Cookies["z"].Value);
// httpbin returns json representation of cookies that were set on the server.
var resp = await "http://httpbin.org/cookies".WithClient(fc).GetJsonAsync();

View File

@ -75,5 +75,25 @@ namespace Flurl.Test.Http
StringAssert.Contains("timed out", ex.Message);
}
}
[Test]
public async Task can_fake_headers() {
HttpTest.RespondWith(headers: new { h1 = "foo" });
var resp = await "http://www.api.com".GetAsync();
Assert.AreEqual(1, resp.Headers.Count());
Assert.AreEqual("h1", resp.Headers.First().Key);
Assert.AreEqual("foo", resp.Headers.First().Value.First());
}
[Test]
public async Task can_fake_cookies() {
HttpTest.RespondWith(cookies: new { c1 = "foo" });
var fc = "http://www.api.com".EnableCookies();
await fc.GetAsync();
Assert.AreEqual(1, fc.Cookies.Count());
Assert.AreEqual("foo", fc.Cookies["c1"].Value);
}
}
}

View File

@ -28,6 +28,11 @@ namespace Flurl.Http.Configuration
/// </summary>
public string AllowedHttpStatusRange { get; set; }
/// <summary>
/// Gets or sets a value indicating whether cookies should be sent/received with each HTTP request.
/// </summary>
public bool CookiesEnabled { get; set; }
/// <summary>
/// Gets or sets a factory used to create HttpClient object used in Flurl HTTP calls. Default value
/// is an instance of DefaultHttpClientFactory. Custom factory implementations should generally
@ -84,6 +89,7 @@ namespace Flurl.Http.Configuration
public void ResetDefaults() {
DefaultTimeout = new HttpClient().Timeout;
AllowedHttpStatusRange = null;
CookiesEnabled = false;
HttpClientFactory = new DefaultHttpClientFactory();
JsonSerializer = new NewtonsoftJsonSerializer(null);
UrlEncodedSerializer = new DefaultUrlEncodedSerializer();

View File

@ -12,18 +12,14 @@ namespace Flurl.Http
/// </summary>
public static class CookieExtensions
{
/// <summary>
/// Gets a collection of cookies that will be sent in calls using this client. (Use FlurlClient.WithCookie/WithCookies to set cookies.)
/// </summary>
public static Dictionary<string, Cookie> GetCookies(this FlurlClient client) {
return GetCookieContainer(client)?.GetCookies(client.HttpClient.BaseAddress).Cast<Cookie>().ToDictionary(c => c.Name, c => c);
}
/// <summary>
/// Allows cookies to be sent and received in calls made with this client. Not necessary to call when setting cookies via WithCookie/WithCookies.
/// </summary>
public static FlurlClient EnableCookies(this FlurlClient client) {
GetCookieContainer(client); // ensures the container has been created
client.Settings.CookiesEnabled = true;
return client;
}
@ -49,7 +45,8 @@ namespace Flurl.Http
/// <returns>The modified FlurlClient.</returns>
/// <exception cref="ArgumentNullException"><paramref name="cookie" /> is null.</exception>
public static FlurlClient WithCookie(this FlurlClient client, Cookie cookie) {
GetCookieContainer(client).Add(client.HttpClient.BaseAddress, cookie);
client.Settings.CookiesEnabled = true;
client.Cookies[cookie.Name] = cookie;
return client;
}
@ -75,7 +72,6 @@ namespace Flurl.Http
return new FlurlClient(url, true).WithCookie(cookie);
}
/// <summary>
/// Sets an HTTP cookie to be sent with all requests made with this FlurlClient.
/// </summary>
@ -86,7 +82,8 @@ namespace Flurl.Http
/// <returns>The modified FlurlClient.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is null.</exception>
public static FlurlClient WithCookie(this FlurlClient client, string name, object value, DateTime? expires = null) {
return client.WithCookie(new Cookie(name, value?.ToInvariantString()) { Expires = expires ?? DateTime.MinValue });
var cookie = new Cookie(name, value?.ToInvariantString()) { Expires = expires ?? DateTime.MinValue };
return client.WithCookie(cookie);
}
/// <summary>
@ -156,16 +153,5 @@ namespace Flurl.Http
public static FlurlClient WithCookies(this string url, object cookies, DateTime? expires = null) {
return new FlurlClient(url, true).WithCookies(cookies);
}
private static CookieContainer GetCookieContainer(FlurlClient client) {
var handler = client.HttpMessageHandler as HttpClientHandler;
if (handler == null)
return null;
if (client.HttpClient.BaseAddress == null)
client.HttpClient.BaseAddress = new Uri(Url.GetRoot(client.Url));
return handler.CookieContainer ?? (handler.CookieContainer = new CookieContainer());
}
}
}

View File

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -59,6 +62,7 @@ namespace Flurl.Http
_parent = this,
Settings = Settings,
Url = Url,
Cookies = Cookies,
AutoDispose = AutoDispose
};
}
@ -77,6 +81,11 @@ namespace Flurl.Http
/// </summary>
public Url Url { get; set; }
/// <summary>
/// Collection of HttpCookies sent and received.
/// </summary>
public IDictionary<string, Cookie> Cookies { get; private set; } = new Dictionary<string, Cookie>();
/// <summary>
/// Gets a value indicating whether the underlying HttpClient
/// should be disposed immediately after the first HTTP call is made.
@ -109,14 +118,59 @@ namespace Flurl.Http
public async Task<HttpResponseMessage> SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken? cancellationToken = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) {
try {
var request = new HttpRequestMessage(verb, Url) { Content = content };
if (Settings.CookiesEnabled)
WriteRequestCookies(request);
HttpCall.Set(request, Settings);
return await HttpClient.SendAsync(request, completionOption, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);
var resp = await HttpClient.SendAsync(request, completionOption, cancellationToken ?? CancellationToken.None).ConfigureAwait(false);
if (Settings.CookiesEnabled)
ReadResponseCookies(resp);
return resp;
}
finally {
if (AutoDispose) Dispose();
}
}
private void WriteRequestCookies(HttpRequestMessage request) {
if (!Cookies.Any()) return;
var uri = request.RequestUri;
var cookieHandler = HttpMessageHandler as HttpClientHandler;
// if the inner handler is an HttpClientHandler (which it usually is), put the cookies in the CookieContainer.
if (cookieHandler != null) {
if (cookieHandler.CookieContainer == null)
cookieHandler.CookieContainer = new CookieContainer();
foreach (var cookie in Cookies.Values)
cookieHandler.CookieContainer.Add(uri, cookie);
}
else {
// http://stackoverflow.com/a/15588878/62600
request.Headers.TryAddWithoutValidation("Cookie", string.Join("; ", Cookies.Values));
}
}
private void ReadResponseCookies(HttpResponseMessage response) {
var uri = response.RequestMessage.RequestUri;
// if the inner handler is an HttpClientHandler (which it usually is), it's already plucked the
// cookies out of the headers and put them in the CookieContainer.
var jar = (HttpMessageHandler as HttpClientHandler)?.CookieContainer;
if (jar == null) {
// http://stackoverflow.com/a/15588878/62600
IEnumerable<string> cookieHeaders;
if (!response.Headers.TryGetValues("Set-Cookie", out cookieHeaders))
return;
jar = new CookieContainer();
foreach (string header in cookieHeaders) {
jar.SetCookies(uri, header);
}
}
foreach (var cookie in jar.GetCookies(uri).Cast<Cookie>())
Cookies[cookie.Name] = cookie;
}
/// <summary>
/// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated
/// to FlurlHttp.HttpClientFactory.
@ -142,6 +196,7 @@ namespace Flurl.Http
_httpClient?.Dispose();
_httpMessageHandler = null;
_httpClient = null;
Cookies = new Dictionary<string, Cookie>();
}
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using Flurl.Http.Content;
using Flurl.Util;
namespace Flurl.Http.Testing
{
@ -32,41 +33,57 @@ namespace Flurl.Http.Testing
}
/// <summary>
/// Adds an HttpResponseMessage to the response queue with the given HTTP status code and content body.
/// Adds an HttpResponseMessage to the response queue.
/// </summary>
public HttpTest RespondWith(int status, string body) {
ResponseQueue.Enqueue(new HttpResponseMessage {
/// <param name="body">The simulated response body string.</param>
/// <param name="status">The simulated HTTP status. Default is 200.</param>
/// <param name="headers">The simulated response headers (optional).</param>
/// <param name="cookies">The simulated response cookies (optional).</param>
/// <returns>The current HttpTest object (so more responses can be chained).</returns>
public HttpTest RespondWith(string body, int status = 200, object headers = null, object cookies = null) {
return RespondWith(new StringContent(body), status, headers, cookies);
}
/// <summary>
/// Adds an HttpResponseMessage to the response queue with the given data serialized to JSON as the content body.
/// </summary>
/// <param name="body">The object to be JSON-serialized and used as the simulated response body.</param>
/// <param name="status">The simulated HTTP status. Default is 200.</param>
/// <param name="headers">The simulated response headers (optional).</param>
/// <param name="cookies">The simulated response cookies (optional).</param>
/// <returns>The current HttpTest object (so more responses can be chained).</returns>
public HttpTest RespondWithJson(object body, int status = 200, object headers = null, object cookies = null) {
var content = new CapturedJsonContent(FlurlHttp.GlobalSettings.JsonSerializer.Serialize(body));
return RespondWith(content, status, headers, cookies);
}
/// <summary>
/// Adds an HttpResponseMessage to the response queue.
/// </summary>
/// <param name="content">The simulated response body content (optional).</param>
/// <param name="status">The simulated HTTP status. Default is 200.</param>
/// <param name="headers">The simulated response headers (optional).</param>
/// <param name="cookies">The simulated response cookies (optional).</param>
/// <returns>The current HttpTest object (so more responses can be chained).</returns>
public HttpTest RespondWith(HttpContent content = null, int status = 200, object headers = null, object cookies = null) {
var response = new HttpResponseMessage {
StatusCode = (HttpStatusCode)status,
Content = new StringContent(body)
});
Content = content
};
if (headers != null) {
foreach (var kv in headers.ToKeyValuePairs())
response.Headers.Add(kv.Key, kv.Value.ToInvariantString());
}
if (cookies != null) {
foreach (var kv in cookies.ToKeyValuePairs()) {
var value = new Cookie(kv.Key, kv.Value.ToInvariantString()).ToString();
response.Headers.Add("Set-Cookie", value);
}
}
ResponseQueue.Enqueue(response);
return this;
}
/// <summary>
/// Adds an HttpResponseMessage to the response queue with a 200 (OK) status code and the given content body.
/// </summary>
public HttpTest RespondWith(string body) {
return RespondWith(200, body);
}
/// <summary>
/// Adds an HttpResponseMessage to the response queue with the given HTTP status code and the given data serialized to JSON as the content body.
/// </summary>
public HttpTest RespondWithJson(int status, object data) {
ResponseQueue.Enqueue(new HttpResponseMessage {
StatusCode = (HttpStatusCode)status,
Content = new CapturedJsonContent(FlurlHttp.GlobalSettings.JsonSerializer.Serialize(data))
});
return this;
}
/// <summary>
/// Adds an HttpResponseMessage to the response queue with a 200 (OK) status code and the given data serialized to JSON as the content body.
/// </summary>
public HttpTest RespondWithJson(object data) {
return RespondWithJson(200, data);
}
/// <summary>
/// Adds a simulated timeout response to the response queue.
/// </summary>