330 lines
12 KiB
C#
330 lines
12 KiB
C#
using FictionArchive.Service.Shared.Services.EventBus;
|
|
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<UserServiceDbContext>()
|
|
.UseInMemoryDatabase($"UserManagementServiceTests-{Guid.NewGuid()}")
|
|
.Options;
|
|
|
|
return new UserServiceDbContext(options, NullLogger<UserServiceDbContext>.Instance);
|
|
}
|
|
|
|
private static UserManagementService CreateService(
|
|
UserServiceDbContext dbContext,
|
|
IAuthenticationServiceClient authClient,
|
|
IEventBus? eventBus = null)
|
|
{
|
|
return new UserManagementService(
|
|
dbContext,
|
|
NullLogger<UserManagementService>.Instance,
|
|
authClient,
|
|
eventBus ?? Substitute.For<IEventBus>());
|
|
}
|
|
|
|
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<IAuthenticationServiceClient>();
|
|
authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
|
.Returns(new AuthentikUserResponse { Pk = 123, Uid = "authentik-uid-456" });
|
|
authClient.SendRecoveryEmailAsync(Arg.Any<int>()).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<IAuthenticationServiceClient>();
|
|
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<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[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<IAuthenticationServiceClient>();
|
|
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<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
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<IAuthenticationServiceClient>();
|
|
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<string>(), Arg.Any<string>(), Arg.Any<string>());
|
|
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<IAuthenticationServiceClient>();
|
|
authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
|
.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<int>());
|
|
|
|
// 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<IAuthenticationServiceClient>();
|
|
authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
|
.Returns(new AuthentikUserResponse { Pk = 123, Uid = "authentik-uid-456" });
|
|
authClient.SendRecoveryEmailAsync(Arg.Any<int>()).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 authentikPk = 456;
|
|
var authClient = Substitute.For<IAuthenticationServiceClient>();
|
|
authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
|
.Returns(new AuthentikUserResponse { Pk = authentikPk, Uid = "authentik-uid-789" });
|
|
authClient.SendRecoveryEmailAsync(Arg.Any<int>()).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(authentikPk.ToString());
|
|
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<IAuthenticationServiceClient>();
|
|
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<IAuthenticationServiceClient>();
|
|
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<IAuthenticationServiceClient>();
|
|
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<IAuthenticationServiceClient>();
|
|
var service = CreateService(dbContext, authClient);
|
|
|
|
// Act
|
|
var result = await service.GetUsers().ToListAsync();
|
|
|
|
// Assert
|
|
result.Should().BeEmpty();
|
|
}
|
|
|
|
#endregion
|
|
}
|