// 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; using System.Collections.Generic; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures { public class CookieTempDataProviderTest { [Fact] public void LoadTempData_ReturnsEmptyDictionary_WhenNoCookieDataIsAvailable() { // Arrange var tempDataProvider = GetProvider(); // Act var tempDataDictionary = tempDataProvider.LoadTempData(new DefaultHttpContext()); // Assert Assert.NotNull(tempDataDictionary); Assert.Empty(tempDataDictionary); } [Fact] public void LoadTempData_Base64UrlDecodesAnd_UnprotectsData_FromCookie() { // Arrange var expectedValues = new Dictionary(); expectedValues.Add("int", 10); var tempDataProviderSerializer = new TempDataSerializer(); var expectedDataToUnprotect = tempDataProviderSerializer.Serialize(expectedValues); var base64AndUrlEncodedDataInCookie = Base64UrlTextEncoder.Encode(expectedDataToUnprotect); var dataProtector = new PassThroughDataProtector(); var tempDataProvider = GetProvider(dataProtector); var requestCookies = new RequestCookieCollection(new Dictionary() { { CookieTempDataProvider.CookieName, base64AndUrlEncodedDataInCookie } }); var httpContext = new Mock(); httpContext .Setup(hc => hc.Request.Cookies) .Returns(requestCookies); // Act var actualValues = tempDataProvider.LoadTempData(httpContext.Object); // Assert Assert.Equal(expectedDataToUnprotect, dataProtector.DataToUnprotect); Assert.Equal(expectedValues, actualValues); } [Fact] public void SaveTempData_ProtectsAnd_Base64UrlEncodesDataAnd_SetsCookie() { // Arrange var values = new Dictionary(); values.Add("int", 10); var tempDataProviderStore = new TempDataSerializer(); var expectedDataToProtect = tempDataProviderStore.Serialize(values); var expectedDataInCookie = Base64UrlTextEncoder.Encode(expectedDataToProtect); var dataProtector = new PassThroughDataProtector(); var tempDataProvider = GetProvider(dataProtector); var responseCookies = new MockResponseCookieCollection(); var httpContext = new Mock(); httpContext .SetupGet(hc => hc.Request.PathBase) .Returns("/"); httpContext .Setup(hc => hc.Response.Cookies) .Returns(responseCookies); // Act tempDataProvider.SaveTempData(httpContext.Object, values); // Assert Assert.Equal(1, responseCookies.Count); var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; Assert.NotNull(cookieInfo); Assert.Equal(expectedDataInCookie, cookieInfo.Value); Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); } [Theory] [InlineData(true)] [InlineData(false)] public void SaveTempData_SetsSecureAttributeOnCookie_OnlyIfRequestIsSecure(bool isSecure) { // Arrange var values = new Dictionary(); values.Add("int", 10); var tempDataProviderStore = new TempDataSerializer(); var expectedDataToProtect = tempDataProviderStore.Serialize(values); var expectedDataInCookie = Base64UrlTextEncoder.Encode(expectedDataToProtect); var dataProtector = new PassThroughDataProtector(); var tempDataProvider = GetProvider(dataProtector); var responseCookies = new MockResponseCookieCollection(); var httpContext = new Mock(); httpContext .SetupGet(hc => hc.Request.PathBase) .Returns("/"); httpContext .SetupGet(hc => hc.Request.IsHttps) .Returns(isSecure); httpContext .Setup(hc => hc.Response.Cookies) .Returns(responseCookies); // Act tempDataProvider.SaveTempData(httpContext.Object, values); // Assert Assert.Equal(1, responseCookies.Count); var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; Assert.NotNull(cookieInfo); Assert.Equal(expectedDataInCookie, cookieInfo.Value); Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); Assert.Equal("/", cookieInfo.Options.Path); Assert.Equal(isSecure, cookieInfo.Options.Secure); Assert.True(cookieInfo.Options.HttpOnly); Assert.Null(cookieInfo.Options.Expires); Assert.Null(cookieInfo.Options.Domain); } [Theory] [InlineData(null, "/")] [InlineData("", "/")] [InlineData("/", "/")] [InlineData("/vdir1", "/vdir1")] public void SaveTempData_DefaultProviderOptions_SetsCookie_WithAppropriateCookieOptions( string pathBase, string expectedCookiePath) { // Arrange var values = new Dictionary(); values.Add("int", 10); var tempDataProviderStore = new TempDataSerializer(); var expectedDataToProtect = tempDataProviderStore.Serialize(values); var expectedDataInCookie = Base64UrlTextEncoder.Encode(expectedDataToProtect); var dataProtector = new PassThroughDataProtector(); var tempDataProvider = GetProvider(dataProtector); var responseCookies = new MockResponseCookieCollection(); var httpContext = new Mock(); httpContext .SetupGet(hc => hc.Request.PathBase) .Returns(pathBase); httpContext .SetupGet(hc => hc.Request.IsHttps) .Returns(false); httpContext .Setup(hc => hc.Response.Cookies) .Returns(responseCookies); // Act tempDataProvider.SaveTempData(httpContext.Object, values); // Assert Assert.Equal(1, responseCookies.Count); var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; Assert.NotNull(cookieInfo); Assert.Equal(expectedDataInCookie, cookieInfo.Value); Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); Assert.Equal(expectedCookiePath, cookieInfo.Options.Path); Assert.False(cookieInfo.Options.Secure); Assert.True(cookieInfo.Options.HttpOnly); Assert.Null(cookieInfo.Options.Expires); Assert.Null(cookieInfo.Options.Domain); } [Theory] [InlineData(null, null, null, "/", null)] [InlineData("", null, null, "/", null)] [InlineData("/", null, null, "/", null)] [InlineData("/", "/vdir1", null, "/vdir1", null)] [InlineData("/", "/vdir1", ".abc.com", "/vdir1", ".abc.com")] [InlineData("/vdir1", "/", ".abc.com", "/", ".abc.com")] public void SaveTempData_CustomProviderOptions_SetsCookie_WithAppropriateCookieOptions( string requestPathBase, string optionsPath, string optionsDomain, string expectedCookiePath, string expectedDomain) { // Arrange var values = new Dictionary(); values.Add("int", 10); var tempDataProviderStore = new TempDataSerializer(); var expectedDataToProtect = tempDataProviderStore.Serialize(values); var expectedDataInCookie = Base64UrlTextEncoder.Encode(expectedDataToProtect); var dataProtector = new PassThroughDataProtector(); var tempDataProvider = GetProvider( dataProtector, new CookieTempDataProviderOptions() { Path = optionsPath, Domain = optionsDomain }); var responseCookies = new MockResponseCookieCollection(); var httpContext = new Mock(); httpContext .SetupGet(hc => hc.Request.IsHttps) .Returns(false); httpContext .SetupGet(hc => hc.Request.PathBase) .Returns(requestPathBase); httpContext .Setup(hc => hc.Response.Cookies) .Returns(responseCookies); // Act tempDataProvider.SaveTempData(httpContext.Object, values); // Assert Assert.Equal(1, responseCookies.Count); var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; Assert.NotNull(cookieInfo); Assert.Equal(expectedDataInCookie, cookieInfo.Value); Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); Assert.Equal(expectedCookiePath, cookieInfo.Options.Path); Assert.Equal(expectedDomain, cookieInfo.Options.Domain); Assert.False(cookieInfo.Options.Secure); Assert.True(cookieInfo.Options.HttpOnly); Assert.Null(cookieInfo.Options.Expires); } [Fact] public void SaveTempData_RemovesCookie_WhenNoDataToSave() { // Arrange var values = new Dictionary(); values.Add("int", 10); var tempDataProviderStore = new TempDataSerializer(); var serializedData = tempDataProviderStore.Serialize(values); var base64AndUrlEncodedData = Base64UrlTextEncoder.Encode(serializedData); var dataProtector = new PassThroughDataProtector(); var tempDataProvider = GetProvider(dataProtector); var requestCookies = new RequestCookieCollection(new Dictionary() { { CookieTempDataProvider.CookieName, base64AndUrlEncodedData } }); var responseCookies = new MockResponseCookieCollection(); var httpContext = new Mock(); httpContext .SetupGet(hc => hc.Request.PathBase) .Returns("/"); httpContext .Setup(hc => hc.Request.Cookies) .Returns(requestCookies); httpContext .Setup(hc => hc.Response.Cookies) .Returns(responseCookies); httpContext .Setup(hc => hc.Response.Headers) .Returns(new HeaderDictionary()); // Act tempDataProvider.SaveTempData(httpContext.Object, new Dictionary()); // Assert Assert.Equal(1, responseCookies.Count); var cookie = responseCookies[CookieTempDataProvider.CookieName]; Assert.NotNull(cookie); Assert.Equal(string.Empty, cookie.Value); Assert.NotNull(cookie.Options.Expires); Assert.True(cookie.Options.Expires.Value < DateTimeOffset.Now); // expired cookie } [Fact] public void SaveAndLoad_StringCanBeStoredAndLoaded() { // Arrange var testProvider = GetProvider(); var input = new Dictionary { { "string", "value" } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var stringVal = Assert.IsType(TempData["string"]); Assert.Equal("value", stringVal); } [Theory] [InlineData(10)] [InlineData(int.MaxValue)] [InlineData(int.MinValue)] public void SaveAndLoad_IntCanBeStoredAndLoaded(int expected) { // Arrange var testProvider = GetProvider(); var input = new Dictionary { { "int", expected } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var intVal = Assert.IsType(TempData["int"]); Assert.Equal(expected, intVal); } [Theory] [InlineData(false)] [InlineData(true)] public void SaveAndLoad_BoolCanBeStoredAndLoaded(bool value) { // Arrange var testProvider = GetProvider(); var input = new Dictionary { { "bool", value } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var boolVal = Assert.IsType(TempData["bool"]); Assert.Equal(value, boolVal); } [Fact] public void SaveAndLoad_DateTimeCanBeStoredAndLoaded() { // Arrange var testProvider = GetProvider(); var inputDatetime = new DateTime(2010, 12, 12, 1, 2, 3, DateTimeKind.Local); var input = new Dictionary { { "DateTime", inputDatetime } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var datetime = Assert.IsType(TempData["DateTime"]); Assert.Equal(inputDatetime, datetime); } [Fact] public void SaveAndLoad_GuidCanBeStoredAndLoaded() { // Arrange var testProvider = GetProvider(); var inputGuid = Guid.NewGuid(); var input = new Dictionary { { "Guid", inputGuid } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var guidVal = Assert.IsType(TempData["Guid"]); Assert.Equal(inputGuid, guidVal); } [Fact] public void SaveAndLoad_EnumCanBeSavedAndLoaded() { // Arrange var key = "EnumValue"; var testProvider = GetProvider(); var expected = DayOfWeek.Friday; var input = new Dictionary { { key, expected } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); var result = TempData[key]; // Assert var actual = (DayOfWeek)result; Assert.Equal(expected, actual); } [Theory] [InlineData(3100000000L)] [InlineData(-3100000000L)] public void SaveAndLoad_LongCanBeSavedAndLoaded(long expected) { // Arrange var key = "LongValue"; var testProvider = GetProvider(); var input = new Dictionary { { key, expected } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); var result = TempData[key]; // Assert var actual = Assert.IsType(result); Assert.Equal(expected, actual); } [Fact] public void SaveAndLoad_ListCanBeStoredAndLoaded() { // Arrange var testProvider = GetProvider(); var input = new Dictionary { { "List`string", new List { "one", "two" } } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var list = (IList)TempData["List`string"]; Assert.Equal(2, list.Count); Assert.Equal("one", list[0]); Assert.Equal("two", list[1]); } [Fact] public void SaveAndLoad_DictionaryCanBeStoredAndLoaded() { // Arrange var testProvider = GetProvider(); var inputDictionary = new Dictionary { { "Hello", "World" }, }; var input = new Dictionary { { "Dictionary", inputDictionary } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var dictionary = Assert.IsType>(TempData["Dictionary"]); Assert.Equal("World", dictionary["Hello"]); } [Fact] public void SaveAndLoad_EmptyDictionary_RoundTripsAsNull() { // Arrange var testProvider = GetProvider(); var input = new Dictionary { { "EmptyDictionary", new Dictionary() } }; var context = GetHttpContext(); // Act testProvider.SaveTempData(context, input); UpdateRequestWithCookies(context); var TempData = testProvider.LoadTempData(context); // Assert var emptyDictionary = (IDictionary)TempData["EmptyDictionary"]; Assert.Null(emptyDictionary); } private static HttpContext GetHttpContext() { var context = new Mock(); context .SetupGet(hc => hc.Request.PathBase) .Returns("/"); context .SetupGet(hc => hc.Response.Cookies) .Returns(new MockResponseCookieCollection()); return context.Object; } private void UpdateRequestWithCookies(HttpContext httpContext) { var responseCookies = (MockResponseCookieCollection)httpContext.Response.Cookies; var values = new Dictionary(); foreach (var responseCookie in responseCookies) { values.Add(responseCookie.Key, responseCookie.Value); } if (values.Count > 0) { httpContext.Request.Cookies = new RequestCookieCollection(values); } } private class MockResponseCookieCollection : IResponseCookies, IEnumerable { private Dictionary _cookies = new Dictionary(StringComparer.OrdinalIgnoreCase); public int Count { get { return _cookies.Count; } } public CookieInfo this[string key] { get { return _cookies[key]; } } public void Append(string key, string value, CookieOptions options) { _cookies[key] = new CookieInfo() { Key = key, Value = value, Options = options }; } public void Append(string key, string value) { Append(key, value, new CookieOptions()); } public void Delete(string key, CookieOptions options) { _cookies.Remove(key); } public void Delete(string key) { _cookies.Remove(key); } public IEnumerator GetEnumerator() { return _cookies.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } private CookieTempDataProvider GetProvider(IDataProtector dataProtector = null, CookieTempDataProviderOptions options = null) { if(dataProtector == null) { dataProtector = new PassThroughDataProtector(); } if(options == null) { options = new CookieTempDataProviderOptions(); } var testOptions = new Mock>(); testOptions.SetupGet(o => o.Value).Returns(options); return new CookieTempDataProvider(new PassThroughDataProtectionProvider(dataProtector), testOptions.Object); } private class PassThroughDataProtectionProvider : IDataProtectionProvider { private readonly IDataProtector _dataProtector; public PassThroughDataProtectionProvider(IDataProtector dataProtector) { _dataProtector = dataProtector; } public IDataProtector CreateProtector(string purpose) { return _dataProtector; } } private class PassThroughDataProtector : IDataProtector { public byte[] DataToUnprotect { get; private set; } public byte[] PlainTextToProtect { get; private set; } public string Purpose { get; private set; } public IDataProtector CreateProtector(string purpose) { Purpose = purpose; return this; } public byte[] Protect(byte[] plaintext) { PlainTextToProtect = plaintext; return PlainTextToProtect; } public byte[] Unprotect(byte[] protectedData) { DataToUnprotect = protectedData; return DataToUnprotect; } } private class CookieInfo { public string Key { get; set; } public string Value { get; set; } public CookieOptions Options { get; set; } } } }