mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
- Legacy reads (by id, all, by client, paged, address book, exists, count) and the vendor preference site check now skip tombstoned sites - Create requires a client, street address, city, state and at least one complete contact
503 lines
21 KiB
C#
503 lines
21 KiB
C#
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using SeaHaven.DataServices.Dto;
|
|
using SeaHaven.DataServices.Implementation;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Exceptions;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Validation;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
/// <summary>
|
|
/// Site registry rules: tenant-scoped unique site codes, immutable codes,
|
|
/// permission-gated tombstone delete, open work order lookup and the
|
|
/// contact/notes write used by the work-order Site dialog.
|
|
/// </summary>
|
|
public class SiteRegistryServiceTests
|
|
{
|
|
private static ApplicationDbContext NewContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
|
|
private static LocationService NewService(ApplicationDbContext ctx, string? role = "Admin") =>
|
|
NewService(new LocationDataService(ctx), new AccountDataService(ctx), role);
|
|
|
|
private static LocationService NewService(
|
|
ILocationDataService data,
|
|
IAccountDataService accounts,
|
|
string? role = "Admin")
|
|
{
|
|
var permissions = new Mock<ITeamPermissionOverrideDataService>();
|
|
permissions
|
|
.Setup(p => p.GetUserAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((string userId, CancellationToken _) => role == null
|
|
? null
|
|
: new TeamPermissionUserData { UserId = userId, RoleName = role });
|
|
|
|
return new LocationService(
|
|
data,
|
|
accounts,
|
|
new CreateLocationValidation(),
|
|
new UpdateLocationValidation(),
|
|
permissions.Object,
|
|
new TeamPermissionPolicy());
|
|
}
|
|
|
|
private static ClaimsPrincipal OrgWide(string role = "Admin") => Principal(role, new Claim(SeaHavenClaimTypes.OrgScope, SeaHavenClaimTypes.OrgScopeAll));
|
|
|
|
private static ClaimsPrincipal AccountUser(int accountId, string role = "Admin") =>
|
|
Principal(role, new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()));
|
|
|
|
private static ClaimsPrincipal Principal(string role, Claim scope) =>
|
|
new(new ClaimsIdentity(new List<Claim>
|
|
{
|
|
new(ClaimTypes.NameIdentifier, "actor-1"),
|
|
new(ClaimTypes.Role, role),
|
|
scope
|
|
}, "test"));
|
|
|
|
private static void SeedAccounts(ApplicationDbContext ctx, params int[] ids)
|
|
{
|
|
foreach (var id in ids)
|
|
ctx.Accounts.Add(new Accounts { Id = id, Name = $"Client {id}", IsDeleted = false });
|
|
ctx.SaveChanges();
|
|
}
|
|
|
|
private static Locations SeedSite(ApplicationDbContext ctx, string code, int? accountId, bool deleted = false)
|
|
{
|
|
var site = new Locations { Name = code, AccountId = accountId, City = "Dallas", State = "TX", IsDeleted = deleted ? true : null };
|
|
ctx.Locations.Add(site);
|
|
ctx.SaveChanges();
|
|
return site;
|
|
}
|
|
|
|
private static WorkOrder Wo(
|
|
int id,
|
|
int? locationId,
|
|
LifecycleStatus? status = LifecycleStatus.Incomplete,
|
|
int? accountId = 1,
|
|
string? siteCode = null,
|
|
string? legacyStatus = null,
|
|
bool deleted = false) => new()
|
|
{
|
|
Id = id,
|
|
LocationId = locationId,
|
|
LifecycleStatus = status,
|
|
LegacyStatus = legacyStatus,
|
|
AccountId = accountId,
|
|
SiteCode = siteCode,
|
|
IsDeleted = deleted ? true : null
|
|
};
|
|
|
|
private static LocationCreateRequestDTO CreateRequest(string code, int? accountId) => new()
|
|
{
|
|
Name = code,
|
|
AccountId = accountId,
|
|
Address = "1 Depot Rd",
|
|
City = "Dallas",
|
|
State = "TX",
|
|
Contacts = new List<SiteContactRequestDTO> { new() { Name = "Main", Phone = "555-0100" } }
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Create_DuplicateSiteCodeInSameClient_IsRejectedCaseInsensitively()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
SeedSite(ctx, "BK5", 1);
|
|
|
|
var act = () => NewService(ctx).CreateLocationFromRequestAsync(CreateRequest(" bk5 ", 1), OrgWide(), CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<SiteCodeConflictException>();
|
|
ctx.Locations.Should().ContainSingle();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_SameSiteCodeForAnotherClient_IsAllowed()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1, 2);
|
|
SeedSite(ctx, "BK5", 1);
|
|
|
|
await NewService(ctx).CreateLocationFromRequestAsync(CreateRequest("BK5", 2), OrgWide(), CancellationToken.None);
|
|
|
|
ctx.Locations.Where(l => l.Name == "BK5").Select(l => l.AccountId).Should().BeEquivalentTo(new int?[] { 1, 2 });
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_SiteCodeOfADeletedSite_CanBeReused()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
SeedSite(ctx, "BK5", 1, deleted: true);
|
|
|
|
await NewService(ctx).CreateLocationFromRequestAsync(CreateRequest("BK5", 1), OrgWide(), CancellationToken.None);
|
|
|
|
ctx.Locations.Count(l => l.Name == "BK5" && l.IsDeleted != true).Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_BlankSiteCode_IsAValidationError()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
|
|
var act = () => NewService(ctx).CreateLocationFromRequestAsync(CreateRequest(" ", 1), OrgWide(), CancellationToken.None);
|
|
|
|
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
|
.Which.Errors.Should().ContainSingle(e => e.ErrorMessage == "Site Code is required.");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_StoresTrimmedCodeNotesAndSitePhoneIndependentOfContacts()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
var request = CreateRequest(" DFW8 ", 1);
|
|
request.Phone = "555-9000";
|
|
request.Notes = " Gate code 4411 ";
|
|
|
|
await NewService(ctx).CreateLocationFromRequestAsync(request, OrgWide(), CancellationToken.None);
|
|
|
|
var site = ctx.Locations.Single();
|
|
site.Name.Should().Be("DFW8");
|
|
site.PhoneNumber.Should().Be("555-9000");
|
|
site.Notes.Should().Be("Gate code 4411");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_ChangingAnExistingSiteCode_IsRejected()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", null);
|
|
|
|
var act = () => NewService(ctx).UpdateLocationFromRequestAsync(
|
|
site.Id, new LocationUpdateRequestDTO { Name = "BK6" }, OrgWide(), CancellationToken.None);
|
|
|
|
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
|
.Which.Errors.Should().ContainSingle(e => e.ErrorMessage == "Site Code cannot be changed.");
|
|
ctx.Locations.AsNoTracking().Single().Name.Should().Be("BK5");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_KeepsCodeAndNotesWhenTheRequestOmitsThem()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", null);
|
|
site.Notes = "Dock 3";
|
|
site.Status = "Active";
|
|
ctx.SaveChanges();
|
|
|
|
await NewService(ctx).UpdateLocationFromRequestAsync(
|
|
site.Id, new LocationUpdateRequestDTO { Name = "bk5", City = "Memphis" }, OrgWide(), CancellationToken.None);
|
|
|
|
var saved = ctx.Locations.AsNoTracking().Single();
|
|
saved.Name.Should().Be("BK5");
|
|
saved.City.Should().Be("Memphis");
|
|
saved.Notes.Should().Be("Dock 3");
|
|
saved.Status.Should().Be("Active");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_BlankLegacyCode_CanBeFilledOnceButMustBeUniqueInTheClient()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
SeedSite(ctx, "BK5", 1);
|
|
var legacy = SeedSite(ctx, "", 1);
|
|
|
|
var duplicate = () => NewService(ctx).UpdateLocationFromRequestAsync(
|
|
legacy.Id, new LocationUpdateRequestDTO { Name = "bk5" }, OrgWide(), CancellationToken.None);
|
|
await duplicate.Should().ThrowAsync<SiteCodeConflictException>();
|
|
|
|
await NewService(ctx).UpdateLocationFromRequestAsync(
|
|
legacy.Id, new LocationUpdateRequestDTO { Name = "MEM1" }, OrgWide(), CancellationToken.None);
|
|
ctx.Locations.AsNoTracking().Single(l => l.Id == legacy.Id).Name.Should().Be("MEM1");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Admin")]
|
|
[InlineData("Scheduler")]
|
|
public async Task Delete_AdminOrScheduler_TombstonesTheSiteAndLeavesWorkOrdersAsTheyWere(string role)
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
var site = SeedSite(ctx, "BK5", 1);
|
|
ctx.Contacts.Add(new Contacts { LocationId = site.Id, FirstName = "Main", PhoneNumber = "555-0100" });
|
|
ctx.workOrders.Add(Wo(10, site.Id));
|
|
ctx.SaveChanges();
|
|
|
|
var deleted = await NewService(ctx, role).DeleteLocationByIdAsync(site.Id, OrgWide(role), CancellationToken.None);
|
|
|
|
deleted.Should().BeTrue();
|
|
var tombstone = ctx.Locations.AsNoTracking().Single();
|
|
tombstone.IsDeleted.Should().BeTrue();
|
|
tombstone.DeleterUserId.Should().Be("actor-1");
|
|
ctx.workOrders.AsNoTracking().Single().LocationId.Should().Be(site.Id, "work orders keep their site reference");
|
|
ctx.Contacts.AsNoTracking().Single().IsDeleted.Should().NotBe(true, "contacts still back open work orders");
|
|
|
|
var data = new LocationDataService(ctx);
|
|
(await data.GetDetailByIdAsync(site.Id, CancellationToken.None)).Should().BeNull();
|
|
(await data.GetSiteOptionsAsync(null, CancellationToken.None)).Should().BeEmpty();
|
|
(await data.GetListPagedAsync(1, 10, null, null, CancellationToken.None)).TotalCount.Should().Be(0);
|
|
(await data.GetAccountScopeAsync(site.Id, CancellationToken.None)).Exists.Should().BeFalse("a deleted site cannot be assigned to new work orders");
|
|
(await NewService(ctx, role).DeleteLocationByIdAsync(site.Id, OrgWide(role), CancellationToken.None)).Should().BeFalse();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Dispatcher")]
|
|
[InlineData(null)]
|
|
public async Task Delete_WithoutDeleteSitesPermission_IsForbiddenAndKeepsTheSite(string? role)
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", null);
|
|
|
|
var act = () => NewService(ctx, role).DeleteLocationByIdAsync(site.Id, OrgWide(role ?? "Dispatcher"), CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<SiteForbiddenException>();
|
|
ctx.Locations.AsNoTracking().Single().IsDeleted.Should().NotBe(true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Delete_SiteOfAnotherClient_IsRejectedForAnAccountScopedCaller()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", 2);
|
|
|
|
var act = () => NewService(ctx).DeleteLocationByIdAsync(site.Id, AccountUser(1), CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<UnauthorizedAccessException>();
|
|
ctx.Locations.AsNoTracking().Single().IsDeleted.Should().NotBe(true);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenWorkOrders_ExcludeTerminalDeletedAndOtherSitesWork()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", 1);
|
|
var other = SeedSite(ctx, "MEM1", 1);
|
|
ctx.workOrders.AddRange(
|
|
Wo(1, site.Id),
|
|
Wo(2, site.Id, LifecycleStatus.Scheduled),
|
|
Wo(3, site.Id, LifecycleStatus.Completed),
|
|
Wo(4, site.Id, LifecycleStatus.Canceled),
|
|
Wo(5, site.Id, deleted: true),
|
|
Wo(6, site.Id, status: null, legacyStatus: "Completed"),
|
|
Wo(7, site.Id, status: null, legacyStatus: "Open"),
|
|
Wo(8, null, siteCode: "bk5"),
|
|
Wo(9, null, siteCode: "BK5", accountId: 2),
|
|
Wo(10, other.Id));
|
|
ctx.SaveChanges();
|
|
|
|
var result = await NewService(ctx).GetOpenWorkOrdersAsync(site.Id, OrgWide(), CancellationToken.None);
|
|
|
|
result!.WorkOrderIds.Should().Equal(1, 2, 7, 8);
|
|
result.Count.Should().Be(4);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenWorkOrders_AccountScopedCaller_SeesOnlyTheirClientsWork()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", 1);
|
|
ctx.workOrders.AddRange(Wo(1, site.Id, accountId: 1), Wo(2, site.Id, accountId: 2));
|
|
ctx.SaveChanges();
|
|
|
|
var own = await NewService(ctx).GetOpenWorkOrdersAsync(site.Id, AccountUser(1), CancellationToken.None);
|
|
own!.WorkOrderIds.Should().Equal(1);
|
|
|
|
var foreign = () => NewService(ctx).GetOpenWorkOrdersAsync(site.Id, AccountUser(2), CancellationToken.None);
|
|
await foreign.Should().ThrowAsync<UnauthorizedAccessException>();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenWorkOrders_ReturnFullCountButCapIds()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", 1);
|
|
ctx.workOrders.AddRange(Enumerable.Range(1, LocationService.MaxOpenWorkOrderIds + 5).Select(id => Wo(id, site.Id)));
|
|
ctx.SaveChanges();
|
|
|
|
var result = await NewService(ctx).GetOpenWorkOrdersAsync(site.Id, OrgWide(), CancellationToken.None);
|
|
|
|
result!.Count.Should().Be(LocationService.MaxOpenWorkOrderIds + 5);
|
|
result.WorkOrderIds.Should().HaveCount(LocationService.MaxOpenWorkOrderIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenWorkOrders_MissingOrDeletedSite_ReturnsNull()
|
|
{
|
|
using var ctx = NewContext();
|
|
var deleted = SeedSite(ctx, "BK5", 1, deleted: true);
|
|
|
|
(await NewService(ctx).GetOpenWorkOrdersAsync(deleted.Id, OrgWide(), CancellationToken.None)).Should().BeNull();
|
|
(await NewService(ctx).GetOpenWorkOrdersAsync(999, OrgWide(), CancellationToken.None)).Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSiteContactInfo_SavesContactsAndNotesToTheSiteRecord()
|
|
{
|
|
using var ctx = NewContext();
|
|
var site = SeedSite(ctx, "BK5", null);
|
|
var main = new Contacts { LocationId = site.Id, FirstName = "Old Main", PhoneNumber = "555-0100", SiteContactOrder = 0 };
|
|
ctx.Contacts.Add(main);
|
|
ctx.SaveChanges();
|
|
|
|
await NewService(ctx).UpdateSiteContactInfoAsync(site.Id, new SiteContactInfoRequestDTO
|
|
{
|
|
Contacts = new List<SiteContactRequestDTO>
|
|
{
|
|
new() { Id = main.Id, Name = "New Main", Phone = "555-0199" },
|
|
new() { Name = "Night Shift", Phone = "555-0200" }
|
|
},
|
|
Notes = " Call ahead "
|
|
}, OrgWide("Dispatcher"), CancellationToken.None);
|
|
|
|
var saved = ctx.Locations.AsNoTracking().Include(l => l.Contacts).Single();
|
|
saved.Notes.Should().Be("Call ahead");
|
|
saved.Contacts!.Where(c => c.IsDeleted != true).OrderBy(c => c.SiteContactOrder)
|
|
.Select(c => (c.Id == main.Id, c.FirstName, c.PhoneNumber))
|
|
.Should().Equal((true, "New Main", "555-0199"), (false, "Night Shift", "555-0200"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSiteContactInfo_RejectsOutOfScopeDeletedAndContactlessRequests()
|
|
{
|
|
using var ctx = NewContext();
|
|
var foreign = SeedSite(ctx, "BK5", 2);
|
|
var deleted = SeedSite(ctx, "MEM1", 1, deleted: true);
|
|
var request = new SiteContactInfoRequestDTO
|
|
{
|
|
Contacts = new List<SiteContactRequestDTO> { new() { Name = "A", Phone = "1" } }
|
|
};
|
|
|
|
await ((Func<Task>)(() => NewService(ctx).UpdateSiteContactInfoAsync(foreign.Id, request, AccountUser(1), CancellationToken.None)))
|
|
.Should().ThrowAsync<UnauthorizedAccessException>();
|
|
await ((Func<Task>)(() => NewService(ctx).UpdateSiteContactInfoAsync(deleted.Id, request, OrgWide(), CancellationToken.None)))
|
|
.Should().ThrowAsync<KeyNotFoundException>();
|
|
await ((Func<Task>)(() => NewService(ctx).UpdateSiteContactInfoAsync(
|
|
foreign.Id, new SiteContactInfoRequestDTO { Notes = "x" }, OrgWide(), CancellationToken.None)))
|
|
.Should().ThrowAsync<FluentValidation.ValidationException>();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NewSiteOperations_ForwardTheCallersCancellationToken()
|
|
{
|
|
using var cts = new CancellationTokenSource();
|
|
var token = cts.Token;
|
|
var site = new Locations { Id = 5, Name = "BK5", AccountId = 1, Contacts = new List<Contacts>() };
|
|
var data = new Mock<ILocationDataService>();
|
|
data.Setup(d => d.GetByIdForUpdateAsync(5, token)).ReturnsAsync(site);
|
|
data.Setup(d => d.GetDetailByIdAsync(5, token)).ReturnsAsync(site);
|
|
data.Setup(d => d.GetOpenWorkOrderIdsAsync(5, "BK5", 1, null, LocationService.MaxOpenWorkOrderIds, token))
|
|
.ReturnsAsync((1, new[] { 7 }));
|
|
var service = NewService(data.Object, Mock.Of<IAccountDataService>());
|
|
|
|
await service.GetOpenWorkOrdersAsync(5, OrgWide(), token);
|
|
await service.UpdateSiteContactInfoAsync(5, new SiteContactInfoRequestDTO
|
|
{
|
|
Contacts = new List<SiteContactRequestDTO> { new() { Name = "A", Phone = "1" } }
|
|
}, OrgWide(), token);
|
|
await service.DeleteLocationByIdAsync(5, OrgWide(), token);
|
|
|
|
data.Verify(d => d.GetOpenWorkOrderIdsAsync(5, "BK5", 1, null, LocationService.MaxOpenWorkOrderIds, token), Times.Once);
|
|
data.Verify(d => d.UpdateAsync(site, token), Times.Exactly(2));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_DuplicateCheck_ForwardsTheCallersCancellationToken()
|
|
{
|
|
using var cts = new CancellationTokenSource();
|
|
var token = cts.Token;
|
|
var data = new Mock<ILocationDataService>();
|
|
data.Setup(d => d.SiteCodeExistsAsync("BK5", 1, null, token)).ReturnsAsync(true);
|
|
var accounts = new Mock<IAccountDataService>();
|
|
accounts.Setup(a => a.ExistsActiveAsync(1, token)).ReturnsAsync(true);
|
|
var service = NewService(data.Object, accounts.Object);
|
|
|
|
var act = () => service.CreateLocationFromRequestAsync(CreateRequest("BK5", 1), OrgWide(), token);
|
|
|
|
await act.Should().ThrowAsync<SiteCodeConflictException>();
|
|
data.Verify(d => d.SiteCodeExistsAsync("BK5", 1, null, token), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompletionSnapshot_FreezesSiteNotesWhenTheWorkOrderHasNone()
|
|
{
|
|
var workOrder = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
Locations = new Locations { Id = 5, Name = "BK5", Notes = "Gate code 4411", Contacts = new List<Contacts>() }
|
|
};
|
|
|
|
WorkOrderCompletionSnapshotMapper.Capture(workOrder);
|
|
|
|
using var frozen = JsonDocument.Parse(workOrder.FrozenPoc!);
|
|
frozen.RootElement.GetProperty("notes").GetString().Should().Be("Gate code 4411");
|
|
}
|
|
|
|
public static TheoryData<string, Action<LocationCreateRequestDTO>, string> MissingSiteFields => new()
|
|
{
|
|
{ "no client", r => r.AccountId = null, "Client is required." },
|
|
{ "no street address", r => r.Address = " ", "Street Address is required." },
|
|
{ "no city", r => r.City = null, "City is required." },
|
|
{ "no state", r => r.State = "", "State is required." },
|
|
{ "no contacts list", r => r.Contacts = null, "At least one contact is required." },
|
|
{ "empty contacts list", r => r.Contacts = new List<SiteContactRequestDTO>(), "At least one contact is required." },
|
|
{ "contact without phone", r => r.Contacts = new List<SiteContactRequestDTO> { new() { Name = "Main", Phone = " " } }, "Contact phone is required." }
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(MissingSiteFields))]
|
|
public async Task Create_WithoutARequiredSiteField_IsAValidationErrorAndStoresNothing(
|
|
string _,
|
|
Action<LocationCreateRequestDTO> strip,
|
|
string expectedMessage)
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
var request = CreateRequest("BK9", 1);
|
|
strip(request);
|
|
|
|
var act = () => NewService(ctx).CreateLocationFromRequestAsync(request, OrgWide(), CancellationToken.None);
|
|
|
|
(await act.Should().ThrowAsync<FluentValidation.ValidationException>())
|
|
.Which.Errors.Should().Contain(e => e.ErrorMessage == expectedMessage);
|
|
ctx.Locations.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Delete_RemovesTheSiteFromEveryLegacyRead()
|
|
{
|
|
using var ctx = NewContext();
|
|
SeedAccounts(ctx, 1);
|
|
var deleted = SeedSite(ctx, "BK5", 1);
|
|
var kept = SeedSite(ctx, "BK6", 1);
|
|
var service = NewService(ctx);
|
|
|
|
(await service.DeleteLocationByIdAsync(deleted.Id, OrgWide(), CancellationToken.None)).Should().BeTrue();
|
|
|
|
(await service.GetLocationByIdAsync(deleted.Id)).Should().BeNull();
|
|
(await service.GetLocationByIdWithDetailsAsync(deleted.Id)).Should().BeNull();
|
|
(await service.LocationExistsAsync(deleted.Id)).Should().BeFalse();
|
|
(await service.GetTotalLocationCountAsync()).Should().Be(1);
|
|
(await service.GetAllLocationsAsync()).Select(l => l.Id).Should().Equal(kept.Id);
|
|
(await service.GetLocationsByAccountIdAsync(1)).Select(l => l.Id).Should().Equal(kept.Id);
|
|
(await service.GetLocationsPagedAsync(1, 10)).Items.Select(l => l.Id).Should().Equal(kept.Id);
|
|
(await new LocationDataService(ctx).GetAddressbookPagedAsync(1, 10)).TotalCount.Should().Be(1);
|
|
(await new VendorOperationsDataService(ctx, Mock.Of<IDispatchDataService>()).LocationExistsAsync(deleted.Id, CancellationToken.None)).Should().BeFalse();
|
|
}
|
|
}
|