Files
tnsu-approval-route/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs
T
gorbenko.pavel 95d460c80a
Tests / frontend-test (push) Successful in 46s
Tests / backend-test (push) Successful in 2m22s
Расширение reminders (#19)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/19
2026-08-27 11:43:45 +00:00

622 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Tnsu.ApprovalRoute.Application.RequestTypes;
using Tnsu.ApprovalRoute.Application.TemplateRoutes;
using Tnsu.ApprovalRoute.Application.TemplateRouteSteps;
using Tnsu.ApprovalRoute.Domain.Enums;
using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDatabaseFixture>
{
private static readonly Guid CompanyId = Guid.Parse("00000000-0000-0000-0000-000000000002");
private readonly TemplateRouteStepService _service;
private readonly TemplateRouteService _routeService;
private readonly RequestTypeService _requestTypeService;
public TemplateRouteStepServiceTests(PostgresDatabaseFixture fixture)
{
var services = TestServiceProviderFactory.Build(fixture.ConnectionString);
_service = services.GetRequiredService<TemplateRouteStepService>();
_routeService = services.GetRequiredService<TemplateRouteService>();
_requestTypeService = services.GetRequiredService<RequestTypeService>();
}
private async Task<int> CreateRouteAsync(string requestTypeName, bool byProject, string? listName = null)
{
var siteUrl = listName is null ? null : "https://example.sharepoint.com/sites/test";
var requestTypeId = (await _requestTypeService.CreateAsync(
new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}", listName, siteUrl, "General"), CancellationToken.None)).Value.Id;
var route = await _routeService.CreateAsync(
new CreateTemplateRouteRequest(requestTypeId, null, CompanyId, byProject, RequiresControl: false),
CancellationToken.None);
return route.Value.Id;
}
[Fact(DisplayName = "Создание шага для несуществующего маршрута возвращает неуспешный результат")]
public async Task CreateAsync_UnknownRoute_ReturnsFailedResult()
{
// Action
var result = await _service.CreateAsync(
999999,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Роль в проекте недопустима для шага маршрута без признака «по проекту»")]
public async Task CreateAsync_ProjectRoleOnNonProjectRoute_ReturnsFailedResult()
{
// Arrange
var routeId = await CreateRouteAsync("Non project route", byProject: false);
// Action
var result = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Повторное создание шага с теми же исполнителем, должностью, ролью, типом руководителя и статусом возвращает конфликт")]
public async Task CreateAsync_DuplicateStep_ReturnsFailedResult()
{
// 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, ConditionsLogic.And);
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, ConditionsLogic.And),
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()
{
// Arrange — тип руководителя резолвится в ОДНОГО конкретного человека
// (см. HasDuplicateHeadTypeAsync), поэтому два шага с одним HeadType в
// одном маршруте не должны допускаться, даже если остальные поля разные.
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, ConditionsLogic.And),
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, ConditionsLogic.And),
CancellationToken.None);
// Assert
second.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Два шага «Руководитель указанного отдела» в одном маршруте — конфликт")]
public async Task CreateAsync_TwoDepartmentHeadSteps_ReturnsFailedResult()
{
// Arrange
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, ConditionsLogic.And),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action
var second = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Execution, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
second.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Разные типы руководителя в одном маршруте — допустимы (конфликт только при совпадении HeadType)")]
public async Task CreateAsync_DifferentHeadTypes_BothSucceed()
{
// Arrange
var routeId = await CreateRouteAsync("Route with different head types", byProject: false);
// Action
var chief = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
var directorTansu = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
chief.IsSuccess.ShouldBeTrue();
directorTansu.IsSuccess.ShouldBeTrue();
}
[Fact(DisplayName = "Два шага с одним типом руководителя, но разными условиями появления — допустимы")]
public async Task CreateAsync_SameHeadTypeDifferentConditions_BothSucceed()
{
// Arrange — условия появления взаимоисключающи по данным заявки, поэтому
// в конкретный резолв попадёт только один из двух шагов.
var routeId = await CreateRouteAsync("Route with head type per condition", byProject: false, listName: "Requests");
// Action
var small = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.LessThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
var big = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
// Assert
small.IsSuccess.ShouldBeTrue();
big.IsSuccess.ShouldBeTrue();
}
[Fact(DisplayName = "Два шага с одним конкретным исполнителем, но разными условиями появления — допустимы")]
public async Task CreateAsync_SameExecutorDifferentConditions_BothSucceed()
{
// Arrange
var routeId = await CreateRouteAsync("Route with executor per condition", byProject: false, listName: "Requests");
// Action
var small = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.LessThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
var big = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, "a@b.kz", null, null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
// Assert
small.IsSuccess.ShouldBeTrue();
big.IsSuccess.ShouldBeTrue();
}
[Fact(DisplayName = "Два шага с одним типом руководителя и одинаковым условием появления — конфликт")]
public async Task CreateAsync_SameHeadTypeSameCondition_ReturnsFailedResult()
{
// Arrange
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, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
first.IsSuccess.ShouldBeTrue();
// Action — то же условие, другой статус
var second = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.Chief, RouteStepStatus.Execution, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
// Assert
second.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Обновление шага, при котором HeadType не меняется, не конфликтует само с собой")]
public async Task UpdateAsync_SameHeadTypeOnSameStep_DoesNotConflictWithItself()
{
// Arrange
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, ConditionsLogic.And),
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, ConditionsLogic.And),
CancellationToken.None);
// Assert
updated.IsSuccess.ShouldBeTrue();
}
[Fact(DisplayName = "Создание шага только с типом руководителя (без исполнителя, должности и роли) успешно")]
public async Task CreateAsync_HeadTypeOnly_ReturnsCreatedStep()
{
// Arrange
var routeId = await CreateRouteAsync("Head type only route", byProject: false);
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, null, null, null, HeadType.Chief, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
created.Value.HeadType.ShouldBe(HeadType.Chief);
created.Value.ExecutorEmail.ShouldBeNull();
}
[Fact(DisplayName = "Создание шага маршрута и получение его по Id возвращает сохранённые данные")]
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedStep()
{
// Arrange
var routeId = await CreateRouteAsync("Simple route", byProject: false);
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
var fetched = await _service.GetAsync(routeId, created.Value.Id, CancellationToken.None);
fetched.IsSuccess.ShouldBeTrue();
fetched.Value.ExecutorEmail.ShouldBe("a@b.kz");
}
[Fact(DisplayName = "Создание шага без роли в проекте на маршруте «по проекту» возвращает предупреждение")]
public async Task CreateAsync_ByProjectRouteWithoutProjectRoleStep_ReturnsWarning()
{
// Arrange
var routeId = await CreateRouteAsync("By-project route without role", byProject: true);
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
created.Value.Warning.ShouldNotBeNull();
created.Value.Warning.ShouldContain("ролью в проекте");
}
[Fact(DisplayName = "Создание шага с ролью в проекте на маршруте «по проекту» не возвращает предупреждение")]
public async Task CreateAsync_ByProjectRouteWithProjectRoleStep_HasNoWarning()
{
// Arrange
var routeId = await CreateRouteAsync("By-project route with role", byProject: true);
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.ProjectManager, null, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
created.Value.Warning.ShouldBeNull();
}
[Fact(DisplayName = "Обновление шага маршрута изменяет исполнителя, статус и разрешение на переназначение")]
public async Task UpdateAsync_ChangesFields()
{
// Arrange
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, ConditionsLogic.And),
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, ConditionsLogic.And),
CancellationToken.None);
// Assert
updated.IsSuccess.ShouldBeTrue();
updated.Value.ExecutorEmail.ShouldBe("b@b.kz");
updated.Value.Status.ShouldBe(RouteStepStatus.Execution);
updated.Value.AllowReassignToOtherApprovers.ShouldBeTrue();
}
[Fact(DisplayName = "Патч шага маршрута меняет только переданные поля, остальные остаются прежними")]
public async Task PatchAsync_UpdatesOnlyProvidedFields()
{
// Arrange
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, ConditionsLogic.And),
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, Conditions: null),
CancellationToken.None);
// Assert
patched.IsSuccess.ShouldBeTrue();
patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
patched.Value.OrderNo.ShouldBe(2);
patched.Value.Status.ShouldBe(RouteStepStatus.Execution);
}
[Fact(DisplayName = "Патч, после которого заполнено больше одного из полей исполнителя, возвращает неуспешный результат")]
public async Task PatchAsync_ResultsInMoreThanOneFilledField_ReturnsFailedResult()
{
// Arrange
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, ConditionsLogic.And),
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, Conditions: null),
CancellationToken.None);
// Assert
patched.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Патч со статусом None возвращает неуспешный результат")]
public async Task PatchAsync_StatusNone_ReturnsFailedResult()
{
// Arrange
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, ConditionsLogic.And),
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, Conditions: null),
CancellationToken.None);
// Assert
patched.IsFailed.ShouldBeTrue();
}
// ConditionsLogic здесь всюду передаётся явно, потому что он обязателен: у
// Create/Update-запроса значения по умолчанию нет вовсе, а пропущенное в JSON
// поле превращается в ConditionsLogic.None и отвергается валидатором на слое
// API (TemplateRouteStepValidators + ValidationActionFilter). Отдельным тестом
// это не покрыто — сервис валидаторы не вызывает.
[Fact(DisplayName = "Явные ProjectRole.None и HeadType.None при патче считаются незаполненными, как и null")]
public async Task PatchAsync_ExplicitNoneValues_TreatedAsUnfilled()
{
// Arrange
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, ConditionsLogic.And),
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, Conditions: null),
CancellationToken.None);
// Assert
patched.IsSuccess.ShouldBeTrue();
patched.Value.ExecutorEmail.ShouldBe("a@b.kz");
}
[Fact(DisplayName = "Создание шага с условием без значения возвращает неуспешный результат")]
public async Task CreateAsync_ConditionFieldsPartiallyFilled_ReturnsFailedResult()
{
// Arrange
var routeId = await CreateRouteAsync("Route with partial condition", byProject: false, listName: "Requests");
// Action — оператор задан, но value пуст: условие наполовину заполнено
var result = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false,
new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "") }, ConditionsLogic.And),
CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Создание шага с условием на типе заявки без list_name возвращает неуспешный результат")]
public async Task CreateAsync_ConditionFieldsWithoutListName_ReturnsFailedResult()
{
// Arrange
var routeId = await CreateRouteAsync("Route without list name", byProject: false);
// Action
var result = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
// Assert
result.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Создание шага с условием на типе заявки со list_name успешно")]
public async Task CreateAsync_ConditionFieldsWithListName_ReturnsCreatedStep()
{
// Arrange
var routeId = await CreateRouteAsync("Route with list name", byProject: false, listName: "Requests");
// Action
var created = await _service.CreateAsync(
routeId,
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", null, null, RouteStepStatus.Approval, false, new[] { new RouteStepConditionDto("Amount", ComparisonOperator.GreaterThan, "1000") }, ConditionsLogic.And),
CancellationToken.None);
// Assert
created.IsSuccess.ShouldBeTrue();
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 = "Патч, после которого поля условия заполнены частично, возвращает неуспешный результат")]
public async Task PatchAsync_ConditionFieldsPartiallyFilled_ReturnsFailedResult()
{
// Arrange
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, ConditionsLogic.And),
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, Conditions: new[] { new RouteStepConditionDto("Amount", ComparisonOperator.None, "") }),
CancellationToken.None);
// Assert
patched.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "Удаление шага маршрута убирает его из базы")]
public async Task DeleteAsync_RemovesStep()
{
// Arrange
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, ConditionsLogic.And),
CancellationToken.None);
// Action
var deleted = await _service.DeleteAsync(routeId, created.Value.Id, CancellationToken.None);
// Assert
deleted.IsSuccess.ShouldBeTrue();
var fetched = await _service.GetAsync(routeId, created.Value.Id, CancellationToken.None);
fetched.IsFailed.ShouldBeTrue();
}
[Fact(DisplayName = "ListByExecutorEmailAsync находит шаги с данным исполнителем сразу по разным маршрутам, регистронезависимо")]
public async Task ListByExecutorEmailAsync_FindsStepsAcrossRoutes_CaseInsensitive()
{
// Arrange
var routeAId = await CreateRouteAsync("Route A for executor search", byProject: false);
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, ConditionsLogic.And),
CancellationToken.None);
await _service.CreateAsync(
routeBId,
new CreateTemplateRouteStepRequest(1, "target@example.kz", "Manager", null, null, RouteStepStatus.Execution, false, null, ConditionsLogic.And),
CancellationToken.None);
// Другой исполнитель — не должен попасть в выдачу.
await _service.CreateAsync(
routeAId,
new CreateTemplateRouteStepRequest(2, "other@example.kz", "Engineer", null, null, RouteStepStatus.Approval, false, null, ConditionsLogic.And),
CancellationToken.None);
// Action
var result = await _service.ListByExecutorEmailAsync("TARGET@EXAMPLE.KZ", CancellationToken.None);
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.Count.ShouldBe(2);
result.Value.ShouldAllBe(x => x.RouteId == routeAId || x.RouteId == routeBId);
}
[Fact(DisplayName = "ListByExecutorEmailAsync с пустым email возвращает пустой список, а не ошибку")]
public async Task ListByExecutorEmailAsync_EmptyEmail_ReturnsEmptyList()
{
// Action
var result = await _service.ListByExecutorEmailAsync(string.Empty, CancellationToken.None);
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.ShouldBeEmpty();
}
}