mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 18:53:12 +00:00
214 lines
9 KiB
C#
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");
|
|
}
|
|
}
|