mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-04 13:42:14 +00:00
fix(dashboard): scope stats to authenticated accounts (SH-336)
This commit is contained in:
parent
4b772c8db3
commit
efd13b6f60
6 changed files with 99 additions and 20 deletions
|
|
@ -1,7 +1,10 @@
|
||||||
|
using System.Security.Claims;
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Implementation;
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using SeaHaven.Services.Exceptions;
|
||||||
|
using SeaHaven.Services.Helpers;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -17,33 +20,90 @@ public class DashboardServiceTests
|
||||||
return new ApplicationDbContext(options);
|
return new ApplicationDbContext(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DashboardService NewService(ApplicationDbContext ctx) =>
|
private static DashboardService NewService(ApplicationDbContext ctx)
|
||||||
new(new DashboardDataService(ctx));
|
{
|
||||||
|
var resolver = new WorkOrderAccountResolver(
|
||||||
|
new AccountDataService(ctx),
|
||||||
|
new LocationDataService(ctx));
|
||||||
|
return new DashboardService(new DashboardDataService(ctx), resolver);
|
||||||
|
}
|
||||||
|
|
||||||
private static WorkOrder Wo(string? status, bool? isTemplate = false, string? assignTo = null) =>
|
private static ClaimsPrincipal AccountUser(int accountId)
|
||||||
new() { Status = status, istemplate = isTemplate, AssignTo = assignTo };
|
{
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
|
||||||
|
new Claim(ClaimTypes.Role, "Dispatcher")
|
||||||
|
};
|
||||||
|
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]
|
[Fact]
|
||||||
public async Task GetStatsAsync_CountsByStatusExcludingTemplates()
|
public async Task GetStatsAsync_CountsByStatusExcludingTemplates()
|
||||||
{
|
{
|
||||||
using var ctx = NewContext();
|
using var ctx = NewContext();
|
||||||
ctx.workOrders.AddRange(
|
ctx.workOrders.AddRange(
|
||||||
Wo("Open", isTemplate: false),
|
new WorkOrder { AccountId = 1, Status = "Open", istemplate = false },
|
||||||
Wo("Open", isTemplate: false, assignTo: "u1"),
|
new WorkOrder { AccountId = 1, Status = "Open", istemplate = false, AssignTo = "u1" },
|
||||||
Wo("In Progress"),
|
new WorkOrder { AccountId = 1, Status = "In Progress" },
|
||||||
Wo("On Hold"),
|
new WorkOrder { AccountId = 1, Status = "On Hold" },
|
||||||
Wo("Done"),
|
new WorkOrder { AccountId = 1, Status = "Done" },
|
||||||
Wo("Done"),
|
new WorkOrder { AccountId = 1, Status = "Done" },
|
||||||
Wo("Open", isTemplate: true),
|
new WorkOrder { AccountId = 1, Status = "Open", istemplate = true },
|
||||||
Wo("Cancelled", isTemplate: false)
|
new WorkOrder { AccountId = 1, Status = "Cancelled" },
|
||||||
|
new WorkOrder { AccountId = 2, Status = "Open" },
|
||||||
|
new WorkOrder { Status = "Open" }
|
||||||
);
|
);
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
|
|
||||||
var stats = await NewService(ctx).GetStatsAsync(CancellationToken.None);
|
var stats = await NewService(ctx).GetStatsAsync(AccountUser(1), CancellationToken.None);
|
||||||
|
|
||||||
stats.Total.Should().Be(7);
|
stats.Total.Should().Be(7);
|
||||||
stats.Open.Should().Be(4);
|
stats.Open.Should().Be(4);
|
||||||
stats.NotDispatched.Should().Be(1);
|
stats.NotDispatched.Should().Be(1);
|
||||||
stats.Completed.Should().Be(2);
|
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(), 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, CancellationToken.None);
|
||||||
|
|
||||||
|
var exception = await act.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||||
|
|
||||||
|
exception.Which.Code.Should().Be("Forbidden");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
[HttpGet("Stats")]
|
[HttpGet("Stats")]
|
||||||
public async Task<IActionResult> GetStats(CancellationToken cancellationToken)
|
public async Task<IActionResult> GetStats(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var stats = await _dashboardService.GetStatsAsync(cancellationToken);
|
var stats = await _dashboardService.GetStatsAsync(User, cancellationToken);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,15 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DashboardCounts> GetWorkOrderCountsAsync(CancellationToken cancellationToken)
|
public async Task<DashboardCounts> GetWorkOrderCountsAsync(
|
||||||
|
int? accountId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var query = _context.workOrders.Where(w => w.istemplate != true);
|
var query = _context.workOrders.Where(w => w.istemplate != true);
|
||||||
|
if (accountId is int scopedAccountId)
|
||||||
|
{
|
||||||
|
query = query.Where(w => w.AccountId == scopedAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
var open = await query.CountAsync(w => w.Status == "Open" || w.Status == "In Progress" || w.Status == "On Hold", cancellationToken);
|
var open = await query.CountAsync(w => w.Status == "Open" || w.Status == "In Progress" || w.Status == "On Hold", cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
public interface IDashboardDataService
|
public interface IDashboardDataService
|
||||||
{
|
{
|
||||||
Task<DashboardCounts> GetWorkOrderCountsAsync(CancellationToken cancellationToken);
|
Task<DashboardCounts> GetWorkOrderCountsAsync(
|
||||||
|
int? accountId,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DashboardCounts
|
public class DashboardCounts
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Security.Claims;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
@ -7,15 +8,22 @@ namespace SeaHaven.Services.Implementation
|
||||||
public class DashboardService : IDashboardService
|
public class DashboardService : IDashboardService
|
||||||
{
|
{
|
||||||
private readonly IDashboardDataService _dataService;
|
private readonly IDashboardDataService _dataService;
|
||||||
|
private readonly IWorkOrderAccountResolver _accountResolver;
|
||||||
|
|
||||||
public DashboardService(IDashboardDataService dataService)
|
public DashboardService(
|
||||||
|
IDashboardDataService dataService,
|
||||||
|
IWorkOrderAccountResolver accountResolver)
|
||||||
{
|
{
|
||||||
_dataService = dataService;
|
_dataService = dataService;
|
||||||
|
_accountResolver = accountResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DashboardStatsDTO> GetStatsAsync(CancellationToken cancellationToken)
|
public async Task<DashboardStatsDTO> GetStatsAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var counts = await _dataService.GetWorkOrderCountsAsync(cancellationToken);
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
||||||
|
var counts = await _dataService.GetWorkOrderCountsAsync(accountId, cancellationToken);
|
||||||
|
|
||||||
return new DashboardStatsDTO
|
return new DashboardStatsDTO
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
using System.Security.Claims;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
|
|
||||||
namespace SeaHaven.Services.Interfaces
|
namespace SeaHaven.Services.Interfaces
|
||||||
{
|
{
|
||||||
public interface IDashboardService
|
public interface IDashboardService
|
||||||
{
|
{
|
||||||
Task<DashboardStatsDTO> GetStatsAsync(CancellationToken cancellationToken);
|
Task<DashboardStatsDTO> GetStatsAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue