Несколько условий шага, объединяемых через И/ИЛИ (#16)
<html> <body> <!--StartFragment--> GetEmployeeByEmailAsync возвращает массив; новый HeadType.CompanyDirectors -- <!--EndFragment--> </body> </html>[GetEmployeeByEmailAsync возвращает массив; новый HeadType.CompanyDirectors](http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/commit/572749b4d7cc11bc8e1ef4184a93f18a14b710ab) --------- Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/16
This commit was merged in pull request #16.
This commit is contained in:
+237
-29
@@ -27,7 +27,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность") });
|
||||
portalClient
|
||||
.Setup(x => x.GetProjectExecutorsAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync([new ProjectExecutorInfo(ProjectRole.ProjectManager, "pm@tnsu.kz")]);
|
||||
@@ -169,7 +169,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -182,6 +182,8 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.Value.RouteId.ShouldBe(route.Value.Id);
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("approver@tnsu.kz");
|
||||
// Исполнитель задан email'ом, а не типом руководителя — PositionCode пуст.
|
||||
resolved.Value.Steps[0].PositionCode.ShouldBeNull();
|
||||
}
|
||||
|
||||
// SLA приезжает в резолв уже с применённым наследованием — это осознанное
|
||||
@@ -204,8 +206,8 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(
|
||||
1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null,
|
||||
stepSla, stepAsk),
|
||||
1, "approver@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null,
|
||||
SlaHours: stepSla, AskOverdueReason: stepAsk),
|
||||
CancellationToken.None);
|
||||
|
||||
var resolved = await _service.ResolveAsync(
|
||||
@@ -265,7 +267,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность") });
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync(It.IsAny<string>(), "chief", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("dup@tnsu.kz");
|
||||
@@ -283,15 +285,15 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "dup@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, "other@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(3, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -304,10 +306,65 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.Value.Steps.Count.ShouldBe(2);
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("dup@tnsu.kz");
|
||||
resolved.Value.Steps[0].OrderNo.ShouldBe(1);
|
||||
// Схлопнутый шаг #1 задан по email (PositionCode пуст), но дубликат — шаг #3
|
||||
// по HeadType.Chief — принёс PositionCode. Он не должен потеряться при схлопывании.
|
||||
resolved.Value.Steps[0].PositionCode.ShouldBe(HeadType.Chief);
|
||||
resolved.Value.Steps[1].ExecutorEmail.ShouldBe("other@tnsu.kz");
|
||||
resolved.Value.Steps[1].OrderNo.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута: один email через два разных HeadType — предупреждение, PositionCode первого из них")]
|
||||
public async Task ResolveAsync_StepsResolveToSameEmailWithDifferentPositionCode_AddsWarningAndKeepsFirst()
|
||||
{
|
||||
// Arrange — оба типа руководителя резолвятся в один и тот же email.
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность") });
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync(It.IsAny<string>(), "chief", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("same@tnsu.kz");
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync(It.IsAny<string>(), "directionHead", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("same@tnsu.kz");
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
var requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("Collapse conflicting position code request", "/collapse-conflicting-position-code", null, null, "General"),
|
||||
CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectionHead, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await routeService.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("same@tnsu.kz");
|
||||
resolved.Value.Steps[0].PositionCode.ShouldBe(HeadType.Chief);
|
||||
var warning = resolved.Value.Steps[0].Warning;
|
||||
warning.ShouldNotBeNull();
|
||||
warning.ShouldContain("Chief");
|
||||
warning.ShouldContain("DirectionHead");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута с шагом «Руководитель указанного отдела» без departmentId в запросе — маршрут формируется, шаг с предупреждением")]
|
||||
public async Task ResolveAsync_DepartmentHeadStepWithoutDepartmentId_ReturnsWarningAndNullExecutor()
|
||||
{
|
||||
@@ -320,7 +377,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action — departmentId в запросе не передан
|
||||
@@ -345,7 +402,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность") });
|
||||
portalClient
|
||||
.Setup(x => x.GetDepartmentHeadEmailAsync(targetDepartmentId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("head-of-department@tnsu.kz");
|
||||
@@ -362,7 +419,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -385,7 +442,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Тестовая должность") });
|
||||
portalClient
|
||||
.Setup(x => x.GetDepartmentHeadEmailAsync(targetDepartmentId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((string?)null);
|
||||
@@ -402,7 +459,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -431,7 +488,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -458,11 +515,11 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(2, "always@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, "always@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -489,7 +546,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, "big-amount@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action — fieldValues не переданы вовсе
|
||||
@@ -514,7 +571,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "urgent@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Priority", ComparisonOperator.Equal, "Urgent"),
|
||||
new CreateTemplateRouteStepRequest(1, "urgent@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -548,7 +605,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "text-condition@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, "Priority", operation, expected),
|
||||
new CreateTemplateRouteStepRequest(1, "text-condition@tnsu.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Priority", operation, expected) }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -562,6 +619,47 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.Value.Steps.Count.ShouldBe(shouldMatch ? 1 : 0);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Резолв маршрута с несколькими условиями шага, объединёнными через И/ИЛИ")]
|
||||
// И: оба условия должны выполниться.
|
||||
[InlineData(ConditionsLogic.And, "1500", "Urgent", true)]
|
||||
[InlineData(ConditionsLogic.And, "500", "Urgent", false)]
|
||||
[InlineData(ConditionsLogic.And, "1500", "Normal", false)]
|
||||
// ИЛИ: достаточно одного.
|
||||
[InlineData(ConditionsLogic.Or, "500", "Urgent", true)]
|
||||
[InlineData(ConditionsLogic.Or, "1500", "Normal", true)]
|
||||
[InlineData(ConditionsLogic.Or, "500", "Normal", false)]
|
||||
public async Task ResolveAsync_MultipleConditions_CombinesWithConfiguredLogic(
|
||||
ConditionsLogic logic, string amount, string priority, bool shouldMatch)
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeWithListAsync($"Multi-condition request {logic} {amount} {priority}");
|
||||
var route = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, "multi-condition@tnsu.kz", null, null, null, RouteStepStatus.Approval, false,
|
||||
new[]
|
||||
{
|
||||
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent")
|
||||
},
|
||||
logic),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await _service.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval,
|
||||
[new ResolveFieldValueDto("Amount", amount), new ResolveFieldValueDto("Priority", priority)]),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.Count.ShouldBe(shouldMatch ? 1 : 0);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв маршрута с ролью в проекте подставляет исполнителя из проекта")]
|
||||
public async Task ResolveAsync_ByProjectRole_UsesProjectExecutorEmail()
|
||||
{
|
||||
@@ -574,7 +672,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -595,7 +693,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Lead Engineer"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Lead Engineer") });
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByPositionAsync("Lead", "ТОО TANSU Construction", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeLookupResult(true, "lead@tnsu.kz", "Иванов Иван Иванович"));
|
||||
@@ -612,7 +710,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, "Lead", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -634,7 +732,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Chief position"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Chief position") });
|
||||
portalClient
|
||||
.Setup(x => x.GetManagerAsync("someone@tnsu.kz", "chief", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("chief@tnsu.kz");
|
||||
@@ -651,7 +749,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -663,6 +761,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("chief@tnsu.kz");
|
||||
resolved.Value.Steps[0].PositionCode.ShouldBe(HeadType.Chief);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв шага с типом руководителя DirectorTansu подставляет захардкоженный email из настроек")]
|
||||
@@ -672,7 +771,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId, "ТОО TANSU Construction", "Director position"));
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo> { new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Director position") });
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
@@ -686,7 +785,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -701,6 +800,115 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
portalClient.Verify(x => x.GetManagerAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв шага с типом руководителя CompanyDirectors резолвится в директора каждой компании инициатора, все с одним OrderNo")]
|
||||
public async Task ResolveAsync_ByHeadTypeCompanyDirectors_ResolvesOneRowPerCompany()
|
||||
{
|
||||
// Arrange — инициатор числится в двух компаниях; для одной из них (KazPromService)
|
||||
// должность директора захардкожена как «Главный механик», а не «Директор».
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo>
|
||||
{
|
||||
new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Начальник отдела службы безопасности"),
|
||||
new(Guid.NewGuid(), DepartmentId, "ТОО KazPromService", "Начальник отдела службы безопасности")
|
||||
});
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByPositionAsync("Директор", "ТОО TANSU Construction", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeLookupResult(true, "director.tansu@tnsu.kz", "Директор TANSU"));
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByPositionAsync("Главный механик", "ТОО KazPromService", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeLookupResult(true, "mechanic.kps@tnsu.kz", "Главный механик KPS"));
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
var requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("CompanyDirectors request", "/company-directors-request", null, null, "General"), CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(5, null, null, null, HeadType.CompanyDirectors, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var resolved = await routeService.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.Count.ShouldBe(2);
|
||||
resolved.Value.Steps.ShouldAllBe(s => s.OrderNo == 5);
|
||||
resolved.Value.Steps.ShouldAllBe(s => s.PositionCode == HeadType.CompanyDirectors);
|
||||
resolved.Value.Steps.ShouldContain(s => s.ExecutorEmail == "director.tansu@tnsu.kz");
|
||||
resolved.Value.Steps.ShouldContain(s => s.ExecutorEmail == "mechanic.kps@tnsu.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв: если Директор Tansu совпадает с одним из Директоров компаний, эта строка убирается из CompanyDirectors, а не конфликтует")]
|
||||
public async Task ResolveAsync_DirectorTansuMatchesOneOfCompanyDirectors_RemovesDuplicateFromCompanyDirectors()
|
||||
{
|
||||
// Arrange — директор ТОО TANSU Construction (из шага CompanyDirectors, OrderNo 5)
|
||||
// и Директор Tansu (из шага DirectorTansu, OrderNo 10, идёт в маршруте последним) —
|
||||
// один и тот же человек с одним и тем же email.
|
||||
var portalClient = new Mock<ITnsuPortalClient>();
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<EmployeeDirectoryInfo>
|
||||
{
|
||||
new(CompanyId, DepartmentId, "ТОО TANSU Construction", "Начальник отдела службы безопасности"),
|
||||
new(Guid.NewGuid(), DepartmentId, "ТОО KazPromService", "Начальник отдела службы безопасности")
|
||||
});
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByPositionAsync("Директор", "ТОО TANSU Construction", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeLookupResult(true, "director.tansu@tnsu.kz", "Директор TANSU"));
|
||||
portalClient
|
||||
.Setup(x => x.GetEmployeeByPositionAsync("Главный механик", "ТОО KazPromService", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new EmployeeLookupResult(true, "mechanic.kps@tnsu.kz", "Главный механик KPS"));
|
||||
|
||||
var services = TestServiceProviderFactory.Build(_connectionString, portalClient: portalClient.Object);
|
||||
var routeService = services.GetRequiredService<TemplateRouteService>();
|
||||
var requestTypeService = services.GetRequiredService<RequestTypeService>();
|
||||
var stepService = services.GetRequiredService<TemplateRouteStepService>();
|
||||
|
||||
var requestTypeId = (await requestTypeService.CreateAsync(
|
||||
new CreateRequestTypeRequest("CompanyDirectors overlaps DirectorTansu request", "/company-directors-overlaps-director-tansu", null, null, "General"),
|
||||
CancellationToken.None)).Value.Id;
|
||||
var route = await routeService.CreateAsync(
|
||||
new CreateTemplateRouteRequest(requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(5, null, null, null, HeadType.CompanyDirectors, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(10, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action — тестовая конфигурация не переопределяет TnsuPortal:DirectorTansuEmail,
|
||||
// поэтому HeadType.DirectorTansu резолвится в дефолт из TnsuPortalOptions
|
||||
// ("director.tansu@tnsu.kz") — тот же email, что вернул GetEmployeeByPositionAsync
|
||||
// для ТОО TANSU Construction.
|
||||
var resolved = await routeService.ResolveAsync(
|
||||
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert — вместо трёх строк (2 CompanyDirectors + 1 DirectorTansu) остаётся
|
||||
// две: KazPromService-директор и Директор Tansu, без предупреждения о конфликте.
|
||||
resolved.IsSuccess.ShouldBeTrue();
|
||||
resolved.Value.Steps.Count.ShouldBe(2);
|
||||
var tansuStep = resolved.Value.Steps.Single(s => s.ExecutorEmail == "director.tansu@tnsu.kz");
|
||||
tansuStep.PositionCode.ShouldBe(HeadType.DirectorTansu);
|
||||
tansuStep.OrderNo.ShouldBe(10);
|
||||
tansuStep.Warning.ShouldBeNull();
|
||||
resolved.Value.Steps.ShouldContain(s => s.ExecutorEmail == "mechanic.kps@tnsu.kz" && s.PositionCode == HeadType.CompanyDirectors);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Резолв шага с типом руководителя Chief возвращает warning, если руководитель не найден")]
|
||||
public async Task ResolveAsync_ByHeadTypeChief_NotFound_ReturnsWarning()
|
||||
{
|
||||
@@ -713,7 +921,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -742,7 +950,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, "Несуществующая должность", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -771,7 +979,7 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
var stepService = _services.GetRequiredService<TemplateRouteStepService>();
|
||||
await stepService.CreateAsync(
|
||||
route.Value.Id,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, ProjectRole.Security, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
|
||||
+107
-46
@@ -43,7 +43,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
999999,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -59,7 +59,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -71,20 +71,79 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with duplicate step", byProject: false);
|
||||
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null);
|
||||
var request = new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, false, null);
|
||||
var first = await _service.CreateAsync(routeId, request, CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action
|
||||
var duplicate = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", "Engineer", null, HeadType.Chief, RouteStepStatus.Approval, true, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
duplicate.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Порядок условий и логика их объединения не влияют на определение дубликата")]
|
||||
public async Task CreateAsync_DuplicateStepWithSameConditionsInDifferentOrder_ReturnsFailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with duplicate conditions", byProject: false, listName: "Requests");
|
||||
var first = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false,
|
||||
new[]
|
||||
{
|
||||
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent")
|
||||
},
|
||||
ConditionsLogic.And),
|
||||
CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action — те же два условия, в обратном порядке, но с той же логикой И
|
||||
var duplicate = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false,
|
||||
new[]
|
||||
{
|
||||
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent"),
|
||||
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000")
|
||||
},
|
||||
ConditionsLogic.And),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
duplicate.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Те же условия, но с другой логикой объединения (И/ИЛИ), — не дубликат")]
|
||||
public async Task CreateAsync_SameConditionsWithDifferentLogic_DoesNotConflict()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with different conditions logic", byProject: false, listName: "Requests");
|
||||
var conditions = new[]
|
||||
{
|
||||
new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new RouteStepConditionDto("Priority", ComparisonOperator.Equal, "Urgent")
|
||||
};
|
||||
var first = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, conditions, ConditionsLogic.And),
|
||||
CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action — те же условия, но объединены через ИЛИ, а не И
|
||||
var second = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, conditions, ConditionsLogic.Or),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
second.IsSuccess.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Второй шаг с тем же типом руководителя в том же маршруте — конфликт, даже с другим статусом/переназначением")]
|
||||
public async Task CreateAsync_SecondStepWithSameHeadType_ReturnsFailedResult()
|
||||
{
|
||||
@@ -94,14 +153,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route with duplicate head type", byProject: false);
|
||||
var first = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action — другой статус (Execution вместо Approval), но тот же HeadType.Chief
|
||||
var second = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -115,14 +174,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route with duplicate department head", byProject: false);
|
||||
var first = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action
|
||||
var second = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Execution, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Execution, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -138,11 +197,11 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var chief = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
var directorTansu = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -160,11 +219,11 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var small = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.LessThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.LessThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
var big = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -181,11 +240,11 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var small = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.LessThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.LessThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
var big = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -200,14 +259,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route with head type same condition", byProject: false, listName: "Requests");
|
||||
var first = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
first.IsSuccess.ShouldBeTrue();
|
||||
|
||||
// Action — то же условие, другой статус
|
||||
var second = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -221,14 +280,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to update head type on itself", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action — HasDuplicateHeadTypeAsync должен исключать сам обновляемый шаг (excludeId)
|
||||
var updated = await _service.UpdateAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new UpdateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
|
||||
new UpdateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Execution, true, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -244,7 +303,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -262,7 +321,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -282,7 +341,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -300,7 +359,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -315,14 +374,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to update step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var updated = await _service.UpdateAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true, null, null, null),
|
||||
new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", null, HeadType.Chief, RouteStepStatus.Execution, true, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -339,14 +398,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: 2, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null, Conditions: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -363,14 +422,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch step into conflict", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "a@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "a@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, Conditions: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -384,14 +443,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch step status none", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.None, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.None, AllowReassignToOtherApprovers: null, Conditions: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -405,14 +464,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route with explicit None values on patch", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: ProjectRole.None, HeadType: HeadType.None, Status: null, AllowReassignToOtherApprovers: null, FieldName: null, Operation: null, Value: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: ProjectRole.None, HeadType: HeadType.None, Status: null, AllowReassignToOtherApprovers: null, Conditions: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -420,16 +479,17 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Создание шага с частично заполненными полями условия возвращает неуспешный результат")]
|
||||
[Fact(DisplayName = "Создание шага с условием без значения возвращает неуспешный результат")]
|
||||
public async Task CreateAsync_ConditionFieldsPartiallyFilled_ReturnsFailedResult()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route with partial condition", byProject: false, listName: "Requests");
|
||||
|
||||
// Action
|
||||
// Action — оператор задан, но value пуст: условие наполовину заполнено
|
||||
var result = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false,
|
||||
new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -445,7 +505,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var result = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -461,14 +521,15 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
// Action
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, "Amount", ComparisonOperator.GreaterThan, "1000"),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
created.IsSuccess.ShouldBeTrue();
|
||||
created.Value.FieldName.ShouldBe("Amount");
|
||||
created.Value.Operation.ShouldBe(ComparisonOperator.GreaterThan);
|
||||
created.Value.Value.ShouldBe("1000");
|
||||
created.Value.Conditions.ShouldHaveSingleItem();
|
||||
created.Value.Conditions[0].FieldName.ShouldBe("Amount");
|
||||
created.Value.Conditions[0].Operation.ShouldBe(ComparisonOperator.GreaterThan);
|
||||
created.Value.Conditions[0].Value.ShouldBe("1000");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Патч, после которого поля условия заполнены частично, возвращает неуспешный результат")]
|
||||
@@ -478,14 +539,14 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to patch condition into conflict", byProject: false, listName: "Requests");
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, FieldName: "Amount", Operation: null, Value: null),
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: null, Position: null, ProjectRole: null, HeadType: null, Status: null, AllowReassignToOtherApprovers: null, Conditions: new[] { new RouteStepConditionDto("Amount", ComparisonOperator.None, "") }),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
@@ -499,7 +560,7 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeId = await CreateRouteAsync("Route to delete step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
@@ -520,16 +581,16 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
var routeBId = await CreateRouteAsync("Route B for executor search", byProject: false);
|
||||
await _service.CreateAsync(
|
||||
routeAId,
|
||||
new CreateTemplateRouteStepRequest(1, "Target@Example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "Target@Example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
await _service.CreateAsync(
|
||||
routeBId,
|
||||
new CreateTemplateRouteStepRequest(1, "target@example.kz", "Manager", null, null, RouteStepStatus.Execution, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(1, "target@example.kz", "Manager", null, null, RouteStepStatus.Execution, false, null),
|
||||
CancellationToken.None);
|
||||
// Другой исполнитель — не должен попасть в выдачу.
|
||||
await _service.CreateAsync(
|
||||
routeAId,
|
||||
new CreateTemplateRouteStepRequest(2, "other@example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, null, null),
|
||||
new CreateTemplateRouteStepRequest(2, "other@example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
|
||||
Reference in New Issue
Block a user