[FA-55] User Service backend initial setup
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FictionArchive.Service.UserService\FictionArchive.Service.UserService.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,326 @@
|
||||
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)
|
||||
{
|
||||
return new UserManagementService(
|
||||
dbContext,
|
||||
NullLogger<UserManagementService>.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<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 authentikUid = "authentik-uid-789";
|
||||
var authClient = Substitute.For<IAuthenticationServiceClient>();
|
||||
authClient.CreateUserAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(new AuthentikUserResponse { Pk = 456, Uid = authentikUid });
|
||||
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(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<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
|
||||
}
|
||||
Reference in New Issue
Block a user