From 0bf5475553a76f13a8e46f93cd57b481009b4836 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 23 Jul 2026 17:38:14 +0500 Subject: [PATCH] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=B0=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RequestTypeServiceTests.cs | 45 ++++++++------- .../TemplateRouteServiceTests.cs | 52 +++++++++++------- .../TemplateRouteStepServiceTests.cs | 55 +++++++++++-------- 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs index f6b9dc9..001b54e 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs @@ -5,91 +5,98 @@ using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices; -[CollectionDefinition(Name)] -public sealed class RequestTypeServiceCollection : ICollectionFixture +public sealed class RequestTypeServiceTests : IClassFixture { - public const string Name = nameof(RequestTypeServiceCollection); -} - -[Collection(RequestTypeServiceCollection.Name)] -public sealed class RequestTypeServiceTests : IDisposable -{ - private readonly ServiceProvider _services; private readonly RequestTypeService _service; public RequestTypeServiceTests(PostgresDatabaseFixture fixture) { - _services = TestServiceProviderFactory.Build(fixture.ConnectionString); - _service = _services.GetRequiredService(); + _service = TestServiceProviderFactory.Build(fixture.ConnectionString).GetRequiredService(); } - public void Dispose() => _services.Dispose(); - - [Fact] + [Fact(DisplayName = "Создание типа заявки и получение его по Id возвращает сохранённые данные")] public async Task CreateAsync_Then_GetAsync_ReturnsCreatedEntity() { + // Action var created = await _service.CreateAsync(new CreateRequestTypeRequest("Vacation", "/vacation"), CancellationToken.None); + // Assert created.IsSuccess.ShouldBeTrue(); created.Value.Name.ShouldBe("Vacation"); created.Value.PageUrl.ShouldBe("/vacation"); + // Action var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None); + // Assert fetched.IsSuccess.ShouldBeTrue(); fetched.Value.Id.ShouldBe(created.Value.Id); fetched.Value.Name.ShouldBe("Vacation"); } - [Fact] + [Fact(DisplayName = "Получение несуществующего типа заявки возвращает неуспешный результат")] public async Task GetAsync_UnknownId_ReturnsFailedResult() { + // Action var result = await _service.GetAsync(Guid.NewGuid(), CancellationToken.None); + // Assert result.IsFailed.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Обновление типа заявки изменяет имя и url страницы")] public async Task UpdateAsync_ChangesFields() { + // Arrange var created = await _service.CreateAsync(new CreateRequestTypeRequest("Old name", "/old"), CancellationToken.None); + // Action var updated = await _service.UpdateAsync( created.Value.Id, new UpdateRequestTypeRequest("New name", "/new"), CancellationToken.None); + // Assert updated.IsSuccess.ShouldBeTrue(); updated.Value.Name.ShouldBe("New name"); updated.Value.PageUrl.ShouldBe("/new"); } - [Fact] + [Fact(DisplayName = "Обновление несуществующего типа заявки возвращает неуспешный результат")] public async Task UpdateAsync_UnknownId_ReturnsFailedResult() { + // Action var result = await _service.UpdateAsync( Guid.NewGuid(), new UpdateRequestTypeRequest("X", "/x"), CancellationToken.None); + // Assert result.IsFailed.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Удаление типа заявки убирает его из базы")] public async Task DeleteAsync_RemovesEntity() { + // Arrange var created = await _service.CreateAsync(new CreateRequestTypeRequest("Temp", "/temp"), CancellationToken.None); + // Action var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None); + + // Assert deleted.IsSuccess.ShouldBeTrue(); var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None); fetched.IsFailed.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Список типов заявок включает ранее созданный тип")] public async Task ListAsync_IncludesCreatedEntity() { + // Arrange var created = await _service.CreateAsync(new CreateRequestTypeRequest("Listed type", "/listed"), CancellationToken.None); + // Action var list = await _service.ListAsync(CancellationToken.None); + // Assert list.IsSuccess.ShouldBeTrue(); list.Value.ShouldContain(x => x.Id == created.Value.Id); } diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs index a982cae..34a8129 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs @@ -7,14 +7,7 @@ using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices; -[CollectionDefinition(Name)] -public sealed class TemplateRouteServiceCollection : ICollectionFixture -{ - public const string Name = nameof(TemplateRouteServiceCollection); -} - -[Collection(TemplateRouteServiceCollection.Name)] -public sealed class TemplateRouteServiceTests : IDisposable +public sealed class TemplateRouteServiceTests : IClassFixture { private static readonly Guid CompanyId = Guid.Parse("00000000-0000-0000-0000-000000000002"); private static readonly Guid DepartmentId = Guid.Parse("00000000-0000-0000-0000-000000000001"); @@ -30,20 +23,21 @@ public sealed class TemplateRouteServiceTests : IDisposable _requestTypeService = _services.GetRequiredService(); } - public void Dispose() => _services.Dispose(); - private async Task CreateRequestTypeAsync(string name) => (await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}"), CancellationToken.None)).Value.Id; - [Fact] + [Fact(DisplayName = "Создание маршрута и получение его по Id возвращает сохранённые данные")] public async Task CreateAsync_ThenGetAsync_ReturnsCreatedEntity() { + // Arrange var requestTypeId = await CreateRequestTypeAsync("Vacation"); + // Action var created = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true), CancellationToken.None); + // Assert created.IsSuccess.ShouldBeTrue(); created.Value.RequestTypeId.ShouldBe(requestTypeId); @@ -52,65 +46,78 @@ public sealed class TemplateRouteServiceTests : IDisposable fetched.Value.CompanyId.ShouldBe(CompanyId); } - [Fact] + [Fact(DisplayName = "Создание маршрута с несуществующим типом заявки возвращает неуспешный результат")] public async Task CreateAsync_UnknownRequestType_ReturnsFailedResult() { + // Action var result = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, Guid.NewGuid(), DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); + // Assert result.IsFailed.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Повторное создание маршрута с тем же типом заявки, отделом, компанией и признаком «по проекту» возвращает конфликт")] public async Task CreateAsync_DuplicateRoute_ReturnsFailedResult() { + // Arrange var requestTypeId = await CreateRequestTypeAsync("Business trip"); var request = new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false); - var first = await _service.CreateAsync(request, CancellationToken.None); first.IsSuccess.ShouldBeTrue(); + // Action var duplicate = await _service.CreateAsync(request, CancellationToken.None); + + // Assert duplicate.IsFailed.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Обновление маршрута изменяет тип маршрута и признак необходимости контроля")] public async Task UpdateAsync_ChangesFields() { + // Arrange var requestTypeId = await CreateRequestTypeAsync("Sick leave"); var created = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); + // Action var updated = await _service.UpdateAsync( created.Value.Id, new UpdateTemplateRouteRequest(RouteType.Parallel, DepartmentId, CompanyId, ByProject: false, RequiresControl: true), CancellationToken.None); + // Assert updated.IsSuccess.ShouldBeTrue(); updated.Value.RouteType.ShouldBe(RouteType.Parallel); updated.Value.RequiresControl.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Удаление маршрута убирает его из базы")] public async Task DeleteAsync_RemovesEntity() { + // Arrange var requestTypeId = await CreateRequestTypeAsync("Remote work"); var created = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); + // Action var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None); + + // Assert deleted.IsSuccess.ShouldBeTrue(); var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None); fetched.IsFailed.ShouldBeTrue(); } - [Fact] + [Fact(DisplayName = "Резолв маршрута по точному совпадению отдела возвращает шаги с нужным статусом")] public async Task ResolveAsync_ExactDepartmentMatch_ReturnsMatchingStepsForStatus() { + // Arrange var requestTypeId = await CreateRequestTypeAsync("Exact match request"); var route = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true), @@ -123,39 +130,46 @@ public sealed class TemplateRouteServiceTests : IDisposable 1, "approver@tnsu.kz", "Lead", ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, false), CancellationToken.None); + // Action var resolved = await _service.ResolveAsync( new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval), CancellationToken.None); + // Assert resolved.IsSuccess.ShouldBeTrue(); resolved.Value.RouteId.ShouldBe(route.Value.Id); resolved.Value.Steps.ShouldHaveSingleItem(); resolved.Value.Steps[0].ExecutorEmail.ShouldBe("approver@tnsu.kz"); } - [Fact] + [Fact(DisplayName = "Резолв маршрута откатывается на маршрут без отдела, если точного совпадения по отделу нет")] public async Task ResolveAsync_NoDepartmentRoute_FallsBackToCompanyWideRoute() { + // Arrange var requestTypeId = await CreateRequestTypeAsync("Company-wide request"); var route = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId: null, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); + // Action var resolved = await _service.ResolveAsync( new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Execution), CancellationToken.None); + // Assert resolved.IsSuccess.ShouldBeTrue(); resolved.Value.RouteId.ShouldBe(route.Value.Id); } - [Fact] + [Fact(DisplayName = "Резолв маршрута с несуществующим типом заявки возвращает неуспешный результат")] public async Task ResolveAsync_UnknownRequestType_ReturnsFailedResult() { + // Action var resolved = await _service.ResolveAsync( new ResolveTemplateRouteRequest(Guid.NewGuid(), "someone@tnsu.kz", null, RouteStepStatus.Approval), CancellationToken.None); + // Assert resolved.IsFailed.ShouldBeTrue(); } } diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs index 4b59a9b..4268e7f 100644 --- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs +++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs @@ -8,32 +8,22 @@ using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices; -[CollectionDefinition(Name)] -public sealed class TemplateRouteStepServiceCollection : ICollectionFixture -{ - public const string Name = nameof(TemplateRouteStepServiceCollection); -} - -[Collection(TemplateRouteStepServiceCollection.Name)] -public sealed class TemplateRouteStepServiceTests : IDisposable +public sealed class TemplateRouteStepServiceTests : IClassFixture { private static readonly Guid CompanyId = Guid.Parse("00000000-0000-0000-0000-000000000002"); - private readonly ServiceProvider _services; private readonly TemplateRouteStepService _service; private readonly TemplateRouteService _routeService; private readonly RequestTypeService _requestTypeService; public TemplateRouteStepServiceTests(PostgresDatabaseFixture fixture) { - _services = TestServiceProviderFactory.Build(fixture.ConnectionString); - _service = _services.GetRequiredService(); - _routeService = _services.GetRequiredService(); - _requestTypeService = _services.GetRequiredService(); + var services = TestServiceProviderFactory.Build(fixture.ConnectionString); + _service = services.GetRequiredService(); + _routeService = services.GetRequiredService(); + _requestTypeService = services.GetRequiredService(); } - public void Dispose() => _services.Dispose(); - private async Task CreateRouteAsync(string requestTypeName, bool byProject) { var requestTypeId = (await _requestTypeService.CreateAsync( @@ -46,40 +36,48 @@ public sealed class TemplateRouteStepServiceTests : IDisposable return route.Value.Id; } - [Fact] + [Fact(DisplayName = "Создание шага для несуществующего маршрута возвращает неуспешный результат")] public async Task CreateAsync_UnknownRoute_ReturnsFailedResult() { + // Action var result = await _service.CreateAsync( Guid.NewGuid(), new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); + // Assert result.IsFailed.ShouldBeTrue(); } - [Fact] + [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.Manager, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); + // Assert result.IsFailed.ShouldBeTrue(); } - [Fact] + [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", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); + // Assert created.IsSuccess.ShouldBeTrue(); var fetched = await _service.GetAsync(routeId, created.Value.Id, CancellationToken.None); @@ -87,65 +85,78 @@ public sealed class TemplateRouteStepServiceTests : IDisposable fetched.Value.ExecutorEmail.ShouldBe("a@b.kz"); } - [Fact] + [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", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); + // Assert created.IsSuccess.ShouldBeTrue(); created.Reasons.ShouldContain(r => r.Message.Contains("ролью в проекте")); } - [Fact] + [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.Manager, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); + // Assert created.IsSuccess.ShouldBeTrue(); created.Reasons.ShouldBeEmpty(); } - [Fact] + [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", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); + // Action var updated = await _service.UpdateAsync( routeId, created.Value.Id, new UpdateTemplateRouteStepRequest(2, "b@b.kz", "Senior Engineer", ProjectRole.None, 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] + [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", ProjectRole.None, HeadType.None, 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);