mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
Tokens now carry a keyed hash of the account's security stamp, and every authenticated request compares it with the stored stamp (cached for 60 s, evicted in-process on change). A password reset or change, a deactivation and a deletion all rotate or remove the stamp, so tokens issued before them get 401. Tokens without the claim get 401 too.
90 lines
3.5 KiB
C#
90 lines
3.5 KiB
C#
using System.Reflection;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using SeaHaven.DataServices.DependencyInjection;
|
|
using SeaHaven.DataServices.Implementation;
|
|
using SeaHaven.Services.DependencyInjection;
|
|
using SeaHaven.Services.Implementation;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public class ServiceRegistrationTests
|
|
{
|
|
private static IServiceCollection BuildConventionServices()
|
|
{
|
|
var configuration = new ConfigurationBuilder().Build();
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddSingleton<IConfiguration>(configuration);
|
|
services.AddDataServices();
|
|
services.AddBusinessServices(configuration);
|
|
return services;
|
|
}
|
|
|
|
private static IEnumerable<(Type Implementation, Type Interface)> ConventionCandidates(Assembly assembly)
|
|
{
|
|
return assembly.GetTypes()
|
|
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
|
|
.Select(t => (
|
|
Implementation: t,
|
|
Interface: t.GetInterfaces().FirstOrDefault(i => i.Name == $"I{t.Name}")))
|
|
.Where(pair => pair.Interface is not null)!;
|
|
}
|
|
|
|
private static void AssertScopedConventionRegistrations(
|
|
IServiceCollection services,
|
|
IEnumerable<(Type Implementation, Type Interface)> candidates,
|
|
string assemblyLabel)
|
|
{
|
|
var candidateList = candidates.ToList();
|
|
candidateList.Should().NotBeEmpty(
|
|
"expected at least one convention candidate in {0}", assemblyLabel);
|
|
|
|
var missing = candidateList
|
|
.Where(pair => !services.Any(d =>
|
|
d.Lifetime == ServiceLifetime.Scoped &&
|
|
d.ServiceType == pair.Interface &&
|
|
d.ImplementationType == pair.Implementation))
|
|
.ToList();
|
|
|
|
missing.Should().BeEmpty(
|
|
"expected a scoped descriptor mapping each convention candidate interface to its implementation in {0}, missing: {1}",
|
|
assemblyLabel,
|
|
string.Join("; ", missing.Select(p => $"{p.Interface!.FullName} -> {p.Implementation.FullName}")));
|
|
}
|
|
|
|
[Fact]
|
|
public void ConventionDi_RegistersEveryServicesImplementationClassAsScoped()
|
|
{
|
|
var services = BuildConventionServices();
|
|
var candidates = ConventionCandidates(typeof(WorkOrderDispatchService).Assembly);
|
|
|
|
AssertScopedConventionRegistrations(services, candidates, "SeaHaven.Services");
|
|
}
|
|
|
|
[Fact]
|
|
public void ConventionDi_RegistersEveryDataServicesImplementationClassAsScoped()
|
|
{
|
|
var services = BuildConventionServices();
|
|
var candidates = ConventionCandidates(typeof(VendorDataService).Assembly);
|
|
|
|
AssertScopedConventionRegistrations(services, candidates, "SeaHaven.DataServices");
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionStampCache_IsOneInstanceForTheWholeProcess()
|
|
{
|
|
// A scoped cache would never be hit, and a stamp change on one request would
|
|
// never evict the value another request cached.
|
|
using var provider = BuildConventionServices().BuildServiceProvider();
|
|
using var first = provider.CreateScope();
|
|
using var second = provider.CreateScope();
|
|
|
|
var cache = first.ServiceProvider.GetRequiredService<SeaHaven.Services.Interfaces.ISessionStampCache>();
|
|
|
|
cache.Should().BeOfType<InMemorySessionStampCache>();
|
|
second.ServiceProvider.GetRequiredService<SeaHaven.Services.Interfaces.ISessionStampCache>().Should().BeSameAs(cache);
|
|
}
|
|
}
|