aspnetcore/test/Microsoft.AspNetCore.Cookie.../CookieConsentTests.cs

660 lines
26 KiB
C#

// 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.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.CookiePolicy.Test
{
public class CookieConsentTests
{
[Fact]
public async Task ConsentChecksOffByDefault()
{
var httpContext = await RunTestAsync(options => { }, requestContext => { }, context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.False(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task ConsentEnabledForTemplateScenario()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { }, context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task NonEssentialCookiesWithOptionsExcluded()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { }, context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
return Task.CompletedTask;
});
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task NonEssentialCookiesCanBeAllowedViaOnAppendCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.OnAppendCookie = context =>
{
Assert.True(context.IsConsentNeeded);
Assert.False(context.HasConsent);
Assert.False(context.IssueCookie);
context.IssueCookie = true;
};
},
requestContext => { }, context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
return Task.CompletedTask;
});
Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task NeedsConsentDoesNotPreventEssentialCookies()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { }, context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
return Task.CompletedTask;
});
Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task EssentialCookiesCanBeExcludedByOnAppendCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.OnAppendCookie = context =>
{
Assert.True(context.IsConsentNeeded);
Assert.True(context.HasConsent);
Assert.True(context.IssueCookie);
context.IssueCookie = false;
};
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
},
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
return Task.CompletedTask;
});
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task HasConsentReadsRequestCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
},
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task HasConsentIgnoresInvalidRequestCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=IAmATeapot";
},
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task GrantConsentSetsCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
feature.GrantConsent();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(2, cookies.Count);
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent", consentCookie.Name);
Assert.Equal("yes", consentCookie.Value);
Assert.True(consentCookie.Expires.HasValue);
Assert.True(consentCookie.Expires.Value > DateTimeOffset.Now + TimeSpan.FromDays(364));
Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
var testCookie = cookies[1];
Assert.Equal("Test", testCookie.Name);
Assert.Equal("Value", testCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
Assert.Null(testCookie.Expires);
}
[Fact]
public async Task GrantConsentAppliesPolicyToConsentCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
options.OnAppendCookie = context =>
{
Assert.Equal(".AspNet.Consent", context.CookieName);
Assert.Equal("yes", context.CookieValue);
Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite);
context.CookieName += "1";
context.CookieValue += "1";
};
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
feature.GrantConsent();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(1, cookies.Count);
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent1", consentCookie.Name);
Assert.Equal("yes1", consentCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
}
[Fact]
public async Task GrantConsentWhenAlreadyHasItDoesNotSetCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
},
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
feature.GrantConsent();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task GrantConsentAfterResponseStartsSetsHasConsentButDoesNotSetCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { },
async context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
await context.Response.WriteAsync("Started.");
feature.GrantConsent();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
Assert.Throws<InvalidOperationException>(() => context.Response.Cookies.Append("Test", "Value"));
await context.Response.WriteAsync("Granted.");
});
var reader = new StreamReader(httpContext.Response.Body);
Assert.Equal("Started.Granted.", await reader.ReadToEndAsync());
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task WithdrawConsentWhenNotHasConsentNoOps()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
feature.WithdrawConsent();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value");
return Task.CompletedTask;
});
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task WithdrawConsentDeletesCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
},
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value1");
feature.WithdrawConsent();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value2");
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(2, cookies.Count);
var testCookie = cookies[0];
Assert.Equal("Test", testCookie.Name);
Assert.Equal("Value1", testCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
Assert.Null(testCookie.Expires);
var consentCookie = cookies[1];
Assert.Equal(".AspNet.Consent", consentCookie.Name);
Assert.Equal("", consentCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
}
[Fact]
public async Task WithdrawConsentAppliesPolicyToDeleteCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
options.OnDeleteCookie = context =>
{
Assert.Equal(".AspNet.Consent", context.CookieName);
context.CookieName += "1";
};
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
},
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
feature.WithdrawConsent();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(1, cookies.Count);
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent1", consentCookie.Name);
Assert.Equal("", consentCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
}
[Fact]
public async Task WithdrawConsentAfterResponseHasStartedDoesNotDeleteCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext =>
{
requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
},
async context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
context.Response.Cookies.Append("Test", "Value1");
await context.Response.WriteAsync("Started.");
feature.WithdrawConsent();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
// Doesn't throw the normal InvalidOperationException because the cookie is never written
context.Response.Cookies.Append("Test", "Value2");
await context.Response.WriteAsync("Withdrawn.");
});
var reader = new StreamReader(httpContext.Response.Body);
Assert.Equal("Started.Withdrawn.", await reader.ReadToEndAsync());
Assert.Equal("Test=Value1; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task DeleteCookieDoesNotRequireConsent()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Delete("Test");
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(1, cookies.Count);
var testCookie = cookies[0];
Assert.Equal("Test", testCookie.Name);
Assert.Equal("", testCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
Assert.NotNull(testCookie.Expires);
}
[Fact]
public async Task OnDeleteCookieCanSuppressCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.OnDeleteCookie = context =>
{
Assert.True(context.IsConsentNeeded);
Assert.False(context.HasConsent);
Assert.True(context.IssueCookie);
context.IssueCookie = false;
};
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
context.Response.Cookies.Delete("Test");
return Task.CompletedTask;
});
Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
}
[Fact]
public async Task CreateConsentCookieMatchesGrantConsentCookie()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
feature.GrantConsent();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
var cookie = feature.CreateConsentCookie();
context.Response.Headers["ManualCookie"] = cookie;
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(1, cookies.Count);
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent", consentCookie.Name);
Assert.Equal("yes", consentCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
Assert.Equal(1, cookies.Count);
var manualCookie = cookies[0];
Assert.Equal(consentCookie.Name, manualCookie.Name);
Assert.Equal(consentCookie.Value, manualCookie.Value);
Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
}
[Fact]
public async Task CreateConsentCookieAppliesPolicy()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
options.OnAppendCookie = context =>
{
Assert.Equal(".AspNet.Consent", context.CookieName);
Assert.Equal("yes", context.CookieValue);
Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite);
context.CookieName += "1";
context.CookieValue += "1";
};
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);
feature.GrantConsent();
Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);
var cookie = feature.CreateConsentCookie();
context.Response.Headers["ManualCookie"] = cookie;
return Task.CompletedTask;
});
var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
Assert.Equal(1, cookies.Count);
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent1", consentCookie.Name);
Assert.Equal("yes1", consentCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
Assert.Equal(1, cookies.Count);
var manualCookie = cookies[0];
Assert.Equal(consentCookie.Name, manualCookie.Name);
Assert.Equal(consentCookie.Value, manualCookie.Value);
Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
}
private Task<HttpContext> RunTestAsync(Action<CookiePolicyOptions> configureOptions, Action<HttpContext> configureRequest, RequestDelegate handleRequest)
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.Configure(configureOptions);
})
.Configure(app =>
{
app.UseCookiePolicy();
app.Run(handleRequest);
});
var server = new TestServer(builder);
return server.SendAsync(configureRequest);
}
}
}