diff --git a/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs b/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs new file mode 100644 index 0000000..db7b4be --- /dev/null +++ b/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs @@ -0,0 +1,28 @@ +using Data.SeaHavenIndustries.Enums; +using SeaHaven.Services.Helpers; + +namespace SeaHavenIndustries.Tests; + +public class LifecycleStatusMapperTests +{ + [Theory] + [InlineData("Open", LifecycleStatus.Incomplete)] + [InlineData("InProgress", LifecycleStatus.InProgress)] + [InlineData("In Progress", LifecycleStatus.InProgress)] + [InlineData("Completed", LifecycleStatus.Complete)] + [InlineData("Cancelled", LifecycleStatus.Canceled)] + [InlineData("On Hold", LifecycleStatus.OnHold)] + [InlineData("Scheduled", LifecycleStatus.Scheduled)] + public void FromLegacyStatus_MapsKnownValues(string input, LifecycleStatus expected) + { + var result = LifecycleStatusMapper.FromLegacyStatus(input); + Assert.Equal(expected, result); + } + + [Fact] + public void FromLegacyStatus_Unknown_ReturnsNeedsReview() + { + var result = LifecycleStatusMapper.FromLegacyStatus("MysteryStatus"); + Assert.Equal(LifecycleStatus.NeedsReview, result); + } +} diff --git a/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj b/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj new file mode 100644 index 0000000..70c68e1 --- /dev/null +++ b/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/SeaHavenIndustries.Tests/SyncFieldMergePolicyTests.cs b/SeaHavenIndustries.Tests/SyncFieldMergePolicyTests.cs new file mode 100644 index 0000000..e88f262 --- /dev/null +++ b/SeaHavenIndustries.Tests/SyncFieldMergePolicyTests.cs @@ -0,0 +1,78 @@ +using Data.SeaHavenIndustries; +using Microsoft.EntityFrameworkCore; +using SeaHaven.Services.Implementation; +using SeaHaven.Services.Interfaces; + +namespace SeaHavenIndustries.Tests; + +public class SyncFieldMergePolicyTests +{ + private static ApplicationDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + [Fact] + public async Task TryApply_WhenFieldLocked_RejectsAndAudits() + { + await using var context = CreateContext(); + var wo = new WorkOrder { Id = 10, Description = "SHOC edited", Status = "Open" }; + context.workOrders.Add(wo); + context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock + { + WorkOrderId = 10, + FieldName = "Description", + LockedAt = DateTime.UtcNow + }); + await context.SaveChangesAsync(); + + var locks = new WorkOrderFieldLockService(context); + var audit = new WorkOrderAuditService(context, locks); + var policy = new SyncFieldMergePolicy(locks, audit); + + var syncContext = new WorkOrderSyncContext + { + WorkOrder = wo, + FieldLocks = locks, + Audit = audit + }; + + var applied = await policy.TryApplyAsync(syncContext, "Description", "Sync value"); + + Assert.False(applied); + Assert.Equal("SHOC edited", wo.Description); + + var logs = await context.WorkOrderAuditLogs.ToListAsync(); + Assert.Single(logs); + Assert.Equal("SyncRejected", logs[0].Action); + Assert.Equal("Sync", logs[0].EventType); + } + + [Fact] + public async Task TryApply_WhenFieldNotLocked_AppliesValue() + { + await using var context = CreateContext(); + var wo = new WorkOrder { Id = 11, Description = "Original" }; + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + + var locks = new WorkOrderFieldLockService(context); + var audit = new WorkOrderAuditService(context, locks); + var policy = new SyncFieldMergePolicy(locks, audit); + + var syncContext = new WorkOrderSyncContext + { + WorkOrder = wo, + FieldLocks = locks, + Audit = audit + }; + + var applied = await policy.TryApplyAsync(syncContext, "Description", "From sync"); + + Assert.True(applied); + Assert.Equal("From sync", wo.Description); + } +} diff --git a/SeaHavenIndustries.Tests/WorkOrderAuditServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderAuditServiceTests.cs new file mode 100644 index 0000000..f3058c3 --- /dev/null +++ b/SeaHavenIndustries.Tests/WorkOrderAuditServiceTests.cs @@ -0,0 +1,55 @@ +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using Microsoft.EntityFrameworkCore; +using SeaHaven.Services.Implementation; +using SeaHaven.Services.Interfaces; + +namespace SeaHavenIndustries.Tests; + +public class WorkOrderAuditServiceTests +{ + private static ApplicationDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + [Fact] + public async Task LogStatusChanged_CreatesSingleAuditWithManualEventType() + { + await using var context = CreateContext(); + context.workOrders.Add(new WorkOrder { Id = 1, Status = "Open" }); + await context.SaveChangesAsync(); + + var locks = new WorkOrderFieldLockService(context); + var audit = new WorkOrderAuditService(context, locks); + + await audit.LogStatusChangedAsync(1, "Open", "In Progress", "user-1"); + + var logs = await context.WorkOrderAuditLogs.ToListAsync(); + Assert.Single(logs); + Assert.Equal("StatusChanged", logs[0].Action); + Assert.Equal("Manual", logs[0].EventType); + Assert.Equal("Dispatcher", logs[0].ActorType); + Assert.Equal("LifecycleStatus", logs[0].FieldName); + } + + [Fact] + public async Task LogStatusChanged_CreatesFieldLock() + { + await using var context = CreateContext(); + context.workOrders.Add(new WorkOrder { Id = 2, Status = "Open" }); + await context.SaveChangesAsync(); + + var locks = new WorkOrderFieldLockService(context); + var audit = new WorkOrderAuditService(context, locks); + + await audit.LogStatusChangedAsync(2, "Open", "Scheduled", "user-1"); + + var fieldLocks = await context.WorkOrderFieldLocks.ToListAsync(); + Assert.Single(fieldLocks); + Assert.Equal("LifecycleStatus", fieldLocks[0].FieldName); + } +} diff --git a/SeaHavenIndustries.Tests/WorkOrderNumberNormalizerTests.cs b/SeaHavenIndustries.Tests/WorkOrderNumberNormalizerTests.cs new file mode 100644 index 0000000..dcc4755 --- /dev/null +++ b/SeaHavenIndustries.Tests/WorkOrderNumberNormalizerTests.cs @@ -0,0 +1,43 @@ +using SeaHaven.Services.Helpers; + +namespace SeaHavenIndustries.Tests; + +public class WorkOrderNumberNormalizerTests +{ + [Fact] + public void TryNormalize_PadsToElevenDigits() + { + var ok = WorkOrderNumberNormalizer.TryNormalize("12345", out var normalized, out var error); + + Assert.True(ok); + Assert.Null(error); + Assert.Equal("00000012345", normalized); + } + + [Fact] + public void TryNormalize_AcceptsElevenDigits() + { + var ok = WorkOrderNumberNormalizer.TryNormalize("10000000001", out var normalized, out _); + + Assert.True(ok); + Assert.Equal("10000000001", normalized); + } + + [Fact] + public void TryNormalize_RejectsEmpty() + { + var ok = WorkOrderNumberNormalizer.TryNormalize("", out _, out var error); + + Assert.False(ok); + Assert.NotNull(error); + } + + [Fact] + public void TryNormalize_RejectsTooLong() + { + var ok = WorkOrderNumberNormalizer.TryNormalize("123456789012", out _, out var error); + + Assert.False(ok); + Assert.Contains("11", error); + } +} diff --git a/SeaHavenIndustries.sln b/SeaHavenIndustries.sln index 5560d76..e97473d 100644 --- a/SeaHavenIndustries.sln +++ b/SeaHavenIndustries.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.Services", "SeaHav EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHaven.DataServices", "SeaHaven.DataServices\SeaHaven.DataServices.csproj", "{60DF3943-9BC3-4444-8E7A-EEC34DDC7806}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeaHavenIndustries.Tests", "SeaHavenIndustries.Tests\SeaHavenIndustries.Tests.csproj", "{0093B3F5-0180-4C02-A088-6A564E06CEBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Debug|Any CPU.Build.0 = Debug|Any CPU {60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Release|Any CPU.ActiveCfg = Release|Any CPU {60DF3943-9BC3-4444-8E7A-EEC34DDC7806}.Release|Any CPU.Build.0 = Release|Any CPU + {0093B3F5-0180-4C02-A088-6A564E06CEBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0093B3F5-0180-4C02-A088-6A564E06CEBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0093B3F5-0180-4C02-A088-6A564E06CEBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0093B3F5-0180-4C02-A088-6A564E06CEBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE