2026-09-16 17:12:03 -03:00
|
|
|
using System.Security.Claims;
|
2026-07-24 17:35:34 -03:00
|
|
|
using Data.SeaHavenIndustries;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using SeaHaven.DataServices.Implementation;
|
2026-09-16 17:12:03 -03:00
|
|
|
using SeaHaven.Services.Exceptions;
|
2026-09-16 17:28:21 -03:00
|
|
|
using SeaHaven.Services.DTOs;
|
2026-09-16 17:12:03 -03:00
|
|
|
using SeaHaven.Services.Helpers;
|
2026-07-24 17:35:34 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 17:12:03 -03:00
|
|
|
private static DashboardService NewService(ApplicationDbContext ctx)
|
|
|
|
|
{
|
|
|
|
|
var resolver = new WorkOrderAccountResolver(
|
|
|
|
|
new AccountDataService(ctx),
|
|
|
|
|
new LocationDataService(ctx));
|
|
|
|
|
return new DashboardService(new DashboardDataService(ctx), resolver);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-16 17:28:21 -03:00
|
|
|
private static ClaimsPrincipal AccountUser(
|
|
|
|
|
int accountId,
|
|
|
|
|
string userId = "dispatcher-1",
|
|
|
|
|
string role = "Admin")
|
2026-09-16 17:12:03 -03:00
|
|
|
{
|
|
|
|
|
var claims = new[]
|
|
|
|
|
{
|
|
|
|
|
new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
|
2026-09-16 17:28:21 -03:00
|
|
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
|
|
|
|
new Claim(ClaimTypes.Role, role)
|
2026-09-16 17:12:03 -03:00
|
|
|
};
|
|
|
|
|
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
|
|
|
|
}
|
2026-07-24 17:35:34 -03:00
|
|
|
|
2026-09-16 17:12:03 -03:00
|
|
|
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"));
|
|
|
|
|
}
|
2026-07-24 17:35:34 -03:00
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetStatsAsync_CountsByStatusExcludingTemplates()
|
|
|
|
|
{
|
|
|
|
|
using var ctx = NewContext();
|
|
|
|
|
ctx.workOrders.AddRange(
|
2026-09-16 17:12:03 -03:00
|
|
|
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" }
|
2026-07-24 17:35:34 -03:00
|
|
|
);
|
|
|
|
|
ctx.SaveChanges();
|
|
|
|
|
|
2026-09-16 17:28:21 -03:00
|
|
|
var stats = await NewService(ctx).GetStatsAsync(
|
|
|
|
|
AccountUser(1), new DashboardStatsQueryDTO(), CancellationToken.None);
|
2026-07-24 17:35:34 -03:00
|
|
|
|
|
|
|
|
stats.Total.Should().Be(7);
|
|
|
|
|
stats.Open.Should().Be(4);
|
|
|
|
|
stats.NotDispatched.Should().Be(1);
|
|
|
|
|
stats.Completed.Should().Be(2);
|
|
|
|
|
}
|
2026-09-16 17:12:03 -03:00
|
|
|
|
|
|
|
|
[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();
|
|
|
|
|
|
2026-09-16 17:28:21 -03:00
|
|
|
var stats = await NewService(ctx).GetStatsAsync(
|
|
|
|
|
OrgWideUser(), new DashboardStatsQueryDTO(), CancellationToken.None);
|
2026-09-16 17:12:03 -03:00
|
|
|
|
|
|
|
|
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"));
|
|
|
|
|
|
2026-09-16 17:28:21 -03:00
|
|
|
var act = () => NewService(ctx).GetStatsAsync(
|
|
|
|
|
user, new DashboardStatsQueryDTO(), CancellationToken.None);
|
2026-09-16 17:12:03 -03:00
|
|
|
|
|
|
|
|
var exception = await act.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
|
|
|
|
|
|
|
|
|
exception.Which.Code.Should().Be("Forbidden");
|
|
|
|
|
}
|
2026-09-16 17:28:21 -03:00
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
}
|
2026-07-24 17:35:34 -03:00
|
|
|
}
|