mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
241 lines
9.3 KiB
C#
241 lines
9.3 KiB
C#
using System.Security.Claims;
|
|
using Data.SeaHavenIndustries;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Implementation;
|
|
using SeaHaven.Services.Exceptions;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Implementation;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public class DashboardServiceTests
|
|
{
|
|
private static ApplicationDbContext NewContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
|
|
private static DashboardService NewService(ApplicationDbContext ctx)
|
|
{
|
|
var resolver = new WorkOrderAccountResolver(
|
|
new AccountDataService(ctx),
|
|
new LocationDataService(ctx));
|
|
return new DashboardService(new DashboardDataService(ctx), resolver);
|
|
}
|
|
|
|
private static ClaimsPrincipal AccountUser(
|
|
int accountId,
|
|
string userId = "dispatcher-1",
|
|
string role = "Admin")
|
|
{
|
|
var claims = new[]
|
|
{
|
|
new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
|
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
|
new Claim(ClaimTypes.Role, role)
|
|
};
|
|
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
|
}
|
|
|
|
private static ClaimsPrincipal OrgWideUser()
|
|
{
|
|
var claims = new[]
|
|
{
|
|
new Claim(SeaHavenClaimTypes.OrgScope, SeaHavenClaimTypes.OrgScopeAll),
|
|
new Claim(ClaimTypes.Role, "Admin")
|
|
};
|
|
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_CountsByStatusExcludingTemplates()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.workOrders.AddRange(
|
|
new WorkOrder { AccountId = 1, Status = "Open", istemplate = false },
|
|
new WorkOrder { AccountId = 1, Status = "Open", istemplate = false, AssignTo = "u1" },
|
|
new WorkOrder { AccountId = 1, Status = "In Progress" },
|
|
new WorkOrder { AccountId = 1, Status = "On Hold" },
|
|
new WorkOrder { AccountId = 1, Status = "Done" },
|
|
new WorkOrder { AccountId = 1, Status = "Done" },
|
|
new WorkOrder { AccountId = 1, Status = "Open", istemplate = true },
|
|
new WorkOrder { AccountId = 1, Status = "Cancelled" },
|
|
new WorkOrder { AccountId = 2, Status = "Open" },
|
|
new WorkOrder { Status = "Open" }
|
|
);
|
|
ctx.SaveChanges();
|
|
|
|
var stats = await NewService(ctx).GetStatsAsync(
|
|
AccountUser(1), new DashboardStatsQueryDTO(), CancellationToken.None);
|
|
|
|
stats.Total.Should().Be(7);
|
|
stats.Open.Should().Be(4);
|
|
stats.NotDispatched.Should().Be(1);
|
|
stats.Completed.Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_OrgWideUserSeesAllNonTemplateOrders()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.workOrders.AddRange(
|
|
new WorkOrder { AccountId = 1, Status = "Open" },
|
|
new WorkOrder { AccountId = 2, Status = "Done" },
|
|
new WorkOrder { Status = "Open", istemplate = true });
|
|
ctx.SaveChanges();
|
|
|
|
var stats = await NewService(ctx).GetStatsAsync(
|
|
OrgWideUser(), new DashboardStatsQueryDTO(), CancellationToken.None);
|
|
|
|
stats.Total.Should().Be(2);
|
|
stats.Open.Should().Be(1);
|
|
stats.Completed.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_MissingScopeFailsClosed()
|
|
{
|
|
using var ctx = NewContext();
|
|
var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
|
{
|
|
new Claim(ClaimTypes.Role, "Dispatcher")
|
|
}, "test"));
|
|
|
|
var act = () => NewService(ctx).GetStatsAsync(
|
|
user, new DashboardStatsQueryDTO(), CancellationToken.None);
|
|
|
|
var exception = await act.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
|
|
|
exception.Which.Code.Should().Be("Forbidden");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_UsesDashboardMetricRulesForDateRange()
|
|
{
|
|
using var ctx = NewContext();
|
|
var today = DashboardBusinessTime.Today();
|
|
ctx.workOrders.AddRange(
|
|
new WorkOrder
|
|
{
|
|
AccountId = 1,
|
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.PM,
|
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Scheduled,
|
|
ScheduledDate = today.ToDateTime(TimeOnly.MinValue),
|
|
OriginalDate = today.AddDays(-6)
|
|
},
|
|
new WorkOrder
|
|
{
|
|
AccountId = 1,
|
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.Emergency,
|
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.InProgress,
|
|
ScheduledDate = today.AddDays(-3).ToDateTime(TimeOnly.MinValue)
|
|
},
|
|
new WorkOrder
|
|
{
|
|
AccountId = 1,
|
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.Reactive,
|
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed,
|
|
ScheduledDate = today.ToDateTime(TimeOnly.MinValue),
|
|
OriginalDate = today.AddDays(-15),
|
|
CompletedDate = today.AddDays(-12).ToDateTime(TimeOnly.MinValue)
|
|
},
|
|
new WorkOrder
|
|
{
|
|
AccountId = 1,
|
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.PM,
|
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Incomplete,
|
|
ScheduledDate = today.AddDays(4).ToDateTime(TimeOnly.MinValue)
|
|
});
|
|
ctx.SaveChanges();
|
|
|
|
var query = new DashboardStatsQueryDTO
|
|
{
|
|
DateFrom = today.AddDays(-10),
|
|
DateTo = today.AddDays(10)
|
|
};
|
|
var stats = await NewService(ctx).GetStatsAsync(
|
|
AccountUser(1), query, CancellationToken.None);
|
|
|
|
stats.Total.Should().Be(4);
|
|
stats.Breakdown.Overdue.Should().Be(1);
|
|
stats.Breakdown.Other.Should().Be(0);
|
|
stats.Breakdown.PM.Should().Be(2);
|
|
stats.Breakdown.Emergency.Should().Be(0);
|
|
stats.Breakdown.Reactive.Should().Be(1);
|
|
(stats.Breakdown.PM + stats.Breakdown.Emergency + stats.Breakdown.Reactive
|
|
+ stats.Breakdown.Overdue + stats.Breakdown.Other).Should().Be(stats.Total);
|
|
stats.DueCount.Should().Be(3);
|
|
stats.CompletedDueCount.Should().Be(1);
|
|
stats.CompletionRate.Should().Be(33.33m);
|
|
stats.AverageResolutionDays.Should().Be(3m);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_DispatcherOnlySeesAssignedOrders()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.workOrders.AddRange(
|
|
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-1" },
|
|
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" });
|
|
ctx.SaveChanges();
|
|
|
|
var stats = await NewService(ctx).GetStatsAsync(
|
|
AccountUser(1, role: "Dispatcher"), new DashboardStatsQueryDTO(), CancellationToken.None);
|
|
|
|
stats.Total.Should().Be(1);
|
|
stats.Open.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetWorkloadAsync_UsesRoleScopeAndExcludesTerminalOrdersFromOpenCount()
|
|
{
|
|
using var ctx = NewContext();
|
|
ctx.Roles.Add(new IdentityRole
|
|
{
|
|
Id = "dispatcher-role",
|
|
Name = "Dispatcher",
|
|
NormalizedName = "DISPATCHER"
|
|
});
|
|
ctx.Users.AddRange(
|
|
new ApplicationUser { Id = "dispatcher-1", FirstName = "Ada", LastName = "One" },
|
|
new ApplicationUser { Id = "dispatcher-2", FirstName = "Ben", LastName = "Two" });
|
|
ctx.UserRoles.AddRange(
|
|
new IdentityUserRole<string> { UserId = "dispatcher-1", RoleId = "dispatcher-role" },
|
|
new IdentityUserRole<string> { UserId = "dispatcher-2", RoleId = "dispatcher-role" });
|
|
ctx.workOrders.AddRange(
|
|
new WorkOrder { AccountId = 1, AssignTo = "dispatcher-1", Status = "Open" },
|
|
new WorkOrder { AccountId = 1, AssignTo = "dispatcher-1", LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed },
|
|
new WorkOrder { AccountId = 1, AssignTo = "dispatcher-2", Status = "Open" });
|
|
ctx.SaveChanges();
|
|
|
|
var result = await NewService(ctx).GetWorkloadAsync(
|
|
AccountUser(1, "dispatcher-1", "Dispatcher"),
|
|
new DashboardStatsQueryDTO(),
|
|
1,
|
|
CancellationToken.None);
|
|
|
|
result.TotalDispatchers.Should().Be(1);
|
|
result.Items.Should().ContainSingle();
|
|
result.Items[0].DispatcherName.Should().Be("Ada One");
|
|
result.Items[0].TotalCount.Should().Be(2);
|
|
result.Items[0].OpenCount.Should().Be(1);
|
|
result.PageSize.Should().Be(10);
|
|
|
|
var adminResult = await NewService(ctx).GetWorkloadAsync(
|
|
AccountUser(1),
|
|
new DashboardStatsQueryDTO(),
|
|
1,
|
|
CancellationToken.None);
|
|
|
|
adminResult.TotalDispatchers.Should().Be(2);
|
|
adminResult.Items.Select(item => item.DispatcherName)
|
|
.Should().ContainInOrder("Ada One", "Ben Two");
|
|
}
|
|
}
|