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) { var requestTypeId = (await _requestTypeService.CreateAsync( new CreateRequestTypeRequest(requestTypeName, $"/{requestTypeName}"), CancellationToken.None)).Value.Id; var route = await _routeService.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, 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), 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), 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); 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), CancellationToken.None); // Assert duplicate.IsFailed.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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), 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), CancellationToken.None); // Assert patched.IsSuccess.ShouldBeTrue(); patched.Value.ExecutorEmail.ShouldBe("a@b.kz"); } [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), 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(); } }