using Microsoft.Extensions.Logging; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentAssertions; using FluentValidation; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Moq; using SeaHaven.DataServices.Implementation; using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Models; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using SeaHaven.Services.Validation; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class VendorServiceTests { private static VendorService NewService(Mock data) => new( data.Object, Mock.Of(), Mock.Of(), Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), new CreateVendorValidation(), new UpdateVendorValidation(), new WorkOrderVendorUpdateValidation(), Mock.Of>()); [Fact] public async Task CreateVendor_PersistsCompleteProfile() { var data = new Mock(); Vendor? saved = null; data.Setup(x => x.AddAsync(It.IsAny())) .ReturnsAsync((Vendor vendor) => { saved = vendor; vendor.Id = 7; return vendor; }); var result = await NewService(data).CreateVendorAsync(new CreateVendorDTO { Name = "Gateway Plumbing", ContactName = "Jordan Lee", Email = "dispatch@gateway.example", Phone = "(555) 555-0100", CompanyPhone = "(555) 555-0199", Address = "10 Lake St", City = "Chicago", State = "IL", Zipcode = "60601", TradeSpecialties = "Plumbing, Backflow", GoogleMapsUrl = "https://maps.google.com/example", Notes = "After-hours dispatch available", IsActive = true }, "42"); saved.Should().NotBeNull(); saved!.ContactName.Should().Be("Jordan Lee"); saved.TradeSpecialties.Should().Be("Plumbing, Backflow"); saved.CompanyPhone.Should().Be("(555) 555-0199"); saved.GoogleMapsUrl.Should().Be("https://maps.google.com/example"); saved.Notes.Should().Be("After-hours dispatch available"); saved.City.Should().Be("Chicago"); result.IsActive.Should().BeTrue(); result.ContactName.Should().Be("Jordan Lee"); } [Fact] public async Task UpdateVendor_UpdatesProfileAndActiveState() { var existing = new Vendor { Id = 8, CompanyName = "Old", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(8)).ReturnsAsync(existing); data.Setup(x => x.GetLinkedWorkOrdersAsync(8)).ReturnsAsync(new List()); data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask); var result = await NewService(data).UpdateVendorAsync(8, new UpdateVendorDTO { Name = "New", ContactName = "Casey", TradeSpecialties = "HVAC", IsActive = false }, "42"); existing.CompanyName.Should().Be("New"); existing.ContactName.Should().Be("Casey"); existing.TradeSpecialties.Should().Be("HVAC"); existing.IsActive.Should().BeFalse(); existing.LastModifierUserId.Should().Be(42); result.IsActive.Should().BeFalse(); } [Fact] public async Task DeleteVendor_DeactivatesWithoutDeletingHistory() { var existing = new Vendor { Id = 9, CompanyName = "Keep History", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(9)).ReturnsAsync(existing); data.Setup(x => x.GetLinkedWorkOrdersAsync(9)).ReturnsAsync(new List()); data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask); await NewService(data).DeleteVendorAsync(9, "42", confirmOpenWorkOrders: false); existing.IsActive.Should().BeFalse(); existing.IsDeleted.Should().NotBeTrue(); existing.DeletionTime.Should().BeNull(); existing.LastModificationTime.Should().NotBeNull(); } [Fact] public async Task GetVendorsPaged_RequestsSelectedStatusAndMapsDirectoryFields() { var data = new Mock(); data.Setup(x => x.GetPagedAsync( 1, 12, "chicago", false, It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), It.IsAny?>())) .ReturnsAsync((new[] { new Vendor { Id = 10, CompanyName = "Inactive Vendor", ContactName = "Taylor", City = "Chicago", State = "IL", TradeSpecialties = "Electrical", IsActive = false } }.AsEnumerable(), 1)); var result = await NewService(data).GetVendorsPagedAsync(1, 12, "chicago", false); result.TotalCount.Should().Be(1); result.Items.Single().Should().BeEquivalentTo(new { Id = 10, Name = "Inactive Vendor", ContactName = "Taylor", City = "Chicago", State = "IL", TradeSpecialties = "Electrical", IsActive = false }, options => options.ExcludingMissingMembers()); } [Fact] public async Task GetVendorDirectoryPaged_ForwardsCancellationToken() { var data = new Mock(); using var cancellation = new CancellationTokenSource(); var token = cancellation.Token; data.Setup(x => x.GetDirectoryPagedAsync( 1, 12, "clayton", true, It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), token)) .ReturnsAsync((Array.Empty(), 0)); await NewService(data).GetVendorDirectoryPagedAsync( 1, 12, "clayton", true, cancellationToken: token); data.Verify(x => x.GetDirectoryPagedAsync( 1, 12, "clayton", true, It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), It.IsAny?>(), token), Times.Once); } [Fact] public async Task GetVendorById_CountsDistinctHistoricalWorkOrders() { var vendor = new Vendor { Id = 11, CompanyName = "History Vendor", Dispatches = new List { new() { WorkOrderId = 100, DispatchWorkOrders = new List { new() { WorkOrderId = 100 }, new() { WorkOrderId = 101 } } }, new() { WorkOrderId = 101 } } }; var data = new Mock(); data.Setup(x => x.GetByIdWithDetailsAsync(11)).ReturnsAsync(vendor); var result = await NewService(data).GetVendorByIdAsync(11); result.Should().NotBeNull(); result!.TotalJobs.Should().Be(2); } [Fact] public async Task GetVendorsPaged_ForwardsFacetFilters() { var companies = new[] { "Gateway Plumbing", "Roto-Rooter Dallas" }; var trades = new[] { "Backflow Preventers" }; var locations = new[] { "Dallas, TX" }; var jobBuckets = new[] { "100-149" }; var data = new Mock(); data.Setup(x => x.GetPagedAsync( 1, 25, "gateway", true, companies, trades, locations, jobBuckets)) .ReturnsAsync((Enumerable.Empty(), 0)); await NewService(data).GetVendorsPagedAsync( 1, 25, "gateway", true, companies, trades, locations, jobBuckets); data.VerifyAll(); } [Fact] public void CreateVendor_AcceptsOptionalTechnicianButRequiresContactGroupAndHttpsUrl() { var validator = new CreateVendorValidation(); // SH-199: technician (ContactName) is optional; zero-technician create is valid // as long as a company phone or email is present and the map URL is HTTPS. var valid = validator.Validate(new CreateVendorDTO { Name = "Gateway Plumbing", ContactName = "", Email = "dispatch@gateway.example", GoogleMapsUrl = "https://maps.google.com/gateway" }); valid.IsValid.Should().BeTrue(); valid.Errors.Should().NotContain(error => error.PropertyName == nameof(CreateVendorDTO.ContactName)); // Missing both company phone and email fails the contact-group rule. var noContact = validator.Validate(new CreateVendorDTO { Name = "Gateway Plumbing", ContactName = "", GoogleMapsUrl = "https://maps.google.com/gateway" }); noContact.IsValid.Should().BeFalse(); noContact.Errors.Should().Contain(error => error.PropertyName == nameof(CreateVendorDTO.CompanyPhone)); // Non-HTTPS map URL still rejected. var badUrl = validator.Validate(new CreateVendorDTO { Name = "Gateway Plumbing", Email = "dispatch@gateway.example", GoogleMapsUrl = "http://maps.google.com/gateway" }); badUrl.IsValid.Should().BeFalse(); badUrl.Errors.Should().Contain(error => error.PropertyName == nameof(CreateVendorDTO.GoogleMapsUrl)); } [Fact] public async Task UpdateVendor_DeactivatingWithOpenWorkOrders_WhenConfirmed_Deactivates() { var existing = new Vendor { Id = 21, CompanyName = "Confirmed", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(21)).ReturnsAsync(existing); data.Setup(x => x.GetLinkedWorkOrdersAsync(21)) .ReturnsAsync(new List { new() { WorkOrderId = 501, WorkOrderNumber = "WO-501", Status = "Scheduled", LifecycleStatus = LifecycleStatus.Scheduled } }); data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask); await NewService(data).UpdateVendorAsync( 21, new UpdateVendorDTO { IsActive = false, ConfirmOpenWorkOrders = true }, "42"); existing.IsActive.Should().BeFalse(); data.Verify(x => x.UpdateAsync(existing), Times.Once); } [Fact] public async Task DeleteVendor_WithOpenWorkOrders_WhenNotConfirmed_ThrowsAndLeavesActive() { var existing = new Vendor { Id = 22, CompanyName = "Guarded Delete", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(22)).ReturnsAsync(existing); data.Setup(x => x.GetLinkedWorkOrdersAsync(22)) .ReturnsAsync(new List { new() { WorkOrderId = 502, WorkOrderNumber = "WO-502", Status = "Scheduled", LifecycleStatus = LifecycleStatus.Scheduled } }); var act = () => NewService(data).DeleteVendorAsync(22, "42", confirmOpenWorkOrders: false); await act.Should().ThrowAsync(); existing.IsActive.Should().BeTrue(); data.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); } [Fact] public async Task DeleteVendor_WithOpenWorkOrders_WhenConfirmed_Deactivates() { var existing = new Vendor { Id = 23, CompanyName = "Confirmed Delete", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(23)).ReturnsAsync(existing); data.Setup(x => x.GetLinkedWorkOrdersAsync(23)) .ReturnsAsync(new List { new() { WorkOrderId = 503, WorkOrderNumber = "WO-503", Status = "Scheduled", LifecycleStatus = LifecycleStatus.Scheduled } }); data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask); await NewService(data).DeleteVendorAsync(23, "42", confirmOpenWorkOrders: true); existing.IsActive.Should().BeFalse(); data.Verify(x => x.UpdateAsync(existing), Times.Once); } [Fact] public async Task UpdateVendor_DeactivatingWithOpenWorkOrders_ThrowsAndLeavesActive() { var existing = new Vendor { Id = 20, CompanyName = "Guarded", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(20)).ReturnsAsync(existing); data.Setup(x => x.GetLinkedWorkOrdersAsync(20)) .ReturnsAsync(new List { new() { WorkOrderId = 500, WorkOrderNumber = "WO-500", Status = "Scheduled", LifecycleStatus = LifecycleStatus.Scheduled } }); var act = () => NewService(data).UpdateVendorAsync(20, new UpdateVendorDTO { IsActive = false }, "42"); await act.Should().ThrowAsync(); existing.IsActive.Should().BeTrue(); data.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); } [Fact] public async Task UpdateVendor_WithUnknownCompanyId_ThrowsAndDoesNotFallBackToName() { var existing = new Vendor { Id = 30, CompanyName = "Existing", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(30)).ReturnsAsync(existing); data.Setup(x => x.GetCompanyByIdAsync(999)).ReturnsAsync((VendorCompany?)null); var act = () => NewService(data).UpdateVendorAsync(30, new UpdateVendorDTO { CompanyId = 999, Name = "Fallback Try" }, "42"); await act.Should().ThrowAsync(); data.Verify(x => x.GetCompanyByNormalizedNameAsync(It.IsAny()), Times.Never); data.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); } [Fact] public async Task CreateVendor_PropagatesExistingCompanyIdWithoutCreatingCompany() { var data = new Mock(); var company = new VendorCompany { Id = 77, Name = "Acme", NormalizedName = "acme" }; data.Setup(x => x.GetCompanyByIdAsync(77)).ReturnsAsync(company); Vendor? saved = null; data.Setup(x => x.AddAsync(It.IsAny())) .ReturnsAsync((Vendor v) => { saved = v; v.Id = 40; return v; }); var result = await NewService(data).CreateVendorAsync(new CreateVendorDTO { Name = "Acme", CompanyId = 77, ContactName = "Riley", Email = "acme@example.com" }, "42"); saved.Should().NotBeNull(); saved!.CompanyId.Should().Be(77); result.CompanyId.Should().Be(77); data.Verify(x => x.AddCompanyAsync(It.IsAny()), Times.Never); } [Fact] public void CreateVendor_RejectsInvalidPhoneAndInvalidPreferredContact() { var validator = new CreateVendorValidation(); var result = validator.Validate(new CreateVendorDTO { Name = "Acme", ContactName = "Riley", Phone = "555-1234", PreferredContact = "Fax" }); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateVendorDTO.Phone)); result.Errors.Should().Contain(e => e.PropertyName == nameof(CreateVendorDTO.PreferredContact)); } [Fact] public async Task CreateVendor_NormalizesCommonNorthAmericanPhoneFormatsBeforeValidation() { var data = new Mock(); Vendor? saved = null; data.Setup(x => x.AddAsync(It.IsAny())) .ReturnsAsync((Vendor vendor) => { saved = vendor; vendor.Id = 41; return vendor; }); await NewService(data).CreateVendorAsync(new CreateVendorDTO { Name = "Acme", ContactName = "Riley", Phone = "+1 312 555 0100", Email = "acme@example.com" }, "42"); saved!.Phone.Should().Be("(312) 555-0100"); } [Fact] public async Task UpdateVendor_RoutesCompanyOwnedFieldsToTheLinkedCompany() { var existing = new Vendor { Id = 42, CompanyId = 77, CompanyName = "Acme", Address = "Old address", IsActive = true }; var company = new VendorCompany { Id = 77, Name = "Acme", NormalizedName = "acme", Address = "Old address" }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(42)).ReturnsAsync(existing); data.Setup(x => x.GetCompanyByIdAsync(77)).ReturnsAsync(company); data.Setup(x => x.UpdateCompanyAsync(company)).Returns(Task.CompletedTask); data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask); await NewService(data).UpdateVendorAsync(42, new UpdateVendorDTO { Name = "Acme Services", Address = "200 New Street" }, "42"); company.Name.Should().Be("Acme Services"); company.NormalizedName.Should().Be("acme services"); company.Address.Should().Be("200 New Street"); existing.Address.Should().Be("200 New Street"); data.Verify(x => x.UpdateCompanyAsync(company), Times.Once); } [Fact] public async Task UpdateVendor_SparseCompanyDoesNotEraseExistingVendorFields() { var existing = new Vendor { Id = 43, CompanyId = 77, CompanyName = "Acme", Address = "Keep this address", CompanyPhone = "(312) 555-0199", IsActive = true }; var sparseCompany = new VendorCompany { Id = 77, Name = "Acme", NormalizedName = "acme" }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(43)).ReturnsAsync(existing); data.Setup(x => x.GetCompanyByIdAsync(77)).ReturnsAsync(sparseCompany); data.Setup(x => x.UpdateCompanyAsync(sparseCompany)).Returns(Task.CompletedTask); data.Setup(x => x.UpdateAsync(existing)).Returns(Task.CompletedTask); await NewService(data).UpdateVendorAsync(43, new UpdateVendorDTO { Name = "Acme" }, "42"); existing.Address.Should().Be("Keep this address"); existing.CompanyPhone.Should().Be("(312) 555-0199"); } [Fact] public void WorkOrderVendorUpdate_AcceptsCanonicalPhoneAndValidPreferredContact() { var validator = new WorkOrderVendorUpdateValidation(); var result = validator.Validate(new WorkOrderVendorUpdateDTO { WorkOrderId = 5, Phone = "(312) 555-0100", PreferredContact = "Text" }); result.IsValid.Should().BeTrue(); } [Fact] public async Task UpdateVendorFromWorkOrder_RejectsUnassignedWorkOrder() { var existing = new Vendor { Id = 50, ContactName = "Original", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(50)).ReturnsAsync(existing); data.Setup(x => x.IsVendorAssignedToWorkOrderAsync(50, 777)).ReturnsAsync(false); var act = () => NewService(data).UpdateVendorFromWorkOrderAsync(50, new WorkOrderVendorUpdateDTO { WorkOrderId = 777, ContactName = "Should Not Apply" }, "42"); await act.Should().ThrowAsync(); existing.ContactName.Should().Be("Original"); data.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); } [Fact] public async Task UpdateVendorFromWorkOrder_RecordsAuditOldAndNewValues() { var existing = new Vendor { Id = 50, CompanyName = "Assigned", ContactName = "Old Name", Phone = "(312) 555-0000", PreferredContact = "Phone", IsActive = true }; var data = new Mock(); data.Setup(x => x.GetByIdAsync(50)).ReturnsAsync(existing); data.Setup(x => x.IsVendorAssignedToWorkOrderAsync(50, 777)).ReturnsAsync(true); var auditLogs = new List(); data.Setup(x => x.UpdateWithAuditLogsAsync( existing, It.IsAny>())) .Callback>((_, logs) => auditLogs.AddRange(logs)) .Returns(Task.CompletedTask); await NewService(data).UpdateVendorFromWorkOrderAsync(50, new WorkOrderVendorUpdateDTO { WorkOrderId = 777, ContactName = "New Name", Phone = "(312) 555-0199", PreferredContact = "Email" }, "42"); existing.ContactName.Should().Be("New Name"); existing.Phone.Should().Be("(312) 555-0199"); existing.PreferredContact.Should().Be("Email"); auditLogs.Should().NotBeEmpty(); auditLogs.Should().Contain(a => a.FieldName == nameof(Vendor.ContactName) && a.OldValue == "Old Name" && a.NewValue == "New Name"); auditLogs.Should().Contain(a => a.FieldName == nameof(Vendor.Phone) && a.OldValue == "(312) 555-0000" && a.NewValue == "(312) 555-0199"); auditLogs.Should().Contain(a => a.FieldName == nameof(Vendor.PreferredContact) && a.OldValue == "Phone" && a.NewValue == "Email"); } [Fact] public async Task GetDeactivationImpact_RejectsUnknownVendor() { var data = new Mock(); data.Setup(x => x.ExistsAsync(404)).ReturnsAsync(false); var act = () => NewService(data).GetDeactivationImpactAsync(404); await act.Should().ThrowAsync() .WithMessage("*404*not found"); data.Verify(x => x.GetLinkedWorkOrdersAsync(It.IsAny()), Times.Never); } [Fact] public async Task GetFacetsAsync_CompaniesAndLocationsUseCompanyOwnedFieldsOverTechnicianFields() { var data = new Mock(); data.Setup(x => x.GetCompaniesForFacetsAsync(true, It.IsAny())) .ReturnsAsync(new List { new() { Id = 1, Name = "Owner Co", CompanyPhone = "(555) 000-0001", Email = "owner@example.com", Address = "Owner Addr", City = "OwnerCity", State = "OS", Zip = "00001", GoogleMapsUrl = "https://owner.example/map" } }); data.Setup(x => x.GetActiveTradesAsync(It.IsAny())) .ReturnsAsync(new List()); var facets = await NewService(data).GetFacetsAsync(true, CancellationToken.None); var company = facets.Companies.Single(); company.Name.Should().Be("Owner Co"); company.CompanyId.Should().Be(1); company.CompanyPhone.Should().Be("(555) 000-0001"); company.Email.Should().Be("owner@example.com"); company.Address.Should().Be("Owner Addr"); company.City.Should().Be("OwnerCity"); company.State.Should().Be("OS"); company.Zip.Should().Be("00001"); company.GoogleMapsUrl.Should().Be("https://owner.example/map"); company.PreferredContact.Should().BeNull(); var location = facets.Locations.Single(); location.City.Should().Be("OwnerCity"); location.State.Should().Be("OS"); location.Label.Should().Be("OwnerCity, OS"); // SH-249: technician free text is no longer a trades source; the canonical // vocabulary comes from the Trade reference table instead. facets.Trades.Should().BeEmpty(); } [Fact] public async Task GetFacetsAsync_LocationsIncludeAddressOnlyCompaniesWithAddressLabel() { var data = new Mock(); data.Setup(x => x.GetCompaniesForFacetsAsync(null, It.IsAny())) .ReturnsAsync(new List { new() { Id = 1, Name = "Address Only Co", Address = " 789 Warehouse Row " }, new() { Id = 2, Name = "City State Co", Address = "12 Ignored Way", City = "Denver", State = "CO" }, new() { Id = 3, Name = "Nowhere Co" } }); data.Setup(x => x.GetActiveTradesAsync(It.IsAny())) .ReturnsAsync(new List()); var facets = await NewService(data).GetFacetsAsync(null, CancellationToken.None); facets.Locations.Select(l => l.Label).Should().BeEquivalentTo( new[] { "789 Warehouse Row", "Denver, CO" }); var addressOnly = facets.Locations.Single(l => l.Label == "789 Warehouse Row"); addressOnly.City.Should().BeNull(); addressOnly.State.Should().BeNull(); var cityState = facets.Locations.Single(l => l.Label == "Denver, CO"); cityState.City.Should().Be("Denver"); cityState.State.Should().Be("CO"); } [Fact] public async Task GetFacetsAsync_CompaniesAreUniqueAndOrderedCaseInsensitively() { var data = new Mock(); data.Setup(x => x.GetCompaniesForFacetsAsync(null, It.IsAny())) .ReturnsAsync(new List { new() { Id = 1, Name = "Beta Co" }, new() { Id = 2, Name = "alpha co" }, new() { Id = 3, Name = "Alpha Co" } }); data.Setup(x => x.GetActiveTradesAsync(It.IsAny())) .ReturnsAsync(new List()); var facets = await NewService(data).GetFacetsAsync(null, CancellationToken.None); // Case-insensitive dedup collapses "alpha co"/"Alpha Co" to one entry, and // ordering places "alpha co" before "Beta Co" (case-insensitive, not Ordinal). facets.Companies.Select(c => c.Name).Should().BeEquivalentTo( new[] { "alpha co", "Beta Co" }, opts => opts.WithStrictOrdering()); } [Fact] public async Task GetFacetsAsync_TradesComeFromCanonicalReferenceDataNotTechnicianFreeText() { using var cts = new CancellationTokenSource(); var data = new Mock(); data.Setup(x => x.GetCompaniesForFacetsAsync(true, It.IsAny())) .ReturnsAsync(new List { new() { Id = 1, Name = "No Trade Co" } }); data.Setup(x => x.GetActiveTradesAsync(cts.Token)) .ReturnsAsync(TradeCatalog.CanonicalTrades .Select((name, index) => new Trade { Id = index + 1, Name = name, NormalizedName = TradeCatalog.Normalize(name), SortOrder = index + 1 }) .ToList()); var facets = await NewService(data).GetFacetsAsync(true, cts.Token); // SH-249: facets serve the canonical Trade vocabulary in catalog order, // regardless of technician TradeSpecialties free text. facets.Trades.Should().BeEquivalentTo( TradeCatalog.CanonicalTrades, opts => opts.WithStrictOrdering()); // G7 evidence: the caller's cancellation token reaches the data service. data.Verify(x => x.GetActiveTradesAsync(cts.Token), Times.Once); } [Fact] public async Task GetFacetsAsync_ReturnsCanonicalTradesOnEmptyVendorDatabase() { await using var context = new ApplicationDbContext(new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); // Empty vendor/company data; only the canonical Trade reference rows exist // (in production these are seeded by the SH249_TradesVocabulary migration). context.Trades.AddRange(TradeCatalog.CanonicalTrades.Select((name, index) => new Trade { Id = index + 1, Name = name, NormalizedName = TradeCatalog.Normalize(name), SortOrder = index + 1 })); await context.SaveChangesAsync(); var service = new VendorService( new VendorDataService(context), Mock.Of(), Mock.Of(), Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), new CreateVendorValidation(), new UpdateVendorValidation(), new WorkOrderVendorUpdateValidation(), Mock.Of>()); var facets = await service.GetFacetsAsync(null, CancellationToken.None); facets.Companies.Should().BeEmpty(); facets.Locations.Should().BeEmpty(); facets.Trades.Should().BeEquivalentTo( TradeCatalog.CanonicalTrades, opts => opts.WithStrictOrdering()); } }