New Angular based web version #1
179
tests/WebApi.Tests.Unit/Authentication/UserAccessorTests.cs
Normal file
179
tests/WebApi.Tests.Unit/Authentication/UserAccessorTests.cs
Normal 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
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
35
tests/WebApi.Tests.Unit/WebApi.Tests.Unit.csproj
Normal file
35
tests/WebApi.Tests.Unit/WebApi.Tests.Unit.csproj
Normal 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>
|
||||
@@ -12,6 +12,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{9FF3C98A-5085-4EBE-A980-DB2148B0C00A} = {C051A684-BD6A-43F2-B0CC-F3C2315D99E3}
|
||||
{5BA94D65-1D04-49EA-B7CC-F3719DE2D97E} = {437DE053-1DAB-4EEF-BEA6-E3B5179692F8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7813E32D-AE19-479C-853B-063882D2D05A}
|
||||
|
||||
Reference in New Issue
Block a user