shoc-backend/Api.SeaHavenIndustries.Tests/VendorCompanyRosterDataServiceTests.cs
Alexandre Brandizzi 669e9b2932
Some checks are pending
Validate and deploy dev / Validate deployable source bundle (push) Waiting to run
Validate and deploy dev / Deploy shoc-backend to Elastic Beanstalk dev (push) Blocked by required conditions
feat(vendors): add company roster management (SH-198) (#48)
* feat(vendors): add company roster management

* fix(security): remove request-controlled write guards

* fix(vendors): synchronize roster company fields

* fix(vendors): source facets from companies
2026-08-03 17:53:24 -03:00

495 lines
18 KiB
C#

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<ApplicationDbContext>()
.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<RosterTechnicianWriteModel>
{
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<RosterTechnicianWriteModel>
{
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<RosterTechnicianWriteModel>
{
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<RosterTechnicianWriteModel>()
}, 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<RosterTechnicianWriteModel>()
}, CancellationToken.None);
await actCall.Should().ThrowAsync<VendorRosterConflictException>();
}
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<OperationCanceledException>();
}
[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<RosterTechnicianWriteModel>
{
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<RosterTechnicianWriteModel>
{
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<RosterTechnicianWriteModel>
{
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<RosterTechnicianWriteModel>
{
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");
}
}