апи для работы с маршрутами согласования #2
@@ -13,8 +13,11 @@
|
|||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
|
||||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||||
|
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
|||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Shouldly;
|
||||||
|
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
||||||
|
using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
|
||||||
|
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public sealed class RequestTypeServiceCollection : ICollectionFixture<PostgresDatabaseFixture>
|
||||||
|
{
|
||||||
|
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<RequestTypeService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _services.Dispose();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_Then_GetAsync_ReturnsCreatedEntity()
|
||||||
|
{
|
||||||
|
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Vacation", "/vacation"), CancellationToken.None);
|
||||||
|
|
||||||
|
created.IsSuccess.ShouldBeTrue();
|
||||||
|
created.Value.Name.ShouldBe("Vacation");
|
||||||
|
created.Value.PageUrl.ShouldBe("/vacation");
|
||||||
|
|
||||||
|
var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None);
|
||||||
|
|
||||||
|
fetched.IsSuccess.ShouldBeTrue();
|
||||||
|
fetched.Value.Id.ShouldBe(created.Value.Id);
|
||||||
|
fetched.Value.Name.ShouldBe("Vacation");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_UnknownId_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
var result = await _service.GetAsync(Guid.NewGuid(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ChangesFields()
|
||||||
|
{
|
||||||
|
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Old name", "/old"), CancellationToken.None);
|
||||||
|
|
||||||
|
var updated = await _service.UpdateAsync(
|
||||||
|
created.Value.Id, new UpdateRequestTypeRequest("New name", "/new"), CancellationToken.None);
|
||||||
|
|
||||||
|
updated.IsSuccess.ShouldBeTrue();
|
||||||
|
updated.Value.Name.ShouldBe("New name");
|
||||||
|
updated.Value.PageUrl.ShouldBe("/new");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_UnknownId_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
var result = await _service.UpdateAsync(
|
||||||
|
Guid.NewGuid(), new UpdateRequestTypeRequest("X", "/x"), CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAsync_RemovesEntity()
|
||||||
|
{
|
||||||
|
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Temp", "/temp"), CancellationToken.None);
|
||||||
|
|
||||||
|
var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None);
|
||||||
|
deleted.IsSuccess.ShouldBeTrue();
|
||||||
|
|
||||||
|
var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None);
|
||||||
|
fetched.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAsync_IncludesCreatedEntity()
|
||||||
|
{
|
||||||
|
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Listed type", "/listed"), CancellationToken.None);
|
||||||
|
|
||||||
|
var list = await _service.ListAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
list.IsSuccess.ShouldBeTrue();
|
||||||
|
list.Value.ShouldContain(x => x.Id == created.Value.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Shouldly;
|
||||||
|
using Tnsu.ApprovalRoute.Application.RequestTypes;
|
||||||
|
using Tnsu.ApprovalRoute.Application.TemplateRoutes;
|
||||||
|
using Tnsu.ApprovalRoute.Domain.Enums;
|
||||||
|
using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
|
||||||
|
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public sealed class TemplateRouteServiceCollection : ICollectionFixture<PostgresDatabaseFixture>
|
||||||
|
{
|
||||||
|
public const string Name = nameof(TemplateRouteServiceCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Collection(TemplateRouteServiceCollection.Name)]
|
||||||
|
public sealed class TemplateRouteServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_services = TestServiceProviderFactory.Build(fixture.ConnectionString);
|
||||||
|
_service = _services.GetRequiredService<TemplateRouteService>();
|
||||||
|
_requestTypeService = _services.GetRequiredService<RequestTypeService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _services.Dispose();
|
||||||
|
|
||||||
|
private async Task<Guid> CreateRequestTypeAsync(string name) =>
|
||||||
|
(await _requestTypeService.CreateAsync(new CreateRequestTypeRequest(name, $"/{name}"), CancellationToken.None)).Value.Id;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedEntity()
|
||||||
|
{
|
||||||
|
var requestTypeId = await CreateRequestTypeAsync("Vacation");
|
||||||
|
|
||||||
|
var created = await _service.CreateAsync(
|
||||||
|
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
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]
|
||||||
|
public async Task CreateAsync_UnknownRequestType_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
var result = await _service.CreateAsync(
|
||||||
|
new CreateTemplateRouteRequest(RouteType.Sequential, Guid.NewGuid(), DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_DuplicateRoute_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
|
||||||
|
var duplicate = await _service.CreateAsync(request, CancellationToken.None);
|
||||||
|
duplicate.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ChangesFields()
|
||||||
|
{
|
||||||
|
var requestTypeId = await CreateRequestTypeAsync("Sick leave");
|
||||||
|
var created = await _service.CreateAsync(
|
||||||
|
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
var updated = await _service.UpdateAsync(
|
||||||
|
created.Value.Id,
|
||||||
|
new UpdateTemplateRouteRequest(RouteType.Parallel, DepartmentId, CompanyId, ByProject: false, RequiresControl: true),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
updated.IsSuccess.ShouldBeTrue();
|
||||||
|
updated.Value.RouteType.ShouldBe(RouteType.Parallel);
|
||||||
|
updated.Value.RequiresControl.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAsync_RemovesEntity()
|
||||||
|
{
|
||||||
|
var requestTypeId = await CreateRequestTypeAsync("Remote work");
|
||||||
|
var created = await _service.CreateAsync(
|
||||||
|
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
var deleted = await _service.DeleteAsync(created.Value.Id, CancellationToken.None);
|
||||||
|
deleted.IsSuccess.ShouldBeTrue();
|
||||||
|
|
||||||
|
var fetched = await _service.GetAsync(created.Value.Id, CancellationToken.None);
|
||||||
|
fetched.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveAsync_ExactDepartmentMatch_ReturnsMatchingStepsForStatus()
|
||||||
|
{
|
||||||
|
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<Application.TemplateRouteSteps.TemplateRouteStepService>();
|
||||||
|
await stepService.CreateAsync(
|
||||||
|
route.Value.Id,
|
||||||
|
new Application.TemplateRouteSteps.CreateTemplateRouteStepRequest(
|
||||||
|
1, "approver@tnsu.kz", "Lead", ProjectRole.None, HeadType.Chief, RouteStepStatus.Approval, false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
var resolved = await _service.ResolveAsync(
|
||||||
|
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
resolved.IsSuccess.ShouldBeTrue();
|
||||||
|
resolved.Value.RouteId.ShouldBe(route.Value.Id);
|
||||||
|
resolved.Value.Steps.ShouldHaveSingleItem();
|
||||||
|
resolved.Value.Steps[0].ExecutorEmail.ShouldBe("approver@tnsu.kz");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveAsync_NoDepartmentRoute_FallsBackToCompanyWideRoute()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
var resolved = await _service.ResolveAsync(
|
||||||
|
new ResolveTemplateRouteRequest(requestTypeId, "someone@tnsu.kz", null, RouteStepStatus.Execution),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
resolved.IsSuccess.ShouldBeTrue();
|
||||||
|
resolved.Value.RouteId.ShouldBe(route.Value.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveAsync_UnknownRequestType_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
var resolved = await _service.ResolveAsync(
|
||||||
|
new ResolveTemplateRouteRequest(Guid.NewGuid(), "someone@tnsu.kz", null, RouteStepStatus.Approval),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
resolved.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public sealed class TemplateRouteStepServiceCollection : ICollectionFixture<PostgresDatabaseFixture>
|
||||||
|
{
|
||||||
|
public const string Name = nameof(TemplateRouteStepServiceCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Collection(TemplateRouteStepServiceCollection.Name)]
|
||||||
|
public sealed class TemplateRouteStepServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
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<TemplateRouteStepService>();
|
||||||
|
_routeService = _services.GetRequiredService<TemplateRouteService>();
|
||||||
|
_requestTypeService = _services.GetRequiredService<RequestTypeService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _services.Dispose();
|
||||||
|
|
||||||
|
private async Task<Guid> 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]
|
||||||
|
public async Task CreateAsync_UnknownRoute_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
var result = await _service.CreateAsync(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_ProjectRoleOnNonProjectRoute_ReturnsFailedResult()
|
||||||
|
{
|
||||||
|
var routeId = await CreateRouteAsync("Non project route", byProject: false);
|
||||||
|
|
||||||
|
var result = await _service.CreateAsync(
|
||||||
|
routeId,
|
||||||
|
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.Manager, HeadType.None, RouteStepStatus.Approval, false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_ThenGetAsync_ReturnsCreatedStep()
|
||||||
|
{
|
||||||
|
var routeId = await CreateRouteAsync("Simple route", byProject: false);
|
||||||
|
|
||||||
|
var created = await _service.CreateAsync(
|
||||||
|
routeId,
|
||||||
|
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
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]
|
||||||
|
public async Task CreateAsync_ByProjectRouteWithoutProjectRoleStep_ReturnsWarning()
|
||||||
|
{
|
||||||
|
var routeId = await CreateRouteAsync("By-project route without role", byProject: true);
|
||||||
|
|
||||||
|
var created = await _service.CreateAsync(
|
||||||
|
routeId,
|
||||||
|
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
created.IsSuccess.ShouldBeTrue();
|
||||||
|
created.Reasons.ShouldContain(r => r.Message.Contains("ролью в проекте"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_ByProjectRouteWithProjectRoleStep_HasNoWarning()
|
||||||
|
{
|
||||||
|
var routeId = await CreateRouteAsync("By-project route with role", byProject: true);
|
||||||
|
|
||||||
|
var created = await _service.CreateAsync(
|
||||||
|
routeId,
|
||||||
|
new CreateTemplateRouteStepRequest(1, "", "", ProjectRole.Manager, HeadType.None, RouteStepStatus.Approval, false),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
created.IsSuccess.ShouldBeTrue();
|
||||||
|
created.Reasons.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ChangesFields()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
updated.IsSuccess.ShouldBeTrue();
|
||||||
|
updated.Value.ExecutorEmail.ShouldBe("b@b.kz");
|
||||||
|
updated.Value.Status.ShouldBe(RouteStepStatus.Execution);
|
||||||
|
updated.Value.AllowReassignToOtherApprovers.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAsync_RemovesStep()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
var deleted = await _service.DeleteAsync(routeId, created.Value.Id, CancellationToken.None);
|
||||||
|
deleted.IsSuccess.ShouldBeTrue();
|
||||||
|
|
||||||
|
var fetched = await _service.GetAsync(routeId, created.Value.Id, CancellationToken.None);
|
||||||
|
fetched.IsFailed.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
|
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||||
|
|
||||||
|
// Отдельная БД на один xUnit-collection (= один тестовый класс) в общем инстансе Postgres.
|
||||||
|
// Тесты внутри класса делят эту БД; разные классы (разные коллекции) получают разные БД.
|
||||||
|
public sealed class PostgresDatabaseFixture : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly string _databaseName = $"tnsu_test_{Guid.NewGuid():N}";
|
||||||
|
private readonly NpgsqlConnectionStringBuilder _adminConnectionBuilder = new(TestSettings.BaseConnectionString);
|
||||||
|
|
||||||
|
public string ConnectionString { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
await using (var adminConnection = new NpgsqlConnection(_adminConnectionBuilder.ConnectionString))
|
||||||
|
{
|
||||||
|
await adminConnection.OpenAsync();
|
||||||
|
await using var createCommand = new NpgsqlCommand($"CREATE DATABASE \"{_databaseName}\"", adminConnection);
|
||||||
|
await createCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionString = new NpgsqlConnectionStringBuilder(_adminConnectionBuilder.ConnectionString)
|
||||||
|
{
|
||||||
|
Database = _databaseName
|
||||||
|
}.ConnectionString;
|
||||||
|
|
||||||
|
var options = new DbContextOptionsBuilder<ApprovalRouteDbContext>()
|
||||||
|
.UseNpgsql(ConnectionString)
|
||||||
|
.UseSnakeCaseNamingConvention()
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
await using var db = new ApprovalRouteDbContext(options);
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await using var adminConnection = new NpgsqlConnection(_adminConnectionBuilder.ConnectionString);
|
||||||
|
await adminConnection.OpenAsync();
|
||||||
|
|
||||||
|
await using (var terminateCommand = new NpgsqlCommand(
|
||||||
|
"""
|
||||||
|
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||||
|
WHERE datname = @databaseName AND pid <> pg_backend_pid();
|
||||||
|
""",
|
||||||
|
adminConnection))
|
||||||
|
{
|
||||||
|
terminateCommand.Parameters.AddWithValue("databaseName", _databaseName);
|
||||||
|
await terminateCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var dropCommand = new NpgsqlCommand($"DROP DATABASE IF EXISTS \"{_databaseName}\"", adminConnection);
|
||||||
|
await dropCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||||
|
|
||||||
|
internal sealed class TestCurrentUser(string? email = null) : ICurrentUser
|
||||||
|
{
|
||||||
|
public string? Email { get; } = email;
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Tnsu.ApprovalRoute.Application;
|
||||||
|
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
||||||
|
using Tnsu.ApprovalRoute.Infrastructure;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||||
|
|
||||||
|
// Собирает такой же DI-контейнер, как реальное приложение (те же extension-методы, что в Program.cs),
|
||||||
|
// но с тестовой БД и тестовым ICurrentUser вместо HttpContext-based реализации.
|
||||||
|
internal static class TestServiceProviderFactory
|
||||||
|
{
|
||||||
|
public static ServiceProvider Build(string connectionString, string? currentUserEmail = "initiator@tnsu.kz")
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["ConnectionStrings:Postgres"] = connectionString
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddApprovalRouteInfrastructure(configuration);
|
||||||
|
services.AddApprovalRouteApplication();
|
||||||
|
services.AddSingleton<ICurrentUser>(new TestCurrentUser(currentUserEmail));
|
||||||
|
|
||||||
|
return services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||||
|
|
||||||
|
internal static class TestSettings
|
||||||
|
{
|
||||||
|
public static string BaseConnectionString =>
|
||||||
|
Environment.GetEnvironmentVariable("TEST_POSTGRES_CONNECTION")
|
||||||
|
?? "Host=localhost;Port=5435;Database=tnsu_approval_route;Username=tnsu_approval_route;Password=tnsu_approval_route";
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
|
||||||
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
|
|
||||||
|
|
||||||
namespace Tnsu.ApprovalRoute.IntegrationTests;
|
|
||||||
|
|
||||||
public class RequestTypePostgresTests
|
|
||||||
{
|
|
||||||
private static string ConnectionString =>
|
|
||||||
Environment.GetEnvironmentVariable("TEST_POSTGRES_CONNECTION")
|
|
||||||
?? "Host=localhost;Port=5435;Database=tnsu_approval_route;Username=tnsu_approval_route;Password=tnsu_approval_route";
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CanInsertAndReadBack_RealPostgres()
|
|
||||||
{
|
|
||||||
var options = new DbContextOptionsBuilder<ApprovalRouteDbContext>()
|
|
||||||
.UseNpgsql(ConnectionString)
|
|
||||||
.UseSnakeCaseNamingConvention()
|
|
||||||
.Options;
|
|
||||||
|
|
||||||
await using var db = new ApprovalRouteDbContext(options);
|
|
||||||
await db.Database.MigrateAsync();
|
|
||||||
|
|
||||||
// Транзакция без коммита — тест не оставляет данных в базе после себя.
|
|
||||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
|
||||||
|
|
||||||
var entity = new RequestType { Name = "Integration test type", PageUrl = "/integration-test" };
|
|
||||||
db.RequestTypes.Add(entity);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
db.ChangeTracker.Clear();
|
|
||||||
var loaded = await db.RequestTypes.AsNoTracking().FirstOrDefaultAsync(x => x.Id == entity.Id);
|
|
||||||
|
|
||||||
Assert.NotNull(loaded);
|
|
||||||
Assert.Equal("Integration test type", loaded.Name);
|
|
||||||
Assert.Equal("/integration-test", loaded.PageUrl);
|
|
||||||
|
|
||||||
await transaction.RollbackAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
@@ -6,11 +6,15 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Tnsu.ApprovalRoute.Infrastructure\Tnsu.ApprovalRoute.Infrastructure.csproj" />
|
<ProjectReference Include="..\..\src\Tnsu.ApprovalRoute.Infrastructure\Tnsu.ApprovalRoute.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Tnsu.ApprovalRoute.Application\Tnsu.ApprovalRoute.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" />
|
<PackageReference Include="coverlet.collector" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="Shouldly" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" />
|
<PackageReference Include="xunit.runner.visualstudio" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user