using FictionArchive.Service.UserService.Models.Database; using FictionArchive.Service.UserService.Services; using FictionArchive.Service.UserService.Services.AuthenticationClient; using FictionArchive.Service.UserService.Services.AuthenticationClient.Authentik; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Xunit; namespace FictionArchive.Service.UserService.Tests; public class UserManagementServiceTests { #region Helper Methods private static UserServiceDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"UserManagementServiceTests-{Guid.NewGuid()}") .Options; return new UserServiceDbContext(options, NullLogger.Instance); } private static UserManagementService CreateService( UserServiceDbContext dbContext, IAuthenticationServiceClient authClient) { return new UserManagementService( dbContext, NullLogger.Instance, authClient); } private static User CreateTestUser(string username, string email, int availableInvites = 5) { return new User { Username = username, Email = email, OAuthProviderId = Guid.NewGuid().ToString(), Disabled = false, AvailableInvites = availableInvites }; } #endregion #region InviteUserAsync Tests [Fact] public async Task InviteUserAsync_WithValidInviter_CreatesUserAndDecrementsInvites() { // Arrange using var dbContext = CreateDbContext(); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 3); dbContext.Users.Add(inviter); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); authClient.CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new AuthentikUserResponse { Pk = 123, Uid = "authentik-uid-456" }); authClient.SendRecoveryEmailAsync(Arg.Any()).Returns(true); var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "new@test.com", "newuser"); // Assert result.Should().NotBeNull(); result!.Username.Should().Be("newuser"); result.Email.Should().Be("new@test.com"); result.InviterId.Should().Be(inviter.Id); result.AvailableInvites.Should().Be(0); inviter.AvailableInvites.Should().Be(2); await authClient.Received(1).CreateUserAsync("newuser", "new@test.com", "newuser"); await authClient.Received(1).SendRecoveryEmailAsync(123); } [Fact] public async Task InviteUserAsync_WithNoAvailableInvites_ReturnsNull() { // Arrange using var dbContext = CreateDbContext(); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 0); dbContext.Users.Add(inviter); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "new@test.com", "newuser"); // Assert result.Should().BeNull(); await authClient.DidNotReceive().CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task InviteUserAsync_WithDuplicateEmail_ReturnsNull() { // Arrange using var dbContext = CreateDbContext(); var existingUser = CreateTestUser("existing", "existing@test.com"); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 3); dbContext.Users.AddRange(existingUser, inviter); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "existing@test.com", "newuser"); // Assert result.Should().BeNull(); await authClient.DidNotReceive().CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()); inviter.AvailableInvites.Should().Be(3); // Not decremented } [Fact] public async Task InviteUserAsync_WithDuplicateUsername_ReturnsNull() { // Arrange using var dbContext = CreateDbContext(); var existingUser = CreateTestUser("existinguser", "existing@test.com"); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 3); dbContext.Users.AddRange(existingUser, inviter); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "new@test.com", "existinguser"); // Assert result.Should().BeNull(); await authClient.DidNotReceive().CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()); inviter.AvailableInvites.Should().Be(3); // Not decremented } [Fact] public async Task InviteUserAsync_WhenAuthentikFails_ReturnsNull() { // Arrange using var dbContext = CreateDbContext(); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 3); dbContext.Users.Add(inviter); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); authClient.CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns((AuthentikUserResponse?)null); var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "new@test.com", "newuser"); // Assert result.Should().BeNull(); await authClient.DidNotReceive().SendRecoveryEmailAsync(Arg.Any()); // Verify no user was added to the database var usersInDb = await dbContext.Users.ToListAsync(); usersInDb.Should().HaveCount(1); // Only the inviter inviter.AvailableInvites.Should().Be(3); // Not decremented } [Fact] public async Task InviteUserAsync_WhenRecoveryEmailFails_StillCreatesUser() { // Arrange using var dbContext = CreateDbContext(); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 3); dbContext.Users.Add(inviter); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); authClient.CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new AuthentikUserResponse { Pk = 123, Uid = "authentik-uid-456" }); authClient.SendRecoveryEmailAsync(Arg.Any()).Returns(false); // Email fails var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "new@test.com", "newuser"); // Assert - User should still be created despite email failure result.Should().NotBeNull(); result!.Username.Should().Be("newuser"); inviter.AvailableInvites.Should().Be(2); // Verify user was added to database var usersInDb = await dbContext.Users.ToListAsync(); usersInDb.Should().HaveCount(2); } [Fact] public async Task InviteUserAsync_SetsCorrectUserProperties() { // Arrange using var dbContext = CreateDbContext(); var inviter = CreateTestUser("inviter", "inviter@test.com", availableInvites: 5); dbContext.Users.Add(inviter); await dbContext.SaveChangesAsync(); var authentikUid = "authentik-uid-789"; var authClient = Substitute.For(); authClient.CreateUserAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new AuthentikUserResponse { Pk = 456, Uid = authentikUid }); authClient.SendRecoveryEmailAsync(Arg.Any()).Returns(true); var service = CreateService(dbContext, authClient); // Act var result = await service.InviteUserAsync(inviter, "newuser@test.com", "newusername"); // Assert result.Should().NotBeNull(); result!.Username.Should().Be("newusername"); result.Email.Should().Be("newuser@test.com"); result.OAuthProviderId.Should().Be(authentikUid); result.InviterId.Should().Be(inviter.Id); result.AvailableInvites.Should().Be(0); result.Disabled.Should().BeFalse(); result.Id.Should().NotBeEmpty(); } #endregion #region GetUserByOAuthProviderIdAsync Tests [Fact] public async Task GetUserByOAuthProviderIdAsync_WithExistingUser_ReturnsUser() { // Arrange using var dbContext = CreateDbContext(); var oAuthProviderId = "oauth-provider-123"; var user = new User { Username = "testuser", Email = "test@test.com", OAuthProviderId = oAuthProviderId, Disabled = false, AvailableInvites = 5 }; dbContext.Users.Add(user); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.GetUserByOAuthProviderIdAsync(oAuthProviderId); // Assert result.Should().NotBeNull(); result!.Id.Should().Be(user.Id); result.Username.Should().Be("testuser"); result.OAuthProviderId.Should().Be(oAuthProviderId); } [Fact] public async Task GetUserByOAuthProviderIdAsync_WithNonExistingUser_ReturnsNull() { // Arrange using var dbContext = CreateDbContext(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.GetUserByOAuthProviderIdAsync("non-existing-id"); // Assert result.Should().BeNull(); } #endregion #region GetUsers Tests [Fact] public async Task GetUsers_ReturnsAllUsers() { // Arrange using var dbContext = CreateDbContext(); var user1 = CreateTestUser("user1", "user1@test.com"); var user2 = CreateTestUser("user2", "user2@test.com"); var user3 = CreateTestUser("user3", "user3@test.com"); dbContext.Users.AddRange(user1, user2, user3); await dbContext.SaveChangesAsync(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.GetUsers().ToListAsync(); // Assert result.Should().HaveCount(3); result.Select(u => u.Username).Should().BeEquivalentTo(new[] { "user1", "user2", "user3" }); } [Fact] public async Task GetUsers_WithEmptyDb_ReturnsEmptyQueryable() { // Arrange using var dbContext = CreateDbContext(); var authClient = Substitute.For(); var service = CreateService(dbContext, authClient); // Act var result = await service.GetUsers().ToListAsync(); // Assert result.Should().BeEmpty(); } #endregion }