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(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(); cache.Should().BeOfType(); second.ServiceProvider.GetRequiredService().Should().BeSameAs(cache); } }