New Angular based web version #1

Closed
thomas.nuyken wants to merge 150 commits from main into ddd
5 changed files with 365 additions and 0 deletions
Showing only changes of commit ff2da66a22 - Show all commits

View File

@@ -0,0 +1,179 @@
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using NSubstitute;
using System.Security.Claims;
using Vegasco.WebApi.Authentication;
namespace WebApi.Tests.Unit.Authentication;
public sealed class UserAccessorTests
{
private readonly UserAccessor _sut;
private readonly IHttpContextAccessor _httpContextAccessor;
private static readonly string _nameClaimType = "name";
private readonly JwtOptions _jwtOptions = new()
{
NameClaimType = _nameClaimType
};
private readonly IOptions<JwtOptions> _options = Substitute.For<IOptions<JwtOptions>>();
private static readonly string _defaultUsername = "username";
private static readonly string _defaultId = "id";
private readonly ClaimsPrincipal _defaultUser = new(new ClaimsIdentity(
[
new Claim(_nameClaimType, _defaultUsername),
new Claim(ClaimTypes.NameIdentifier, _defaultId)
]));
public UserAccessorTests()
{
_httpContextAccessor = new HttpContextAccessor
{
HttpContext = new DefaultHttpContext()
{
User = _defaultUser
}
};
_options.Value.Returns(_jwtOptions);
_sut = new UserAccessor(_httpContextAccessor, _options);
}
#region GetUsername
[Fact]
public void GetUsername_ShouldReturnUsername_WhenOptionsNameClaimTypeMatches()
{
// Arrange
// Act
var result = _sut.GetUsername();
// Assert
result.Should().Be(_defaultUsername);
}
[Fact]
public void GetUsername_ShouldReturnUsername_WhenNameClaimTypeIsNotSetAndUsernameIsInUriNameClaimType()
{
// Arrange
_jwtOptions.NameClaimType = null;
_httpContextAccessor.HttpContext!.User = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim(ClaimTypes.Name, _defaultUsername)
]));
// Act
var result = _sut.GetUsername();
// Assert
result.Should().Be(_defaultUsername);
}
[Fact]
public void GetUsername_ShouldCacheUsername_WhenFirstCalled()
{
// Arrange
_ = _sut.GetUsername();
_options.ClearReceivedCalls();
// Act
var result = _sut.GetUsername();
// Assert
result.Should().Be(_defaultUsername);
_ = _options.Received(0).Value;
}
[Fact]
public void GetUsername_ShouldThrowInvalidOperationException_WhenHttpContextIsNull()
{
// Arrange
_httpContextAccessor.HttpContext = null;
// Act
var action = () => _sut.GetUsername();
// Assert
action.Should().ThrowExactly<InvalidOperationException>()
.Which.Message.Should().Be("No HttpContext available.");
}
[Fact]
public void GetUsername_ShouldThrowInvalidOperationException_WhenNameClaimIsNotFound()
{
// Arrange
_httpContextAccessor.HttpContext!.User = new ClaimsPrincipal();
// Act
var action = () => _sut.GetUsername();
// Assert
action.Should().ThrowExactly<InvalidOperationException>()
.Which.Message.Should().Be($"No claim of type '{_nameClaimType}' found on the current user.");
}
#endregion
#region GetUserId
[Fact]
public void GetUserId_ShouldReturnUserId_WhenUserIdClaimExists()
{
// Arrange
// Act
var result = _sut.GetUserId();
// Assert
result.Should().Be(_defaultId);
}
[Fact]
public void GetUserId_ShouldCacheUserId_WhenFirstCalled()
{
// Arrange
_ = _sut.GetUserId();
_options.ClearReceivedCalls();
// Act
var result = _sut.GetUserId();
// Assert
result.Should().Be(_defaultId);
_ = _options.Received(0).Value;
}
[Fact]
public void GetUserId_ShouldThrowInvalidOperationException_WhenHttpContextIsNull()
{
// Arrange
_httpContextAccessor.HttpContext = null;
// Act
var action = () => _sut.GetUserId();
// Assert
action.Should().ThrowExactly<InvalidOperationException>()
.Which.Message.Should().Be("No HttpContext available.");
}
[Fact]
public void GetUserId_ShouldThrowInvalidOperationException_WhenIdClaimIsNotFound()
{
// Arrange
_httpContextAccessor.HttpContext!.User = new ClaimsPrincipal();
// Act
var action = () => _sut.GetUserId();
// Assert
action.Should().ThrowExactly<InvalidOperationException>()
.Which.Message.Should().Be($"No claim of type '{ClaimTypes.NameIdentifier}' found on the current user.");
}
#endregion
}

View File

@@ -0,0 +1,71 @@
using FluentAssertions;
using Vegasco.WebApi.Cars;
namespace WebApi.Tests.Unit.Cars;
public sealed class CreateCarRequestValidatorTests
{
private readonly CreateCar.Validator _sut = new();
private readonly CreateCar.Request _validRequest = new("Ford Focus");
[Fact]
public async Task ValidateAsync_ShouldBeValid_WhenRequestIsValid()
{
// Arrange
// Act
var result = await _sut.ValidateAsync(_validRequest);
// Assert
result.IsValid.Should().BeTrue();
}
[Theory]
[InlineData(1)]
[InlineData(50)]
public async Task ValidateAsync_ShouldBeValid_WhenNameIsJustWithinTheLimits(int nameLength)
{
// Arrange
var request = _validRequest with { Name = new string('s', nameLength) };
// Act
var result = await _sut.ValidateAsync(request);
// Assert
result.IsValid.Should().BeTrue();
}
[Fact]
public async Task ValidateAsync_ShouldNotBeValid_WhenNameIsEmpty()
{
// Arrange
var request = _validRequest with { Name = "" };
// Act
var result = await _sut.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().ContainSingle()
.Which
.PropertyName.Should().Be(nameof(CreateCar.Request.Name));
}
[Fact]
public async Task ValidateAsync_ShouldNotBeValid_WhenNameIsTooLong()
{
// Arrange
const int nameMaxLength = 50;
var request = _validRequest with { Name = new string('s', nameMaxLength + 1) };
// Act
var result = await _sut.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().ContainSingle()
.Which
.PropertyName.Should().Be(nameof(CreateCar.Request.Name));
}
}

View File

@@ -0,0 +1,71 @@
using FluentAssertions;
using Vegasco.WebApi.Cars;
namespace WebApi.Tests.Unit.Cars;
public sealed class UpdateCarRequestValidatorTests
{
private readonly UpdateCar.Validator _sut = new();
private readonly UpdateCar.Request _validRequest = new("Ford Focus");
[Fact]
public async Task ValidateAsync_ShouldBeValid_WhenRequestIsValid()
{
// Arrange
// Act
var result = await _sut.ValidateAsync(_validRequest);
// Assert
result.IsValid.Should().BeTrue();
}
[Theory]
[InlineData(1)]
[InlineData(50)]
public async Task ValidateAsync_ShouldBeValid_WhenNameIsJustWithinTheLimits(int nameLength)
{
// Arrange
var request = _validRequest with { Name = new string('s', nameLength) };
// Act
var result = await _sut.ValidateAsync(request);
// Assert
result.IsValid.Should().BeTrue();
}
[Fact]
public async Task ValidateAsync_ShouldNotBeValid_WhenNameIsEmpty()
{
// Arrange
var request = _validRequest with { Name = "" };
// Act
var result = await _sut.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().ContainSingle()
.Which
.PropertyName.Should().Be(nameof(UpdateCar.Request.Name));
}
[Fact]
public async Task ValidateAsync_ShouldNotBeValid_WhenNameIsTooLong()
{
// Arrange
const int nameMaxLength = 50;
var request = _validRequest with { Name = new string('s', nameMaxLength + 1) };
// Act
var result = await _sut.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Errors.Should().ContainSingle()
.Which
.PropertyName.Should().Be(nameof(UpdateCar.Request.Name));
}
}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WebApi\WebApi.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -12,6 +12,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{437DE053-1DAB-4EEF-BEA6-E3B5179692F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.Tests.Unit", "tests\WebApi.Tests.Unit\WebApi.Tests.Unit.csproj", "{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -22,12 +26,17 @@ Global
{9FF3C98A-5085-4EBE-A980-DB2148B0C00A}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FF3C98A-5085-4EBE-A980-DB2148B0C00A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FF3C98A-5085-4EBE-A980-DB2148B0C00A}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FF3C98A-5085-4EBE-A980-DB2148B0C00A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FF3C98A-5085-4EBE-A980-DB2148B0C00A}.Release|Any CPU.Build.0 = Release|Any CPU {9FF3C98A-5085-4EBE-A980-DB2148B0C00A}.Release|Any CPU.Build.0 = Release|Any CPU
{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{9FF3C98A-5085-4EBE-A980-DB2148B0C00A} = {C051A684-BD6A-43F2-B0CC-F3C2315D99E3} {9FF3C98A-5085-4EBE-A980-DB2148B0C00A} = {C051A684-BD6A-43F2-B0CC-F3C2315D99E3}
{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E} = {437DE053-1DAB-4EEF-BEA6-E3B5179692F8}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7813E32D-AE19-479C-853B-063882D2D05A} SolutionGuid = {7813E32D-AE19-479C-853B-063882D2D05A}