adds unit tests to cover 80%
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user