Files
tnsu-approval-route/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs
T
2026-07-27 15:44:25 +05:00

271 lines
12 KiB
C#

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)
{
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();
}
}