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 { 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(); _routeService = services.GetRequiredService(); _requestTypeService = services.GetRequiredService(); } private async Task 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), 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), 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); 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), 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), 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), 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), CancellationToken.None); first.IsSuccess.ShouldBeTrue(); // Action var second = await _service.CreateAsync( routeId, new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DepartmentHead, RouteStepStatus.Execution, false, null), 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), CancellationToken.None); var directorTansu = await _service.CreateAsync( routeId, new CreateTemplateRouteStepRequest(2, null, null, null, HeadType.DirectorTansu, RouteStepStatus.Approval, false, null), 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") }), 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") }), 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") }), 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") }), 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") }), 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") }), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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(); } [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), 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, "") }), 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") }), 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") }), 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), 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), 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), CancellationToken.None); await _service.CreateAsync( routeBId, 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), 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(); } }