adds unit tests to cover 80%

This commit is contained in:
2026-06-26 00:27:27 +02:00
parent b84e2f8e3e
commit 0e01ca1e1c
69 changed files with 90596 additions and 46 deletions
@@ -0,0 +1,251 @@
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NSubstitute;
using SlpModularCms.Core.Data;
using SlpModularCms.Core.Exceptions;
using SlpModularCms.Core.Identity.Entities;
using SlpModularCms.Core.Identity.Models;
using SlpModularCms.Core.Identity.Services;
using Xunit;
namespace SlpModularCms.Core.Tests.Identity;
public class AuthServiceTests
{
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly AuthService _service;
public AuthServiceTests()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new ApplicationDbContext(options);
var store = Substitute.For<IUserStore<ApplicationUser>>();
_userManager = Substitute.For<UserManager<ApplicationUser>>(
store, null, null, null, null, null, null, null, null);
var jwtSettings = new JwtSettings
{
Secret = "test-signing-key-at-least-32-chars-long!",
Issuer = "test-issuer",
Audience = "test-audience",
ExpiryMinutes = 60,
RefreshTokenExpiryDays = 7
};
_service = new AuthService(_userManager, _context, Options.Create(jwtSettings));
}
[Fact]
public async Task AuthenticateAsync_ShouldThrow_WhenUserNotFound()
{
_userManager.FindByEmailAsync(Arg.Any<string>()).Returns((ApplicationUser?)null);
var act = () => _service.AuthenticateAsync("nouser@example.com", "password");
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task AuthenticateAsync_ShouldThrow_WhenUserIsInactive()
{
var user = new ApplicationUser { Email = "user@example.com", IsActive = false };
_userManager.FindByEmailAsync(Arg.Any<string>()).Returns(user);
var act = () => _service.AuthenticateAsync("user@example.com", "password");
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task AuthenticateAsync_ShouldThrow_WhenPasswordInvalid()
{
var user = new ApplicationUser { Email = "user@example.com", IsActive = true };
_userManager.FindByEmailAsync(Arg.Any<string>()).Returns(user);
_userManager.CheckPasswordAsync(user, Arg.Any<string>()).Returns(false);
var act = () => _service.AuthenticateAsync("user@example.com", "wrongpassword");
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task AuthenticateAsync_ShouldReturnTokenResponse_WhenCredentialsValid()
{
var user = new ApplicationUser
{
Id = Guid.NewGuid(),
Email = "user@example.com",
IsActive = true,
DisplayName = "Test User"
};
_userManager.FindByEmailAsync(Arg.Any<string>()).Returns(user);
_userManager.CheckPasswordAsync(user, "Password123!").Returns(true);
_userManager.GetRolesAsync(user).Returns(new List<string> { "User" });
var result = await _service.AuthenticateAsync("user@example.com", "Password123!");
result.Should().NotBeNull();
result.AccessToken.Should().NotBeNullOrEmpty();
result.RefreshToken.Should().NotBeNullOrEmpty();
result.User.Email.Should().Be("user@example.com");
var savedToken = await _context.RefreshTokens.FirstOrDefaultAsync();
savedToken.Should().NotBeNull();
}
[Fact]
public async Task RefreshTokenAsync_ShouldThrow_WhenTokenNotFound()
{
var act = () => _service.RefreshTokenAsync("nonexistent-token");
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task RefreshTokenAsync_ShouldThrow_WhenTokenRevoked()
{
var user = new ApplicationUser { Id = Guid.NewGuid(), Email = "user@example.com" };
_context.Users.Add(user);
await _context.SaveChangesAsync();
_context.RefreshTokens.Add(new RefreshToken
{
Token = "revoked-token",
UserId = user.Id,
ExpiryDate = DateTimeOffset.UtcNow.AddDays(1),
IsRevoked = true
});
await _context.SaveChangesAsync();
var act = () => _service.RefreshTokenAsync("revoked-token");
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task RefreshTokenAsync_ShouldRevokeAllTokensAndThrow_WhenTokenAlreadyUsed()
{
var user = new ApplicationUser { Id = Guid.NewGuid(), Email = "user@example.com" };
_context.Users.Add(user);
await _context.SaveChangesAsync();
_context.RefreshTokens.Add(new RefreshToken
{
Token = "used-token",
UserId = user.Id,
ExpiryDate = DateTimeOffset.UtcNow.AddDays(1),
IsUsed = true
});
await _context.SaveChangesAsync();
var act = () => _service.RefreshTokenAsync("used-token");
await act.Should().ThrowAsync<UnauthorizedException>();
var allTokens = await _context.RefreshTokens.ToListAsync();
allTokens.Should().AllSatisfy(t => t.IsRevoked.Should().BeTrue());
}
[Fact]
public async Task RefreshTokenAsync_ShouldReturnNewTokens_WhenTokenValid()
{
var user = new ApplicationUser
{
Id = Guid.NewGuid(),
Email = "user@example.com",
IsActive = true
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
_context.RefreshTokens.Add(new RefreshToken
{
Token = "valid-refresh-token",
UserId = user.Id,
ExpiryDate = DateTimeOffset.UtcNow.AddDays(7)
});
await _context.SaveChangesAsync();
_userManager.GetRolesAsync(Arg.Any<ApplicationUser>()).Returns(new List<string> { "User" });
var result = await _service.RefreshTokenAsync("valid-refresh-token");
result.Should().NotBeNull();
result.AccessToken.Should().NotBeNullOrEmpty();
result.RefreshToken.Should().NotBeNullOrEmpty();
var oldToken = await _context.RefreshTokens.FirstAsync(t => t.Token == "valid-refresh-token");
oldToken.IsUsed.Should().BeTrue();
}
[Fact]
public async Task RevokeTokenAsync_ShouldMarkTokenRevoked()
{
var user = new ApplicationUser { Id = Guid.NewGuid(), Email = "user@example.com" };
_context.Users.Add(user);
await _context.SaveChangesAsync();
_context.RefreshTokens.Add(new RefreshToken
{
Token = "token-to-revoke",
UserId = user.Id,
ExpiryDate = DateTimeOffset.UtcNow.AddDays(1)
});
await _context.SaveChangesAsync();
await _service.RevokeTokenAsync("token-to-revoke");
var saved = await _context.RefreshTokens.FirstAsync(t => t.Token == "token-to-revoke");
saved.IsRevoked.Should().BeTrue();
}
[Fact]
public async Task RevokeTokenAsync_ShouldNotThrow_WhenTokenNotFound()
{
var act = () => _service.RevokeTokenAsync("nonexistent-token");
await act.Should().NotThrowAsync();
}
[Fact]
public async Task ChangePasswordAsync_ShouldThrow_WhenUserNotFound()
{
_userManager.FindByIdAsync(Arg.Any<string>()).Returns((ApplicationUser?)null);
var act = () => _service.ChangePasswordAsync(Guid.NewGuid(), "old", "new");
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task ChangePasswordAsync_ShouldThrow_WhenPasswordChangeFails()
{
var user = new ApplicationUser { Id = Guid.NewGuid(), Email = "user@example.com" };
_userManager.FindByIdAsync(user.Id.ToString()).Returns(user);
_userManager.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>())
.Returns(IdentityResult.Failed(new IdentityError { Description = "Password too weak" }));
var act = () => _service.ChangePasswordAsync(user.Id, "old", "weak");
await act.Should().ThrowAsync<ValidationException>();
}
[Fact]
public async Task ChangePasswordAsync_ShouldSucceed_WhenValid()
{
var user = new ApplicationUser { Id = Guid.NewGuid(), Email = "user@example.com" };
_userManager.FindByIdAsync(user.Id.ToString()).Returns(user);
_userManager.ChangePasswordAsync(user, "OldPassword1!", "NewPassword1!")
.Returns(IdentityResult.Success);
var act = () => _service.ChangePasswordAsync(user.Id, "OldPassword1!", "NewPassword1!");
await act.Should().NotThrowAsync();
}
}
@@ -88,10 +88,124 @@ public class InvitationServiceTests
[Fact]
public async Task CompleteInvitationAsync_ShouldThrow_WhenTokenInvalid()
{
// Act
var act = () => _service.CompleteInvitationAsync("invalid-token", "password");
// Assert
await act.Should().ThrowAsync<UnauthorizedException>();
}
[Fact]
public async Task CreateInvitationAsync_ShouldThrow_WhenInvitationAlreadyExists()
{
await _service.CreateInvitationAsync("test@example.com", "User");
var act = () => _service.CreateInvitationAsync("test@example.com", "User");
await act.Should().ThrowAsync<InvitationOrUserAlreadyExistsException>();
}
[Fact]
public async Task CreateInvitationAsync_ShouldThrow_WhenUserAlreadyExists()
{
var email = "existing@example.com";
_userManager.FindByEmailAsync(email)
.Returns(new ApplicationUser { Email = email });
var act = () => _service.CreateInvitationAsync(email, "User");
await act.Should().ThrowAsync<InvitationOrUserAlreadyExistsException>();
}
[Fact]
public async Task ValidateInvitationAsync_ShouldReturnFalse_WhenTokenInvalid()
{
var isValid = await _service.ValidateInvitationAsync("nonexistent-token");
isValid.Should().BeFalse();
}
[Fact]
public async Task GetPendingInvitationByEmailAsync_ShouldReturnFalse_WhenNoInvitation()
{
var (isPending, token) = await _service.GetPendingInvitationByEmailAsync("nobody@example.com");
isPending.Should().BeFalse();
token.Should().BeNull();
}
[Fact]
public async Task GetPendingInvitationByEmailAsync_ShouldReturnTrue_WhenPendingInvitationExists()
{
var invitationToken = await _service.CreateInvitationAsync("invited@example.com", "User");
var (isPending, token) = await _service.GetPendingInvitationByEmailAsync("invited@example.com");
isPending.Should().BeTrue();
token.Should().Be(invitationToken);
}
[Fact]
public async Task DeletePendingInvitationsByEmailAsync_ShouldRemoveMatchingInvitations()
{
await _service.CreateInvitationAsync("invited@example.com", "User");
await _service.DeletePendingInvitationsByEmailAsync("invited@example.com");
var (isPending, _) = await _service.GetPendingInvitationByEmailAsync("invited@example.com");
isPending.Should().BeFalse();
}
[Fact]
public async Task GetAllPendingInvitationsAsync_ShouldReturnAllActiveInvitations()
{
await _service.CreateInvitationAsync("user1@example.com", "User");
await _service.CreateInvitationAsync("user2@example.com", "Administrator");
var invitations = await _service.GetAllPendingInvitationsAsync();
invitations.Should().HaveCount(2);
invitations.Select(i => i.Email).Should().Contain(["user1@example.com", "user2@example.com"]);
}
[Fact]
public async Task DeleteInvitationByIdAsync_ShouldReturnTrue_WhenFound()
{
await _service.CreateInvitationAsync("invited@example.com", "User");
var invitations = await _service.GetAllPendingInvitationsAsync();
var id = invitations[0].Id;
var result = await _service.DeleteInvitationByIdAsync(id);
result.Should().BeTrue();
(await _service.GetAllPendingInvitationsAsync()).Should().BeEmpty();
}
[Fact]
public async Task DeleteInvitationByIdAsync_ShouldReturnFalse_WhenNotFound()
{
var result = await _service.DeleteInvitationByIdAsync(Guid.NewGuid());
result.Should().BeFalse();
}
[Fact]
public async Task UpdateInvitationRoleAsync_ShouldUpdateRole()
{
await _service.CreateInvitationAsync("invited@example.com", "User");
var invitations = await _service.GetAllPendingInvitationsAsync();
var id = invitations[0].Id;
var result = await _service.UpdateInvitationRoleAsync(id, "Administrator");
result.Should().BeTrue();
var updated = await _service.GetAllPendingInvitationsAsync();
updated[0].Role.Should().Be("Administrator");
}
[Fact]
public async Task UpdateInvitationRoleAsync_ShouldReturnFalse_WhenNotFound()
{
var result = await _service.UpdateInvitationRoleAsync(Guid.NewGuid(), "Administrator");
result.Should().BeFalse();
}
}
@@ -0,0 +1,141 @@
using FluentAssertions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using SlpModularCms.Core.Data;
using SlpModularCms.Core.Exceptions;
using SlpModularCms.Core.Identity.Entities;
using SlpModularCms.Core.Identity.Services;
using Xunit;
namespace SlpModularCms.Core.Tests.Identity;
public class SetupServiceTests
{
private static ApplicationDbContext CreateContext() =>
new(new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options);
private static UserManager<ApplicationUser> CreateUserManager(ApplicationDbContext context)
{
var store = new UserStore<ApplicationUser, ApplicationRole, ApplicationDbContext, Guid>(context);
return new UserManager<ApplicationUser>(
store,
Options.Create(new IdentityOptions()),
new PasswordHasher<ApplicationUser>(),
new IUserValidator<ApplicationUser>[] { new UserValidator<ApplicationUser>() },
new IPasswordValidator<ApplicationUser>[] { new PasswordValidator<ApplicationUser>() },
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
null!,
NullLogger<UserManager<ApplicationUser>>.Instance);
}
private static RoleManager<ApplicationRole> CreateRoleManager(ApplicationDbContext context)
{
var store = new RoleStore<ApplicationRole, ApplicationDbContext, Guid>(context);
return new RoleManager<ApplicationRole>(
store,
new IRoleValidator<ApplicationRole>[] { new RoleValidator<ApplicationRole>() },
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
NullLogger<RoleManager<ApplicationRole>>.Instance);
}
[Fact]
public async Task IsSystemInitializedAsync_ShouldReturnFalse_WhenNoUsers()
{
var context = CreateContext();
var service = new SetupService(CreateUserManager(context), CreateRoleManager(context));
var result = await service.IsSystemInitializedAsync();
result.Should().BeFalse();
}
[Fact]
public async Task IsSystemInitializedAsync_ShouldReturnTrue_WhenUsersExist()
{
var context = CreateContext();
var userManager = CreateUserManager(context);
var service = new SetupService(userManager, CreateRoleManager(context));
await userManager.CreateAsync(
new ApplicationUser { UserName = "owner@example.com", Email = "owner@example.com" },
"Password123!");
var result = await service.IsSystemInitializedAsync();
result.Should().BeTrue();
}
[Fact]
public async Task CreateInitialOwnerAsync_ShouldThrow_WhenSystemAlreadyInitialized()
{
var context = CreateContext();
var userManager = CreateUserManager(context);
var service = new SetupService(userManager, CreateRoleManager(context));
await userManager.CreateAsync(
new ApplicationUser { UserName = "existing@example.com", Email = "existing@example.com" },
"Password123!");
var act = () => service.CreateInitialOwnerAsync("Owner", "owner@example.com", "Password123!");
await act.Should().ThrowAsync<ValidationException>();
}
[Fact]
public async Task CreateInitialOwnerAsync_ShouldCreateUserWithOwnerRole()
{
var context = CreateContext();
var userManager = CreateUserManager(context);
var roleManager = CreateRoleManager(context);
var service = new SetupService(userManager, roleManager);
await service.CreateInitialOwnerAsync("Test Owner", "owner@example.com", "Password123!");
var user = await userManager.FindByEmailAsync("owner@example.com");
user.Should().NotBeNull();
user!.DisplayName.Should().Be("Test Owner");
user.IsActive.Should().BeTrue();
var roles = await userManager.GetRolesAsync(user);
roles.Should().Contain("Owner");
}
[Fact]
public async Task CreateInitialOwnerAsync_ShouldCreateAllRoles()
{
var context = CreateContext();
var userManager = CreateUserManager(context);
var roleManager = CreateRoleManager(context);
var service = new SetupService(userManager, roleManager);
await service.CreateInitialOwnerAsync("Test Owner", "owner@example.com", "Password123!");
(await roleManager.RoleExistsAsync("Owner")).Should().BeTrue();
(await roleManager.RoleExistsAsync("Administrator")).Should().BeTrue();
(await roleManager.RoleExistsAsync("User")).Should().BeTrue();
}
[Fact]
public async Task CreateInitialOwnerAsync_ShouldNotDuplicateRoles_WhenCalledAfterPartialSetup()
{
var context = CreateContext();
var userManager = CreateUserManager(context);
var roleManager = CreateRoleManager(context);
var service = new SetupService(userManager, roleManager);
// Pre-create the Owner role to simulate partial setup
await roleManager.CreateAsync(new ApplicationRole { Name = "Owner" });
await service.CreateInitialOwnerAsync("Test Owner", "owner@example.com", "Password123!");
var ownerRoles = await context.Roles.Where(r => r.Name == "Owner").ToListAsync();
ownerRoles.Should().HaveCount(1);
}
}