using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentAssertions; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using SeaHaven.DataServices.Models; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class VendorCompanyRosterDataServiceTests { private static ApplicationDbContext NewContext(string dbName) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(dbName) .Options; return new ApplicationDbContext(options); } private static async Task<(int companyId, Vendor vendor)> SeedCompanyWithTechnicianAsync(ApplicationDbContext context, string companyName, params string[] technicianNames) { var company = new VendorCompany { Name = companyName, NormalizedName = companyName.ToLowerInvariant() }; context.VendorCompanies.Add(company); foreach (var name in technicianNames) { context.Vendors.Add(new Vendor { CompanyName = companyName, ContactName = name, IsActive = true, IsDeleted = false, Company = company }); } await context.SaveChangesAsync(); return (company.Id, context.Vendors.First(v => v.CompanyId == company.Id)); } [Fact] public async Task CreateRoster_PersistsCompanyAndTechniciansAtomically() { var dbName = Guid.NewGuid().ToString(); using var context = NewContext(dbName); var service = new VendorCompanyRosterDataService(context); var roster = await service.CreateRosterAsync(new VendorCompanyRosterWriteModel { Name = "Atomic Co", Email = "atomic@example.com", Notes = "shared notes", ActorUserId = "42", Technicians = new List { new() { ContactName = "T1" }, new() { ContactName = "T2" } } }, CancellationToken.None); roster.CompanyId.Should().BeGreaterThan(0); roster.Technicians.Should().HaveCount(2); roster.Notes.Should().Be("shared notes"); var company = context.VendorCompanies.Single(c => c.Id == roster.CompanyId); company.Name.Should().Be("Atomic Co"); context.Vendors.Count(v => v.CompanyId == company.Id).Should().Be(2); context.Vendors.Where(v => v.CompanyId == company.Id).Should().OnlyContain(v => v.CompanyName == "Atomic Co"); } [Fact] public async Task GetRoster_ByVendorId_ResolvesCompanyAndAllTechnicians() { var dbName = Guid.NewGuid().ToString(); int companyId, vendorId; using (var seed = NewContext(dbName)) { var (cid, _) = await SeedCompanyWithTechnicianAsync(seed, "Resolve Co", "A", "B"); companyId = cid; vendorId = seed.Vendors.First(v => v.CompanyId == cid && v.ContactName == "A").Id; } using var context = NewContext(dbName); var service = new VendorCompanyRosterDataService(context); var roster = await service.GetRosterAsync(vendorId, null, CancellationToken.None); roster.Should().NotBeNull(); roster!.CompanyId.Should().Be(companyId); roster.Technicians.Select(t => t.ContactName).Should().BeEquivalentTo(new[] { "A", "B" }); } [Fact] public async Task GetRoster_WithMismatchedVendorAndCompanyIds_ReturnsNull() { var dbName = Guid.NewGuid().ToString(); int firstCompanyId, secondVendorId; using (var seed = NewContext(dbName)) { var (firstId, _) = await SeedCompanyWithTechnicianAsync(seed, "First Co", "A"); var (_, secondVendor) = await SeedCompanyWithTechnicianAsync(seed, "Second Co", "B"); firstCompanyId = firstId; secondVendorId = secondVendor.Id; } using var context = NewContext(dbName); var service = new VendorCompanyRosterDataService(context); var roster = await service.GetRosterAsync(secondVendorId, firstCompanyId, CancellationToken.None); roster.Should().BeNull(); } [Fact] public async Task SaveRoster_RenamesCompanyAndTechnicianAndUpdatesNotes() { var dbName = Guid.NewGuid().ToString(); int companyId, techId; using (var seed = NewContext(dbName)) { var (cid, vendor) = await SeedCompanyWithTechnicianAsync(seed, "Old Co", "Old Name"); companyId = cid; techId = vendor.Id; } using var context = NewContext(dbName); var service = new VendorCompanyRosterDataService(context); var roster = await service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, Name = "New Co", Email = "new@example.com", Notes = "updated notes", ActorUserId = "42", Technicians = new List { new() { Id = techId, ContactName = "New Name", IsActive = true } } }, CancellationToken.None); roster.Name.Should().Be("New Co"); roster.Notes.Should().Be("updated notes"); var tech = context.Vendors.Single(v => v.Id == techId); tech.ContactName.Should().Be("New Name"); tech.CompanyName.Should().Be("New Co"); context.VendorCompanies.Single(c => c.Id == companyId).Notes.Should().Be("updated notes"); } [Fact] public async Task SaveRoster_SoftDeletesOmittedTechnicianWithoutOpenWorkOrders() { var dbName = Guid.NewGuid().ToString(); int companyId, keepId, dropId; using (var seed = NewContext(dbName)) { var (cid, _) = await SeedCompanyWithTechnicianAsync(seed, "Drop Co", "Keep", "Drop"); companyId = cid; keepId = seed.Vendors.First(v => v.CompanyId == cid && v.ContactName == "Keep").Id; dropId = seed.Vendors.First(v => v.CompanyId == cid && v.ContactName == "Drop").Id; } using (var act = NewContext(dbName)) { var service = new VendorCompanyRosterDataService(act); await service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, Name = "Drop Co", Email = "drop@example.com", ActorUserId = "42", Technicians = new List { new() { Id = keepId, ContactName = "Keep", IsActive = true } } }, CancellationToken.None); } using var verify = NewContext(dbName); var kept = verify.Vendors.Single(v => v.Id == keepId); kept.IsDeleted.Should().NotBeTrue(); var dropped = verify.Vendors.Single(v => v.Id == dropId); dropped.IsDeleted.Should().BeTrue(); dropped.IsActive.Should().BeFalse(); } [Fact] public async Task SaveRoster_RemovingAllTechniciansLeavesZeroVendorsAndCompanyRemains() { var dbName = Guid.NewGuid().ToString(); int companyId; using (var seed = NewContext(dbName)) { var (cid, _) = await SeedCompanyWithTechnicianAsync(seed, "Empty Co", "T1", "T2"); companyId = cid; } using (var act = NewContext(dbName)) { var service = new VendorCompanyRosterDataService(act); await service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, Name = "Empty Co", Email = "empty@example.com", ActorUserId = "42", Technicians = new List() }, CancellationToken.None); } using var verify = NewContext(dbName); verify.Vendors.Count(v => v.CompanyId == companyId && (v.IsDeleted == null || v.IsDeleted == false)).Should().Be(0); verify.VendorCompanies.Any(c => c.Id == companyId).Should().BeTrue(); } [Fact] public async Task SaveRoster_RemovalBlockedByOpenWorkOrdersFailsWholeSave() { var dbName = Guid.NewGuid().ToString(); int companyId, vendorId; using (var seed = NewContext(dbName)) { var (cid, vendor) = await SeedCompanyWithTechnicianAsync(seed, "Blocked Co", "Busy"); companyId = cid; vendorId = vendor.Id; seed.workOrders.Add(new WorkOrder { Id = 800, LifecycleStatus = LifecycleStatus.Scheduled, Status = "Scheduled" }); seed.Dispatches.Add(new Dispatch { VendorId = vendorId, WorkOrderId = 800, Vendor = vendor }); await seed.SaveChangesAsync(); } using (var act = NewContext(dbName)) { var service = new VendorCompanyRosterDataService(act); var actCall = () => service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, RowVersion = new byte[] { 0, 0, 0, 0, 0, 0, 0, 1 }, Name = "Blocked Co", Email = "blocked@example.com", ActorUserId = "42", Technicians = new List() }, CancellationToken.None); await actCall.Should().ThrowAsync(); } using var verify = NewContext(dbName); var tech = verify.Vendors.Single(v => v.Id == vendorId); tech.IsDeleted.Should().NotBeTrue(); tech.IsActive.Should().BeTrue(); } [Fact] public async Task GetRoster_ForwardsCancellation() { var dbName = Guid.NewGuid().ToString(); using var context = NewContext(dbName); var service = new VendorCompanyRosterDataService(context); using var cts = new CancellationTokenSource(); cts.Cancel(); var act = () => service.GetRosterAsync(1, null, cts.Token); await act.Should().ThrowAsync(); } [Fact] public async Task SaveRoster_PropagatesCompanyLocationAndPhoneToRetainedTechnician() { var dbName = Guid.NewGuid().ToString(); int companyId, techId; using (var seed = NewContext(dbName)) { var company = new VendorCompany { Name = "Loc Co", NormalizedName = "loc co", CompanyPhone = "(111) 111-0001", Address = "Old Addr", City = "OldCity", State = "OS", Zip = "00001", GoogleMapsUrl = "https://old.example/map" }; seed.VendorCompanies.Add(company); seed.Vendors.Add(new Vendor { CompanyName = "Loc Co", ContactName = "Keeper", IsActive = true, IsDeleted = false, Company = company, CompanyPhone = "(111) 111-0001", Address = "Old Addr", City = "OldCity", State = "OS", Zip = "00001", GoogleMapsUrl = "https://old.example/map" }); await seed.SaveChangesAsync(); companyId = company.Id; techId = seed.Vendors.First(v => v.CompanyId == companyId).Id; } using (var act = NewContext(dbName)) { var service = new VendorCompanyRosterDataService(act); await service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, Name = "Loc Co", CompanyPhone = "(222) 222-0002", Address = "New Addr", City = "NewCity", State = "NS", Zip = "00002", GoogleMapsUrl = "https://new.example/map", ActorUserId = "42", Technicians = new List { new() { Id = techId, ContactName = "Keeper", IsActive = true } } }, CancellationToken.None); } using var verify = NewContext(dbName); var tech = verify.Vendors.Single(v => v.Id == techId); tech.CompanyPhone.Should().Be("(222) 222-0002"); tech.Address.Should().Be("New Addr"); tech.City.Should().Be("NewCity"); tech.State.Should().Be("NS"); tech.Zip.Should().Be("00002"); tech.GoogleMapsUrl.Should().Be("https://new.example/map"); var directory = new VendorDataService(verify); var newCity = await directory.GetDirectoryPagedAsync(1, 10, search: "NewCity"); newCity.Items.Should().ContainSingle(row => row.VendorId == techId); var oldCity = await directory.GetDirectoryPagedAsync(1, 10, search: "OldCity"); oldCity.Items.Should().NotContain(row => row.VendorId == techId); } [Fact] public async Task SaveRoster_ClearsCompanyOwnedFieldsOnRetainedTechnicianWhenCompanyClearsThem() { var dbName = Guid.NewGuid().ToString(); int companyId, techId; using (var seed = NewContext(dbName)) { var company = new VendorCompany { Name = "Clear Co", NormalizedName = "clear co", CompanyPhone = "(333) 333-0003", Address = "Keep Addr", City = "KeepCity", State = "KS", Zip = "00003", GoogleMapsUrl = "https://keep.example/map" }; seed.VendorCompanies.Add(company); seed.Vendors.Add(new Vendor { CompanyName = "Clear Co", ContactName = "Keeper", IsActive = true, IsDeleted = false, Company = company, CompanyPhone = "(333) 333-0003", Address = "Keep Addr", City = "KeepCity", State = "KS", Zip = "00003", GoogleMapsUrl = "https://keep.example/map" }); await seed.SaveChangesAsync(); companyId = company.Id; techId = seed.Vendors.First(v => v.CompanyId == companyId).Id; } using (var act = NewContext(dbName)) { var service = new VendorCompanyRosterDataService(act); await service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, Name = "Clear Co", CompanyPhone = null, Address = null, City = null, State = null, Zip = null, GoogleMapsUrl = null, ActorUserId = "42", Technicians = new List { new() { Id = techId, ContactName = "Keeper", IsActive = true } } }, CancellationToken.None); } using var verify = NewContext(dbName); var tech = verify.Vendors.Single(v => v.Id == techId); tech.CompanyPhone.Should().BeNull(); tech.Address.Should().BeNull(); tech.City.Should().BeNull(); tech.State.Should().BeNull(); tech.Zip.Should().BeNull(); tech.GoogleMapsUrl.Should().BeNull(); } [Fact] public async Task SaveRoster_NewTechnicianReceivesCompanyPhoneAndLocation() { var dbName = Guid.NewGuid().ToString(); int companyId; using (var seed = NewContext(dbName)) { var company = new VendorCompany { Name = "Add Co", NormalizedName = "add co" }; seed.VendorCompanies.Add(company); await seed.SaveChangesAsync(); companyId = company.Id; } int newTechId; using (var act = NewContext(dbName)) { var service = new VendorCompanyRosterDataService(act); await service.SaveRosterAsync(new VendorCompanyRosterWriteModel { CompanyId = companyId, Name = "Add Co", CompanyPhone = "(444) 444-0004", Address = "Add Addr", City = "AddCity", State = "AS", Zip = "00004", GoogleMapsUrl = "https://add.example/map", ActorUserId = "42", Technicians = new List { new() { ContactName = "Fresh", IsActive = true } } }, CancellationToken.None); newTechId = act.Vendors.First(v => v.CompanyId == companyId && v.ContactName == "Fresh").Id; } using var verify = NewContext(dbName); var tech = verify.Vendors.Single(v => v.Id == newTechId); tech.CompanyPhone.Should().Be("(444) 444-0004"); tech.Address.Should().Be("Add Addr"); tech.City.Should().Be("AddCity"); tech.State.Should().Be("AS"); tech.Zip.Should().Be("00004"); tech.GoogleMapsUrl.Should().Be("https://add.example/map"); } [Fact] public async Task CreateRoster_NewTechnicianReceivesCompanyPhoneAndLocation() { var dbName = Guid.NewGuid().ToString(); using var context = NewContext(dbName); var service = new VendorCompanyRosterDataService(context); var roster = await service.CreateRosterAsync(new VendorCompanyRosterWriteModel { Name = "Create Co", CompanyPhone = "(555) 555-0005", Address = "Create Addr", City = "CreateCity", State = "CS", Zip = "00005", GoogleMapsUrl = "https://create.example/map", ActorUserId = "42", Technicians = new List { new() { ContactName = "Brand", IsActive = true } } }, CancellationToken.None); var tech = context.Vendors.Single(v => v.CompanyId == roster.CompanyId); tech.CompanyPhone.Should().Be("(555) 555-0005"); tech.Address.Should().Be("Create Addr"); tech.City.Should().Be("CreateCity"); tech.State.Should().Be("CS"); tech.Zip.Should().Be("00005"); tech.GoogleMapsUrl.Should().Be("https://create.example/map"); } }