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