diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..95c11cb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/tests/AnonymousUserTests/bin/Debug/net6.0/AnonymousUserTests.dll", + "args": [], + "cwd": "${workspaceFolder}/tests/AnonymousUserTests", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..6a0b954 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/AspNetCoreExtensions.sln b/AspNetCoreExtensions.sln index a2b6e94..81fda1c 100644 --- a/AspNetCoreExtensions.sln +++ b/AspNetCoreExtensions.sln @@ -3,6 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.6.30114.105 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{38DD8F4A-2C3A-486C-AE3A-7208007EB1B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnonymousUser", "src\AnonymousUser\AnonymousUser.csproj", "{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CEDA0A6-C7E0-4542-896C-241C3D796D89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnonymousUserTests", "tests\AnonymousUserTests\AnonymousUserTests.csproj", "{82E9B456-9918-4499-A165-A30423BA0740}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,4 +23,34 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x64.ActiveCfg = Debug|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x64.Build.0 = Debug|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x86.ActiveCfg = Debug|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x86.Build.0 = Debug|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|Any CPU.Build.0 = Release|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x64.ActiveCfg = Release|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x64.Build.0 = Release|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x86.ActiveCfg = Release|Any CPU + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x86.Build.0 = Release|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Debug|x64.ActiveCfg = Debug|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Debug|x64.Build.0 = Debug|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Debug|x86.ActiveCfg = Debug|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Debug|x86.Build.0 = Debug|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Release|Any CPU.Build.0 = Release|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Release|x64.ActiveCfg = Release|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Release|x64.Build.0 = Release|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Release|x86.ActiveCfg = Release|Any CPU + {82E9B456-9918-4499-A165-A30423BA0740}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9FB11E67-7313-4BC7-9412-3FF4CAF35C96} = {38DD8F4A-2C3A-486C-AE3A-7208007EB1B5} + {82E9B456-9918-4499-A165-A30423BA0740} = {9CEDA0A6-C7E0-4542-896C-241C3D796D89} + EndGlobalSection EndGlobal diff --git a/CodeAnalysis.Library.ruleset b/CodeAnalysis.Library.ruleset index 2c05d5e..d549136 100644 --- a/CodeAnalysis.Library.ruleset +++ b/CodeAnalysis.Library.ruleset @@ -5,6 +5,7 @@ <Rule Id="CA1006" Action="None" /> <Rule Id="CA1303" Action="None" /> <Rule Id="CA2243" Action="None" /> + <Rule Id="CA2007" Action="None" /> </Rules> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers"> <Rule Id="SA1101" Action="None" /> <!-- Prefix local calls with this --> diff --git a/appveyor.yml b/appveyor.yml index f1a37bf..41ba269 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ max_jobs: 1 environment: CAKE_SETTINGS_SKIPPACKAGEVERSIONCHECK: "true" -image: Visual Studio 2019 +image: Visual Studio 2022 cache: - '%LocalAppData%\NuGet\v3-cache' diff --git a/src/AnonymousUser/AnonymousUser.csproj b/src/AnonymousUser/AnonymousUser.csproj new file mode 100644 index 0000000..f3b02ee --- /dev/null +++ b/src/AnonymousUser/AnonymousUser.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.1</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> + </ItemGroup> + +</Project> \ No newline at end of file diff --git a/src/AnonymousUser/AnonymousUserExtensions.cs b/src/AnonymousUser/AnonymousUserExtensions.cs new file mode 100644 index 0000000..e961394 --- /dev/null +++ b/src/AnonymousUser/AnonymousUserExtensions.cs @@ -0,0 +1,27 @@ +using System; +using InsightArchitectures.Extensions.AspNetCore.AnonymousUser; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for the ASP NET Core application builder. + /// </summary> + public static class AnonymousUserExtensions + { + /// <summary> + /// Adds the <see cref="AnonymousUserMiddleware" /> to the middleware pipeline. + /// </summary> + /// <param name="builder">The application builder object.</param> + /// <param name="configure">An action to customise the middleware options.</param> + public static IApplicationBuilder UseAnonymousUser(this IApplicationBuilder builder, Action<AnonymousUserOptions> configure = null) + { + var options = new AnonymousUserOptions(); + + configure?.Invoke(options); + + _ = options.ClaimType ?? throw new NullReferenceException($"{nameof(options.ClaimType)} is null. Please provide a claim type name when configuring the middleware."); + + return builder.UseMiddleware<AnonymousUserMiddleware>(options); + } + } +} \ No newline at end of file diff --git a/src/AnonymousUser/AnonymousUserMiddleware.cs b/src/AnonymousUser/AnonymousUserMiddleware.cs new file mode 100644 index 0000000..0f18b9c --- /dev/null +++ b/src/AnonymousUser/AnonymousUserMiddleware.cs @@ -0,0 +1,78 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser +{ + /// <summary> + /// The anonymous user middleware. It either creates a new or reads an existing cookie + /// and maps the value to a claim. + /// </summary> + public class AnonymousUserMiddleware + { + private RequestDelegate _nextDelegate; + private AnonymousUserOptions _options; + + /// <summary> + /// Constructor requires the next delegate and options. + /// </summary> + public AnonymousUserMiddleware(RequestDelegate nextDelegate, AnonymousUserOptions options) + { + _nextDelegate = nextDelegate ?? throw new ArgumentNullException(nameof(nextDelegate)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + private async Task HandleRequestAsync(HttpContext httpContext) + { + var cookieEncoder = _options.EncoderService ?? throw new ArgumentNullException(nameof(_options.EncoderService), $"{nameof(_options.EncoderService)} is null and should have a valid encoder."); + _ = _options.UserIdentifierFactory ?? throw new ArgumentNullException(nameof(_options.UserIdentifierFactory), $"{nameof(_options.UserIdentifierFactory)} is null and should have a valid factory."); + + if (httpContext.User.Identity?.IsAuthenticated == true) + { + return; + } + + var encodedValue = httpContext.Request.Cookies[_options.CookieName]; + + if (_options.Secure && !httpContext.Request.IsHttps) + { + if (!string.IsNullOrWhiteSpace(encodedValue)) + { + httpContext.Response.Cookies.Delete(_options.CookieName); + } + + return; + } + + var uid = await cookieEncoder.DecodeAsync(encodedValue); + + if (string.IsNullOrWhiteSpace(uid)) + { + uid = _options.UserIdentifierFactory.Invoke(httpContext); + var encodedUid = await cookieEncoder.EncodeAsync(uid); + + var cookieOptions = new CookieOptions + { + Expires = _options.Expires, + }; + + httpContext.Response.Cookies.Append(_options.CookieName, encodedUid, cookieOptions); + } + + var identity = new ClaimsIdentity(new[] { new Claim(_options.ClaimType, uid) }); + httpContext.User.AddIdentity(identity); + } + + /// <summary> + /// Called by the pipeline, runs the handler. + /// </summary> + public async Task InvokeAsync(HttpContext httpContext) + { + _ = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); + await HandleRequestAsync(httpContext); + + await _nextDelegate.Invoke(httpContext); + } + } +} \ No newline at end of file diff --git a/src/AnonymousUser/AnonymousUserOptions.cs b/src/AnonymousUser/AnonymousUserOptions.cs new file mode 100644 index 0000000..c874aef --- /dev/null +++ b/src/AnonymousUser/AnonymousUserOptions.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser +{ + /// <summary> + /// Configuration options for the middleware. + /// </summary> + public class AnonymousUserOptions + { + /// <summary>The name of the cookie.</summary> + public string CookieName { get; set; } = "tid"; + + /// <summary>The expiration date of the cookie. Default set to 10 years.</summary> + public DateTimeOffset Expires { get; set; } = DateTimeOffset.UtcNow.AddDays(3652); + + /// <summary>The type name of the claim holding the ID.</summary> + public string ClaimType { get; set; } = "ExternalId"; + + /// <summary>Should the cookie only be allowed on https requests.</summary> + public bool Secure { get; set; } + + /// <summary>Can be overridden to customise the ID generation.</summary> + public Func<HttpContext, string> UserIdentifierFactory { get; set; } = _ => Guid.NewGuid().ToString(); + + /// <summary>The encoder service to encode/decode the cookie value. Default set to internal base64 encoder.</summary> + public ICookieEncoder EncoderService { get; set; } = new Base64CookieEncoder(); + } +} \ No newline at end of file diff --git a/src/AnonymousUser/Base64CookieEncoder.cs b/src/AnonymousUser/Base64CookieEncoder.cs new file mode 100644 index 0000000..914f471 --- /dev/null +++ b/src/AnonymousUser/Base64CookieEncoder.cs @@ -0,0 +1,46 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser +{ + /// <summary> + /// Default cookie value encoder/decoder. Uses base64 for serialisation. + /// </summary> + public class Base64CookieEncoder : ICookieEncoder + { + /// <summary> + /// Deserialises a base64 value into clear text. + /// <param name="encodedValue">A base64 encoded value.</param> + /// <returns>Returns null if <paramref name="encodedValue" /> is null, otherwise the decoded value.</returns> + /// </summary> + public Task<string> DecodeAsync(string encodedValue) + { + if (string.IsNullOrWhiteSpace(encodedValue)) + { + return Task.FromResult((string)null); + } + + var bytes = Convert.FromBase64String(encodedValue); + + return Task.FromResult(Encoding.UTF8.GetString(bytes)); + } + + /// <summary> + /// Serialiases a clear text value into base64. + /// <param name="value">A clear text value.</param> + /// <returns>Returns null if <paramref name="value" /> is null, otherwise the encoded value.</returns> + /// </summary> + public Task<string> EncodeAsync(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Task.FromResult((string)null); + } + + var bytes = Encoding.UTF8.GetBytes(value); + + return Task.FromResult(Convert.ToBase64String(bytes)); + } + } +} \ No newline at end of file diff --git a/src/AnonymousUser/ICookieEncoder.cs b/src/AnonymousUser/ICookieEncoder.cs new file mode 100644 index 0000000..bd192e6 --- /dev/null +++ b/src/AnonymousUser/ICookieEncoder.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; + +namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser +{ + /// <summary> + /// Interface for cookie value serialisation. + /// </summary> + public interface ICookieEncoder + { + /// <summary> + /// Serialiases a clear text value. + /// <param name="value">A clear text value</param> + /// </summary> + Task<string> EncodeAsync(string value); + + /// <summary> + /// Deserialises the given value into clear text. + /// <param name="encodedValue">The serialised value</param> + /// </summary> + Task<string> DecodeAsync(string encodedValue); + } +} \ No newline at end of file diff --git a/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs b/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs new file mode 100644 index 0000000..5d75138 --- /dev/null +++ b/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using AutoFixture.NUnit3; +using InsightArchitectures.Extensions.AspNetCore.AnonymousUser; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Moq; +using NUnit.Framework; + +namespace AnonymousUserTests +{ + public class AnonymousUserMiddlewareTests + { + [Test, CustomAutoDataAttribute] + public async Task NoCookiesShouldCreateCookie(HttpContext context, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut) + { + await sut.InvokeAsync(context); + + var actual = GetCookieValueFromResponse(context.Response, options.CookieName); + + Assert.IsFalse(string.IsNullOrWhiteSpace(actual)); + } + + [Test, CustomAutoDataAttribute] + public async Task ExistingCookieShouldNotAddCookieToResponse(HttpContext context, [Frozen] Mock<HttpRequest> httpRequest, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut) + { + var cookies = new Dictionary<string, string> + { + [options.CookieName] = await options.EncoderService.EncodeAsync("RANDOM") + }; + httpRequest.Setup(x => x.Cookies).Returns(new RequestCookieCollection(cookies)); + + await sut.InvokeAsync(context); + + var actual = GetCookieValueFromResponse(context.Response, options.CookieName); + + Assert.IsTrue(string.IsNullOrWhiteSpace(actual)); + } + + [Test, CustomAutoDataAttribute] + public async Task SecureCookieWithHttpShouldExpire(HttpContext context, [Frozen] Mock<HttpRequest> httpRequest, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut) + { + var cookies = new Dictionary<string, string> + { + [options.CookieName] = await options.EncoderService.EncodeAsync("RANDOM") + }; + httpRequest.Setup(x => x.Cookies).Returns(new RequestCookieCollection(cookies)); + + options.Secure = true; + + await sut.InvokeAsync(context); + + var actual = GetCookieValueFromResponse(context.Response, options.CookieName); + + Assert.IsEmpty(actual); + } + + [Test, CustomAutoDataAttribute] + public async Task AuthenticatedUserShouldSkipMiddleware(HttpContext context, [Frozen] Mock<ClaimsPrincipal> claimsPrincipal, AnonymousUserMiddleware sut) + { + claimsPrincipal.Setup(x => x.Identity).Returns(new ClaimsIdentity(null, "Test")); + claimsPrincipal.Setup(x => x.AddIdentity(It.IsAny<ClaimsIdentity>())).Verifiable(); + + await sut.InvokeAsync(context); + + claimsPrincipal.Verify(x => x.AddIdentity(It.IsAny<ClaimsIdentity>()), Times.Never); + } + + private string? GetCookieValueFromResponse(HttpResponse response, string cookieName) + { + foreach (var headers in response.Headers.Values) + { + foreach (var header in headers) + { + if (header.StartsWith(cookieName)) + { + { + var p1 = header.IndexOf('='); + var p2 = header.IndexOf(';'); + return header.Substring(p1 + 1, p2 - p1 - 1); + } + } + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/tests/AnonymousUserTests/AnonymousUserTests.csproj b/tests/AnonymousUserTests/AnonymousUserTests.csproj new file mode 100644 index 0000000..04d51fb --- /dev/null +++ b/tests/AnonymousUserTests/AnonymousUserTests.csproj @@ -0,0 +1,25 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AutoFixture" Version="4.17.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageReference Include="AutoFixture.NUnit3" Version="4.17.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> + <PackageReference Include="coverlet.collector" Version="3.1.0" /> + <PackageReference Include="System.ComponentModel.TypeConverter" Version="4.3.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\src\AnonymousUser\AnonymousUser.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/AnonymousUserTests/Base64CookieEncoderTests.cs b/tests/AnonymousUserTests/Base64CookieEncoderTests.cs new file mode 100644 index 0000000..e6e568b --- /dev/null +++ b/tests/AnonymousUserTests/Base64CookieEncoderTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using InsightArchitectures.Extensions.AspNetCore.AnonymousUser; +using NUnit.Framework; + +namespace AnonymousUserTests +{ + public class Base64CookieEncoderTests + { + [Test, CustomAutoDataAttribute] + public async Task EncodingAndDecodingShouldReturnInitialValue(Base64CookieEncoder sut, string initialValue) + { + var encodedValue = await sut.EncodeAsync(initialValue); + var decodedValue = await sut.DecodeAsync(encodedValue); + + Assert.AreEqual(initialValue, decodedValue); + } + + [Test, CustomAutoDataAttribute] + public async Task EncodingShouldReturnBase64String(Base64CookieEncoder sut, string decodedValue) + { + var encodedValue = await sut.EncodeAsync(decodedValue); + + Assert.IsTrue(IsBase64String(encodedValue)); + + bool IsBase64String(string value) + { + Span<byte> buffer = stackalloc byte[value.Length]; + return Convert.TryFromBase64String(value, buffer, out _); + } + } + + [Test, CustomAutoDataAttribute] + public async Task NullInputShouldReturnNull(Base64CookieEncoder sut) + { + Assert.IsNull(await sut.EncodeAsync(null)); + Assert.IsNull(await sut.DecodeAsync(null)); + } + } +} \ No newline at end of file diff --git a/tests/AnonymousUserTests/CustomAutoDataAttribute.cs b/tests/AnonymousUserTests/CustomAutoDataAttribute.cs new file mode 100644 index 0000000..3246090 --- /dev/null +++ b/tests/AnonymousUserTests/CustomAutoDataAttribute.cs @@ -0,0 +1,59 @@ +using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using AutoFixture.NUnit3; +using InsightArchitectures.Extensions.AspNetCore.AnonymousUser; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Moq; + +namespace AnonymousUserTests +{ + + [AttributeUsage(AttributeTargets.Method)] + public class CustomAutoDataAttribute : AutoDataAttribute + { + public CustomAutoDataAttribute() : base(FixtureHelpers.CreateFixture) + { + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class CustomInlineAutoDataAttribute : InlineAutoDataAttribute + { + public CustomInlineAutoDataAttribute(params object[] args) : base(FixtureHelpers.CreateFixture, args) + { + } + } + + internal static class FixtureHelpers + { + public static IFixture CreateFixture() + { + var fixture = new Fixture(); + + fixture.Customize(new AutoMoqCustomization + { + ConfigureMembers = true, + GenerateDelegates = true, + }); + + fixture.Customize<Mock<HttpRequest>>(sb => sb.FromFactory(() => new Mock<HttpRequest>())); + + var requestMock = fixture.Freeze<Mock<HttpRequest>>(); + requestMock.Setup(x => x.Cookies).Returns(new RequestCookieCollection()); + requestMock.Setup(x => x.IsHttps).Returns(false); + + var contextMock = fixture.Freeze<Mock<HttpContext>>(); + contextMock.Setup(x => x.Response).Returns(new DefaultHttpResponse(new DefaultHttpContext())); + + fixture.Customize<AnonymousUserOptions>(sb => + { + return sb.With(x => x.EncoderService, new Base64CookieEncoder()) + .With(x => x.Secure, false); + }); + + return fixture; + } + } +} \ No newline at end of file