Перевести FakeTenantProvisioner, FailingTenantProvisioner и FakeRateLimitCounterStore на NSubstitute
ci / build-test (push) Canceled after 0s
ci / build-test (push) Canceled after 0s
Хелперы Support/TestTenantProvisioner (журнал схем + сценарий сбоя заданных схем, счётчик через When/Do) и Support/TestRateLimitCounterStore (словарь окон, Increment/GetCount/Reset/DeleteExpired читают состояние на момент вызова). Потребители (9 файлов) перетипизированы, фейк-классы удалены, тесты 1340.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Api.Configuration;
|
using Deal.Api.Configuration;
|
||||||
using Deal.Api.Hosting;
|
using Deal.Api.Hosting;
|
||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -31,9 +32,9 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
|
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
|
||||||
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 700, warned80: true);
|
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 700, warned80: true);
|
||||||
|
|
||||||
var counters = new FakeRateLimitCounterStore();
|
var counters = new TestRateLimitCounterStore();
|
||||||
await counters.IncrementAsync("expired", now.AddHours(-2), now.AddHours(-2).AddMinutes(1), 1, CancellationToken.None);
|
await counters.Store.IncrementAsync("expired", now.AddHours(-2), now.AddHours(-2).AddMinutes(1), 1, CancellationToken.None);
|
||||||
await counters.IncrementAsync("active", now, now.AddMinutes(1), 1, CancellationToken.None);
|
await counters.Store.IncrementAsync("active", now, now.AddMinutes(1), 1, CancellationToken.None);
|
||||||
|
|
||||||
await using ServiceProvider provider = BuildProvider(audit, limits, counters);
|
await using ServiceProvider provider = BuildProvider(audit, limits, counters);
|
||||||
DataRetentionScheduler scheduler = new(
|
DataRetentionScheduler scheduler = new(
|
||||||
@@ -51,8 +52,8 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
Assert.Equal(0, limits.UsedTokens(TenantId));
|
Assert.Equal(0, limits.UsedTokens(TenantId));
|
||||||
|
|
||||||
// Счётчики: завершившееся окно удалено, активное осталось.
|
// Счётчики: завершившееся окно удалено, активное осталось.
|
||||||
Assert.Equal(0, await counters.GetCountAsync("expired", now.AddHours(-2), CancellationToken.None));
|
Assert.Equal(0, await counters.Store.GetCountAsync("expired", now.AddHours(-2), CancellationToken.None));
|
||||||
Assert.Equal(1, await counters.GetCountAsync("active", now, CancellationToken.None));
|
Assert.Equal(1, await counters.Store.GetCountAsync("active", now, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -66,7 +67,7 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
await audit.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
|
await audit.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new FakeTenantLimitStore();
|
||||||
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 500);
|
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 500);
|
||||||
await using ServiceProvider provider = BuildProvider(audit, limits, new FakeRateLimitCounterStore());
|
await using ServiceProvider provider = BuildProvider(audit, limits, new TestRateLimitCounterStore());
|
||||||
DataRetentionScheduler scheduler = new(
|
DataRetentionScheduler scheduler = new(
|
||||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||||
new DataRetentionOptions { AuditRetentionDays = RetentionDays },
|
new DataRetentionOptions { AuditRetentionDays = RetentionDays },
|
||||||
@@ -82,17 +83,17 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
// Строит DI-провайдер теста: три фейк-хранилища в scope прохода.
|
// Строит DI-провайдер теста: три фейк-хранилища в scope прохода.
|
||||||
// audit: Фейк-хранилище аудита (записи посеяны сценарием).
|
// audit: Фейк-хранилище аудита (записи посеяны сценарием).
|
||||||
// limits: Фейк-хранилище лимитов (строки посеяны сценарием).
|
// limits: Фейк-хранилище лимитов (строки посеяны сценарием).
|
||||||
// counters: Фейк-хранилище счётчиков (окна посеяны сценарием).
|
// counters: Хелпер счётчиков (окна посеяны сценарием).
|
||||||
// Возвращает: Провайдер с сервисами цикла.
|
// Возвращает: Провайдер с сервисами цикла.
|
||||||
private static ServiceProvider BuildProvider(
|
private static ServiceProvider BuildProvider(
|
||||||
FakeAuditLogStore audit,
|
FakeAuditLogStore audit,
|
||||||
FakeTenantLimitStore limits,
|
FakeTenantLimitStore limits,
|
||||||
FakeRateLimitCounterStore counters)
|
TestRateLimitCounterStore counters)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddScoped<IAuditLogStore>(_ => audit);
|
services.AddScoped<IAuditLogStore>(_ => audit);
|
||||||
services.AddScoped<ITenantLimitStore>(_ => limits);
|
services.AddScoped<ITenantLimitStore>(_ => limits);
|
||||||
services.AddScoped<IRateLimitCounterStore>(_ => counters);
|
services.AddScoped<IRateLimitCounterStore>(_ => counters.Store);
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Deal.Api.Configuration;
|
using Deal.Api.Configuration;
|
||||||
using Deal.Api.Services;
|
using Deal.Api.Services;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Api;
|
namespace Deal.Tests.Unit.Api;
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ public sealed class LoginAttemptGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task DisabledByDefaultInDev_DoesNotCountOrBlock()
|
public async Task DisabledByDefaultInDev_DoesNotCountOrBlock()
|
||||||
{
|
{
|
||||||
var guard = new LoginAttemptGuard(new RateLimitOptions(), new FakeRateLimitCounterStore());
|
var guard = new LoginAttemptGuard(new RateLimitOptions(), new TestRateLimitCounterStore().Store);
|
||||||
|
|
||||||
for (int attempt = 0; attempt < 10; attempt++)
|
for (int attempt = 0; attempt < 10; attempt++)
|
||||||
{
|
{
|
||||||
@@ -199,6 +200,6 @@ public sealed class LoginAttemptGuardTests
|
|||||||
private static LoginAttemptGuard NewGuard(Func<DateTimeOffset> clock) =>
|
private static LoginAttemptGuard NewGuard(Func<DateTimeOffset> clock) =>
|
||||||
new(
|
new(
|
||||||
new RateLimitOptions { Enabled = true },
|
new RateLimitOptions { Enabled = true },
|
||||||
new FakeRateLimitCounterStore(),
|
new TestRateLimitCounterStore().Store,
|
||||||
clock);
|
clock);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-15
@@ -1,5 +1,6 @@
|
|||||||
using Deal.Infrastructure.Tenancy;
|
using Deal.Infrastructure.Tenancy;
|
||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
@@ -20,7 +21,7 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MigrateAllAsync_EmptyRegistry_ReturnsEmptySummary()
|
public async Task MigrateAllAsync_EmptyRegistry_ReturnsEmptySummary()
|
||||||
{
|
{
|
||||||
var service = NewService(new FakeTenantRepository(), new FakeTenantProvisioner());
|
var service = NewService(new FakeTenantRepository(), new TestTenantProvisioner().Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -33,11 +34,11 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MigrateAllAsync_ProvisionsEveryTenantSchema()
|
public async Task MigrateAllAsync_ProvisionsEveryTenantSchema()
|
||||||
{
|
{
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var repository = new FakeTenantRepository(
|
var repository = new FakeTenantRepository(
|
||||||
TenantRecord(TenantA, "A"),
|
TenantRecord(TenantA, "A"),
|
||||||
TenantRecord(TenantB, "B"));
|
TenantRecord(TenantB, "B"));
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -54,11 +55,11 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
public async Task MigrateAllAsync_OneSchemaFails_ContinuesAndReportsFailed()
|
public async Task MigrateAllAsync_OneSchemaFails_ContinuesAndReportsFailed()
|
||||||
{
|
{
|
||||||
string failingSchema = $"tenant_{TenantA:N}";
|
string failingSchema = $"tenant_{TenantA:N}";
|
||||||
var provisioner = new FailingTenantProvisioner(failingSchema);
|
var provisioner = new TestTenantProvisioner(failingSchema);
|
||||||
var repository = new FakeTenantRepository(
|
var repository = new FakeTenantRepository(
|
||||||
TenantRecord(TenantA, "A"),
|
TenantRecord(TenantA, "A"),
|
||||||
TenantRecord(TenantB, "B"));
|
TenantRecord(TenantB, "B"));
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -74,9 +75,9 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MigrateAllAsync_MultiplePages_ReadsEveryTenant()
|
public async Task MigrateAllAsync_MultiplePages_ReadsEveryTenant()
|
||||||
{
|
{
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var repository = new FakeTenantRepository(Records(5));
|
var repository = new FakeTenantRepository(Records(5));
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(4, 2, CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(4, 2, CancellationToken.None);
|
||||||
|
|
||||||
@@ -95,9 +96,9 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MigrateAllAsync_PageSizeOne_ReadsEveryTenant()
|
public async Task MigrateAllAsync_PageSizeOne_ReadsEveryTenant()
|
||||||
{
|
{
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var repository = new FakeTenantRepository(Records(4));
|
var repository = new FakeTenantRepository(Records(4));
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(2, 1, CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(2, 1, CancellationToken.None);
|
||||||
|
|
||||||
@@ -116,9 +117,9 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
{
|
{
|
||||||
TenantRecordDto[] records = Records(3);
|
TenantRecordDto[] records = Records(3);
|
||||||
string failingSchema = $"tenant_{records[0].Id:N}";
|
string failingSchema = $"tenant_{records[0].Id:N}";
|
||||||
var provisioner = new FailingTenantProvisioner(failingSchema);
|
var provisioner = new TestTenantProvisioner(failingSchema);
|
||||||
var repository = new FakeTenantRepository(records);
|
var repository = new FakeTenantRepository(records);
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(1, 2, CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(1, 2, CancellationToken.None);
|
||||||
|
|
||||||
@@ -134,9 +135,9 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MigrateAllAsync_NonPositiveParameters_ClampToMinimum()
|
public async Task MigrateAllAsync_NonPositiveParameters_ClampToMinimum()
|
||||||
{
|
{
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var repository = new FakeTenantRepository(Records(3));
|
var repository = new FakeTenantRepository(Records(3));
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(0, 0, CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(0, 0, CancellationToken.None);
|
||||||
|
|
||||||
@@ -150,11 +151,11 @@ public sealed class TenantSchemaMigrationServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MigrateAllAsync_WithExplicitParallelism_ProvisionsEveryTenant()
|
public async Task MigrateAllAsync_WithExplicitParallelism_ProvisionsEveryTenant()
|
||||||
{
|
{
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var repository = new FakeTenantRepository(
|
var repository = new FakeTenantRepository(
|
||||||
TenantRecord(TenantA, "A"),
|
TenantRecord(TenantA, "A"),
|
||||||
TenantRecord(TenantB, "B"));
|
TenantRecord(TenantB, "B"));
|
||||||
var service = NewService(repository, provisioner);
|
var service = NewService(repository, provisioner.Provisioner);
|
||||||
|
|
||||||
TenantMigrationSummary summary = await service.MigrateAllAsync(2, CancellationToken.None);
|
TenantMigrationSummary summary = await service.MigrateAllAsync(2, CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
|
||||||
using Deal.SharedKernel.Tenants.Models;
|
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-memory реализация <see cref="ITenantProvisioner"/> для тестов пакетной миграции
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FailingTenantProvisioner : ITenantProvisioner
|
|
||||||
{
|
|
||||||
private readonly HashSet<string> _failingSchemaNames;
|
|
||||||
private readonly List<string> _provisionedSchemaNames = [];
|
|
||||||
private readonly object _gate = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создаёт провижинер, роняющий провижининг указанных схем.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="failingSchemaNames">Имена схем (tenant_<id>), провижининг которых бросает исключение.</param>
|
|
||||||
public FailingTenantProvisioner(params string[] failingSchemaNames)
|
|
||||||
{
|
|
||||||
_failingSchemaNames = new HashSet<string>(failingSchemaNames, StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Имена успешно провижиненных схем в порядке вызовов.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<string> ProvisionedSchemaNames
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
return _provisionedSchemaNames.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task ProvisionAsync(TenantId tenantId, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (_failingSchemaNames.Contains(tenantId.SchemaName))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Тест: схема {tenantId.SchemaName} недоступна");
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_provisionedSchemaNames.Add(tenantId.SchemaName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-memory реализация <see cref="IRateLimitCounterStore"/> для юнит/HTTP-тестов
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FakeRateLimitCounterStore : IRateLimitCounterStore
|
|
||||||
{
|
|
||||||
private sealed class Row
|
|
||||||
{
|
|
||||||
public DateTimeOffset WindowStart { get; set; }
|
|
||||||
|
|
||||||
public DateTimeOffset ExpiresAt { get; set; }
|
|
||||||
|
|
||||||
public int Count { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<string, Row> _rows = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<int> IncrementAsync(
|
|
||||||
string key,
|
|
||||||
DateTimeOffset windowStart,
|
|
||||||
DateTimeOffset windowEnd,
|
|
||||||
int amount,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (!_rows.TryGetValue(key, out Row? row) || row.WindowStart != windowStart)
|
|
||||||
{
|
|
||||||
row = new Row { WindowStart = windowStart, ExpiresAt = windowEnd, Count = amount };
|
|
||||||
_rows[key] = row;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
row.Count += amount;
|
|
||||||
row.ExpiresAt = windowEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(row.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<int> GetCountAsync(
|
|
||||||
string key,
|
|
||||||
DateTimeOffset windowStart,
|
|
||||||
CancellationToken ct)
|
|
||||||
=> Task.FromResult(_rows.TryGetValue(key, out Row? row) && row.WindowStart == windowStart ? row.Count : 0);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task ResetAsync(string key, CancellationToken ct)
|
|
||||||
{
|
|
||||||
_rows.Remove(key);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<int> DeleteExpiredAsync(DateTimeOffset now, CancellationToken ct)
|
|
||||||
{
|
|
||||||
List<string> expired = _rows.Where(pair => pair.Value.ExpiresAt < now).Select(pair => pair.Key).ToList();
|
|
||||||
foreach (string key in expired)
|
|
||||||
{
|
|
||||||
_rows.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(expired.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Число заведённых счётчиков
|
|
||||||
/// </summary>
|
|
||||||
public int Count => _rows.Count;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
|
||||||
using Deal.SharedKernel.Tenants.Models;
|
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-memory реализация <see cref="ITenantProvisioner"/> для unit/HTTP-тестов join-потока
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FakeTenantProvisioner : ITenantProvisioner
|
|
||||||
{
|
|
||||||
private readonly List<string> _provisionedSchemaNames = [];
|
|
||||||
private readonly object _gate = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Имена провижиненных схем
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<string> ProvisionedSchemaNames
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
return _provisionedSchemaNames.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task ProvisionAsync(TenantId tenantId, CancellationToken ct)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_provisionedSchemaNames.Add(tenantId.SchemaName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,11 +27,11 @@ public sealed class JoinFlowTests
|
|||||||
{
|
{
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var passwordHasher = TestHashers.New();
|
var passwordHasher = TestHashers.New();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, passwordHasher);
|
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, passwordHasher);
|
||||||
|
|
||||||
// Email с регистром/пробелами нормализуется и совпадает с инвайтом; name задаёт имя нового тенанта.
|
// Email с регистром/пробелами нормализуется и совпадает с инвайтом; name задаёт имя нового тенанта.
|
||||||
var result = await service.ActivateAsync(Code, " NEW-USER@Example.COM ", "Acme", Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, " NEW-USER@Example.COM ", "Acme", Password, CancellationToken.None);
|
||||||
@@ -70,7 +70,7 @@ public sealed class JoinFlowTests
|
|||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), new FakeAuthStore(), TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: " ", Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: " ", Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -84,11 +84,11 @@ public sealed class JoinFlowTests
|
|||||||
var tenantId = Guid.NewGuid();
|
var tenantId = Guid.NewGuid();
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
|
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
|
||||||
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
||||||
var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
|
var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
||||||
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ public sealed class JoinFlowTests
|
|||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(WrongCode, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(WrongCode, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ public sealed class JoinFlowTests
|
|||||||
{
|
{
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
|
||||||
var service = NewService(inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), TestHashers.New());
|
var service = NewService(inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ public sealed class JoinFlowTests
|
|||||||
{
|
{
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
|
||||||
var service = NewService(inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), TestHashers.New());
|
var service = NewService(inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, OtherEmail, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, OtherEmail, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -223,8 +223,8 @@ public sealed class JoinFlowTests
|
|||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash"));
|
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash"));
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -265,10 +265,10 @@ public sealed class JoinFlowTests
|
|||||||
{
|
{
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
JoinResultDto first = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
JoinResultDto first = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
JoinResultDto second = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
JoinResultDto second = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
@@ -288,8 +288,8 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -310,8 +310,8 @@ public sealed class JoinFlowTests
|
|||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New());
|
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||||
|
|
||||||
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -355,7 +355,7 @@ public sealed class JoinFlowTests
|
|||||||
private static JoinService NewService(
|
private static JoinService NewService(
|
||||||
FakeInviteStore inviteStore,
|
FakeInviteStore inviteStore,
|
||||||
FakeTenantStore tenantStore,
|
FakeTenantStore tenantStore,
|
||||||
FakeTenantProvisioner provisioner,
|
ITenantProvisioner provisioner,
|
||||||
FakeAuthStore authStore,
|
FakeAuthStore authStore,
|
||||||
IPasswordHasher passwordHasher) =>
|
IPasswordHasher passwordHasher) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class TenantAdminServiceTests
|
|||||||
public async Task CreateAsync_WithName_CreatesTenantAndProvisionsSchema()
|
public async Task CreateAsync_WithName_CreatesTenantAndProvisionsSchema()
|
||||||
{
|
{
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var service = NewService(tenantStore, new FakeAuthStore(), provisioner);
|
var service = NewService(tenantStore, new FakeAuthStore(), provisioner);
|
||||||
|
|
||||||
TenantCreateResultDto result = await service.CreateAsync(" Новый тенант ", email: null, CancellationToken.None);
|
TenantCreateResultDto result = await service.CreateAsync(" Новый тенант ", email: null, CancellationToken.None);
|
||||||
@@ -44,7 +44,7 @@ public sealed class TenantAdminServiceTests
|
|||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var hasher = TestHashers.New();
|
var hasher = TestHashers.New();
|
||||||
var service = NewService(tenantStore, authStore, new FakeTenantProvisioner(), hasher);
|
var service = NewService(tenantStore, authStore, new TestTenantProvisioner(), hasher);
|
||||||
|
|
||||||
TenantCreateResultDto result = await service.CreateAsync("Тенант с владельцем", " Owner@Example.COM ", CancellationToken.None);
|
TenantCreateResultDto result = await service.CreateAsync("Тенант с владельцем", " Owner@Example.COM ", CancellationToken.None);
|
||||||
|
|
||||||
@@ -200,10 +200,10 @@ public sealed class TenantAdminServiceTests
|
|||||||
private static TenantAdminService NewService(
|
private static TenantAdminService NewService(
|
||||||
FakeTenantStore tenantStore,
|
FakeTenantStore tenantStore,
|
||||||
FakeAuthStore authStore,
|
FakeAuthStore authStore,
|
||||||
FakeTenantProvisioner? provisioner = null,
|
TestTenantProvisioner? provisioner = null,
|
||||||
IPasswordHasher? hasher = null)
|
IPasswordHasher? hasher = null)
|
||||||
{
|
{
|
||||||
var tenantService = new TenantService(tenantStore, provisioner ?? new FakeTenantProvisioner());
|
var tenantService = new TenantService(tenantStore, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner);
|
||||||
return new TenantAdminService(tenantStore, authStore, tenantService, hasher ?? TestHashers.New());
|
return new TenantAdminService(tenantStore, authStore, tenantService, hasher ?? TestHashers.New());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ public sealed class JoinEndpointHttpTests
|
|||||||
inviteStore.AddInvite(NewInvite(Email));
|
inviteStore.AddInvite(NewInvite(Email));
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var auditStore = new FakeAuditLogStore();
|
var auditStore = new FakeAuditLogStore();
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, tenantStore, provisioner, authStore, auditStore,
|
inviteStore, tenantStore, provisioner.Provisioner, authStore, auditStore,
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -83,7 +83,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), authStore, new FakeAuditLogStore(),
|
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using (HttpResponseMessage first = await PostJsonAsync(
|
using (HttpResponseMessage first = await PostJsonAsync(
|
||||||
@@ -111,7 +111,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), authStore, new FakeAuditLogStore(),
|
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -132,7 +132,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
inviteStore.AddInvite(NewInvite(Email, status: InviteStatuses.Revoked));
|
inviteStore.AddInvite(NewInvite(Email, status: InviteStatuses.Revoked));
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), new FakeAuditLogStore(),
|
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -150,7 +150,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
inviteStore.AddInvite(NewInvite(Email, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
inviteStore.AddInvite(NewInvite(Email, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), new FakeAuditLogStore(),
|
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -165,7 +165,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
public async Task Join_WithUnknownCode_Returns400NotFound()
|
public async Task Join_WithUnknownCode_Returns400NotFound()
|
||||||
{
|
{
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
new FakeInviteStore(), new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), new FakeAuditLogStore(),
|
new FakeInviteStore(), new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -183,7 +183,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
inviteStore.AddInvite(NewInvite(Email));
|
inviteStore.AddInvite(NewInvite(Email));
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), new FakeAuditLogStore(),
|
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -205,7 +205,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: "active", "hash"));
|
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: "active", "hash"));
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), authStore, new FakeAuditLogStore(),
|
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -227,11 +227,11 @@ public sealed class JoinEndpointHttpTests
|
|||||||
inviteStore.AddInvite(NewInvite(Email, tenantId: tenantId));
|
inviteStore.AddInvite(NewInvite(Email, tenantId: tenantId));
|
||||||
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
|
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
|
||||||
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new TestTenantProvisioner();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
|
|
||||||
await RunAsync(
|
await RunAsync(
|
||||||
inviteStore, tenantStore, provisioner, authStore, new FakeAuditLogStore(),
|
inviteStore, tenantStore, provisioner.Provisioner, authStore, new FakeAuditLogStore(),
|
||||||
async (baseAddress, client) =>
|
async (baseAddress, client) =>
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await PostJsonAsync(
|
using HttpResponseMessage response = await PostJsonAsync(
|
||||||
@@ -251,7 +251,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
private static async Task RunAsync(
|
private static async Task RunAsync(
|
||||||
FakeInviteStore inviteStore,
|
FakeInviteStore inviteStore,
|
||||||
FakeTenantStore tenantStore,
|
FakeTenantStore tenantStore,
|
||||||
FakeTenantProvisioner provisioner,
|
ITenantProvisioner provisioner,
|
||||||
FakeAuthStore authStore,
|
FakeAuthStore authStore,
|
||||||
FakeAuditLogStore auditStore,
|
FakeAuditLogStore auditStore,
|
||||||
Func<string, HttpClient, Task> scenario)
|
Func<string, HttpClient, Task> scenario)
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ internal static class OperatorAuthHttpHost
|
|||||||
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
|
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
|
||||||
builder.Services.AddScoped<TelegramKeysService>();
|
builder.Services.AddScoped<TelegramKeysService>();
|
||||||
builder.Services.AddSingleton<ITokenUsageEventStore>(effectiveTokenUsageStore);
|
builder.Services.AddSingleton<ITokenUsageEventStore>(effectiveTokenUsageStore);
|
||||||
builder.Services.AddSingleton<ITenantProvisioner, FakeTenantProvisioner>();
|
builder.Services.AddSingleton(_ => new TestTenantProvisioner().Provisioner);
|
||||||
builder.Services.AddScoped<TenantSchemaMigrationService>();
|
builder.Services.AddScoped<TenantSchemaMigrationService>();
|
||||||
// Опции кук — по умолчанию (deal_session/deal_operator_session, без конфиг-секции в тесте).
|
// Опции кук — по умолчанию (deal_session/deal_operator_session, без конфиг-секции в тесте).
|
||||||
builder.Services.AddOptions<TenantCookieOptions>();
|
builder.Services.AddOptions<TenantCookieOptions>();
|
||||||
@@ -209,7 +209,7 @@ internal static class OperatorAuthHttpHost
|
|||||||
builder.Services.AddSingleton<ServiceHealthProbe>();
|
builder.Services.AddSingleton<ServiceHealthProbe>();
|
||||||
builder.Services.AddSingleton<Deal.Api.Observability.RuntimeDepthsCollector>();
|
builder.Services.AddSingleton<Deal.Api.Observability.RuntimeDepthsCollector>();
|
||||||
builder.Services.AddSingleton(effectiveRateLimitOptions);
|
builder.Services.AddSingleton(effectiveRateLimitOptions);
|
||||||
builder.Services.AddSingleton<IRateLimitCounterStore>(new FakeRateLimitCounterStore());
|
builder.Services.AddSingleton<IRateLimitCounterStore>(new TestRateLimitCounterStore().Store);
|
||||||
builder.Services.AddScoped<LoginAttemptGuard>();
|
builder.Services.AddScoped<LoginAttemptGuard>();
|
||||||
builder.Services.AddScoped<SuspiciousActivityReporter>();
|
builder.Services.AddScoped<SuspiciousActivityReporter>();
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ public sealed class RateLimitHttpTests
|
|||||||
|
|
||||||
if (options.Enabled)
|
if (options.Enabled)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton<IRateLimitCounterStore>(new FakeRateLimitCounterStore());
|
builder.Services.AddSingleton<IRateLimitCounterStore>(new TestRateLimitCounterStore().Store);
|
||||||
builder.Services.AddDealRateLimiter(options);
|
builder.Services.AddDealRateLimiter(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ internal static class TelegramIngressTestHost
|
|||||||
});
|
});
|
||||||
if (rateLimitOptions is { Enabled: true })
|
if (rateLimitOptions is { Enabled: true })
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton<IRateLimitCounterStore>(new FakeRateLimitCounterStore());
|
builder.Services.AddSingleton<IRateLimitCounterStore>(new TestRateLimitCounterStore().Store);
|
||||||
builder.Services.AddSingleton(provider =>
|
builder.Services.AddSingleton(provider =>
|
||||||
IngressRateLimitInterceptor.CreateLimiter(
|
IngressRateLimitInterceptor.CreateLimiter(
|
||||||
provider.GetRequiredService<IServiceScopeFactory>(),
|
provider.GetRequiredService<IServiceScopeFactory>(),
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка <see cref="IRateLimitCounterStore"/> на словаре: сервисы получают NSubstitute-подставку
|
||||||
|
/// (<see cref="Store"/>), окно счётчика сбрасывается при смене начала периода.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestRateLimitCounterStore
|
||||||
|
{
|
||||||
|
private sealed class Row
|
||||||
|
{
|
||||||
|
public DateTimeOffset WindowStart { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Row> _rows = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта счётчиков (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public IRateLimitCounterStore Store { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт подставку с пустым словарём.
|
||||||
|
/// </summary>
|
||||||
|
public TestRateLimitCounterStore()
|
||||||
|
{
|
||||||
|
Store = Substitute.For<IRateLimitCounterStore>();
|
||||||
|
Store.IncrementAsync(Arg.Any<string>(), Arg.Any<DateTimeOffset>(), Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => Increment(ci.ArgAt<string>(0), ci.ArgAt<DateTimeOffset>(1), ci.ArgAt<DateTimeOffset>(2), ci.ArgAt<int>(3)));
|
||||||
|
Store.GetCountAsync(Arg.Any<string>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => GetCount(ci.ArgAt<string>(0), ci.ArgAt<DateTimeOffset>(1)));
|
||||||
|
Store.When(s => s.ResetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||||
|
.Do(ci => _rows.Remove(ci.ArgAt<string>(0)));
|
||||||
|
Store.DeleteExpiredAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => DeleteExpired(ci.ArgAt<DateTimeOffset>(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Increment(string key, DateTimeOffset windowStart, DateTimeOffset windowEnd, int amount)
|
||||||
|
{
|
||||||
|
if (!_rows.TryGetValue(key, out Row? row) || row.WindowStart != windowStart)
|
||||||
|
{
|
||||||
|
row = new Row { WindowStart = windowStart, ExpiresAt = windowEnd, Count = amount };
|
||||||
|
_rows[key] = row;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
row.Count += amount;
|
||||||
|
row.ExpiresAt = windowEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetCount(string key, DateTimeOffset windowStart)
|
||||||
|
=> _rows.TryGetValue(key, out Row? row) && row.WindowStart == windowStart ? row.Count : 0;
|
||||||
|
|
||||||
|
private int DeleteExpired(DateTimeOffset now)
|
||||||
|
{
|
||||||
|
List<string> expired = _rows.Where(pair => pair.Value.ExpiresAt < now).Select(pair => pair.Key).ToList();
|
||||||
|
foreach (string key in expired)
|
||||||
|
{
|
||||||
|
_rows.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expired.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
using Deal.SharedKernel.Tenants.Models;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка <see cref="ITenantProvisioner"/> с журналом провижиненных схем: сервис получает
|
||||||
|
/// NSubstitute-подставку (<see cref="Provisioner"/>), тесты проверяют журнал через свойства.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestTenantProvisioner
|
||||||
|
{
|
||||||
|
private readonly List<string> _provisionedSchemaNames = [];
|
||||||
|
private readonly HashSet<string> _failingSchemaNames;
|
||||||
|
private readonly object _gate = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта провижининга (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public ITenantProvisioner Provisioner { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Имена провижиненных схем
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> ProvisionedSchemaNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
return _provisionedSchemaNames.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт подставку, записывающую схему каждого вызова.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="failingSchemaNames">Имена схем (tenant_<id>), провижининг которых бросает исключение.</param>
|
||||||
|
public TestTenantProvisioner(params string[] failingSchemaNames)
|
||||||
|
{
|
||||||
|
_failingSchemaNames = new HashSet<string>(failingSchemaNames, StringComparer.Ordinal);
|
||||||
|
Provisioner = Substitute.For<ITenantProvisioner>();
|
||||||
|
Provisioner.When(p => p.ProvisionAsync(Arg.Any<TenantId>(), Arg.Any<CancellationToken>()))
|
||||||
|
.Do(ci =>
|
||||||
|
{
|
||||||
|
string schemaName = ci.Arg<TenantId>().SchemaName;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_failingSchemaNames.Contains(schemaName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Тестовый сбой провижининга схемы {schemaName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_provisionedSchemaNames.Add(schemaName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user