основное по тестам готово
Tests / test (pull_request) Successful in 4m5s

This commit is contained in:
gorbenko.pavel
2026-07-23 17:21:30 +05:00
parent 253fafa575
commit e201823f69
10 changed files with 522 additions and 40 deletions
+3
View File
@@ -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" />
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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();
}
}
@@ -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>