mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
fix(architecture): address backend review follow-ups
This commit is contained in:
parent
ce7d612ac9
commit
6f6c7e04cd
5 changed files with 78 additions and 96 deletions
|
|
@ -82,7 +82,7 @@ public class DispatchBatchingBehaviorTests
|
|||
{
|
||||
WorkOrderIds = new() { 30, 10 },
|
||||
VendorIds = new() { 1 }
|
||||
});
|
||||
}, CancellationToken.None);
|
||||
|
||||
saved.Should().NotBeNull();
|
||||
saved!.WorkOrderId.Should().Be(30);
|
||||
|
|
@ -102,7 +102,7 @@ public class DispatchBatchingBehaviorTests
|
|||
{
|
||||
WorkOrderIds = new() { 10 },
|
||||
VendorIds = new() { 2, 2, 999 }
|
||||
});
|
||||
}, CancellationToken.None);
|
||||
|
||||
result.Dispatches.Should().HaveCount(1);
|
||||
result.Dispatches.Single().VendorName.Should().Be("Only Co");
|
||||
|
|
@ -137,7 +137,7 @@ public class DispatchBatchingBehaviorTests
|
|||
WorkOrderIds = new() { 10 },
|
||||
VendorIds = new() { 1, 2 },
|
||||
TaskListTemplateId = 7
|
||||
});
|
||||
}, CancellationToken.None);
|
||||
|
||||
templateData.Verify(d => d.GetByIdWithItemsAsync(7), Times.Once);
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ public class DispatchBatchingBehaviorTests
|
|||
{
|
||||
WorkOrderIds = new() { 10 },
|
||||
VendorIds = new() { 1, 2, 3 }
|
||||
});
|
||||
}, CancellationToken.None);
|
||||
|
||||
vendorData.Verify(d => d.GetByIdsOrderedAsync(It.IsAny<IReadOnlyCollection<int>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
vendorData.Verify(d => d.GetByIdAsync(It.IsAny<int>()), Times.Never);
|
||||
|
|
@ -176,7 +176,7 @@ public class DispatchBatchingBehaviorTests
|
|||
{
|
||||
WorkOrderIds = new() { 10, 11 },
|
||||
VendorIds = new() { 1, 2 }
|
||||
});
|
||||
}, CancellationToken.None);
|
||||
|
||||
woData.Verify(d => d.AddAuditLogsAsync(It.Is<IReadOnlyCollection<WorkOrderAuditLog>>(logs => logs.Count == 2)), Times.Exactly(2));
|
||||
woData.Verify(d => d.AddAuditLogAsync(It.IsAny<WorkOrderAuditLog>()), Times.Never);
|
||||
|
|
@ -192,8 +192,32 @@ public class DispatchBatchingBehaviorTests
|
|||
{
|
||||
WorkOrderIds = new() { 404 },
|
||||
VendorIds = new() { 1 }
|
||||
});
|
||||
}, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Dispatch_ForwardsSameCancellationTokenToBothBatchDataServices()
|
||||
{
|
||||
var woData = WorkOrderDataReturning(new WorkOrder { Id = 10, InternalWONumber = "WO-10" });
|
||||
var vendorData = VendorDataReturning(new Vendor { Id = 1, CompanyName = "V1", Email = "a@x.io" });
|
||||
var service = BuildService(woData, vendorData);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var token = cts.Token;
|
||||
|
||||
await service.DispatchToVendorAsync(new DispatchToVendorInput
|
||||
{
|
||||
WorkOrderIds = new() { 10 },
|
||||
VendorIds = new() { 1 }
|
||||
}, token);
|
||||
|
||||
woData.Verify(d => d.GetByRequestedIdsWithDetailsAsync(
|
||||
It.IsAny<IReadOnlyCollection<int>>(),
|
||||
token), Times.Once);
|
||||
vendorData.Verify(d => d.GetByIdsOrderedAsync(
|
||||
It.IsAny<IReadOnlyCollection<int>>(),
|
||||
token), Times.Once);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,117 +1,75 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SeaHaven.DataServices.DependencyInjection;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DependencyInjection;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.SeaHavenIndustries.Tests;
|
||||
|
||||
public class ServiceRegistrationTests
|
||||
{
|
||||
private static ServiceProvider BuildProvider()
|
||||
private static IServiceCollection BuildConventionServices()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder().Build();
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddSingleton<IZipCodeDistance>(_ => new DummyZipCodeDistance());
|
||||
services.AddSingleton<IEmailSender>(_ => new DummyEmailSender());
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()));
|
||||
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||
|
||||
services.AddDataServices();
|
||||
services.AddBusinessServices(configuration);
|
||||
return services;
|
||||
}
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
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_RegistersAllFiveFeatureServices()
|
||||
public void ConventionDi_RegistersEveryServicesImplementationClassAsScoped()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
var services = BuildConventionServices();
|
||||
var candidates = ConventionCandidates(typeof(WorkOrderDispatchService).Assembly);
|
||||
|
||||
provider.GetRequiredService<ICalendarService>().Should().BeOfType<CalendarService>();
|
||||
provider.GetRequiredService<ICommonService>().Should().BeOfType<CommonService>();
|
||||
provider.GetRequiredService<IDashboardService>().Should().BeOfType<DashboardService>();
|
||||
provider.GetRequiredService<IDropdownOptionsService>().Should().BeOfType<DropdownOptionsService>();
|
||||
provider.GetRequiredService<ITaskListTemplateService>().Should().BeOfType<TaskListTemplateService>();
|
||||
AssertScopedConventionRegistrations(services, candidates, "SeaHaven.Services");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConventionDi_RegistersFeatureDataServices()
|
||||
public void ConventionDi_RegistersEveryDataServicesImplementationClassAsScoped()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
var services = BuildConventionServices();
|
||||
var candidates = ConventionCandidates(typeof(VendorDataService).Assembly);
|
||||
|
||||
provider.GetRequiredService<ICalendarDataService>().Should().BeOfType<CalendarDataService>();
|
||||
provider.GetRequiredService<ICommonDataService>().Should().BeOfType<CommonDataService>();
|
||||
provider.GetRequiredService<IDashboardDataService>().Should().BeOfType<DashboardDataService>();
|
||||
provider.GetRequiredService<IDropdownOptionsDataService>().Should().BeOfType<DropdownOptionsDataService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConventionDi_RegistersFollowUpLocationDispatchSettingsServices()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
|
||||
provider.GetRequiredService<IFollowUpService>().Should().BeOfType<FollowUpService>();
|
||||
provider.GetRequiredService<ILocationService>().Should().BeOfType<LocationService>();
|
||||
provider.GetRequiredService<IDispatchService>().Should().BeOfType<DispatchService>();
|
||||
provider.GetRequiredService<ISettingsService>().Should().BeOfType<SettingsService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConventionDi_RegistersFollowUpLocationDispatchSettingsDataServices()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
|
||||
provider.GetRequiredService<IFollowUpDataService>().Should().BeOfType<FollowUpDataService>();
|
||||
provider.GetRequiredService<ILocationDataService>().Should().BeOfType<LocationDataService>();
|
||||
provider.GetRequiredService<IDispatchDataService>().Should().BeOfType<DispatchDataService>();
|
||||
provider.GetRequiredService<ISettingsDataService>().Should().BeOfType<SettingsDataService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConventionDi_RegistersVendorAndUpliftServices()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
|
||||
provider.GetRequiredService<IVendorService>().Should().BeOfType<VendorService>();
|
||||
provider.GetRequiredService<IVendorPortalService>().Should().BeOfType<VendorPortalService>();
|
||||
provider.GetRequiredService<IVendorPortalTokenService>()
|
||||
.Should().BeOfType<SeaHaven.Services.Implementation.VendorPortalTokenService>();
|
||||
provider.GetRequiredService<IUpliftService>().Should().BeOfType<UpliftService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConventionDi_RegistersVendorAndUpliftDataServices()
|
||||
{
|
||||
using var provider = BuildProvider();
|
||||
|
||||
provider.GetRequiredService<IVendorDataService>().Should().BeOfType<VendorDataService>();
|
||||
provider.GetRequiredService<IUpliftDataService>().Should().BeOfType<UpliftDataService>();
|
||||
provider.GetRequiredService<ICommentDataService>().Should().BeOfType<CommentDataService>();
|
||||
}
|
||||
|
||||
private sealed class DummyZipCodeDistance : IZipCodeDistance
|
||||
{
|
||||
public double? GetDistanceMiles(string? zip1, string? zip2) => null;
|
||||
}
|
||||
|
||||
private sealed class DummyEmailSender : IEmailSender
|
||||
{
|
||||
public Task<bool> SendEmailAsync(string emailTo, string subject, string body) =>
|
||||
Task.FromResult(true);
|
||||
AssertScopedConventionRegistrations(services, candidates, "SeaHaven.DataServices");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
[HttpPost]
|
||||
[Route("DispatchToVendor")]
|
||||
public async Task<IActionResult> DispatchToVendor([FromBody] Dispatch_DTO model)
|
||||
public async Task<IActionResult> DispatchToVendor([FromBody] Dispatch_DTO model, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -360,7 +360,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
UserId = userId
|
||||
};
|
||||
|
||||
var result = await _dispatchService.DispatchToVendorAsync(input);
|
||||
var result = await _dispatchService.DispatchToVendorAsync(input, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ namespace SeaHaven.Services.Implementation
|
|||
_frontendOptions = frontendOptions.Value;
|
||||
}
|
||||
|
||||
public async Task<DispatchToVendorResult> DispatchToVendorAsync(DispatchToVendorInput input)
|
||||
public async Task<DispatchToVendorResult> DispatchToVendorAsync(DispatchToVendorInput input, CancellationToken cancellationToken)
|
||||
{
|
||||
var workOrders = await _workOrderDataService.GetByRequestedIdsWithDetailsAsync(
|
||||
input.WorkOrderIds,
|
||||
CancellationToken.None);
|
||||
cancellationToken);
|
||||
|
||||
if (workOrders.Count == 0)
|
||||
throw new InvalidOperationException("No work orders found");
|
||||
|
|
@ -66,7 +66,7 @@ namespace SeaHaven.Services.Implementation
|
|||
var templateItemTexts = await LoadTemplateItemTextsAsync(input.TaskListTemplateId);
|
||||
|
||||
var distinctVendorIds = input.VendorIds.Distinct().ToList();
|
||||
var vendors = await _vendorDataService.GetByIdsOrderedAsync(distinctVendorIds, CancellationToken.None);
|
||||
var vendors = await _vendorDataService.GetByIdsOrderedAsync(distinctVendorIds, cancellationToken);
|
||||
|
||||
var dispatches = new List<DispatchResultItem>();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace SeaHaven.Services.Interfaces
|
|||
{
|
||||
public interface IWorkOrderDispatchService
|
||||
{
|
||||
Task<DispatchToVendorResult> DispatchToVendorAsync(DispatchToVendorInput input);
|
||||
Task<DispatchToVendorResult> DispatchToVendorAsync(DispatchToVendorInput input, CancellationToken cancellationToken);
|
||||
Task<IEnumerable<DispatchReadModel>> GetDispatchesAsync(int workOrderId);
|
||||
Task<DispatchDetailReadModel?> GetDispatchDetailAsync(int id);
|
||||
Task<bool> UpdateDispatchAsync(UpdateDispatchInput input);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue