mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
403 lines
17 KiB
C#
403 lines
17 KiB
C#
|
|
using System.Security.Claims;
|
||
|
|
using Api.SeaHavenIndustries.Controllers;
|
||
|
|
using Data.SeaHavenIndustries;
|
||
|
|
using Data.SeaHavenIndustries.Enums;
|
||
|
|
using FluentAssertions;
|
||
|
|
using Microsoft.AspNetCore.Http;
|
||
|
|
using Microsoft.AspNetCore.Mvc;
|
||
|
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||
|
|
using Microsoft.EntityFrameworkCore;
|
||
|
|
using SeaHaven.DataServices.Implementation;
|
||
|
|
using SeaHaven.DataServices.Models;
|
||
|
|
using SeaHaven.Services.DTOs;
|
||
|
|
using SeaHaven.Services.Exceptions;
|
||
|
|
using SeaHaven.Services.Helpers;
|
||
|
|
using SeaHaven.Services.Implementation;
|
||
|
|
using SeaHaven.Services.Interfaces;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace Api.SeaHavenIndustries.Tests;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// SEV response-window alerts in the Notification Center: at risk from 50% of the window (banner set and a
|
||
|
|
/// dismissable row), breached from 100% (an acknowledge row that only acknowledging removes).
|
||
|
|
/// </summary>
|
||
|
|
public class NotificationSlaTests
|
||
|
|
{
|
||
|
|
private static readonly DateTime Now = new(2026, 9, 18, 16, 0, 0, DateTimeKind.Utc);
|
||
|
|
|
||
|
|
private sealed class FixedTimeProvider : TimeProvider
|
||
|
|
{
|
||
|
|
public override DateTimeOffset GetUtcNow() => new(Now);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static ApplicationDbContext NewContext()
|
||
|
|
{
|
||
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||
|
|
.Options;
|
||
|
|
return new ApplicationDbContext(options);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static WorkOrderAccountResolver Resolver(ApplicationDbContext context)
|
||
|
|
=> new(new AccountDataService(context), new LocationDataService(context));
|
||
|
|
|
||
|
|
private static NotificationFeedService NewFeed(ApplicationDbContext context)
|
||
|
|
=> new(
|
||
|
|
new NotificationFeedDataService(context),
|
||
|
|
new VendorOperationsDataService(context, new DispatchDataService(context)),
|
||
|
|
Resolver(context),
|
||
|
|
new FixedTimeProvider());
|
||
|
|
|
||
|
|
private static SlaBreachAcknowledgementService NewAcknowledgement(ApplicationDbContext context)
|
||
|
|
=> new(
|
||
|
|
new NotificationFeedDataService(context),
|
||
|
|
new WorkOrderAuditDataService(context),
|
||
|
|
new UserDataService(context),
|
||
|
|
Resolver(context),
|
||
|
|
new FixedTimeProvider());
|
||
|
|
|
||
|
|
private static ClaimsPrincipal User(int accountId, string role, string userId = "disp-1")
|
||
|
|
=> new(new ClaimsIdentity(new[]
|
||
|
|
{
|
||
|
|
new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
|
||
|
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
||
|
|
new Claim(ClaimTypes.Role, role)
|
||
|
|
}, "test"));
|
||
|
|
|
||
|
|
private static WorkOrder Sla(
|
||
|
|
int id,
|
||
|
|
TimeSpan age,
|
||
|
|
string? severity = "1",
|
||
|
|
WorkOrderType? type = WorkOrderType.Reactive,
|
||
|
|
int accountId = 1,
|
||
|
|
string? assignTo = "disp-1",
|
||
|
|
DateTime? scheduled = null,
|
||
|
|
LifecycleStatus status = LifecycleStatus.Scheduled)
|
||
|
|
=> new()
|
||
|
|
{
|
||
|
|
Id = id,
|
||
|
|
AccountId = accountId,
|
||
|
|
AssignTo = assignTo,
|
||
|
|
InternalWONumber = $"{1000 + id}",
|
||
|
|
WorkOrderType = type,
|
||
|
|
Severity = severity,
|
||
|
|
CreatedDate = Now - age,
|
||
|
|
ScheduledDate = scheduled,
|
||
|
|
LifecycleStatus = status
|
||
|
|
};
|
||
|
|
|
||
|
|
private static TimeSpan Window(int severity) => SlaResponseWindows.Respond[severity];
|
||
|
|
|
||
|
|
private static NotificationSectionDto? SlaSection(NotificationFeedDto feed)
|
||
|
|
=> feed.Sections.SingleOrDefault(section => section.Reason == NotificationReasons.Sla);
|
||
|
|
|
||
|
|
private static IEnumerable<string> ItemIds(NotificationFeedDto feed)
|
||
|
|
=> SlaSection(feed)?.Items.Select(item => item.Id) ?? Enumerable.Empty<string>();
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void ResponseWindows_AreTheRespondDeadlinesOnTheSeverityBadge()
|
||
|
|
{
|
||
|
|
SlaResponseWindows.Respond.ToDictionary(pair => pair.Key, pair => pair.Value.TotalMinutes)
|
||
|
|
.Should().Equal(new Dictionary<int, double>
|
||
|
|
{
|
||
|
|
[1] = 120,
|
||
|
|
[2] = 240,
|
||
|
|
[3] = 480,
|
||
|
|
[4] = 1440,
|
||
|
|
[5] = 4320
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
[Theory]
|
||
|
|
[InlineData(1)]
|
||
|
|
[InlineData(2)]
|
||
|
|
[InlineData(3)]
|
||
|
|
[InlineData(4)]
|
||
|
|
[InlineData(5)]
|
||
|
|
public async Task Thresholds_AtRiskFromHalfTheWindow_BreachedFromTheWholeWindow(int severity)
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
var second = TimeSpan.FromSeconds(1);
|
||
|
|
var window = Window(severity);
|
||
|
|
var level = severity.ToString();
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, window / 2 - second, level),
|
||
|
|
Sla(2, window / 2, level),
|
||
|
|
Sla(3, window / 2 + second, level),
|
||
|
|
Sla(4, window - second, level),
|
||
|
|
Sla(5, window, level),
|
||
|
|
Sla(6, window + second, level));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(User(1, "Dispatcher"), CancellationToken.None);
|
||
|
|
|
||
|
|
feed.SlaAtRisk.Select(workOrder => workOrder.Id).Should().BeEquivalentTo(new[] { 2, 3, 4 });
|
||
|
|
ItemIds(feed).Should().BeEquivalentTo(
|
||
|
|
"sla-at-risk-2", "sla-at-risk-3", "sla-at-risk-4", "sla-breach-5", "sla-breach-6");
|
||
|
|
|
||
|
|
var section = SlaSection(feed)!;
|
||
|
|
section.Label.Should().Be("SLA at Risk");
|
||
|
|
section.Count.Should().Be(5);
|
||
|
|
section.Severity.Should().Be(NotificationSeverities.Critical);
|
||
|
|
section.Items.Where(item => item.Id.StartsWith("sla-breach-")).Should().OnlyContain(item =>
|
||
|
|
item.RowType == NotificationRowTypes.Acknowledge
|
||
|
|
&& item.Severity == NotificationSeverities.Critical
|
||
|
|
&& item.Title.EndsWith("missed its response deadline"));
|
||
|
|
section.Items.Where(item => item.Id.StartsWith("sla-at-risk-")).Should().OnlyContain(item =>
|
||
|
|
item.RowType == NotificationRowTypes.Dismissable
|
||
|
|
&& item.Severity == NotificationSeverities.High
|
||
|
|
&& item.Title.EndsWith("is at risk of missing its response deadline"));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task AtRisk_CarriesTheServerComputedClock()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.workOrders.Add(Sla(7, TimeSpan.FromMinutes(90), "1"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(User(1, "Dispatcher"), CancellationToken.None);
|
||
|
|
|
||
|
|
var atRisk = feed.SlaAtRisk.Should().ContainSingle().Subject;
|
||
|
|
atRisk.Number.Should().Be("1007");
|
||
|
|
atRisk.Severity.Should().Be(1);
|
||
|
|
atRisk.StartedAt.Should().Be(Now.AddMinutes(-90));
|
||
|
|
atRisk.DeadlineAt.Should().Be(Now.AddMinutes(30));
|
||
|
|
atRisk.DeadlineAt.Kind.Should().Be(DateTimeKind.Utc);
|
||
|
|
atRisk.PercentElapsed.Should().Be(75);
|
||
|
|
SlaSection(feed)!.Items.Single().Title.Should().Be("WO #1007 is at risk of missing its response deadline");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task OnlyOpenReactiveOrEmergencyWorkOrdersWithASeverityLevelAreTracked()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
var late = Window(1) * 2;
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, late, "1", WorkOrderType.Reactive),
|
||
|
|
Sla(2, late, "2", WorkOrderType.Emergency),
|
||
|
|
Sla(3, late, "1", WorkOrderType.PM),
|
||
|
|
Sla(4, late, "1", WorkOrderType.Inspection),
|
||
|
|
Sla(5, late, "1", type: null),
|
||
|
|
Sla(6, late, severity: null),
|
||
|
|
Sla(7, late, severity: "9"),
|
||
|
|
Sla(8, late, "1", status: LifecycleStatus.Completed),
|
||
|
|
Sla(9, late, "1", status: LifecycleStatus.Canceled));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(User(1, "Dispatcher"), CancellationToken.None);
|
||
|
|
|
||
|
|
ItemIds(feed).Should().BeEquivalentTo("sla-breach-1", "sla-breach-2");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task TheClockStartsAtCreation_WhateverTheScheduledDate()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, Window(3) + TimeSpan.FromMinutes(1), "3", scheduled: null),
|
||
|
|
// Scheduled next week, but logged three days ago: the deadline is long gone.
|
||
|
|
Sla(2, TimeSpan.FromDays(3), "4", scheduled: Now.AddDays(7)),
|
||
|
|
// Scheduled days ago, but logged ten minutes ago: nothing is due yet.
|
||
|
|
Sla(3, TimeSpan.FromMinutes(10), "4", scheduled: Now.AddDays(-5)));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(User(1, "Dispatcher"), CancellationToken.None);
|
||
|
|
|
||
|
|
ItemIds(feed).Should().BeEquivalentTo("sla-breach-1", "sla-breach-2");
|
||
|
|
feed.SlaAtRisk.Should().BeEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Dispatcher_OnlySeesTheirOwnWorkOrders_AndTheAccountStaysTheBoundary()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
var atRisk = Window(2) * 3 / 4;
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, atRisk, "2", assignTo: "disp-1"),
|
||
|
|
Sla(2, atRisk, "2", assignTo: "disp-2"),
|
||
|
|
Sla(3, atRisk, "2", assignTo: null),
|
||
|
|
Sla(4, atRisk, "2", assignTo: "disp-1", accountId: 2));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var dispatcher = await NewFeed(context).GetFeedAsync(User(1, "Dispatcher", "disp-1"), CancellationToken.None);
|
||
|
|
var admin = await NewFeed(context).GetFeedAsync(User(1, "Admin", "admin-1"), CancellationToken.None);
|
||
|
|
var otherAccount = await NewFeed(context).GetFeedAsync(User(2, "Admin", "admin-2"), CancellationToken.None);
|
||
|
|
|
||
|
|
dispatcher.SlaAtRisk.Select(workOrder => workOrder.Id).Should().Equal(1);
|
||
|
|
ItemIds(dispatcher).Should().Equal("sla-at-risk-1");
|
||
|
|
admin.SlaAtRisk.Select(workOrder => workOrder.Id).Should().BeEquivalentTo(new[] { 1, 2, 3 });
|
||
|
|
otherAccount.SlaAtRisk.Select(workOrder => workOrder.Id).Should().Equal(4);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Breaches_AreCappedSeparately_SoAtRiskRowsAlwaysShow()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
for (var id = 1; id <= NotificationFeedService.SectionItemLimit + 10; id++)
|
||
|
|
context.workOrders.Add(Sla(id, Window(1) + TimeSpan.FromMinutes(id), "1"));
|
||
|
|
context.workOrders.Add(Sla(500, Window(5) * 3 / 4, "5"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(User(1, "Dispatcher"), CancellationToken.None);
|
||
|
|
|
||
|
|
var section = SlaSection(feed)!;
|
||
|
|
section.Count.Should().Be(NotificationFeedService.SectionItemLimit + 11);
|
||
|
|
section.Items.Count(item => item.RowType == NotificationRowTypes.Acknowledge)
|
||
|
|
.Should().Be(NotificationFeedService.SectionItemLimit);
|
||
|
|
section.Items.Should().Contain(item => item.Id == "sla-at-risk-500");
|
||
|
|
// The newest breaches are kept.
|
||
|
|
section.Items.Should().Contain(item => item.Id == "sla-breach-1");
|
||
|
|
section.Items.Should().NotContain(item => item.Id == $"sla-breach-{NotificationFeedService.SectionItemLimit + 10}");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Acknowledge_RecordsWhoAndWhenInTheAuditHistory_AndRemovesTheRow()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.Users.Add(new ApplicationUser { Id = "disp-1", UserName = "jane", FirstName = "Jane", LastName = "Doe" });
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, Window(1) + TimeSpan.FromMinutes(5), "1"),
|
||
|
|
Sla(2, Window(1) + TimeSpan.FromMinutes(9), "1"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
var user = User(1, "Dispatcher", "disp-1");
|
||
|
|
|
||
|
|
var outcome = await NewAcknowledgement(context).AcknowledgeAsync(user, 1, CancellationToken.None);
|
||
|
|
|
||
|
|
outcome.Should().Be(SlaBreachAcknowledgementOutcome.Acknowledged);
|
||
|
|
var audit = await context.WorkOrderAuditLogs.SingleAsync();
|
||
|
|
audit.WorkOrderId.Should().Be(1);
|
||
|
|
audit.UserId.Should().Be("disp-1");
|
||
|
|
audit.CreatedAt.Should().Be(Now);
|
||
|
|
audit.Action.Should().Be("SLA breach acknowledged by Jane Doe");
|
||
|
|
audit.FieldName.Should().Be(SlaBreachAcknowledgementAudit.FieldName);
|
||
|
|
audit.NewValue.Should().Be("1");
|
||
|
|
audit.EventType.Should().Be("system");
|
||
|
|
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(user, CancellationToken.None);
|
||
|
|
ItemIds(feed).Should().Equal("sla-breach-2");
|
||
|
|
SlaSection(feed)!.Count.Should().Be(1);
|
||
|
|
|
||
|
|
var again = await NewAcknowledgement(context).AcknowledgeAsync(user, 1, CancellationToken.None);
|
||
|
|
again.Should().Be(SlaBreachAcknowledgementOutcome.AlreadyAcknowledged);
|
||
|
|
(await context.WorkOrderAuditLogs.CountAsync()).Should().Be(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Acknowledgement_CoversOnlyTheSeverityThatWasBreached()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
var workOrder = Sla(1, TimeSpan.FromHours(30), "2");
|
||
|
|
context.workOrders.Add(workOrder);
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
var user = User(1, "Dispatcher", "disp-1");
|
||
|
|
await NewAcknowledgement(context).AcknowledgeAsync(user, 1, CancellationToken.None);
|
||
|
|
|
||
|
|
workOrder.Severity = "4";
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
var feed = await NewFeed(context).GetFeedAsync(user, CancellationToken.None);
|
||
|
|
|
||
|
|
ItemIds(feed).Should().Equal("sla-breach-1");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Acknowledge_IsRefusedOutsideTheCallersFeed()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
var late = Window(1) * 2;
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, late, "1", accountId: 2, assignTo: "disp-1"),
|
||
|
|
Sla(2, late, "1", assignTo: "disp-2"),
|
||
|
|
Sla(3, late, "1", status: LifecycleStatus.Completed),
|
||
|
|
Sla(4, late, "1", WorkOrderType.PM));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
var service = NewAcknowledgement(context);
|
||
|
|
var dispatcher = User(1, "Dispatcher", "disp-1");
|
||
|
|
|
||
|
|
(await service.AcknowledgeAsync(dispatcher, 1, CancellationToken.None))
|
||
|
|
.Should().Be(SlaBreachAcknowledgementOutcome.NotFound);
|
||
|
|
(await service.AcknowledgeAsync(User(1, "Admin", "admin-1"), 1, CancellationToken.None))
|
||
|
|
.Should().Be(SlaBreachAcknowledgementOutcome.NotFound);
|
||
|
|
(await service.AcknowledgeAsync(dispatcher, 2, CancellationToken.None))
|
||
|
|
.Should().Be(SlaBreachAcknowledgementOutcome.NotFound);
|
||
|
|
(await service.AcknowledgeAsync(dispatcher, 3, CancellationToken.None))
|
||
|
|
.Should().Be(SlaBreachAcknowledgementOutcome.NotFound);
|
||
|
|
(await service.AcknowledgeAsync(dispatcher, 4, CancellationToken.None))
|
||
|
|
.Should().Be(SlaBreachAcknowledgementOutcome.NotFound);
|
||
|
|
(await context.WorkOrderAuditLogs.CountAsync()).Should().Be(0);
|
||
|
|
|
||
|
|
var otherAccountAdmin = User(2, "Admin", "admin-2");
|
||
|
|
(await service.AcknowledgeAsync(otherAccountAdmin, 1, CancellationToken.None))
|
||
|
|
.Should().Be(SlaBreachAcknowledgementOutcome.Acknowledged);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Acknowledge_BeforeTheDeadlineIsRefused()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.workOrders.Add(Sla(1, Window(1) * 3 / 4, "1"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var outcome = await NewAcknowledgement(context)
|
||
|
|
.AcknowledgeAsync(User(1, "Dispatcher", "disp-1"), 1, CancellationToken.None);
|
||
|
|
|
||
|
|
outcome.Should().Be(SlaBreachAcknowledgementOutcome.NotBreached);
|
||
|
|
(await context.WorkOrderAuditLogs.CountAsync()).Should().Be(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Acknowledge_WithoutANotificationRole_FailsClosed()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.workOrders.Add(Sla(1, Window(1) * 2, "1"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
|
||
|
|
var act = () => NewAcknowledgement(context).AcknowledgeAsync(User(1, "Vendor"), 1, CancellationToken.None);
|
||
|
|
|
||
|
|
(await act.Should().ThrowAsync<WorkOrderBoardValidationException>()).Which.Code.Should().Be("Forbidden");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Theory]
|
||
|
|
[InlineData(1, "Dispatcher", 1, StatusCodes.Status204NoContent)]
|
||
|
|
[InlineData(2, "Admin", 1, StatusCodes.Status404NotFound)]
|
||
|
|
[InlineData(1, "Dispatcher", 2, StatusCodes.Status409Conflict)]
|
||
|
|
[InlineData(1, "Vendor", 1, StatusCodes.Status403Forbidden)]
|
||
|
|
public async Task Controller_MapsAcknowledgeOutcomes(int accountId, string role, int workOrderId, int expectedStatus)
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.workOrders.AddRange(
|
||
|
|
Sla(1, Window(1) * 2, "1"),
|
||
|
|
Sla(2, Window(1) * 3 / 4, "1"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
var controller = new NotificationsController(NewFeed(context), NewAcknowledgement(context))
|
||
|
|
{
|
||
|
|
ControllerContext = new ControllerContext
|
||
|
|
{
|
||
|
|
HttpContext = new DefaultHttpContext { User = User(accountId, role, "disp-1") }
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
var result = await controller.AcknowledgeSlaBreach(workOrderId, CancellationToken.None);
|
||
|
|
|
||
|
|
result.Should().BeAssignableTo<IStatusCodeActionResult>()
|
||
|
|
.Which.StatusCode.Should().Be(expectedStatus);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Acknowledge_ForwardsCancellation()
|
||
|
|
{
|
||
|
|
using var context = NewContext();
|
||
|
|
context.workOrders.Add(Sla(1, Window(1) * 2, "1"));
|
||
|
|
await context.SaveChangesAsync();
|
||
|
|
using var cancellation = new CancellationTokenSource();
|
||
|
|
cancellation.Cancel();
|
||
|
|
|
||
|
|
var act = () => NewAcknowledgement(context)
|
||
|
|
.AcknowledgeAsync(User(1, "Dispatcher", "disp-1"), 1, cancellation.Token);
|
||
|
|
|
||
|
|
await act.Should().ThrowAsync<OperationCanceledException>();
|
||
|
|
(await context.WorkOrderAuditLogs.CountAsync()).Should().Be(0);
|
||
|
|
}
|
||
|
|
}
|