diff --git a/src/Microsoft.AspNet.Http.Abstractions/HttpContext.cs b/src/Microsoft.AspNet.Http.Abstractions/HttpContext.cs index 0ad675693c..1b853075da 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/HttpContext.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/HttpContext.cs @@ -17,22 +17,22 @@ namespace Microsoft.AspNet.Http public abstract ConnectionInfo Connection { get; } + public abstract WebSocketManager WebSockets { get; } + public abstract AuthenticationManager Authentication { get; } public abstract ClaimsPrincipal User { get; set; } - public abstract IDictionary Items { get; } + public abstract IDictionary Items { get; set; } public abstract IServiceProvider ApplicationServices { get; set; } public abstract IServiceProvider RequestServices { get; set; } - public abstract CancellationToken RequestAborted { get; } + public abstract CancellationToken RequestAborted { get; set; } public abstract ISessionCollection Session { get; } - public abstract WebSocketManager WebSockets { get; } - public abstract void Abort(); public abstract void Dispose(); diff --git a/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs b/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs index 80caec4502..4cbbf62a6f 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/HttpRequest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Http /// Returns true if the owin.RequestScheme is https. /// /// true if this request is using https; otherwise, false. - public abstract bool IsHttps { get; } + public abstract bool IsHttps { get; set; } /// /// Gets or set the Host header. May include the port. @@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Http /// Gets the query value collection parsed from owin.RequestQueryString. /// /// The query value collection parsed from owin.RequestQueryString. - public abstract IReadableStringCollection Query { get; } + public abstract IReadableStringCollection Query { get; set; } /// /// Gets or set the owin.RequestProtocol. @@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Http /// Gets the collection of Cookies for this request. /// /// The collection of Cookies for this request. - public abstract IReadableStringCollection Cookies { get; } + public abstract IReadableStringCollection Cookies { get; set; } /// /// Gets or sets the Content-Length header diff --git a/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs b/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs index 6d156bc7ee..7ad1300622 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/QueryString.cs @@ -111,6 +111,7 @@ namespace Microsoft.AspNet.Http /// /// The un-encoded parameter name /// The un-encoded parameter value + /// The resulting QueryString public static QueryString Create(string name, string value) { return new QueryString("?" + UrlEncoder.Default.UrlEncode(name) + '=' + UrlEncoder.Default.UrlEncode(value)); @@ -120,7 +121,7 @@ namespace Microsoft.AspNet.Http /// Creates a query string composed from the given name value pairs. /// /// - /// + /// The resulting QueryString public static QueryString Create(IEnumerable> parameters) { var builder = new StringBuilder(); @@ -137,6 +138,30 @@ namespace Microsoft.AspNet.Http return new QueryString(builder.ToString()); } + /// + /// Creates a query string composed from the given name value pairs. + /// + /// + /// The resulting QueryString + public static QueryString Create(IEnumerable> parameters) + { + var builder = new StringBuilder(); + bool first = true; + foreach (var pair in parameters) + { + foreach (var value in pair.Value) + { + builder.Append(first ? "?" : "&"); + first = false; + builder.Append(UrlEncoder.Default.UrlEncode(pair.Key)); + builder.Append("="); + builder.Append(UrlEncoder.Default.UrlEncode(value)); + } + } + + return new QueryString(builder.ToString()); + } + public QueryString Add(QueryString other) { if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) diff --git a/src/Microsoft.AspNet.Http.Features/IHttpRequestLifetimeFeature.cs b/src/Microsoft.AspNet.Http.Features/IHttpRequestLifetimeFeature.cs index b30704650e..e007c4a818 100644 --- a/src/Microsoft.AspNet.Http.Features/IHttpRequestLifetimeFeature.cs +++ b/src/Microsoft.AspNet.Http.Features/IHttpRequestLifetimeFeature.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Http.Features { public interface IHttpRequestLifetimeFeature { - CancellationToken RequestAborted { get; } + CancellationToken RequestAborted { get; set; } void Abort(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Constants.cs b/src/Microsoft.AspNet.Http/Constants.cs index 376bb55abd..d01466f549 100644 --- a/src/Microsoft.AspNet.Http/Constants.cs +++ b/src/Microsoft.AspNet.Http/Constants.cs @@ -5,7 +5,8 @@ namespace Microsoft.AspNet.Http.Internal { internal static class Constants { - internal const string Https = "HTTPS"; + internal const string Http = "http"; + internal const string Https = "https"; internal static class BuilderProperties { diff --git a/src/Microsoft.AspNet.Http/DefaultHttpContext.cs b/src/Microsoft.AspNet.Http/DefaultHttpContext.cs index 4c0174e5b1..6090eed1b9 100644 --- a/src/Microsoft.AspNet.Http/DefaultHttpContext.cs +++ b/src/Microsoft.AspNet.Http/DefaultHttpContext.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Http.Internal private IHttpRequestLifetimeFeature LifetimeFeature { - get { return _lifetime.Fetch(_features); } + get { return _lifetime.Fetch(_features) ?? _lifetime.Update(_features, new HttpRequestLifetimeFeature()); } } private ISessionFeature SessionFeature @@ -103,6 +103,7 @@ namespace Microsoft.AspNet.Http.Internal public override IDictionary Items { get { return ItemsFeature.Items; } + set { ItemsFeature.Items = value; } } public override IServiceProvider ApplicationServices @@ -117,19 +118,10 @@ namespace Microsoft.AspNet.Http.Internal set { ServiceProvidersFeature.RequestServices = value; } } - public int Revision { get { return _features.Revision; } } - public override CancellationToken RequestAborted { - get - { - var lifetime = LifetimeFeature; - if (lifetime != null) - { - return lifetime.RequestAborted; - } - return CancellationToken.None; - } + get { return LifetimeFeature.RequestAborted; } + set { LifetimeFeature.RequestAborted = value; } } public override ISessionCollection Session @@ -167,11 +159,7 @@ namespace Microsoft.AspNet.Http.Internal public override void Abort() { - var lifetime = LifetimeFeature; - if (lifetime != null) - { - lifetime.Abort(); - } + LifetimeFeature.Abort(); } public override void Dispose() diff --git a/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs b/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs index f06e2d396e..bf374999b9 100644 --- a/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNet.Http/DefaultHttpRequest.cs @@ -101,6 +101,7 @@ namespace Microsoft.AspNet.Http.Internal public override bool IsHttps { get { return string.Equals(Constants.Https, Scheme, StringComparison.OrdinalIgnoreCase); } + set { Scheme = value ? Constants.Https : Constants.Http; } } public override HostString Host @@ -112,6 +113,7 @@ namespace Microsoft.AspNet.Http.Internal public override IReadableStringCollection Query { get { return QueryFeature.Query; } + set { QueryFeature.Query = value; } } public override string Protocol @@ -128,6 +130,7 @@ namespace Microsoft.AspNet.Http.Internal public override IReadableStringCollection Cookies { get { return RequestCookiesFeature.Cookies; } + set { RequestCookiesFeature.Cookies = value; } } public override string ContentType diff --git a/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs b/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs new file mode 100644 index 0000000000..1b773b93f1 --- /dev/null +++ b/src/Microsoft.AspNet.Http/Features/HttpRequestLifetimeFeature.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Http.Features.Internal +{ + public class HttpRequestLifetimeFeature : IHttpRequestLifetimeFeature + { + public CancellationToken RequestAborted { get; set; } + + public void Abort() + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/IItemsFeature.cs b/src/Microsoft.AspNet.Http/Features/IItemsFeature.cs index 96b2ce78fe..a30c3ac6fb 100644 --- a/src/Microsoft.AspNet.Http/Features/IItemsFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/IItemsFeature.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNet.Http.Features.Internal { public interface IItemsFeature { - IDictionary Items { get; } + IDictionary Items { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs b/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs index a1143fb8cd..a814e38501 100644 --- a/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/IQueryFeature.cs @@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Http.Features.Internal { public interface IQueryFeature { - IReadableStringCollection Query { get; } + IReadableStringCollection Query { get; set; } } } diff --git a/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs b/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs index 6b2f7c742e..73f23d032d 100644 --- a/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/IRequestCookiesFeature.cs @@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Http.Features.Internal { public interface IRequestCookiesFeature { - IReadableStringCollection Cookies { get; } + IReadableStringCollection Cookies { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/ItemsFeature.cs b/src/Microsoft.AspNet.Http/Features/ItemsFeature.cs index fc839357a7..c80abe6d65 100644 --- a/src/Microsoft.AspNet.Http/Features/ItemsFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/ItemsFeature.cs @@ -13,6 +13,6 @@ namespace Microsoft.AspNet.Http.Features.Internal Items = new ItemsDictionary(); } - public IDictionary Items { get; private set; } + public IDictionary Items { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs index 9fb2bd4a4b..a46b6ecb83 100644 --- a/src/Microsoft.AspNet.Http/Features/QueryFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/QueryFeature.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http.Internal; @@ -41,13 +42,22 @@ namespace Microsoft.AspNet.Http.Features.Internal } var queryString = _request.Fetch(_features).QueryString; - if (_query == null || _queryString != queryString) + if (_query == null || !string.Equals(_queryString, queryString, StringComparison.Ordinal)) { _queryString = queryString; _query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString)); } return _query; } + set + { + _query = value; + if (_features != null) + { + _queryString = _query == null ? string.Empty : QueryString.Create(_query).ToString(); + _request.Fetch(_features).QueryString = _queryString; + } + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs index eed8fc2d95..e0ba1dcc3e 100644 --- a/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs +++ b/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs @@ -50,19 +50,37 @@ namespace Microsoft.AspNet.Http.Features.Internal values = new string[0]; } - if (_cookiesCollection == null) - { - _cookieHeaders = values; - _cookiesCollection = new RequestCookiesCollection(); - _cookiesCollection.Reparse(values); - } - else if (!Enumerable.SequenceEqual(_cookieHeaders, values, StringComparer.Ordinal)) + if (_cookieHeaders == null || !Enumerable.SequenceEqual(_cookieHeaders, values, StringComparer.Ordinal)) { _cookieHeaders = values; + if (_cookiesCollection == null) + { + _cookiesCollection = new RequestCookiesCollection(); + _cookies = _cookiesCollection; + } _cookiesCollection.Reparse(values); } - return _cookiesCollection; + return _cookies; + } + set + { + _cookies = value; + _cookieHeaders = null; + _cookiesCollection = _cookies as RequestCookiesCollection; + if (_cookies != null && _features != null) + { + var headers = new List(); + foreach (var pair in _cookies) + { + foreach (var cookieValue in pair.Value) + { + headers.Add(new CookieHeaderValue(pair.Key, cookieValue).ToString()); + } + } + _cookieHeaders = headers.ToArray(); + _request.Fetch(_features).Headers[HeaderNames.Cookie] = _cookieHeaders; + } } } } diff --git a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs index 7aee9b3c78..b54f38b69a 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -247,6 +247,7 @@ namespace Microsoft.AspNet.Owin CancellationToken IHttpRequestLifetimeFeature.RequestAborted { get { return Prop(OwinConstants.CallCancelled); } + set { Prop(OwinConstants.CallCancelled, value); } } void IHttpRequestLifetimeFeature.Abort() diff --git a/test/Microsoft.AspNet.Http.Tests/Authentication/DefaultAuthenticationManagerTests.cs b/test/Microsoft.AspNet.Http.Tests/Authentication/DefaultAuthenticationManagerTests.cs new file mode 100644 index 0000000000..652f3a1091 --- /dev/null +++ b/test/Microsoft.AspNet.Http.Tests/Authentication/DefaultAuthenticationManagerTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Features.Authentication; +using Microsoft.AspNet.Http.Features.Authentication.Internal; +using Microsoft.AspNet.Http.Internal; +using Xunit; + +namespace Microsoft.AspNet.Http.Authentication.Internal +{ + public class AuthenticationManagerTests + { + + [Fact] + public async Task AuthenticateWithNoAuthMiddlewareThrows() + { + var context = CreateContext(); + Assert.Throws(() => context.Authentication.Authenticate("Foo")); + await Assert.ThrowsAsync(async () => await context.Authentication.AuthenticateAsync("Foo")); + } + + [Fact] + public void ChallengeWithNoAuthMiddlewareMayThrow() + { + var context = CreateContext(); + context.Authentication.Challenge(); + Assert.Equal(401, context.Response.StatusCode); + + Assert.Throws(() => context.Authentication.Challenge("Foo")); + } + + [Fact] + public void SignInWithNoAuthMiddlewareThrows() + { + var context = CreateContext(); + Assert.Throws(() => context.Authentication.SignIn("Foo", new ClaimsPrincipal())); + } + + [Fact] + public void SignOutWithNoAuthMiddlewareMayThrow() + { + var context = CreateContext(); + context.Authentication.SignOut(); + + Assert.Throws(() => context.Authentication.SignOut("Foo")); + } + + [Fact] + public void SignInOutIn() + { + var context = CreateContext(); + var handler = new AuthHandler(); + context.SetFeature(new HttpAuthenticationFeature() { Handler = handler }); + var user = new ClaimsPrincipal(); + context.Authentication.SignIn("ignored", user); + Assert.True(handler.SignedIn); + context.Authentication.SignOut("ignored"); + Assert.False(handler.SignedIn); + context.Authentication.SignIn("ignored", user); + Assert.True(handler.SignedIn); + context.Authentication.SignOut("ignored", new AuthenticationProperties() { RedirectUri = "~/logout" }); + Assert.False(handler.SignedIn); + } + + private class AuthHandler : IAuthenticationHandler + { + public bool SignedIn { get; set; } + + public void Authenticate(AuthenticateContext context) + { + throw new NotImplementedException(); + } + + public Task AuthenticateAsync(AuthenticateContext context) + { + throw new NotImplementedException(); + } + + public void Challenge(ChallengeContext context) + { + throw new NotImplementedException(); + } + + public void GetDescriptions(DescribeSchemesContext context) + { + throw new NotImplementedException(); + } + + public void SignIn(SignInContext context) + { + SignedIn = true; + context.Accept(); + } + + public void SignOut(SignOutContext context) + { + SignedIn = false; + context.Accept(); + } + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } + } +} diff --git a/test/Microsoft.AspNet.Http.Tests/DefaultHttpContextTests.cs b/test/Microsoft.AspNet.Http.Tests/DefaultHttpContextTests.cs index cffbc5c385..0caea097ec 100644 --- a/test/Microsoft.AspNet.Http.Tests/DefaultHttpContextTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/DefaultHttpContextTests.cs @@ -1,14 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNet.FeatureModel; -using Microsoft.AspNet.Http.Authentication; -using Microsoft.AspNet.Http.Features.Authentication; -using Microsoft.AspNet.Http.Features.Authentication.Internal; +using Microsoft.AspNet.Http.Features.Internal; using Xunit; namespace Microsoft.AspNet.Http.Internal @@ -44,91 +41,31 @@ namespace Microsoft.AspNet.Http.Internal } [Fact] - public async Task AuthenticateWithNoAuthMiddlewareThrows() + public void GetItems_DefaultCollectionProvided() { - var context = CreateContext(); - Assert.Throws(() => context.Authentication.Authenticate("Foo")); - await Assert.ThrowsAsync(async () => await context.Authentication.AuthenticateAsync("Foo")); + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.GetFeature()); + var items = context.Items; + Assert.NotNull(context.GetFeature()); + Assert.NotNull(items); + Assert.Same(items, context.Items); + var item = new object(); + context.Items["foo"] = item; + Assert.Same(item, context.Items["foo"]); } [Fact] - public void ChallengeWithNoAuthMiddlewareMayThrow() + public void SetItems_NewCollectionUsed() { - var context = CreateContext(); - context.Authentication.Challenge(); - Assert.Equal(401, context.Response.StatusCode); - - Assert.Throws(() => context.Authentication.Challenge("Foo")); - } - - [Fact] - public void SignInWithNoAuthMiddlewareThrows() - { - var context = CreateContext(); - Assert.Throws(() => context.Authentication.SignIn("Foo", new ClaimsPrincipal())); - } - - [Fact] - public void SignOutWithNoAuthMiddlewareMayThrow() - { - var context = CreateContext(); - context.Authentication.SignOut(); - - Assert.Throws(() => context.Authentication.SignOut("Foo")); - } - - [Fact] - public void SignInOutIn() - { - var context = CreateContext(); - var handler = new AuthHandler(); - context.SetFeature(new HttpAuthenticationFeature() { Handler = handler }); - var user = new ClaimsPrincipal(); - context.Authentication.SignIn("ignored", user); - Assert.True(handler.SignedIn); - context.Authentication.SignOut("ignored"); - Assert.False(handler.SignedIn); - context.Authentication.SignIn("ignored", user); - Assert.True(handler.SignedIn); - context.Authentication.SignOut("ignored", new AuthenticationProperties() { RedirectUri = "~/logout" }); - Assert.False(handler.SignedIn); - } - - private class AuthHandler : IAuthenticationHandler - { - public bool SignedIn { get; set; } - - public void Authenticate(AuthenticateContext context) - { - throw new NotImplementedException(); - } - - public Task AuthenticateAsync(AuthenticateContext context) - { - throw new NotImplementedException(); - } - - public void Challenge(ChallengeContext context) - { - throw new NotImplementedException(); - } - - public void GetDescriptions(DescribeSchemesContext context) - { - throw new NotImplementedException(); - } - - public void SignIn(SignInContext context) - { - SignedIn = true; - context.Accept(); - } - - public void SignOut(SignOutContext context) - { - SignedIn = false; - context.Accept(); - } + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.GetFeature()); + var items = new Dictionary(); + context.Items = items; + Assert.NotNull(context.GetFeature()); + Assert.Same(items, context.Items); + var item = new object(); + items["foo"] = item; + Assert.Same(item, context.Items["foo"]); } private HttpContext CreateContext() diff --git a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs index a3115bf5ee..81b268ba1f 100644 --- a/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNet.Http.Tests/DefaultHttpRequestTests.cs @@ -115,6 +115,78 @@ namespace Microsoft.AspNet.Http.Internal Assert.Equal(expected, headers["Host"][0]); } + [Fact] + public void IsHttps_CorrectlyReflectsScheme() + { + var request = new DefaultHttpContext().Request; + Assert.Equal(string.Empty, request.Scheme); + Assert.False(request.IsHttps); + request.IsHttps = true; + Assert.Equal("https", request.Scheme); + request.IsHttps = false; + Assert.Equal("http", request.Scheme); + request.Scheme = "ftp"; + Assert.False(request.IsHttps); + request.Scheme = "HTTPS"; + Assert.True(request.IsHttps); + } + + [Fact] + public void Query_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var requestFeature = request.HttpContext.GetFeature(); + Assert.Equal(string.Empty, requestFeature.QueryString); + Assert.Equal(QueryString.Empty, request.QueryString); + var query0 = request.Query; + Assert.NotNull(query0); + Assert.Equal(0, query0.Count); + + requestFeature.QueryString = "?name0=value0&name1=value1"; + var query1 = request.Query; + Assert.NotSame(query0, query1); + Assert.Equal(2, query1.Count); + Assert.Equal("value0", query1["name0"]); + Assert.Equal("value1", query1["name1"]); + + var query2 = new ReadableStringCollection(new Dictionary() + { + { "name2", new[] { "value2" } } + }); + + request.Query = query2; + Assert.Same(query2, request.Query); + Assert.Equal("?name2=value2", requestFeature.QueryString); + Assert.Equal(new QueryString("?name2=value2"), request.QueryString); + } + + [Fact] + public void Cookies_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var cookieHeaders = request.Headers.GetValues("Cookie"); + Assert.Null(cookieHeaders); + var cookies0 = request.Cookies; + Assert.Equal(0, cookies0.Count); + + request.Headers.SetValues("Cookie", new[] { "name0=value0", "name1=value1" }); + var cookies1 = request.Cookies; + Assert.Same(cookies0, cookies1); + Assert.Equal(2, cookies1.Count); + Assert.Equal("value0", cookies1["name0"]); + Assert.Equal("value1", cookies1["name1"]); + + var cookies2 = new ReadableStringCollection(new Dictionary() + { + { "name2", new[] { "value2" } } + }); + request.Cookies = cookies2; + Assert.Same(cookies2, request.Cookies); + Assert.Equal("value2", request.Cookies["name2"]); + cookieHeaders = request.Headers.GetValues("Cookie"); + Assert.Equal(new[] { "name2=value2" }, cookieHeaders); + } + private static HttpRequest CreateRequest(IDictionary headers) { var context = new DefaultHttpContext();