diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 845d105..496525d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,7 @@ stages: - test - deploy -build_job: +build: stage: build tags: - docker @@ -20,22 +20,25 @@ build_job: - artifacts/*.snupkg expire_in: 1 day -test_job: +test: stage: test tags: - docker - coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/' + # branch-coverage + # coverage: '/Total[^|]*\|[^|]*\|\s*([0-9.%]+)/' + # line-coverage + coverage: '/Total[^|]*\|\s*([0-9.%]+)/' script: - dotnet test -c Release - dependencies: - - build_job + dependencies: + - build -deploy_job: +deploy: stage: deploy tags: - docker script: - dotnet nuget push -k $APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg - dependencies: - - build_job - - test_job + dependencies: + - build + - test diff --git a/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs index 603d40e..f13044e 100644 --- a/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/BasicAuthenticationAttribute.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Authorization /// /// A basic authentication as attribute to use for specific actions. /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class BasicAuthenticationAttribute : Attribute, IAsyncAuthorizationFilter { /// @@ -53,11 +54,14 @@ namespace Microsoft.AspNetCore.Authorization var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]); byte[] decoded = Convert.FromBase64String(authHeader.Parameter); string plain = Encoding.UTF8.GetString(decoded); - string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); + + // See: https://www.rfc-editor.org/rfc/rfc2617, page 6 + string username = plain.Split(':').First(); + string password = plain[(username.Length + 1)..]; if (!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password)) { - if (Username == credentials.First() && Password == credentials.Last()) + if (Username == username && Password == password) return; } @@ -82,7 +86,7 @@ namespace Microsoft.AspNetCore.Authorization context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic"; if (!string.IsNullOrWhiteSpace(realm)) - context.HttpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm.Trim().Replace("\"", "")}\""; + context.HttpContext.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{realm.Trim().Replace("\"", "")}\""; context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized); @@ -98,22 +102,29 @@ namespace Microsoft.AspNetCore.Authorization var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]); byte[] decoded = Convert.FromBase64String(authHeader.Parameter); string plain = Encoding.UTF8.GetString(decoded); - string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); + + // See: https://www.rfc-editor.org/rfc/rfc2617, page 6 + string username = plain.Split(':').First(); + string password = plain[(username.Length + 1)..]; var validator = context.HttpContext.RequestServices.GetService(); - var result = await validator?.ValidateAsync(credentials.First(), credentials.Last(), context.HttpContext.GetRemoteIpAddress()); - if (result != null) - context.HttpContext.User = result; + if (validator == null) + return null; + var result = await validator.ValidateAsync(username, password, context.HttpContext.GetRemoteIpAddress()); + if (result == null) + return null; + + context.HttpContext.User = result; return result; } + return null; } catch (Exception ex) { logger?.LogError(ex, $"Using validator to get HTTP user failed: {ex.InnerException?.Message ?? ex.Message}"); + return null; } - - return null; } } } diff --git a/AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs b/AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs index 86cbde5..37a6cd6 100644 --- a/AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs +++ b/AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Filters ///
/// The score from google can be found on HttpContext.Items[GoogleReCaptchaAttribute.ScoreKey]. /// - + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class GoogleReCaptchaAttribute : ActionFilterAttribute { /// diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs index fbde971..ac0fd14 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationHandler.cs @@ -16,6 +16,7 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication /// /// Implements the for Basic Authentication. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class BasicAuthenticationHandler : AuthenticationHandler { private readonly ILogger logger; @@ -51,10 +52,13 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication { var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); string plain = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter)); - string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); + + // See: https://www.rfc-editor.org/rfc/rfc2617, page 6 + string username = plain.Split(':').First(); + string password = plain[(username.Length + 1)..]; var ipAddress = Context.GetRemoteIpAddress(); - principal = await validator.ValidateAsync(credentials.First(), credentials.Last(), ipAddress); + principal = await validator.ValidateAsync(username, password, ipAddress); } catch (Exception ex) { diff --git a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs index ea574a1..0857f83 100644 --- a/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs +++ b/AMWD.Common.AspNetCore/BasicAuthentication/BasicAuthenticationMiddleware.cs @@ -4,6 +4,7 @@ using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; namespace AMWD.Common.AspNetCore.BasicAuthentication { @@ -45,9 +46,12 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); byte[] decoded = Convert.FromBase64String(authHeader.Parameter); string plain = Encoding.UTF8.GetString(decoded); - string[] credentials = plain.Split(':', 2, StringSplitOptions.RemoveEmptyEntries); - var principal = await validator.ValidateAsync(credentials.First(), credentials.Last(), httpContext.GetRemoteIpAddress()); + // See: https://www.rfc-editor.org/rfc/rfc2617, page 6 + string username = plain.Split(':').First(); + string password = plain[(username.Length + 1)..]; + + var principal = await validator.ValidateAsync(username, password, httpContext.GetRemoteIpAddress()); if (principal == null) { SetAuthenticateRequest(httpContext, validator.Realm); @@ -56,9 +60,11 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication await next.Invoke(httpContext); } - catch + catch (Exception ex) { - SetAuthenticateRequest(httpContext, validator.Realm); + var logger = (ILogger)httpContext.RequestServices.GetService(typeof(ILogger)); + logger?.LogError(ex, $"Falied to execute basic authentication middleware: {ex.InnerException?.Message ?? ex.Message}"); + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; } } @@ -66,7 +72,7 @@ namespace AMWD.Common.AspNetCore.BasicAuthentication { httpContext.Response.Headers["WWW-Authenticate"] = "Basic"; if (!string.IsNullOrWhiteSpace(realm)) - httpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm.Replace("\"", "")}\""; + httpContext.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{realm.Replace("\"", "")}\""; httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; } diff --git a/AMWD.Common.AspNetCore/Extensions/ApplicationBuilderExtensions.cs b/AMWD.Common.AspNetCore/Extensions/ApplicationBuilderExtensions.cs index 334ed04..34b492f 100644 --- a/AMWD.Common.AspNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Builder /// /// Extensions for the . /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class ApplicationBuilderExtensions { /// diff --git a/AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs b/AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs index b64dd7d..86ca2ca 100644 --- a/AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs @@ -7,6 +7,7 @@ namespace AMWD.Common.AspNetCore.Extensions /// /// Extensions for the HTML (e.g. ). /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class HtmlExtensions { /// diff --git a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs index 1e593c7..d630d61 100644 --- a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs @@ -30,13 +30,11 @@ namespace Microsoft.AspNetCore.Http /// The ip address of the client. public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string headerName = "X-Forwarded-For") { - var remote = httpContext.Connection.RemoteIpAddress; - string forwardedHeader = httpContext.Request.Headers[headerName].ToString(); if (!string.IsNullOrWhiteSpace(forwardedHeader) && IPAddress.TryParse(forwardedHeader, out var forwarded)) return forwarded; - return remote; + return httpContext.Connection.RemoteIpAddress; } /// @@ -58,11 +56,13 @@ namespace Microsoft.AspNetCore.Http /// public static string GetReturnUrl(this HttpContext httpContext) { - string url = httpContext.Items["OriginalRequest"]?.ToString(); - if (string.IsNullOrWhiteSpace(url)) - url = httpContext.Request.Query["ReturnUrl"].ToString(); + if (httpContext.Items.ContainsKey("OriginalRequest")) + return httpContext.Items["OriginalRequest"].ToString(); - return url; + if (httpContext.Request.Query.ContainsKey("ReturnUrl")) + return httpContext.Request.Query["ReturnUrl"].ToString(); + + return null; } /// @@ -70,6 +70,6 @@ namespace Microsoft.AspNetCore.Http /// /// The current . public static void ClearSession(this HttpContext httpContext) - => httpContext?.Session?.Clear(); + => httpContext.Session?.Clear(); } } diff --git a/AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs b/AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs index 969020b..88ae054 100644 --- a/AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs @@ -6,6 +6,7 @@ namespace Microsoft.Extensions.Logging /// /// Extensions for the . /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static class LoggerExtensions { // Found here: diff --git a/AMWD.Common.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/AMWD.Common.AspNetCore/Extensions/ServiceCollectionExtensions.cs index 5d24a0a..7d4511e 100644 --- a/AMWD.Common.AspNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// Extensions for the . /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class ServiceCollectionExtensions { /// diff --git a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs index 706d792..b335dbc 100644 --- a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs +++ b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their issue with other cultures than en-US. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class InvariantFloatingPointModelBinder : IModelBinder { private readonly NumberStyles supportedNumberStyles; diff --git a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinderProvider.cs b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinderProvider.cs index cb61ed5..b15d7ce 100644 --- a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinderProvider.cs +++ b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinderProvider.cs @@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// options.ModelBinderProviders.Insert(0, new CustomFloatingPointModelBinderProvider());
/// }); /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class InvariantFloatingPointModelBinderProvider : IModelBinderProvider { /// diff --git a/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs index 7d22355..ca65fab 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// A tag helper that adds a CSS class attribute based on a condition. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement(Attributes = ClassPrefix + "*")] public class ConditionClassTagHelper : TagHelper { diff --git a/AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs index 8e8cac0..521132b 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// A tag helper to create a obfuscated email link. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)] public class EmailTagHelper : TagHelper { diff --git a/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs index d4a3e83..036ddea 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// A tag helper to dynamically create integrity checks for linked sources. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("link")] [HtmlTargetElement("script")] public class IntegrityHashTagHelper : TagHelper diff --git a/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs index 4b2308a..10aad90 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs @@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// /// Adds additional behavior to the modelbinding for numeric properties. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("input", Attributes = "asp-for")] public class NumberInputTagHelper : InputTagHelper { diff --git a/AMWD.Common.AspNetCore/Utilities/BackgroundServiceStarter.cs b/AMWD.Common.AspNetCore/Utilities/BackgroundServiceStarter.cs index 6e3afeb..c4b7345 100644 --- a/AMWD.Common.AspNetCore/Utilities/BackgroundServiceStarter.cs +++ b/AMWD.Common.AspNetCore/Utilities/BackgroundServiceStarter.cs @@ -7,6 +7,7 @@ namespace Microsoft.Extensions.Hosting /// Wrapper class to start a background service. /// /// The service type. + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class BackgroundServiceStarter : IHostedService where TService : class, IHostedService { diff --git a/AMWD.Common.AspNetCore/Utilities/HtmlHelper.cs b/AMWD.Common.AspNetCore/Utilities/HtmlHelper.cs index 1909e1a..fc7ed87 100644 --- a/AMWD.Common.AspNetCore/Utilities/HtmlHelper.cs +++ b/AMWD.Common.AspNetCore/Utilities/HtmlHelper.cs @@ -17,7 +17,7 @@ namespace AMWD.Common.AspNetCore.Utilities public static bool IsDarkColor(string color) { if (string.IsNullOrWhiteSpace(color)) - return false; + throw new ArgumentNullException(nameof(color)); int r, g, b; @@ -27,9 +27,9 @@ namespace AMWD.Common.AspNetCore.Utilities if (rgbMatch.Success) { - r = Convert.ToInt32(rgbMatch.Groups[1].Value); - g = Convert.ToInt32(rgbMatch.Groups[2].Value); - b = Convert.ToInt32(rgbMatch.Groups[3].Value); + r = Convert.ToInt32(rgbMatch.Groups[1].Value, 10); + g = Convert.ToInt32(rgbMatch.Groups[2].Value, 10); + b = Convert.ToInt32(rgbMatch.Groups[3].Value, 10); } else if (hexMatchFull.Success) { @@ -45,7 +45,7 @@ namespace AMWD.Common.AspNetCore.Utilities } else { - return false; + throw new NotSupportedException($"Unknown color value '{color}'"); } double luminance = (r * 0.299 + g * 0.587 + b * 0.114) / 255; diff --git a/AMWD.Common/Extensions/DateTimeExtensions.cs b/AMWD.Common/Extensions/DateTimeExtensions.cs index 195ed8e..2da0626 100644 --- a/AMWD.Common/Extensions/DateTimeExtensions.cs +++ b/AMWD.Common/Extensions/DateTimeExtensions.cs @@ -1,6 +1,6 @@ using System.Text; -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AMWD.Common.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] namespace System { diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e04722..addca60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://git.am-wd.de/AM.WD/common/compare/v1.6.1...master) - 0000-00-00 +### Added +- UnitTests for `AspNetCore` as far as testable without massive work. + ### Changed - `BasicAuthenticationAttribute` now respects the `IBasicAuthenticationValidator.Realm` when the own `Realm` property is not set. - CI scripts diff --git a/Common.sln b/Common.sln index 94c0254..ed2b069 100644 --- a/Common.sln +++ b/Common.sln @@ -21,14 +21,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Common.Tests", "AMWD.Common.Tests\AMWD.Common.Tests.csproj", "{086E3C11-454A-4C8F-AEAA-215BAE9C443F}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F2C7556A-99EB-43EB-8954-56A24AFE928F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E5DF156A-6C8B-4004-BA4C-A8DDE6FD3ECD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AMWD.Common.Moq", "AMWD.Common.Moq\AMWD.Common.Moq.csproj", "{6EBA2792-0B66-4C90-89A1-4E1D26D16443}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{9469D87B-126E-4338-92E3-701F762CB54D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,14 +47,14 @@ Global {7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Debug|Any CPU.Build.0 = Debug|Any CPU {7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Release|Any CPU.ActiveCfg = Release|Any CPU {7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Release|Any CPU.Build.0 = Release|Any CPU - {086E3C11-454A-4C8F-AEAA-215BAE9C443F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {086E3C11-454A-4C8F-AEAA-215BAE9C443F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {086E3C11-454A-4C8F-AEAA-215BAE9C443F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {086E3C11-454A-4C8F-AEAA-215BAE9C443F}.Release|Any CPU.Build.0 = Release|Any CPU {6EBA2792-0B66-4C90-89A1-4E1D26D16443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6EBA2792-0B66-4C90-89A1-4E1D26D16443}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EBA2792-0B66-4C90-89A1-4E1D26D16443}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EBA2792-0B66-4C90-89A1-4E1D26D16443}.Release|Any CPU.Build.0 = Release|Any CPU + {9469D87B-126E-4338-92E3-701F762CB54D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9469D87B-126E-4338-92E3-701F762CB54D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,8 +63,8 @@ Global {F512C474-B670-4E47-911E-7C0674AA8E7E} = {F2C7556A-99EB-43EB-8954-56A24AFE928F} {725F40C9-8172-487F-B3D0-D7E38B4DB197} = {F2C7556A-99EB-43EB-8954-56A24AFE928F} {7091CECF-C981-4FB9-9CC6-91C4E65A6356} = {F2C7556A-99EB-43EB-8954-56A24AFE928F} - {086E3C11-454A-4C8F-AEAA-215BAE9C443F} = {E5DF156A-6C8B-4004-BA4C-A8DDE6FD3ECD} {6EBA2792-0B66-4C90-89A1-4E1D26D16443} = {F2C7556A-99EB-43EB-8954-56A24AFE928F} + {9469D87B-126E-4338-92E3-701F762CB54D} = {E5DF156A-6C8B-4004-BA4C-A8DDE6FD3ECD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {961E8DF8-DDF5-4D10-A510-CE409E9962AC} diff --git a/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs b/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs new file mode 100644 index 0000000..51c49ac --- /dev/null +++ b/UnitTests/AspNetCore/Attributes/BasicAuthenticationAttributeTests.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using AMWD.Common.AspNetCore.BasicAuthentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UnitTests.AspNetCore.Attributes +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class BasicAuthenticationAttributeTests + { + private Mock requestHeaderMock; + private Mock responseHeaderMock; + + private Mock requestMock; + private Mock responseMock; + + private Mock contextMock; + + private Dictionary requestHeaders; + private string validatorRealm; + private ClaimsPrincipal validatorResult; + + private string responseHeaderAuthCallback; + + [TestInitialize] + public void InitializeTest() + { + requestHeaders = new Dictionary(); + validatorRealm = null; + validatorResult = null; + + responseHeaderAuthCallback = null; + } + + [TestMethod] + public async Task ShouldValidateViaUsernamePassword() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Username = "user", + Password = "password" + }; + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}:{attribute.Password}"))}"); + + var context = GetContext(); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNull(context.Result); + Assert.IsTrue(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + } + + [TestMethod] + public async Task ShouldValidateViaValidator() + { + // arrange + var attribute = new BasicAuthenticationAttribute(); + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}:{attribute.Password}"))}"); + validatorResult = new ClaimsPrincipal(); + + var context = GetContext(hasValidator: true); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNull(context.Result); + Assert.IsTrue(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + } + + [TestMethod] + public async Task ShouldAllowAnonymous() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Username = "user", + Password = "password" + }; + var context = GetContext(isAnonymousAllowed: true); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNull(context.Result); + Assert.IsTrue(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + } + + [TestMethod] + public async Task ShouldAskOnUsernamePasswordWithoutRealm() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Username = "user", + Password = "password" + }; + var context = GetContext(); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(401, ((StatusCodeResult)context.Result).StatusCode); + + Assert.IsFalse(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + Assert.AreEqual("Basic", responseHeaderAuthCallback); + } + + [TestMethod] + public async Task ShouldAskOnUsernamePasswordWithRealm() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Username = "user", + Password = "password", + Realm = "re:al\"m" + }; + var context = GetContext(); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(401, ((StatusCodeResult)context.Result).StatusCode); + + Assert.IsFalse(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + Assert.AreEqual("Basic realm=\"re:alm\"", responseHeaderAuthCallback); + } + + [TestMethod] + public async Task ShouldAskOnUsernamePasswordWrongUser() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Username = "user", + Password = "password" + }; + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}a:{attribute.Password}"))}"); + var context = GetContext(); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(401, ((StatusCodeResult)context.Result).StatusCode); + + Assert.IsFalse(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + Assert.AreEqual("Basic", responseHeaderAuthCallback); + } + + [TestMethod] + public async Task ShouldAskOnUsernamePasswordWrongPassword() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Username = "user", + Password = "password" + }; + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}:{attribute.Password}a"))}"); + var context = GetContext(); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(401, ((StatusCodeResult)context.Result).StatusCode); + + Assert.IsFalse(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + Assert.AreEqual("Basic", responseHeaderAuthCallback); + } + + [TestMethod] + public async Task ShouldAskOnValidatorWithRealmOnAttribute() + { + // arrange + var attribute = new BasicAuthenticationAttribute + { + Realm = "attribute" + }; + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}:{attribute.Password}"))}"); + var context = GetContext(hasValidator: true); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(401, ((StatusCodeResult)context.Result).StatusCode); + + Assert.IsFalse(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + Assert.AreEqual("Basic realm=\"attribute\"", responseHeaderAuthCallback); + } + + [TestMethod] + public async Task ShouldAskOnValidatorWithRealmOnValidator() + { + // arrange + validatorRealm = "validator"; + var attribute = new BasicAuthenticationAttribute(); + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}:{attribute.Password}"))}"); + var context = GetContext(hasValidator: true); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(401, ((StatusCodeResult)context.Result).StatusCode); + + Assert.IsFalse(string.IsNullOrWhiteSpace(responseHeaderAuthCallback)); + Assert.AreEqual("Basic realm=\"validator\"", responseHeaderAuthCallback); + } + + [TestMethod] + public async Task ShouldReturnInternalError() + { + // arrange + var attribute = new BasicAuthenticationAttribute(); + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{attribute.Username}"))}"); + var context = GetContext(); + + // act + await attribute.OnAuthorizationAsync(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(500, ((StatusCodeResult)context.Result).StatusCode); + } + + private AuthorizationFilterContext GetContext(bool isAnonymousAllowed = false, bool hasValidator = false) + { + requestHeaderMock = new Mock(); + foreach (var header in requestHeaders) + { + requestHeaderMock + .Setup(h => h.ContainsKey(header.Key)) + .Returns(true); + requestHeaderMock + .Setup(h => h[header.Key]) + .Returns(header.Value); + } + + responseHeaderMock = new Mock(); + responseHeaderMock + .SetupSet(h => h["WWW-Authenticate"] = It.IsAny()) + .Callback((key, value) => + { + responseHeaderAuthCallback = value; + }); + + requestMock = new Mock(); + requestMock + .Setup(r => r.Headers) + .Returns(requestHeaderMock.Object); + + responseMock = new Mock(); + responseMock + .Setup(r => r.Headers) + .Returns(responseHeaderMock.Object); + + var requestServicesMock = new Mock(); + + if (hasValidator) + { + var validatorMock = new Mock(); + validatorMock + .Setup(v => v.Realm) + .Returns(validatorRealm); + validatorMock + .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(validatorResult); + + requestServicesMock + .Setup(rs => rs.GetService(typeof(IBasicAuthenticationValidator))) + .Returns(validatorMock.Object); + } + + var connectionInfoMock = new Mock(); + connectionInfoMock + .Setup(ci => ci.RemoteIpAddress) + .Returns(IPAddress.Loopback); + + contextMock = new Mock(); + contextMock + .Setup(c => c.Request) + .Returns(requestMock.Object); + contextMock + .Setup(c => c.Response) + .Returns(responseMock.Object); + contextMock + .Setup(c => c.RequestServices) + .Returns(requestServicesMock.Object); + contextMock + .Setup(c => c.Connection) + .Returns(connectionInfoMock.Object); + + var routeDataMock = new Mock(); + + var actionDescriptor = new ActionDescriptor + { + EndpointMetadata = new List() + }; + if (isAnonymousAllowed) + actionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); + + return new AuthorizationFilterContext(new ActionContext + { + HttpContext = contextMock.Object, + RouteData = routeDataMock.Object, + ActionDescriptor = actionDescriptor, + }, new List()); + } + } +} diff --git a/UnitTests/AspNetCore/Attributes/IPBlacklistAttributeTests.cs b/UnitTests/AspNetCore/Attributes/IPBlacklistAttributeTests.cs new file mode 100644 index 0000000..5ca53c5 --- /dev/null +++ b/UnitTests/AspNetCore/Attributes/IPBlacklistAttributeTests.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UnitTests.AspNetCore.Attributes +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class IPBlacklistAttributeTests + { + private Dictionary requestHeaders; + private Dictionary itemsCallback; + private string configKey; + private bool configExists; + private List restrictedIpsConfig; + + [TestInitialize] + public void InitializeTest() + { + requestHeaders = new Dictionary(); + itemsCallback = new Dictionary(); + configKey = null; + configExists = false; + restrictedIpsConfig = new List(); + } + + [TestMethod] + public void ShouldAllowOnNoConfiguration() + { + // arrange + var remote = IPAddress.Parse("192.168.178.1"); + var attribute = new IPBlacklistAttribute(); + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldAllowOnWrongConfiguration() + { + // arrange + var remote = IPAddress.Parse("192.168.178.1"); + var attribute = new IPBlacklistAttribute + { + RestrictedIpAddresses = "192.168.178:1" + }; + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldAllowLocalAccess() + { + // arrange + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = false, + RestrictedIpAddresses = "127.0.0.0/8" + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldBlockLocalAccess() + { + // arrange + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = true, + RestrictedIpAddresses = ",127.0.0.0/8" + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [DataTestMethod] + [DataRow("192.168.178.10")] + [DataRow("192.168.178.20")] + public void ShouldBlockSpecificAddress(string address) + { + // arrange + var remote = IPAddress.Parse(address); + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = true, + RestrictedIpAddresses = "127.0.0.0/8,192.168.178.10" + }; + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + if (address == "192.168.178.10") + { + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + } + else + { + Assert.IsNull(context.Result); + } + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldAllowLocalAccessConfig() + { + // arrange + configKey = "Black:List"; + configExists = true; + restrictedIpsConfig.Add("127.0.0.0/8"); + restrictedIpsConfig.Add("192.168.178.10"); + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = false, + ConfigurationKey = configKey + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldBlockLocalAccessConfig() + { + // arrange + configKey = "Black:List"; + configExists = true; + restrictedIpsConfig.Add(""); + restrictedIpsConfig.Add("127.0.0.0/8"); + restrictedIpsConfig.Add("192.168.178.10"); + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = true, + ConfigurationKey = configKey + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [DataTestMethod] + [DataRow("192.168.178.10")] + [DataRow("192.168.178.20")] + public void ShouldBlockSpecificAddressConfig(string address) + { + // arrange + configKey = "Black:List"; + configExists = true; + restrictedIpsConfig.Add("127.0.0.0/8"); + restrictedIpsConfig.Add("192.168.178.10"); + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = true, + ConfigurationKey = configKey + }; + var remote = IPAddress.Parse(address); + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + if (address == "192.168.178.10") + { + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + } + else + { + Assert.IsNull(context.Result); + } + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldAllowOnMissingConfiguration() + { + // arrange + configKey = "Black:List"; + configExists = false; + var attribute = new IPBlacklistAttribute + { + RestrictLocalAccess = true, + ConfigurationKey = configKey + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + private ActionExecutingContext GetContext(IPAddress remote = null) + { + var requestHeaderMock = new Mock(); + foreach (var header in requestHeaders) + { + requestHeaderMock + .Setup(h => h.ContainsKey(header.Key)) + .Returns(true); + requestHeaderMock + .Setup(h => h[header.Key]) + .Returns(header.Value); + } + + var requestMock = new Mock(); + requestMock + .Setup(r => r.Headers) + .Returns(requestHeaderMock.Object); + + var connectionInfoMock = new Mock(); + connectionInfoMock + .Setup(ci => ci.LocalIpAddress) + .Returns(IPAddress.Loopback); + connectionInfoMock + .Setup(ci => ci.RemoteIpAddress) + .Returns(remote ?? IPAddress.Loopback); + + var itemsMock = new Mock>(); + itemsMock + .SetupSet(i => i[It.IsAny()] = It.IsAny()) + .Callback((key, val) => itemsCallback.Add(key, val)); + + var configurationMock = new Mock(); + var children = new List(); + foreach (string ipAddress in restrictedIpsConfig) + { + var csm = new Mock(); + csm.Setup(cs => cs.Value).Returns(ipAddress); + + children.Add(csm.Object); + } + + var configSectionMock = new Mock(); + configSectionMock + .Setup(cs => cs.GetChildren()) + .Returns(children); + + configurationMock + .Setup(c => c.GetSection(configKey)) + .Returns(configExists ? configSectionMock.Object : null); + + var requestServicesMock = new Mock(); + requestServicesMock + .Setup(s => s.GetService(typeof(IConfiguration))) + .Returns(configurationMock.Object); + + var contextMock = new Mock(); + contextMock + .Setup(c => c.Request) + .Returns(requestMock.Object); + contextMock + .Setup(c => c.RequestServices) + .Returns(requestServicesMock.Object); + contextMock + .Setup(c => c.Items) + .Returns(itemsMock.Object); + contextMock + .Setup(c => c.Connection) + .Returns(connectionInfoMock.Object); + + var routeDataMock = new Mock(); + var actionDescriptorMock = new Mock(); + + return new ActionExecutingContext(new ActionContext + { + HttpContext = contextMock.Object, + RouteData = routeDataMock.Object, + ActionDescriptor = actionDescriptorMock.Object, + }, new List(), new Dictionary(), null); + } + } +} diff --git a/UnitTests/AspNetCore/Attributes/IPWhitelistAttributeTests.cs b/UnitTests/AspNetCore/Attributes/IPWhitelistAttributeTests.cs new file mode 100644 index 0000000..0545aac --- /dev/null +++ b/UnitTests/AspNetCore/Attributes/IPWhitelistAttributeTests.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UnitTests.AspNetCore.Attributes +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class IPWhitelistAttributeTests + { + private Dictionary requestHeaders; + private Dictionary itemsCallback; + private string configKey; + private bool configExists; + private List allowedIpsConfig; + + [TestInitialize] + public void InitializeTest() + { + requestHeaders = new Dictionary(); + itemsCallback = new Dictionary(); + configKey = null; + configExists = false; + allowedIpsConfig = new List(); + } + + [TestMethod] + public void ShouldDenyOnNoConfiguration() + { + // arrange + var remote = IPAddress.Parse("192.168.178.1"); + var attribute = new IPWhitelistAttribute(); + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldDenyOnWrongConfiguration() + { + // arrange + var remote = IPAddress.Parse("192.168.178.1"); + var attribute = new IPWhitelistAttribute + { + AllowedIpAddresses = "192.168.178:1" + }; + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldAllowLocalAccess() + { + // arrange + var attribute = new IPWhitelistAttribute(); + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldDenyLocalAccess() + { + // arrange + var attribute = new IPWhitelistAttribute + { + AllowLocalAccess = false + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [DataTestMethod] + [DataRow("192.168.178.10")] + [DataRow("192.168.178.20")] + public void ShouldAllowSpecificAddress(string address) + { + // arrange + var remote = IPAddress.Parse(address); + var attribute = new IPWhitelistAttribute + { + AllowLocalAccess = false, + AllowedIpAddresses = ",127.0.0.0/8,192.168.178.10" + }; + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + if (address == "192.168.178.10") + { + Assert.IsNull(context.Result); + } + else + { + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + } + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldAllowLocalAccessConfig() + { + // arrange + configKey = "White:List"; + configExists = true; + allowedIpsConfig.Add("127.0.0.0/8"); + allowedIpsConfig.Add("192.168.178.10"); + var attribute = new IPWhitelistAttribute + { + AllowLocalAccess = true, + ConfigurationKey = configKey + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNull(context.Result); + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldDenyLocalAccessConfig() + { + // arrange + configKey = "White:List"; + configExists = true; + allowedIpsConfig.Add(""); + allowedIpsConfig.Add("192.168.178.10"); + var attribute = new IPWhitelistAttribute + { + AllowLocalAccess = false, + ConfigurationKey = configKey + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + [DataTestMethod] + [DataRow("192.168.178.10")] + [DataRow("192.168.178.20")] + public void ShouldAllowSpecificAddressConfig(string address) + { + // arrange + configKey = "White:List"; + configExists = true; + allowedIpsConfig.Add("192.168.178.10"); + var attribute = new IPWhitelistAttribute + { + AllowLocalAccess = false, + ConfigurationKey = configKey + }; + var remote = IPAddress.Parse(address); + var context = GetContext(remote); + + // act + attribute.OnActionExecuting(context); + + // assert + if (address == "192.168.178.10") + { + Assert.IsNull(context.Result); + } + else + { + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + } + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(remote, itemsCallback["RemoteAddress"]); + } + + [TestMethod] + public void ShouldDenyOnMissingConfiguration() + { + // arrange + configKey = "White:List"; + configExists = false; + var attribute = new IPWhitelistAttribute + { + AllowLocalAccess = false, + ConfigurationKey = configKey + }; + var context = GetContext(); + + // act + attribute.OnActionExecuting(context); + + // assert + Assert.IsNotNull(context.Result); + Assert.IsTrue(context.Result is StatusCodeResult); + Assert.AreEqual(403, ((StatusCodeResult)context.Result).StatusCode); + + Assert.AreEqual(1, itemsCallback.Count); + Assert.AreEqual(IPAddress.Loopback, itemsCallback["RemoteAddress"]); + } + + private ActionExecutingContext GetContext(IPAddress remote = null) + { + var requestHeaderMock = new Mock(); + foreach (var header in requestHeaders) + { + requestHeaderMock + .Setup(h => h.ContainsKey(header.Key)) + .Returns(true); + requestHeaderMock + .Setup(h => h[header.Key]) + .Returns(header.Value); + } + + var requestMock = new Mock(); + requestMock + .Setup(r => r.Headers) + .Returns(requestHeaderMock.Object); + + var connectionInfoMock = new Mock(); + connectionInfoMock + .Setup(ci => ci.LocalIpAddress) + .Returns(IPAddress.Loopback); + connectionInfoMock + .Setup(ci => ci.RemoteIpAddress) + .Returns(remote ?? IPAddress.Loopback); + + var itemsMock = new Mock>(); + itemsMock + .SetupSet(i => i[It.IsAny()] = It.IsAny()) + .Callback((key, val) => itemsCallback.Add(key, val)); + + var configurationMock = new Mock(); + var children = new List(); + foreach (string ipAddress in allowedIpsConfig) + { + var csm = new Mock(); + csm.Setup(cs => cs.Value).Returns(ipAddress); + + children.Add(csm.Object); + } + + var configSectionMock = new Mock(); + configSectionMock + .Setup(cs => cs.GetChildren()) + .Returns(children); + + configurationMock + .Setup(c => c.GetSection(configKey)) + .Returns(configExists ? configSectionMock.Object : null); + + var requestServicesMock = new Mock(); + requestServicesMock + .Setup(s => s.GetService(typeof(IConfiguration))) + .Returns(configurationMock.Object); + + var contextMock = new Mock(); + contextMock + .Setup(c => c.Request) + .Returns(requestMock.Object); + contextMock + .Setup(c => c.RequestServices) + .Returns(requestServicesMock.Object); + contextMock + .Setup(c => c.Items) + .Returns(itemsMock.Object); + contextMock + .Setup(c => c.Connection) + .Returns(connectionInfoMock.Object); + + var routeDataMock = new Mock(); + var actionDescriptorMock = new Mock(); + + return new ActionExecutingContext(new ActionContext + { + HttpContext = contextMock.Object, + RouteData = routeDataMock.Object, + ActionDescriptor = actionDescriptorMock.Object, + }, new List(), new Dictionary(), null); + } + } +} diff --git a/UnitTests/AspNetCore/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs b/UnitTests/AspNetCore/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs new file mode 100644 index 0000000..4827566 --- /dev/null +++ b/UnitTests/AspNetCore/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using AMWD.Common.AspNetCore.BasicAuthentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UnitTests.AspNetCore.BasicAuthentication +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class BasicAuthenticationMiddlewareTests + { + private Dictionary requestHeaders; + + private Dictionary responseHeadersCallback; + private int responseStatusCodeCallback; + + private string validatorRealm; + private ClaimsPrincipal validatorResponse; + private List<(string username, string password, IPAddress ipAddr)> validatorCallback; + + [TestInitialize] + public void InitializeTests() + { + requestHeaders = new Dictionary(); + + responseHeadersCallback = new Dictionary(); + responseStatusCodeCallback = 0; + + validatorRealm = null; + validatorResponse = null; + validatorCallback = new List<(string username, string password, IPAddress ipAddr)>(); + } + + [TestMethod] + public async Task ShouldAllowAccess() + { + // arrange + string username = "user"; + string password = "pass:word"; + + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"))}"); + validatorResponse = new ClaimsPrincipal(); + + var middleware = GetMiddleware(); + var context = GetContext(); + + // act + await middleware.InvokeAsync(context); + + // assert + Assert.AreEqual(0, responseStatusCodeCallback); // not triggered + Assert.AreEqual(0, responseHeadersCallback.Count); + Assert.AreEqual(1, validatorCallback.Count); + + Assert.AreEqual(username, validatorCallback.First().username); + Assert.AreEqual(password, validatorCallback.First().password); + Assert.AreEqual(IPAddress.Loopback, validatorCallback.First().ipAddr); + } + + [TestMethod] + public async Task ShouldDenyMissingHeader() + { + // arrange + var middleware = GetMiddleware(); + var context = GetContext(); + + // act + await middleware.InvokeAsync(context); + + // assert + Assert.AreEqual(401, responseStatusCodeCallback); + + Assert.AreEqual(0, validatorCallback.Count); + + Assert.AreEqual(1, responseHeadersCallback.Count); + Assert.AreEqual("WWW-Authenticate", responseHeadersCallback.Keys.First()); + Assert.AreEqual("Basic", responseHeadersCallback.Values.First()); + } + + [TestMethod] + public async Task ShouldDenyNoResult() + { + // arrange + string username = "user"; + string password = "pw"; + + validatorRealm = "TEST"; + var remote = IPAddress.Parse("1.2.3.4"); + + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"))}"); + + var middleware = GetMiddleware(); + var context = GetContext(remote); + + // act + await middleware.InvokeAsync(context); + + // assert + Assert.AreEqual(401, responseStatusCodeCallback); + + Assert.AreEqual(1, responseHeadersCallback.Count); + Assert.AreEqual("WWW-Authenticate", responseHeadersCallback.Keys.First()); + Assert.AreEqual($"Basic realm=\"{validatorRealm}\"", responseHeadersCallback.Values.First()); + + Assert.AreEqual(1, validatorCallback.Count); + Assert.AreEqual(username, validatorCallback.First().username); + Assert.AreEqual(password, validatorCallback.First().password); + Assert.AreEqual(remote, validatorCallback.First().ipAddr); + } + + [TestMethod] + public async Task ShouldBreakOnException() + { + // arrange + string username = "user"; + + requestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}"))}"); + + var middleware = GetMiddleware(); + var context = GetContext(); + + // act + await middleware.InvokeAsync(context); + + // assert + Assert.AreEqual(500, responseStatusCodeCallback); + } + + private BasicAuthenticationMiddleware GetMiddleware() + { + var nextMock = new Mock(); + var validatorMock = new Mock(); + validatorMock + .Setup(v => v.Realm) + .Returns(validatorRealm); + validatorMock + .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((username, password, ipAddress) => validatorCallback.Add((username, password, ipAddress))) + .ReturnsAsync(validatorResponse); + + return new BasicAuthenticationMiddleware(nextMock.Object, validatorMock.Object); + } + + private HttpContext GetContext(IPAddress remote = null) + { + // Request + var requestHeaderMock = new Mock(); + foreach (var header in requestHeaders) + { + requestHeaderMock + .Setup(h => h.ContainsKey(header.Key)) + .Returns(true); + requestHeaderMock + .Setup(h => h[header.Key]) + .Returns(header.Value); + } + + var requestMock = new Mock(); + requestMock + .Setup(r => r.Headers) + .Returns(requestHeaderMock.Object); + + // Response + var responseHeaderMock = new Mock(); + responseHeaderMock + .SetupSet(h => h[It.IsAny()] = It.IsAny()) + .Callback((key, value) => responseHeadersCallback[key] = value); + + var responseMock = new Mock(); + responseMock + .Setup(r => r.Headers) + .Returns(responseHeaderMock.Object); + responseMock + .SetupSet(r => r.StatusCode = It.IsAny()) + .Callback((code) => responseStatusCodeCallback = code); + + // Connection + var connectionInfoMock = new Mock(); + connectionInfoMock + .Setup(ci => ci.LocalIpAddress) + .Returns(IPAddress.Loopback); + connectionInfoMock + .Setup(ci => ci.RemoteIpAddress) + .Returns(remote ?? IPAddress.Loopback); + + // Request Services + var requestServicesMock = new Mock(); + + var contextMock = new Mock(); + contextMock + .Setup(c => c.Request) + .Returns(requestMock.Object); + contextMock + .Setup(c => c.Response) + .Returns(responseMock.Object); + contextMock + .Setup(c => c.Connection) + .Returns(connectionInfoMock.Object); + contextMock + .Setup(c => c.RequestServices) + .Returns(requestServicesMock.Object); + + return contextMock.Object; + } + } +} diff --git a/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs b/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs new file mode 100644 index 0000000..247459b --- /dev/null +++ b/UnitTests/AspNetCore/Extensions/HttpContextExtensionsTests.cs @@ -0,0 +1,430 @@ +using System; +using System.Collections.Generic; +using System.Net; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace UnitTests.AspNetCore.Extensions +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class HttpContextExtensionsTests + { + private Mock sessionMock; + + private string tokenName; + private string tokenValue; + + private Dictionary requestHeaders; + private Dictionary requestQueries; + private Dictionary items; + + private IPAddress remote; + + [TestInitialize] + public void InitializeTests() + { + tokenName = null; + tokenValue = null; + + requestHeaders = new Dictionary(); + requestQueries = new Dictionary(); + items = new Dictionary(); + + remote = IPAddress.Loopback; + } + + #region Antiforgery + + [TestMethod] + public void ShouldReturnAntiforgery() + { + // arrange + tokenName = "af-token"; + tokenValue = "security_first"; + + var context = GetContext(); + + // act + var result = context.GetAntiforgeryToken(); + + // assert + Assert.AreEqual(tokenName, result.Name); + Assert.AreEqual(tokenValue, result.Value); + } + + [TestMethod] + public void ShouldReturnAntiforgeryNullService() + { + // arrange + tokenName = "af-token"; + tokenValue = "security_first"; + + var context = GetContext(hasAntiforgery: false); + + // act + var result = context.GetAntiforgeryToken(); + + // assert + Assert.AreEqual(null, result.Name); + Assert.AreEqual(null, result.Value); + } + + [TestMethod] + public void ShouldReturnAntiforgeryNullToken() + { + // arrange + var context = GetContext(); + + // act + var result = context.GetAntiforgeryToken(); + + // assert + Assert.AreEqual(null, result.Name); + Assert.AreEqual(null, result.Value); + } + + #endregion Antiforgery + + #region RemoteAddres + + [TestMethod] + public void ShouldReturnRemoteAddress() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + + var context = GetContext(); + + // act + var result = context.GetRemoteIpAddress(); + + // assert + Assert.AreEqual(remote, result); + } + + [TestMethod] + public void ShouldReturnDefaultHeader() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + var header = IPAddress.Parse("5.6.7.8"); + requestHeaders.Add("X-Forwarded-For", header.ToString()); + + var context = GetContext(); + + // act + var result = context.GetRemoteIpAddress(); + + // assert + Assert.AreNotEqual(remote, result); + Assert.AreEqual(header, result); + } + + [TestMethod] + public void ShouldReturnCustomHeader() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + string headerName = "FooBar"; + var headerIp = IPAddress.Parse("5.6.7.8"); + requestHeaders.Add(headerName, headerIp.ToString()); + + var context = GetContext(); + + // act + var result = context.GetRemoteIpAddress(headerName: headerName); + + // assert + Assert.AreNotEqual(remote, result); + Assert.AreEqual(headerIp, result); + } + + [TestMethod] + public void ShouldReturnAddressInvalidHeader() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + requestHeaders.Add("X-Forwarded-For", "1.2.3:4"); + + var context = GetContext(); + + // act + var result = context.GetRemoteIpAddress(); + + // assert + Assert.AreEqual(remote, result); + } + + #endregion RemoteAddres + + #region Local Request + + [TestMethod] + public void ShouldReturnTrueOnLocal() + { + // arrange + remote = IPAddress.Loopback; + + var context = GetContext(); + + // act + bool result = context.IsLocalRequest(); + + // assert + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldReturnFalseOnRemote() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + + var context = GetContext(); + + // act + bool result = context.IsLocalRequest(); + + // assert + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldReturnTrueOnDefaultHeader() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + var headerIp = IPAddress.Loopback; + requestHeaders.Add("X-Forwarded-For", headerIp.ToString()); + + var context = GetContext(); + + // act + bool result = context.IsLocalRequest(); + + // assert + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldReturnTrueOnCustomHeader() + { + // arrange + remote = IPAddress.Parse("1.2.3.4"); + string headerName = "FooBar"; + var headerIp = IPAddress.Loopback; + requestHeaders.Add(headerName, headerIp.ToString()); + + var context = GetContext(); + + // act + bool result = context.IsLocalRequest(headerName: headerName); + + // assert + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldReturnFalseOnDefaultHeader() + { + // arrange + var headerIp = IPAddress.Parse("1.2.3.4"); + requestHeaders.Add("X-Forwarded-For", headerIp.ToString()); + + var context = GetContext(); + + // act + bool result = context.IsLocalRequest(); + + // assert + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldReturnFalseOnCustomHeader() + { + // arrange + string headerName = "FooBar"; + var headerIp = IPAddress.Parse("1.2.3.4"); + requestHeaders.Add(headerName, headerIp.ToString()); + + var context = GetContext(); + + // act + bool result = context.IsLocalRequest(headerName: headerName); + + // assert + Assert.IsFalse(result); + } + + #endregion Local Request + + #region ReturnUrl + + [TestMethod] + public void ShouldReturnNull() + { + // arrange + var context = GetContext(); + + // act + string result = context.GetReturnUrl(); + + // assert + Assert.IsNull(result); + } + + [TestMethod] + public void ShouldReturnOriginalRequest() + { + // arrange + string request = "abc"; + string query = "def"; + + items.Add("OriginalRequest", request); + requestQueries.Add("ReturnUrl", query); + + var context = GetContext(); + + // act + string result = context.GetReturnUrl(); + + // assert + Assert.AreEqual(request, result); + Assert.AreNotEqual(query, result); + } + + [TestMethod] + public void ShouldReturnUrl() + { + // arrange + string query = "def"; + requestQueries.Add("ReturnUrl", query); + + var context = GetContext(); + + // act + string result = context.GetReturnUrl(); + + // assert + Assert.AreEqual(query, result); + } + + #endregion ReturnUrl + + #region Session + + [TestMethod] + public void ShouldClearSession() + { + // arrange + var context = GetContext(); + + // act + context.ClearSession(); + + // assert + sessionMock.Verify(s => s.Clear(), Times.Once); + } + + [TestMethod] + public void ShouldSkipWhenNoSession() + { + // arrange + var context = GetContext(hasSession: false); + + // act + context.ClearSession(); + + // assert + sessionMock.Verify(s => s.Clear(), Times.Never); + } + + #endregion Session + + private HttpContext GetContext(bool hasAntiforgery = true, bool hasSession = true) + { + // Request + var requestHeaderMock = new Mock(); + foreach (var header in requestHeaders) + { + requestHeaderMock + .Setup(h => h.ContainsKey(header.Key)) + .Returns(true); + requestHeaderMock + .Setup(h => h[header.Key]) + .Returns(header.Value); + } + + var requestQueryMock = new Mock(); + foreach (var query in requestQueries) + { + requestQueryMock + .Setup(h => h.ContainsKey(query.Key)) + .Returns(true); + requestQueryMock + .Setup(h => h[query.Key]) + .Returns(query.Value); + } + + var requestMock = new Mock(); + requestMock + .Setup(r => r.Headers) + .Returns(requestHeaderMock.Object); + requestMock + .Setup(r => r.Query) + .Returns(requestQueryMock.Object); + + // Request Services + var requestServicesMock = new Mock(); + if (hasAntiforgery) + { + var antiforgeryMock = new Mock(); + antiforgeryMock + .Setup(af => af.GetAndStoreTokens(It.IsAny())) + .Returns(string.IsNullOrWhiteSpace(tokenName) ? null : new AntiforgeryTokenSet(tokenValue, tokenValue, tokenName, tokenName)); + + requestServicesMock + .Setup(rs => rs.GetService(typeof(IAntiforgery))) + .Returns(antiforgeryMock.Object); + } + + // Connection + var connectionInfoMock = new Mock(); + connectionInfoMock + .Setup(ci => ci.LocalIpAddress) + .Returns(IPAddress.Loopback); + connectionInfoMock + .Setup(ci => ci.RemoteIpAddress) + .Returns(remote); + + // Session + sessionMock = new Mock(); + + var contextMock = new Mock(); + contextMock + .Setup(c => c.Request) + .Returns(requestMock.Object); + contextMock + .Setup(c => c.RequestServices) + .Returns(requestServicesMock.Object); + contextMock + .Setup(c => c.Connection) + .Returns(connectionInfoMock.Object); + contextMock + .Setup(c => c.Items) + .Returns(items); + if (hasSession) + { + contextMock + .Setup(c => c.Session) + .Returns(sessionMock.Object); + } + + return contextMock.Object; + } + } +} diff --git a/UnitTests/AspNetCore/Extensions/ModelStateDictionaryExtensionsTests.cs b/UnitTests/AspNetCore/Extensions/ModelStateDictionaryExtensionsTests.cs new file mode 100644 index 0000000..5889b44 --- /dev/null +++ b/UnitTests/AspNetCore/Extensions/ModelStateDictionaryExtensionsTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.AspNetCore.Extensions +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class ModelStateDictionaryExtensionsTests + { + private TestModel testModel; + + [TestInitialize] + public void InitializeTests() + { + testModel = new TestModel + { + ValueA = "A", + ValueB = "B", + SubModel = new TestSubModel + { + SubValueA = "a", + SubValueB = "b" + } + }; + } + + [TestMethod] + public void ShouldAddNormalModelError() + { + // arrange + var modelState = new ModelStateDictionary(); + + // act + modelState.AddModelError(testModel, m => m.ValueA, "ShitHappens"); + + // assert + Assert.AreEqual(1, modelState.Count); + Assert.AreEqual("ValueA", modelState.Keys.First()); + Assert.AreEqual("ShitHappens", modelState.Values.First().Errors.First().ErrorMessage); + } + + [TestMethod] + public void ShouldAddExtendedModelError() + { + // arrange + var modelState = new ModelStateDictionary(); + + // act + modelState.AddModelError(testModel, m => m.SubModel.SubValueB, "ShitHappens"); + + // assert + Assert.AreEqual(1, modelState.Count); + Assert.AreEqual("SubModel.SubValueB", modelState.Keys.First()); + Assert.AreEqual("ShitHappens", modelState.Values.First().Errors.First().ErrorMessage); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowArgumentNull() + { + // arrange + ModelStateDictionary modelState = null; + + // act + modelState.AddModelError(testModel, m => m.SubModel.SubValueB, "ShitHappens"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ShouldThrowInvalidOperation() + { + // arrange + var modelState = new ModelStateDictionary(); + + // act + modelState.AddModelError(testModel, m => m, "ShitHappens"); + } + + internal class TestModel + { + public string ValueA { get; set; } + + public string ValueB { get; set; } + + public TestSubModel SubModel { get; set; } + } + + internal class TestSubModel + { + public string SubValueA { get; set; } + + public string SubValueB { get; set; } + } + } +} diff --git a/UnitTests/AspNetCore/Extensions/SessionExtensionsTests.cs b/UnitTests/AspNetCore/Extensions/SessionExtensionsTests.cs new file mode 100644 index 0000000..9c2c230 --- /dev/null +++ b/UnitTests/AspNetCore/Extensions/SessionExtensionsTests.cs @@ -0,0 +1,159 @@ +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; + +namespace UnitTests.AspNetCore.Extensions +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class SessionExtensionsTests + { + private Mock sessionMock; + + private string sessionKey; + private byte[] sessionValue; + + private TestModel model; + + internal class TestModel + { + public string ValueA { get; set; } + + public string ValueB { get; set; } + } + + [TestInitialize] + public void InitializeTests() + { + sessionKey = null; + sessionValue = null; + + model = new TestModel + { + ValueA = "A", + ValueB = "B" + }; + } + + [TestMethod] + public void ShouldCheckKeyExists() + { + // arrange + sessionKey = "exists"; + var session = GetSession(); + + // act + bool result1 = session.HasKey("exists"); + bool result2 = session.HasKey("somewhere"); + + // assert + Assert.IsTrue(result1); + Assert.IsFalse(result2); + } + + [TestMethod] + public void ShouldGetValue() + { + // arrange + sessionKey = "test"; + sessionValue = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); + var session = GetSession(); + + // act + var result = session.GetValue(sessionKey); + + // assert + Assert.IsNotNull(result); + Assert.AreEqual(model.ValueA, result.ValueA); + Assert.AreEqual(model.ValueB, result.ValueB); + } + + [TestMethod] + public void ShouldGetNull() + { + // arrange + sessionKey = "foo"; + sessionValue = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); + var session = GetSession(); + + // act + var result = session.GetValue("bar"); + + // assert + Assert.IsNull(result); + } + + [TestMethod] + public void ShouldGetValueWithFallback() + { + // arrange + sessionKey = "test"; + sessionValue = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); + var session = GetSession(); + + // act + var result = session.GetValue(sessionKey, new TestModel()); + + // assert + Assert.IsNotNull(result); + Assert.AreEqual(model.ValueA, result.ValueA); + Assert.AreEqual(model.ValueB, result.ValueB); + } + + [TestMethod] + public void ShouldGetFallback() + { + // arrange + sessionKey = "foo"; + sessionValue = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); + var session = GetSession(); + + // act + var result = session.GetValue("bar", new TestModel()); + + // assert + Assert.IsNotNull(result); + Assert.AreEqual(null, result.ValueA); + Assert.AreEqual(null, result.ValueB); + } + + [TestMethod] + public void ShouldSetValue() + { + // arrange + string key = "foobar"; + var session = GetSession(); + + // act + session.SetValue(key, model); + + // arrange + Assert.AreEqual(key, sessionKey); + Assert.AreEqual(JsonConvert.SerializeObject(model), Encoding.UTF8.GetString(sessionValue)); + } + + private ISession GetSession() + { + string[] keys = new[] { sessionKey }; + + sessionMock = new Mock(); + sessionMock + .Setup(s => s.TryGetValue(It.IsAny(), out sessionValue)) + .Returns((key, value) => sessionKey == key); + sessionMock + .Setup(s => s.Set(It.IsAny(), It.IsAny())) + .Callback((key, value) => + { + sessionKey = key; + sessionValue = value; + }); + sessionMock + .Setup(s => s.Keys) + .Returns(keys); + + return sessionMock.Object; + } + } +} diff --git a/UnitTests/AspNetCore/Utilities/HtmlHelperTests.cs b/UnitTests/AspNetCore/Utilities/HtmlHelperTests.cs new file mode 100644 index 0000000..593dde4 --- /dev/null +++ b/UnitTests/AspNetCore/Utilities/HtmlHelperTests.cs @@ -0,0 +1,155 @@ +using System; +using AMWD.Common.AspNetCore.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.AspNetCore.Utilities +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class HtmlHelperTests + { + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ShouldThrowErrorOnEmptyColor() + { + // arrange + + // act + HtmlHelper.IsDarkColor(""); + + // assert + // exception thrown + } + + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void ShouldThrowErrorOnUnsupportedColor() + { + // arrange + + // act + HtmlHelper.IsDarkColor("hsv(1, 2, 3)"); + + // assert + // exception thrown + } + + [DataTestMethod] + [DataRow("rgb(255, 255, 255)")] + [DataRow("rgba(255, 255, 255, .5)")] + public void ShouldReturnLightColorForWhiteRgb(string white) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(white); + + // assert + Assert.IsFalse(isDark); + } + + [DataTestMethod] + [DataRow("#ffFFff")] + [DataRow("FFffFF")] + public void ShouldReturnLightColorForWhiteFullHex(string white) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(white); + + // assert + Assert.IsFalse(isDark); + } + + [DataTestMethod] + [DataRow("#fFf")] + [DataRow("FfF")] + public void ShouldReturnLightColorForWhiteShortHex(string white) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(white); + + // assert + Assert.IsFalse(isDark); + } + + [DataTestMethod] + [DataRow("rgb(0, 0, 0)")] + [DataRow("rgba(0, 0, 0, .5)")] + public void ShouldReturnDarkColorForBlackRgb(string black) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(black); + + // assert + Assert.IsTrue(isDark); + } + + [DataTestMethod] + [DataRow("#000000")] + [DataRow("000000")] + public void ShouldReturnDarkColorForBlackFullHex(string black) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(black); + + // assert + Assert.IsTrue(isDark); + } + + [DataTestMethod] + [DataRow("#000")] + [DataRow("000")] + public void ShouldReturnDarkColorForBlackShortHex(string black) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(black); + + // assert + Assert.IsTrue(isDark); + } + + [DataTestMethod] + [DataRow("rgb(255, 88, 0)")] + [DataRow("rgb(0, 218, 0)")] + [DataRow("rgb(0, 168, 255)")] + [DataRow("rgb(255, 38, 255)")] + [DataRow("rgb(128, 128, 128)")] + public void ShouldReturnLightColorForBorderColors(string color) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(color); + + // assert + Assert.IsFalse(isDark); + } + + [DataTestMethod] + [DataRow("rgb(253, 88, 0)")] + [DataRow("rgb(0, 217, 0)")] + [DataRow("rgb(0, 168, 253)")] + [DataRow("rgb(254, 38, 254)")] + [DataRow("rgb(127, 127, 127)")] + public void ShouldReturnDarkColorForBorderColors(string color) + { + // arrange + + // act + bool isDark = HtmlHelper.IsDarkColor(color); + + // assert + Assert.IsTrue(isDark); + } + } +} diff --git a/UnitTests/AspNetCore/Utilities/PasswordHelperTests.cs b/UnitTests/AspNetCore/Utilities/PasswordHelperTests.cs new file mode 100644 index 0000000..c2d3f87 --- /dev/null +++ b/UnitTests/AspNetCore/Utilities/PasswordHelperTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.AspNetCore.Utilities +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class PasswordHelperTests + { + [TestMethod] + public void ShouldReturnNullHashWhenNullProvided() + { + // arrange + string password = null; + + // act + string hash = PasswordHelper.HashPassword(password); + + // assert + Assert.IsNull(hash); + } + + [TestMethod] + public void ShouldReturnEmptyHashWhenSpacesProvided() + { + // arrange + string password = " "; + + // act + string hash = PasswordHelper.HashPassword(password); + + // assert + Assert.AreEqual("", hash); + } + + [TestMethod] + public void ShouldReturnHashWhenTextProvided() + { + // arrange + string password = "password"; + + // act + string hash = PasswordHelper.HashPassword(password); + + // assert + Assert.IsTrue(!string.IsNullOrWhiteSpace(hash)); + } + + [TestMethod] + public void ShouldReturnFalseOnNullPassword() + { + // arrange + string password = null; + string hash = PasswordHelper.HashPassword(password); + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsFalse(isValid); + Assert.IsFalse(rehashNeeded); + } + + [TestMethod] + public void ShouldReturnFalseOnEmptyPassword() + { + // arrange + string password = " "; + string hash = PasswordHelper.HashPassword(password); + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsFalse(isValid); + Assert.IsFalse(rehashNeeded); + } + + [TestMethod] + public void ShouldReturnFalseOnNullHash() + { + // arrange + string password = "password"; + string hash = null; + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsFalse(isValid); + Assert.IsFalse(rehashNeeded); + } + + [TestMethod] + public void ShouldReturnFalseOnEmptyHash() + { + // arrange + string password = "password"; + string hash = ""; + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsFalse(isValid); + Assert.IsFalse(rehashNeeded); + } + + [TestMethod] + public void ShouldReturnTrueOnSuccess() + { + // arrange + string password = "password"; + string hash = PasswordHelper.HashPassword(password); + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsTrue(isValid); + Assert.IsFalse(rehashNeeded); + } + + [TestMethod] + public void ShouldReturnFalseOnError() + { + // arrange + string password = "pass"; + string hash = PasswordHelper.HashPassword(password + "word"); + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsFalse(isValid); + Assert.IsFalse(rehashNeeded); + } + + [TestMethod] + public void ShouldReturnTrueWithRehash() + { + // arrange + var devHasher = new PasswordHasher(); + var field = devHasher.GetType().GetField("_compatibilityMode", BindingFlags.NonPublic | BindingFlags.Instance); + field.SetValue(devHasher, PasswordHasherCompatibilityMode.IdentityV2); + + string password = "password"; + string hash = devHasher.HashPassword(null, password); + + // act + bool isValid = PasswordHelper.VerifyPassword(password, hash, out bool rehashNeeded); + + // assert + Assert.IsTrue(isValid); + Assert.IsTrue(rehashNeeded); + } + } +} diff --git a/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs b/UnitTests/Common/Extensions/CryptographyHelperExtensionsTests.cs similarity index 94% rename from AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs rename to UnitTests/Common/Extensions/CryptographyHelperExtensionsTests.cs index 4852e6a..7aea6d7 100644 --- a/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs +++ b/UnitTests/Common/Extensions/CryptographyHelperExtensionsTests.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs b/UnitTests/Common/Extensions/DateTimeExtensionsTests.cs similarity index 78% rename from AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs rename to UnitTests/Common/Extensions/DateTimeExtensionsTests.cs index 25355a2..d421a50 100644 --- a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs +++ b/UnitTests/Common/Extensions/DateTimeExtensionsTests.cs @@ -1,8 +1,8 @@ using System; -using AMWD.Common.Tests.Utils; +using UnitTests.Common.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] @@ -13,7 +13,7 @@ namespace AMWD.Common.Tests.Extensions { // arrange var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); - using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + using var _ = TimeZoneInfoLocalMock.Create(timeZoneInfo); var utc = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Utc); var local = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Local); @@ -39,7 +39,7 @@ namespace AMWD.Common.Tests.Extensions { // arrange var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); - using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + using var _ = TimeZoneInfoLocalMock.Create(timeZoneInfo); var utc = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Utc); var local = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Local); @@ -64,7 +64,7 @@ namespace AMWD.Common.Tests.Extensions public void ShouldReturnCorrectUtcAlignmentDaylightSavingEnd() { // arrange - var utcNow = new DateTime(2021, 10, 30, 12, 15, 30, 45, DateTimeKind.Utc); + var dateTime = new DateTime(2021, 10, 30, 12, 15, 30, 45, DateTimeKind.Utc); var intervalThreeSeconds = TimeSpan.FromSeconds(3); var intervalSixMinutes = TimeSpan.FromMinutes(6); @@ -75,13 +75,13 @@ namespace AMWD.Common.Tests.Extensions var offsetFourHours = TimeSpan.FromHours(4); // act - var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(utcNow); - var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(utcNow); - var diffTwoHours = intervalTwoHours.GetAlignedInterval(utcNow); - var diffDay = intervalDay.GetAlignedInterval(utcNow); + var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(dateTime); + var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(dateTime); + var diffTwoHours = intervalTwoHours.GetAlignedInterval(dateTime); + var diffDay = intervalDay.GetAlignedInterval(dateTime); - var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(utcNow, offsetTwoMinutes); - var diffDayOffset = intervalDay.GetAlignedInterval(utcNow, offsetFourHours); + var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(dateTime, offsetTwoMinutes); + var diffDayOffset = intervalDay.GetAlignedInterval(dateTime, offsetFourHours); // assert Assert.AreEqual(TimeSpan.Parse("00:00:02.955"), diffThreeSeconds); @@ -97,7 +97,7 @@ namespace AMWD.Common.Tests.Extensions public void ShouldReturnCorrectUtcAlignmentDaylightSavingStart() { // arrange - var utcNow = new DateTime(2022, 3, 26, 12, 15, 30, 45, DateTimeKind.Utc); + var dateTime = new DateTime(2022, 3, 26, 12, 15, 30, 45, DateTimeKind.Utc); var intervalThreeSeconds = TimeSpan.FromSeconds(3); var intervalSixMinutes = TimeSpan.FromMinutes(6); @@ -108,13 +108,13 @@ namespace AMWD.Common.Tests.Extensions var offsetFourHours = TimeSpan.FromHours(4); // act - var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(utcNow); - var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(utcNow); - var diffTwoHours = intervalTwoHours.GetAlignedInterval(utcNow); - var diffDay = intervalDay.GetAlignedInterval(utcNow); + var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(dateTime); + var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(dateTime); + var diffTwoHours = intervalTwoHours.GetAlignedInterval(dateTime); + var diffDay = intervalDay.GetAlignedInterval(dateTime); - var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(utcNow, offsetTwoMinutes); - var diffDayOffset = intervalDay.GetAlignedInterval(utcNow, offsetFourHours); + var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(dateTime, offsetTwoMinutes); + var diffDayOffset = intervalDay.GetAlignedInterval(dateTime, offsetFourHours); // assert Assert.AreEqual(TimeSpan.Parse("00:00:02.955"), diffThreeSeconds); @@ -131,9 +131,9 @@ namespace AMWD.Common.Tests.Extensions { // arrange var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); - using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + using var _ = TimeZoneInfoLocalMock.Create(timeZoneInfo); - var utcNow = new DateTime(2021, 10, 30, 12, 15, 30, 45, DateTimeKind.Local); + var dateTime = new DateTime(2021, 10, 30, 12, 15, 30, 45, DateTimeKind.Local); var intervalThreeSeconds = TimeSpan.FromSeconds(3); var intervalSixMinutes = TimeSpan.FromMinutes(6); @@ -144,13 +144,13 @@ namespace AMWD.Common.Tests.Extensions var offsetFourHours = TimeSpan.FromHours(4); // act - var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(utcNow); - var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(utcNow); - var diffTwoHours = intervalTwoHours.GetAlignedInterval(utcNow); - var diffDay = intervalDay.GetAlignedInterval(utcNow); + var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(dateTime); + var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(dateTime); + var diffTwoHours = intervalTwoHours.GetAlignedInterval(dateTime); + var diffDay = intervalDay.GetAlignedInterval(dateTime); - var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(utcNow, offsetTwoMinutes); - var diffDayOffset = intervalDay.GetAlignedInterval(utcNow, offsetFourHours); + var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(dateTime, offsetTwoMinutes); + var diffDayOffset = intervalDay.GetAlignedInterval(dateTime, offsetFourHours); // assert Assert.AreEqual(TimeSpan.Parse("00:00:02.955"), diffThreeSeconds); @@ -167,9 +167,9 @@ namespace AMWD.Common.Tests.Extensions { // arrange var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); - using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + using var _ = TimeZoneInfoLocalMock.Create(timeZoneInfo); - var utcNow = new DateTime(2022, 3, 26, 12, 15, 30, 45, DateTimeKind.Local); + var dateTime = new DateTime(2022, 3, 26, 12, 15, 30, 45, DateTimeKind.Local); var intervalThreeSeconds = TimeSpan.FromSeconds(3); var intervalSixMinutes = TimeSpan.FromMinutes(6); @@ -180,13 +180,13 @@ namespace AMWD.Common.Tests.Extensions var offsetFourHours = TimeSpan.FromHours(4); // act - var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(utcNow); - var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(utcNow); - var diffTwoHours = intervalTwoHours.GetAlignedInterval(utcNow); - var diffDay = intervalDay.GetAlignedInterval(utcNow); + var diffThreeSeconds = intervalThreeSeconds.GetAlignedInterval(dateTime); + var diffSixMinutes = intervalSixMinutes.GetAlignedInterval(dateTime); + var diffTwoHours = intervalTwoHours.GetAlignedInterval(dateTime); + var diffDay = intervalDay.GetAlignedInterval(dateTime); - var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(utcNow, offsetTwoMinutes); - var diffDayOffset = intervalDay.GetAlignedInterval(utcNow, offsetFourHours); + var diffTwoHoursOffset = intervalTwoHours.GetAlignedInterval(dateTime, offsetTwoMinutes); + var diffDayOffset = intervalDay.GetAlignedInterval(dateTime, offsetFourHours); // assert Assert.AreEqual(TimeSpan.Parse("00:00:02.955"), diffThreeSeconds); @@ -198,6 +198,26 @@ namespace AMWD.Common.Tests.Extensions Assert.AreEqual(TimeSpan.Parse("16:44:29.955"), diffDayOffset); // has to be plus one hour due to daylight saving started } + [TestMethod] + public void ShouldReturnCorrectAlignment() + { + // arrange + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); + using var _ = TimeZoneInfoLocalMock.Create(timeZoneInfo); + + var interval = TimeSpan.FromDays(1); + + // act + var intervalUtc = interval.GetAlignedIntervalUtc(); + var intervalLocal = interval.GetAlignedIntervalLocal(); + + // assert + Assert.AreEqual(DateTime.UtcNow.TimeOfDay.RoundToSecond(), (interval - intervalUtc).RoundToSecond()); + Assert.AreEqual(DateTime.Now.TimeOfDay.RoundToSecond(), (interval - intervalLocal).RoundToSecond()); + + Assert.AreEqual((DateTime.Now - DateTime.UtcNow).RoundToSecond(), (intervalUtc - intervalLocal).RoundToSecond()); + } + [TestMethod] public void ShouldReturnCorrectShortStringForTimeSpan() { diff --git a/AMWD.Common.Tests/Extensions/EnumExtensionsTests.cs b/UnitTests/Common/Extensions/EnumExtensionsTests.cs similarity index 93% rename from AMWD.Common.Tests/Extensions/EnumExtensionsTests.cs rename to UnitTests/Common/Extensions/EnumExtensionsTests.cs index 0689300..babac92 100644 --- a/AMWD.Common.Tests/Extensions/EnumExtensionsTests.cs +++ b/UnitTests/Common/Extensions/EnumExtensionsTests.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using AMWD.Common.Tests.Utils; +using UnitTests.Common.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Extensions/ExceptionExtensionsTests.cs b/UnitTests/Common/Extensions/ExceptionExtensionsTests.cs similarity index 95% rename from AMWD.Common.Tests/Extensions/ExceptionExtensionsTests.cs rename to UnitTests/Common/Extensions/ExceptionExtensionsTests.cs index 8e691dc..6b6bcfa 100644 --- a/AMWD.Common.Tests/Extensions/ExceptionExtensionsTests.cs +++ b/UnitTests/Common/Extensions/ExceptionExtensionsTests.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs b/UnitTests/Common/Extensions/JsonExtensionsTests.cs similarity index 93% rename from AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs rename to UnitTests/Common/Extensions/JsonExtensionsTests.cs index ef2af2f..76faa5d 100644 --- a/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs +++ b/UnitTests/Common/Extensions/JsonExtensionsTests.cs @@ -1,11 +1,11 @@ using System; using System.Collections; -using AMWD.Common.Tests.Utils; +using UnitTests.Common.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] @@ -91,7 +91,7 @@ namespace AMWD.Common.Tests.Extensions { // arrange var testObject = new JsonTestClass(); - string expected = @"{""$type"":""AMWD.Common.Tests.Utils.JsonTestClass, AMWD.Common.Tests"",""stringValue"":""Hello World!"",""isBoolTrue"":true,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":42.21,""localTimestamp"":""2021-11-16T20:15:34+01:00"",""utcTimestamp"":""2021-11-16T20:15:34Z"",""object"":{""$type"":""AMWD.Common.Tests.Utils.JsonTestSubClass, AMWD.Common.Tests"",""integerValue"":42,""stringValue"":""Foo-Bar""}}"; + string expected = @"{""$type"":""UnitTests.Common.Utils.JsonTestClass, UnitTests"",""stringValue"":""Hello World!"",""isBoolTrue"":true,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":42.21,""localTimestamp"":""2021-11-16T20:15:34+01:00"",""utcTimestamp"":""2021-11-16T20:15:34Z"",""object"":{""$type"":""UnitTests.Common.Utils.JsonTestSubClass, UnitTests"",""integerValue"":42,""stringValue"":""Foo-Bar""}}"; // act string json = testObject.SerializeJson(includeType: true); diff --git a/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs b/UnitTests/Common/Extensions/ReaderWriterLockSlimExtensionsTests.cs similarity index 95% rename from AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs rename to UnitTests/Common/Extensions/ReaderWriterLockSlimExtensionsTests.cs index 12bea95..385e7c2 100644 --- a/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs +++ b/UnitTests/Common/Extensions/ReaderWriterLockSlimExtensionsTests.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Extensions/StreamExtensionsTests.cs b/UnitTests/Common/Extensions/StreamExtensionsTests.cs similarity index 94% rename from AMWD.Common.Tests/Extensions/StreamExtensionsTests.cs rename to UnitTests/Common/Extensions/StreamExtensionsTests.cs index 8fd902f..10b6371 100644 --- a/AMWD.Common.Tests/Extensions/StreamExtensionsTests.cs +++ b/UnitTests/Common/Extensions/StreamExtensionsTests.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs b/UnitTests/Common/Extensions/StringExtensionsTests.cs similarity index 95% rename from AMWD.Common.Tests/Extensions/StringExtensionsTests.cs rename to UnitTests/Common/Extensions/StringExtensionsTests.cs index 3fcabc3..740a9c8 100644 --- a/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs +++ b/UnitTests/Common/Extensions/StringExtensionsTests.cs @@ -4,7 +4,7 @@ using System.Net; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Extensions +namespace UnitTests.Common.Extensions { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Utilities/CryptographyHelperTests.cs b/UnitTests/Common/Utilities/CryptographyHelperTests.cs similarity index 94% rename from AMWD.Common.Tests/Utilities/CryptographyHelperTests.cs rename to UnitTests/Common/Utilities/CryptographyHelperTests.cs index 22ca96a..1b523f5 100644 --- a/AMWD.Common.Tests/Utilities/CryptographyHelperTests.cs +++ b/UnitTests/Common/Utilities/CryptographyHelperTests.cs @@ -1,13 +1,12 @@ using System; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Security.Cryptography; using System.Text.RegularExpressions; -using AMWD.Common.Tests.Utils; +using UnitTests.Common.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Utilities +namespace UnitTests.Common.Utilities { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] @@ -39,7 +38,7 @@ namespace AMWD.Common.Tests.Utilities public void ShouldEncryptAesWithoutSalt() // required to test the encryption itself { // arrange - using var disposable = CryptographyHelperSaltMock.Create(0); + using var _ = CryptographyHelperSaltMock.Create(0); byte[] bytes = new byte[] { 0xaf, 0xfe }; string str = "ABC"; @@ -66,7 +65,7 @@ namespace AMWD.Common.Tests.Utilities public void ShouldDecryptAesWithoutSalt() // required to test the decryption itself { // arrange - using var disposable = CryptographyHelperSaltMock.Create(0); + using var _ = CryptographyHelperSaltMock.Create(0); string cipherStr = "ueLuhFNpCuYmx8v3hczHtg=="; byte[] cipherBytes = new byte[] { 0x7c, 0x7b, 0x77, 0x56, 0x91, 0x1a, 0xd9, 0xc0, 0x72, 0x70, 0x36, 0x88, 0x9f, 0xb4, 0xb5, 0xbc }; @@ -153,7 +152,7 @@ namespace AMWD.Common.Tests.Utilities public void ShouldEncryptTdesWithoutSalt() // required to test the encryption itself { // arrange - using var disposable = CryptographyHelperSaltMock.Create(0); + using var _ = CryptographyHelperSaltMock.Create(0); byte[] bytes = new byte[] { 0xaf, 0xfe }; string str = "ABC"; @@ -180,7 +179,7 @@ namespace AMWD.Common.Tests.Utilities public void ShouldDecryptTdesWithoutSalt() // required to test the decryption itself { // arrange - using var disposable = CryptographyHelperSaltMock.Create(0); + using var _ = CryptographyHelperSaltMock.Create(0); string cipherStr = "1l74soBuuEI="; byte[] cipherBytes = new byte[] { 0xbf, 0x59, 0x1f, 0x48, 0x69, 0xab, 0x18, 0xc7 }; diff --git a/AMWD.Common.Tests/Utilities/DelayedTaskTests.cs b/UnitTests/Common/Utilities/DelayedTaskTests.cs similarity index 96% rename from AMWD.Common.Tests/Utilities/DelayedTaskTests.cs rename to UnitTests/Common/Utilities/DelayedTaskTests.cs index 3023e8a..6f0304f 100644 --- a/AMWD.Common.Tests/Utilities/DelayedTaskTests.cs +++ b/UnitTests/Common/Utilities/DelayedTaskTests.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using AMWD.Common.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Utilities +namespace UnitTests.Common.Utilities { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Utilities/DelayedTaskWithResultTests.cs b/UnitTests/Common/Utilities/DelayedTaskWithResultTests.cs similarity index 96% rename from AMWD.Common.Tests/Utilities/DelayedTaskWithResultTests.cs rename to UnitTests/Common/Utilities/DelayedTaskWithResultTests.cs index 4e67d50..855ff92 100644 --- a/AMWD.Common.Tests/Utilities/DelayedTaskWithResultTests.cs +++ b/UnitTests/Common/Utilities/DelayedTaskWithResultTests.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using AMWD.Common.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace AMWD.Common.Tests.Utilities +namespace UnitTests.Common.Utilities { [TestClass] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] diff --git a/AMWD.Common.Tests/Utils/CryptographyHelperSaltMock.cs b/UnitTests/Common/Utils/CryptographyHelperSaltMock.cs similarity index 91% rename from AMWD.Common.Tests/Utils/CryptographyHelperSaltMock.cs rename to UnitTests/Common/Utils/CryptographyHelperSaltMock.cs index e59c6b9..4db64f9 100644 --- a/AMWD.Common.Tests/Utils/CryptographyHelperSaltMock.cs +++ b/UnitTests/Common/Utils/CryptographyHelperSaltMock.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using ReflectionMagic; -namespace AMWD.Common.Tests.Utils +namespace UnitTests.Common.Utils { [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal class CryptographyHelperSaltMock : IDisposable diff --git a/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs b/UnitTests/Common/Utils/CustomMultipleAttribute.cs similarity index 85% rename from AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs rename to UnitTests/Common/Utils/CustomMultipleAttribute.cs index e59fbbb..4981a07 100644 --- a/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs +++ b/UnitTests/Common/Utils/CustomMultipleAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace AMWD.Common.Tests.Utils +namespace UnitTests.Common.Utils { [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] diff --git a/AMWD.Common.Tests/Utils/JsonTestClass.cs b/UnitTests/Common/Utils/JsonTestClass.cs similarity index 93% rename from AMWD.Common.Tests/Utils/JsonTestClass.cs rename to UnitTests/Common/Utils/JsonTestClass.cs index 2bf41d2..b1da24c 100644 --- a/AMWD.Common.Tests/Utils/JsonTestClass.cs +++ b/UnitTests/Common/Utils/JsonTestClass.cs @@ -1,6 +1,6 @@ using System; -namespace AMWD.Common.Tests.Utils +namespace UnitTests.Common.Utils { [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal class JsonTestClass diff --git a/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs b/UnitTests/Common/Utils/TimeZoneInfoLocalMock.cs similarity index 92% rename from AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs rename to UnitTests/Common/Utils/TimeZoneInfoLocalMock.cs index ff14a14..2238e0c 100644 --- a/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs +++ b/UnitTests/Common/Utils/TimeZoneInfoLocalMock.cs @@ -1,7 +1,7 @@ using System; using ReflectionMagic; -namespace AMWD.Common.Tests.Utils +namespace UnitTests.Common.Utils { [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal class TimeZoneInfoLocalMock : IDisposable diff --git a/AMWD.Common.Tests/AMWD.Common.Tests.csproj b/UnitTests/UnitTests.csproj similarity index 71% rename from AMWD.Common.Tests/AMWD.Common.Tests.csproj rename to UnitTests/UnitTests.csproj index bf1fc7d..df96bfd 100644 --- a/AMWD.Common.Tests/AMWD.Common.Tests.csproj +++ b/UnitTests/UnitTests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -12,12 +12,15 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + +