diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props
index 022fa01..59c877f 100644
--- a/backend/Directory.Packages.props
+++ b/backend/Directory.Packages.props
@@ -13,8 +13,11 @@
+
+
+
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs
new file mode 100644
index 0000000..f6b9dc9
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/RequestTypeServiceTests.cs
@@ -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
+{
+ 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();
+ }
+
+ 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);
+ }
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs
new file mode 100644
index 0000000..a982cae
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteServiceTests.cs
@@ -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
+{
+ 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();
+ _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]
+ 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();
+ 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();
+ }
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs
new file mode 100644
index 0000000..4b59a9b
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/TemplateRouteStepServiceTests.cs
@@ -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
+{
+ 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();
+ _routeService = _services.GetRequiredService();
+ _requestTypeService = _services.GetRequiredService();
+ }
+
+ public void Dispose() => _services.Dispose();
+
+ private async Task 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();
+ }
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/PostgresDatabaseFixture.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/PostgresDatabaseFixture.cs
new file mode 100644
index 0000000..cb76a16
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/PostgresDatabaseFixture.cs
@@ -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()
+ .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();
+ }
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestCurrentUser.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestCurrentUser.cs
new file mode 100644
index 0000000..020d450
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestCurrentUser.cs
@@ -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;
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestServiceProviderFactory.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestServiceProviderFactory.cs
new file mode 100644
index 0000000..1290809
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestServiceProviderFactory.cs
@@ -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
+ {
+ ["ConnectionStrings:Postgres"] = connectionString
+ })
+ .Build();
+
+ var services = new ServiceCollection();
+ services.AddApprovalRouteInfrastructure(configuration);
+ services.AddApprovalRouteApplication();
+ services.AddSingleton(new TestCurrentUser(currentUserEmail));
+
+ return services.BuildServiceProvider();
+ }
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestSettings.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestSettings.cs
new file mode 100644
index 0000000..da4f2dc
--- /dev/null
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Infrastructure/TestSettings.cs
@@ -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";
+}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/RequestTypePostgresTests.cs b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/RequestTypePostgresTests.cs
deleted file mode 100644
index 43f4131..0000000
--- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/RequestTypePostgresTests.cs
+++ /dev/null
@@ -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()
- .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();
- }
-}
diff --git a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj
index efe1f0d..1ee25ea 100644
--- a/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj
+++ b/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/Tnsu.ApprovalRoute.IntegrationTests.csproj
@@ -6,11 +6,15 @@
+
+
+
+