mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
163 lines
6.4 KiB
C#
163 lines
6.4 KiB
C#
|
|
using System.Security.Claims;
|
||
|
|
using Data.SeaHavenIndustries;
|
||
|
|
using Data.SeaHavenIndustries.Enums;
|
||
|
|
using Microsoft.AspNetCore.Identity;
|
||
|
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||
|
|
using Microsoft.Data.Sqlite;
|
||
|
|
using Microsoft.EntityFrameworkCore;
|
||
|
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
||
|
|
using SeaHaven.DataServices.Dto;
|
||
|
|
using SeaHaven.DataServices.Implementation;
|
||
|
|
using SeaHaven.DataServices.Interfaces;
|
||
|
|
using SeaHaven.Services.DTOs;
|
||
|
|
using SeaHaven.Services.Implementation;
|
||
|
|
|
||
|
|
namespace SeaHavenIndustries.Tests;
|
||
|
|
|
||
|
|
public sealed class TeamMemberCreateTransactionTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public async Task Create_MidSequencePersistenceFailure_RollsBackAllMemberRows()
|
||
|
|
{
|
||
|
|
await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
|
||
|
|
await connection.OpenAsync();
|
||
|
|
|
||
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||
|
|
.UseSqlite(connection)
|
||
|
|
.Options;
|
||
|
|
|
||
|
|
await using (var setup = new SqliteTeamMemberTestDbContext(options))
|
||
|
|
await setup.Database.EnsureCreatedAsync();
|
||
|
|
|
||
|
|
await using (var createScope = new SqliteTeamMemberTestDbContext(options))
|
||
|
|
{
|
||
|
|
var permissionData = new ThrowingAfterPersistPermissionDataService(
|
||
|
|
new TeamPermissionOverrideDataService(createScope));
|
||
|
|
var service = new TeamMemberService(
|
||
|
|
CreateUserManager(createScope),
|
||
|
|
CreateRoleManager(createScope),
|
||
|
|
new UserServiceAreaDataService(createScope),
|
||
|
|
permissionData,
|
||
|
|
new UserDataService(createScope),
|
||
|
|
permissionService: null!);
|
||
|
|
|
||
|
|
var failure = await Record.ExceptionAsync(() => service.CreateAsync(
|
||
|
|
ValidRequest(),
|
||
|
|
Admin(),
|
||
|
|
CancellationToken.None));
|
||
|
|
|
||
|
|
Assert.IsType<InvalidOperationException>(failure);
|
||
|
|
Assert.Equal("injected mid-sequence failure", failure!.Message);
|
||
|
|
}
|
||
|
|
|
||
|
|
await using var verify = new SqliteTeamMemberTestDbContext(options);
|
||
|
|
Assert.Empty(await verify.Users.AsNoTracking().ToListAsync());
|
||
|
|
Assert.Empty(await verify.Roles.AsNoTracking().ToListAsync());
|
||
|
|
Assert.Empty(await verify.UserRoles.AsNoTracking().ToListAsync());
|
||
|
|
Assert.Empty(await verify.UserServiceAreas.AsNoTracking().ToListAsync());
|
||
|
|
Assert.Empty(await verify.UserPermissionOverrides.AsNoTracking().ToListAsync());
|
||
|
|
}
|
||
|
|
|
||
|
|
private static UserManager<ApplicationUser> CreateUserManager(ApplicationDbContext context) =>
|
||
|
|
new(
|
||
|
|
new UserStore<ApplicationUser>(context),
|
||
|
|
Microsoft.Extensions.Options.Options.Create(new IdentityOptions()),
|
||
|
|
new PasswordHasher<ApplicationUser>(),
|
||
|
|
Array.Empty<IUserValidator<ApplicationUser>>(),
|
||
|
|
Array.Empty<IPasswordValidator<ApplicationUser>>(),
|
||
|
|
new UpperInvariantLookupNormalizer(),
|
||
|
|
new IdentityErrorDescriber(),
|
||
|
|
new EmptyServiceProvider(),
|
||
|
|
NullLogger<UserManager<ApplicationUser>>.Instance);
|
||
|
|
|
||
|
|
private static RoleManager<IdentityRole> CreateRoleManager(ApplicationDbContext context) =>
|
||
|
|
new(
|
||
|
|
new RoleStore<IdentityRole>(context),
|
||
|
|
Array.Empty<IRoleValidator<IdentityRole>>(),
|
||
|
|
new UpperInvariantLookupNormalizer(),
|
||
|
|
new IdentityErrorDescriber(),
|
||
|
|
NullLogger<RoleManager<IdentityRole>>.Instance);
|
||
|
|
|
||
|
|
private static CreateTeamMemberRequestDTO ValidRequest() => new()
|
||
|
|
{
|
||
|
|
Name = "Taylor Dispatcher",
|
||
|
|
Role = "dispatcher",
|
||
|
|
Color = "#F59E0B",
|
||
|
|
Email = "taylor@example.com",
|
||
|
|
Phone = "555-0100",
|
||
|
|
ServiceAreas = new[] { "east", "West" },
|
||
|
|
PermissionOverrides = new Dictionary<string, UserPermissionState>
|
||
|
|
{
|
||
|
|
["deleteSites"] = UserPermissionState.Allow
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
private static ClaimsPrincipal Admin() =>
|
||
|
|
new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "Admin") }, "test"));
|
||
|
|
|
||
|
|
private sealed class SqliteTeamMemberTestDbContext : ApplicationDbContext
|
||
|
|
{
|
||
|
|
public SqliteTeamMemberTestDbContext(DbContextOptions<ApplicationDbContext> options)
|
||
|
|
: base(options)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void OnModelCreating(ModelBuilder builder)
|
||
|
|
{
|
||
|
|
base.OnModelCreating(builder);
|
||
|
|
|
||
|
|
foreach (var index in builder.Model.GetEntityTypes().SelectMany(entity => entity.GetIndexes()))
|
||
|
|
{
|
||
|
|
if (index.GetFilter() is not null)
|
||
|
|
index.SetFilter(null);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var property in builder.Model.GetEntityTypes()
|
||
|
|
.SelectMany(entity => entity.GetProperties())
|
||
|
|
.Where(property => property.Name == "RowVersion" && property.ClrType == typeof(byte[])))
|
||
|
|
{
|
||
|
|
property.ValueGenerated = ValueGenerated.Never;
|
||
|
|
property.IsConcurrencyToken = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private sealed class ThrowingAfterPersistPermissionDataService : ITeamPermissionOverrideDataService
|
||
|
|
{
|
||
|
|
private readonly ITeamPermissionOverrideDataService _inner;
|
||
|
|
|
||
|
|
public ThrowingAfterPersistPermissionDataService(ITeamPermissionOverrideDataService inner)
|
||
|
|
{
|
||
|
|
_inner = inner;
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<TeamPermissionUserData?> GetUserAsync(string userId, CancellationToken cancellationToken) =>
|
||
|
|
_inner.GetUserAsync(userId, cancellationToken);
|
||
|
|
|
||
|
|
public Task SetOverrideAsync(
|
||
|
|
string userId,
|
||
|
|
string permissionKey,
|
||
|
|
UserPermissionState state,
|
||
|
|
CancellationToken cancellationToken) =>
|
||
|
|
_inner.SetOverrideAsync(userId, permissionKey, state, cancellationToken);
|
||
|
|
|
||
|
|
public async Task SetOverridesAsync(
|
||
|
|
string userId,
|
||
|
|
IReadOnlyDictionary<string, UserPermissionState> overrides,
|
||
|
|
CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
await _inner.SetOverridesAsync(userId, overrides, cancellationToken);
|
||
|
|
throw new InvalidOperationException("injected mid-sequence failure");
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken) =>
|
||
|
|
_inner.ClearOverridesAsync(userId, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
private sealed class EmptyServiceProvider : IServiceProvider
|
||
|
|
{
|
||
|
|
public object? GetService(Type serviceType) => null;
|
||
|
|
}
|
||
|
|
}
|