From 5383c5d1b0ae623c9332391999996f0e2a7aa8ce Mon Sep 17 00:00:00 2001 From: ThompsonNye <88248872+ThompsonNye@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:44:32 +0200 Subject: [PATCH] Add unit tests --- .../Authentication/UserAccessorTests.cs | 179 ++++++++++++++++++ .../Cars/CreateCarRequestValidatorTests.cs | 71 +++++++ .../Cars/UpdateCarRequestValidatorTests.cs | 71 +++++++ .../WebApi.Tests.Unit.csproj | 35 ++++ vegasco-server.sln | 9 + 5 files changed, 365 insertions(+) create mode 100644 tests/WebApi.Tests.Unit/Authentication/UserAccessorTests.cs create mode 100644 tests/WebApi.Tests.Unit/Cars/CreateCarRequestValidatorTests.cs create mode 100644 tests/WebApi.Tests.Unit/Cars/UpdateCarRequestValidatorTests.cs create mode 100644 tests/WebApi.Tests.Unit/WebApi.Tests.Unit.csproj diff --git a/tests/WebApi.Tests.Unit/Authentication/UserAccessorTests.cs b/tests/WebApi.Tests.Unit/Authentication/UserAccessorTests.cs new file mode 100644 index 0000000..c32f769 --- /dev/null +++ b/tests/WebApi.Tests.Unit/Authentication/UserAccessorTests.cs @@ -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 _options = Substitute.For>(); + + 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() + .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() + .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() + .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() + .Which.Message.Should().Be($"No claim of type '{ClaimTypes.NameIdentifier}' found on the current user."); + } + + #endregion +} diff --git a/tests/WebApi.Tests.Unit/Cars/CreateCarRequestValidatorTests.cs b/tests/WebApi.Tests.Unit/Cars/CreateCarRequestValidatorTests.cs new file mode 100644 index 0000000..127da32 --- /dev/null +++ b/tests/WebApi.Tests.Unit/Cars/CreateCarRequestValidatorTests.cs @@ -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)); + } +} diff --git a/tests/WebApi.Tests.Unit/Cars/UpdateCarRequestValidatorTests.cs b/tests/WebApi.Tests.Unit/Cars/UpdateCarRequestValidatorTests.cs new file mode 100644 index 0000000..8e2d1e0 --- /dev/null +++ b/tests/WebApi.Tests.Unit/Cars/UpdateCarRequestValidatorTests.cs @@ -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)); + } +} \ No newline at end of file diff --git a/tests/WebApi.Tests.Unit/WebApi.Tests.Unit.csproj b/tests/WebApi.Tests.Unit/WebApi.Tests.Unit.csproj new file mode 100644 index 0000000..2f6089a --- /dev/null +++ b/tests/WebApi.Tests.Unit/WebApi.Tests.Unit.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/vegasco-server.sln b/vegasco-server.sln index da38768..4ea8158 100644 --- a/vegasco-server.sln +++ b/vegasco-server.sln @@ -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}