shoc-backend/Api.SeaHavenIndustries.Tests/LocationDataServiceTests.cs
2026-09-16 20:45:15 -03:00

214 lines
9 KiB
C#

using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Implementation;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class LocationDataServiceTests
{
private static ApplicationDbContext NewContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
private static async Task SeedAsync(ApplicationDbContext ctx)
{
ctx.Locations.AddRange(
new Locations { Id = 1, Name = "Alpha Site", City = "Austin", State = "TX" },
new Locations { Id = 2, Name = "Beta Site", City = "Dallas", State = "TX" },
new Locations { Id = 3, Name = "Gamma Yard", City = "Kansas City", State = "MO" },
new Locations { Id = 4, Name = "Delta Hub", City = "Indianapolis", State = "indiana" },
new Locations { Id = 5, Name = "Epsilon Depot", City = "Houston", State = "TX" },
new Locations { Id = 6, Name = "Zeta Lot", City = "Nowhere", State = null });
await ctx.SaveChangesAsync();
}
[Fact]
public async Task GetListPagedAsync_WithoutStates_ReturnsAllRows()
{
await using var ctx = NewContext();
await SeedAsync(ctx);
var (items, totalCount) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, null, CancellationToken.None);
totalCount.Should().Be(6);
items.Should().HaveCount(6);
}
[Fact]
public async Task GetListPagedAsync_AppliesStatesFilterBeforeCountAndPaging()
{
await using var ctx = NewContext();
await SeedAsync(ctx);
var (page1, totalCount) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 2, null, new[] { "TX" }, CancellationToken.None);
totalCount.Should().Be(3, "count must reflect the state filter, not the whole table");
page1.Should().HaveCount(2);
page1.Select(l => l.Name).Should().ContainInOrder("Alpha Site", "Beta Site");
var (page2, totalCountAgain) = await new LocationDataService(ctx)
.GetListPagedAsync(2, 2, null, new[] { "TX" }, CancellationToken.None);
totalCountAgain.Should().Be(3);
page2.Select(l => l.Name).Should().ContainSingle("Epsilon Depot");
}
[Fact]
public async Task GetListPagedAsync_MultipleStates_KeepSearchFilter()
{
await using var ctx = NewContext();
await SeedAsync(ctx);
var (items, totalCount) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, "Dallas", new[] { "TX", "MO" }, CancellationToken.None);
totalCount.Should().Be(1);
items.Should().ContainSingle().Which.Name.Should().Be("Beta Site");
}
[Fact]
public async Task GetListPagedAsync_StatesFilter_ExcludesNullAndNonMatchingStates()
{
await using var ctx = NewContext();
await SeedAsync(ctx);
var (items, totalCount) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, new[] { "IN", "INDIANA", "MO", "MISSOURI" }, CancellationToken.None);
totalCount.Should().Be(2);
items.Select(l => l.State).Should().BeEquivalentTo("indiana", "MO");
}
private static async Task SeedRegistryAsync(ApplicationDbContext ctx)
{
ctx.Accounts.AddRange(
new Accounts { Id = 1, Name = "Acme Co", IsDeleted = false },
new Accounts { Id = 2, Name = "Borealis LLC", IsDeleted = false });
ctx.Locations.AddRange(
new Locations { Id = 10, Name = "STL8", Address1 = "9 River Rd", City = "St. Louis", State = "MO", AccountId = 1 },
new Locations { Id = 11, Name = "DFW8", Address1 = "2 Prairie Ave", City = "Dallas", State = "TX", AccountId = 2 },
new Locations { Id = 12, Name = "IND9", Address1 = "5 Circle Blvd", City = "Indianapolis", State = "IN", AccountId = 1 },
new Locations { Id = 13, Name = "MIA2", Address1 = "1 Bay Dr", City = "Miami", State = "FL", AccountId = 2 });
ctx.Contacts.AddRange(
new Contacts { Id = 100, LocationId = 10, SiteContactOrder = 0, FirstName = "Cara Lane", PhoneNumber = "555-0001" },
new Contacts { Id = 101, LocationId = 10, SiteContactOrder = 1, FirstName = "Alan Ford", PhoneNumber = "555-0002" },
new Contacts { Id = 102, LocationId = 11, SiteContactOrder = 1, FirstName = "Zoe Park", PhoneNumber = "555-0003" },
new Contacts { Id = 103, LocationId = 11, SiteContactOrder = 0, FirstName = "Removed Poc", PhoneNumber = "555-0004", IsDeleted = true },
new Contacts { Id = 104, LocationId = 12, SiteContactOrder = 0, FirstName = "Bob Quinn", PhoneNumber = "555-0005" });
await ctx.SaveChangesAsync();
}
[Theory]
[InlineData("STL", new[] { 10 }, "site code")]
[InlineData("Borealis", new[] { 11, 13 }, "client name")]
[InlineData("Prairie", new[] { 11 }, "street address")]
[InlineData("Miami", new[] { 13 }, "city")]
public async Task GetListPagedAsync_SearchMatchesCodeClientAddressAndCity(string term, int[] expectedIds, string becauseField)
{
await using var ctx = NewContext();
await SeedRegistryAsync(ctx);
var (items, totalCount) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, term, null, CancellationToken.None);
totalCount.Should().Be(expectedIds.Length, $"search must match the {becauseField}");
items.Select(l => l.Id).Should().BeEquivalentTo(expectedIds);
}
[Theory]
[InlineData("code", 11, 12, 13, 10)]
[InlineData("client", 10, 12, 11, 13)]
[InlineData("address", 13, 11, 12, 10)]
[InlineData("city", 11, 12, 13, 10)]
[InlineData("state", 13, 12, 10, 11)]
[InlineData("poc", 13, 12, 10, 11)]
public async Task GetListPagedAsync_SortByAllowlistedColumn_OrdersAscending_WithIdTiebreak(
string sortBy, params int[] expectedIds)
{
await using var ctx = NewContext();
await SeedRegistryAsync(ctx);
var (items, _) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, null, CancellationToken.None, sortBy, "asc");
items.Select(l => l.Id).Should().ContainInOrder(expectedIds);
}
[Fact]
public async Task GetListPagedAsync_SortByPoc_IgnoresDeletedContacts_AndSortsByFirstActiveContact()
{
await using var ctx = NewContext();
await SeedRegistryAsync(ctx);
var (items, _) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, null, CancellationToken.None, "poc", "asc");
// DFW8's order-0 row is soft deleted, so its POC is "Zoe Park" (order 1) and it must
// sort last; including the deleted row ("Removed Poc") would instead place it second.
items.Select(l => l.Id).Should().ContainInOrder(13, 12, 10, 11);
}
[Fact]
public async Task GetListPagedAsync_SortByClient_Desc_ReversesAccountOrder()
{
await using var ctx = NewContext();
await SeedRegistryAsync(ctx);
var (items, _) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, null, CancellationToken.None, "client", "desc");
items.Select(l => l.Id).Should().ContainInOrder(new[] { 11, 13, 10, 12 });
}
[Fact]
public async Task GetListPagedAsync_UnknownSortColumn_FallsBackToLegacyCodeOrder()
{
await using var ctx = NewContext();
await SeedRegistryAsync(ctx);
var (items, _) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, null, CancellationToken.None, "not-a-column", "sideways");
items.Select(l => l.Name).Should().ContainInOrder("DFW8", "IND9", "MIA2", "STL8");
}
[Fact]
public async Task GetListPagedAsync_EqualSortValues_BreakTiesById_AcrossPages()
{
await using var ctx = NewContext();
ctx.Locations.AddRange(
new Locations { Id = 20, Name = "HOU1", City = "Dallas" },
new Locations { Id = 21, Name = "AUS3", City = "Dallas" },
new Locations { Id = 22, Name = "ELP4", City = "Dallas" });
await ctx.SaveChangesAsync();
var service = new LocationDataService(ctx);
var (page1, totalCount) = await service.GetListPagedAsync(1, 2, null, null, CancellationToken.None, "city", "asc");
var (page2, _) = await service.GetListPagedAsync(2, 2, null, null, CancellationToken.None, "city", "asc");
totalCount.Should().Be(3);
page1.Select(l => l.Id).Should().ContainInOrder(20, 21);
page2.Select(l => l.Id).Should().ContainInOrder(22);
}
[Fact]
public async Task GetListPagedAsync_ListLoadIncludesAccountNameForProjection()
{
await using var ctx = NewContext();
await SeedRegistryAsync(ctx);
var (items, _) = await new LocationDataService(ctx)
.GetListPagedAsync(1, 10, null, null, CancellationToken.None);
items.Single(l => l.Id == 10).Account!.Name.Should().Be("Acme Co");
}
}