Skip to content

Commit 7b50676

Browse files
committed
TempDataTest
1 parent 21a3c3b commit 7b50676

File tree

3 files changed

+328
-18
lines changed

3 files changed

+328
-18
lines changed

src/Components/Endpoints/src/DependencyInjection/TempDataService.cs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,43 @@ private static IDataProtector GetDataProtector(HttpContext httpContext)
2222

2323
public static TempData Load(HttpContext httpContext)
2424
{
25-
var returnTempData = new TempData();
26-
var serializedDataFromCookie = httpContext.Request.Cookies[CookieName];
27-
if (serializedDataFromCookie is null)
25+
try
2826
{
29-
return returnTempData;
30-
}
27+
var returnTempData = new TempData();
28+
var serializedDataFromCookie = httpContext.Request.Cookies[CookieName];
29+
if (serializedDataFromCookie is null)
30+
{
31+
return returnTempData;
32+
}
3133

32-
var protectedBytes = WebEncoders.Base64UrlDecode(serializedDataFromCookie);
33-
var unprotectedBytes = GetDataProtector(httpContext).Unprotect(protectedBytes);
34-
var dataFromCookie = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(unprotectedBytes);
34+
var protectedBytes = WebEncoders.Base64UrlDecode(serializedDataFromCookie);
35+
var unprotectedBytes = GetDataProtector(httpContext).Unprotect(protectedBytes);
36+
var dataFromCookie = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(unprotectedBytes);
3537

36-
if (dataFromCookie is null)
37-
{
38+
if (dataFromCookie is null)
39+
{
40+
return returnTempData;
41+
}
42+
43+
var convertedData = new Dictionary<string, object?>();
44+
foreach (var kvp in dataFromCookie)
45+
{
46+
convertedData[kvp.Key] = ConvertJsonElement(kvp.Value);
47+
}
48+
49+
returnTempData.Load(convertedData);
3850
return returnTempData;
3951
}
40-
41-
var convertedData = new Dictionary<string, object?>();
42-
foreach (var kvp in dataFromCookie)
52+
catch
4353
{
44-
convertedData[kvp.Key] = ConvertJsonElement(kvp.Value);
54+
// If any error occurs during loading (e.g. data protection key changed, malformed cookie),
55+
// return an empty TempData dictionary.
56+
httpContext.Response.Cookies.Delete(CookieName, new CookieOptions
57+
{
58+
Path = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : "/",
59+
});
60+
return new TempData();
4561
}
46-
47-
returnTempData.Load(convertedData);
48-
return returnTempData;
4962
}
5063

5164
public static void Save(HttpContext httpContext, TempData tempData)
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components;
5+
using Microsoft.AspNetCore.Components.Endpoints;
6+
using Microsoft.AspNetCore.DataProtection;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.Features;
9+
10+
namespace Microsoft.Extensions.DependencyInjection;
11+
12+
public class TempDataServiceTest
13+
{
14+
[Fact]
15+
public void Load_ReturnsEmptyTempData_WhenNoCookieExists()
16+
{
17+
var httpContext = CreateHttpContext();
18+
19+
var tempData = TempDataService.Load(httpContext);
20+
21+
Assert.NotNull(tempData);
22+
Assert.Empty(tempData.Save());
23+
}
24+
25+
[Fact]
26+
public void Save_DeletesCookie_WhenNoDataToSave()
27+
{
28+
var httpContext = CreateHttpContext();
29+
var tempData = new TempData();
30+
31+
TempDataService.Save(httpContext, tempData);
32+
33+
var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
34+
Assert.NotNull(cookieFeature);
35+
Assert.Contains(".AspNetCore.Components.TempData", cookieFeature.DeletedCookies);
36+
}
37+
38+
[Fact]
39+
public void Save_SetsCookie_WhenDataExists()
40+
{
41+
var httpContext = CreateHttpContext();
42+
var tempData = new TempData();
43+
tempData["Key1"] = "Value1";
44+
45+
TempDataService.Save(httpContext, tempData);
46+
47+
var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
48+
Assert.NotNull(cookieFeature);
49+
Assert.True(cookieFeature.SetCookies.ContainsKey(".AspNetCore.Components.TempData"));
50+
}
51+
52+
[Fact]
53+
public void RoundTrip_PreservesStringValue()
54+
{
55+
var httpContext = CreateHttpContext();
56+
var tempData = new TempData();
57+
tempData["StringKey"] = "StringValue";
58+
59+
TempDataService.Save(httpContext, tempData);
60+
SimulateCookieRoundTrip(httpContext);
61+
var loadedTempData = TempDataService.Load(httpContext);
62+
63+
Assert.Equal("StringValue", loadedTempData.Peek("StringKey"));
64+
}
65+
66+
[Fact]
67+
public void RoundTrip_PreservesIntValue()
68+
{
69+
var httpContext = CreateHttpContext();
70+
var tempData = new TempData();
71+
tempData["IntKey"] = 42;
72+
73+
TempDataService.Save(httpContext, tempData);
74+
SimulateCookieRoundTrip(httpContext);
75+
var loadedTempData = TempDataService.Load(httpContext);
76+
77+
Assert.Equal(42, loadedTempData.Peek("IntKey"));
78+
}
79+
80+
[Fact]
81+
public void RoundTrip_PreservesBoolValue()
82+
{
83+
var httpContext = CreateHttpContext();
84+
var tempData = new TempData();
85+
tempData["BoolKey"] = true;
86+
87+
TempDataService.Save(httpContext, tempData);
88+
SimulateCookieRoundTrip(httpContext);
89+
var loadedTempData = TempDataService.Load(httpContext);
90+
91+
Assert.Equal(true, loadedTempData.Peek("BoolKey"));
92+
}
93+
94+
[Fact]
95+
public void RoundTrip_PreservesGuidValue()
96+
{
97+
var httpContext = CreateHttpContext();
98+
var tempData = new TempData();
99+
var guid = Guid.NewGuid();
100+
tempData["GuidKey"] = guid;
101+
102+
TempDataService.Save(httpContext, tempData);
103+
SimulateCookieRoundTrip(httpContext);
104+
var loadedTempData = TempDataService.Load(httpContext);
105+
106+
Assert.Equal(guid, loadedTempData.Peek("GuidKey"));
107+
}
108+
109+
[Fact]
110+
public void RoundTrip_PreservesDateTimeValue()
111+
{
112+
var httpContext = CreateHttpContext();
113+
var tempData = new TempData();
114+
var dateTime = new DateTime(2025, 12, 15, 10, 30, 0, DateTimeKind.Utc);
115+
tempData["DateTimeKey"] = dateTime;
116+
117+
TempDataService.Save(httpContext, tempData);
118+
SimulateCookieRoundTrip(httpContext);
119+
var loadedTempData = TempDataService.Load(httpContext);
120+
121+
Assert.Equal(dateTime, loadedTempData.Peek("DateTimeKey"));
122+
}
123+
124+
[Fact]
125+
public void RoundTrip_PreservesStringArray()
126+
{
127+
var httpContext = CreateHttpContext();
128+
var tempData = new TempData();
129+
var array = new[] { "one", "two", "three" };
130+
tempData["ArrayKey"] = array;
131+
132+
TempDataService.Save(httpContext, tempData);
133+
SimulateCookieRoundTrip(httpContext);
134+
var loadedTempData = TempDataService.Load(httpContext);
135+
136+
Assert.Equal(array, loadedTempData.Peek("ArrayKey"));
137+
}
138+
139+
[Fact]
140+
public void RoundTrip_PreservesIntArray()
141+
{
142+
var httpContext = CreateHttpContext();
143+
var tempData = new TempData();
144+
var array = new[] { 1, 2, 3 };
145+
tempData["ArrayKey"] = array;
146+
147+
TempDataService.Save(httpContext, tempData);
148+
SimulateCookieRoundTrip(httpContext);
149+
var loadedTempData = TempDataService.Load(httpContext);
150+
151+
Assert.Equal(array, loadedTempData.Peek("ArrayKey"));
152+
}
153+
154+
[Fact]
155+
public void RoundTrip_PreservesDictionary()
156+
{
157+
var httpContext = CreateHttpContext();
158+
var tempData = new TempData();
159+
var dict = new Dictionary<string, string> { ["a"] = "1", ["b"] = "2" };
160+
tempData["DictKey"] = dict;
161+
162+
TempDataService.Save(httpContext, tempData);
163+
SimulateCookieRoundTrip(httpContext);
164+
var loadedTempData = TempDataService.Load(httpContext);
165+
166+
var loadedDict = Assert.IsType<Dictionary<string, string>>(loadedTempData.Peek("DictKey"));
167+
Assert.Equal("1", loadedDict["a"]);
168+
Assert.Equal("2", loadedDict["b"]);
169+
}
170+
171+
[Fact]
172+
public void RoundTrip_PreservesMultipleDifferentValues()
173+
{
174+
var httpContext = CreateHttpContext();
175+
var tempData = new TempData();
176+
tempData["Key1"] = "Value1";
177+
tempData["Key2"] = 123;
178+
tempData["Key3"] = true;
179+
180+
TempDataService.Save(httpContext, tempData);
181+
SimulateCookieRoundTrip(httpContext);
182+
var loadedTempData = TempDataService.Load(httpContext);
183+
184+
Assert.Equal("Value1", loadedTempData.Peek("Key1"));
185+
Assert.Equal(123, loadedTempData.Peek("Key2"));
186+
Assert.Equal(true, loadedTempData.Peek("Key3"));
187+
}
188+
189+
[Fact]
190+
public void Save_ThrowsForUnsupportedType()
191+
{
192+
var httpContext = CreateHttpContext();
193+
var tempData = new TempData();
194+
tempData["Key"] = new object();
195+
196+
Assert.Throws<InvalidOperationException>(() => TempDataService.Save(httpContext, tempData));
197+
}
198+
199+
[Fact]
200+
public void Load_ReturnsEmptyTempData_ForInvalidBase64Cookie()
201+
{
202+
var httpContext = CreateHttpContext();
203+
httpContext.Request.Headers["Cookie"] = ".AspNetCore.Components.TempData=not-valid-base64!!!";
204+
205+
var tempData = TempDataService.Load(httpContext);
206+
207+
Assert.NotNull(tempData);
208+
Assert.Empty(tempData.Save());
209+
}
210+
211+
[Fact]
212+
public void Load_ReturnsEmptyTempData_ForUnsupportedType()
213+
{
214+
var httpContext = CreateHttpContext();
215+
var json = "{\"Key\":[true, false, true]}";
216+
var encoded = Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(json));
217+
httpContext.Request.Headers["Cookie"] = $".AspNetCore.Components.TempData={encoded}";
218+
219+
var tempData = TempDataService.Load(httpContext);
220+
221+
Assert.NotNull(tempData);
222+
Assert.Empty(tempData.Save());
223+
}
224+
225+
private static DefaultHttpContext CreateHttpContext()
226+
{
227+
var services = new ServiceCollection()
228+
.AddSingleton<IDataProtectionProvider, PassThroughDataProtectionProvider>()
229+
.BuildServiceProvider();
230+
231+
var httpContext = new DefaultHttpContext
232+
{
233+
RequestServices = services
234+
};
235+
httpContext.Request.Scheme = "https";
236+
httpContext.Request.Host = new HostString("localhost");
237+
238+
var cookieFeature = new TestResponseCookiesFeature();
239+
httpContext.Features.Set(cookieFeature);
240+
httpContext.Features.Set<IResponseCookiesFeature>(cookieFeature);
241+
242+
return httpContext;
243+
}
244+
245+
private static void SimulateCookieRoundTrip(HttpContext httpContext)
246+
{
247+
var cookieFeature = httpContext.Features.Get<TestResponseCookiesFeature>();
248+
if (cookieFeature != null && cookieFeature.SetCookies.TryGetValue(".AspNetCore.Components.TempData", out var cookieValue))
249+
{
250+
httpContext.Request.Headers["Cookie"] = $".AspNetCore.Components.TempData={cookieValue}";
251+
}
252+
}
253+
254+
private class PassThroughDataProtectionProvider : IDataProtectionProvider
255+
{
256+
public IDataProtector CreateProtector(string purpose) => new PassThroughDataProtector();
257+
258+
private class PassThroughDataProtector : IDataProtector
259+
{
260+
public IDataProtector CreateProtector(string purpose) => this;
261+
public byte[] Protect(byte[] plaintext) => plaintext;
262+
public byte[] Unprotect(byte[] protectedData) => protectedData;
263+
}
264+
}
265+
266+
private class TestResponseCookiesFeature : IResponseCookiesFeature
267+
{
268+
public Dictionary<string, string> SetCookies { get; } = new();
269+
public HashSet<string> DeletedCookies { get; } = new();
270+
271+
public IResponseCookies Cookies => new TestResponseCookies(this);
272+
273+
private class TestResponseCookies : IResponseCookies
274+
{
275+
private readonly TestResponseCookiesFeature _feature;
276+
277+
public TestResponseCookies(TestResponseCookiesFeature feature)
278+
{
279+
_feature = feature;
280+
}
281+
282+
public void Append(string key, string value) => Append(key, value, new CookieOptions());
283+
284+
public void Append(string key, string value, CookieOptions options)
285+
{
286+
_feature.SetCookies[key] = value;
287+
}
288+
289+
public void Delete(string key) => Delete(key, new CookieOptions());
290+
291+
public void Delete(string key, CookieOptions options)
292+
{
293+
_feature.DeletedCookies.Add(key);
294+
}
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)