using Microsoft.Extensions.DependencyInjection; using Moq; 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.Infrastructure.TnsuPortal; using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices; 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"); private readonly ServiceProvider _services; private readonly TemplateRouteService _service; private readonly RequestTypeService _requestTypeService; public TemplateRouteServiceTests(PostgresDatabaseFixture fixture) { var portalClient = new Mock(); portalClient .Setup(x => x.GetEmployeeByEmailAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new EmployeeDirectoryInfo(CompanyId, DepartmentId)); portalClient .Setup(x => x.GetProjectExecutorsAsync(It.IsAny(), It.IsAny())) .ReturnsAsync([new ProjectExecutorInfo(ProjectRole.ProjectManager, "pm@tnsu.kz")]); _services = TestServiceProviderFactory.Build(fixture.ConnectionString, portalClient: portalClient.Object); _service = _services.GetRequiredService(); _requestTypeService = _services.GetRequiredService(); } private async Task CreateRequestTypeAsync(string name) => (await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}"), CancellationToken.None)).Value.Id; [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); var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None); fetched.IsSuccess.ShouldBeTrue(); fetched.Value.CompanyId.ShouldBe(CompanyId); } [Fact(DisplayName = "Создание маршрута с несуществующим типом заявки возвращает неуспешный результат")] public async Task CreateAsync_UnknownRequestType_ReturnsFailedResult() { // Action var result = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, 999999, DepartmentId, CompanyId, ByProject: false, RequiresControl: false), CancellationToken.None); // Assert result.IsFailed.ShouldBeTrue(); } [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(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(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(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), CancellationToken.None); var stepService = _services.GetRequiredService(); await stepService.CreateAsync( route.Value.Id, new CreateTemplateRouteStepRequest(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(DisplayName = "Резолв маршрута с ролью в проекте подставляет исполнителя из проекта")] public async Task ResolveAsync_ByProjectRole_UsesProjectExecutorEmail() { // Arrange var requestTypeId = await CreateRequestTypeAsync("Project request"); var route = await _service.CreateAsync( new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: true, RequiresControl: false), CancellationToken.None); var stepService = _services.GetRequiredService(); await stepService.CreateAsync( route.Value.Id, new CreateTemplateRouteStepRequest(1, "", "Lead", ProjectRole.ProjectManager, HeadType.None, RouteStepStatus.Approval, false), CancellationToken.None); // Action var resolved = await _service.ResolveAsync( new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", Guid.NewGuid(), RouteStepStatus.Approval), CancellationToken.None); // Assert resolved.IsSuccess.ShouldBeTrue(); resolved.Value.Steps.ShouldHaveSingleItem(); resolved.Value.Steps[0].ExecutorEmail.ShouldBe("pm@tnsu.kz"); } [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(DisplayName = "Резолв маршрута с несуществующим типом заявки возвращает неуспешный результат")] public async Task ResolveAsync_UnknownRequestType_ReturnsFailedResult() { // Action var resolved = await _service.ResolveAsync( new ResolveTemplateRouteRequest(999999, "someone@tnsu.kz", null, RouteStepStatus.Approval), CancellationToken.None); // Assert resolved.IsFailed.ShouldBeTrue(); } }