Перевести FakeTenantStore на NSubstitute
ci / build-test (push) Successful in 2m54s

Хелпер Support/TestTenantStore: реестр на списке с Create/UpdateStatus,
DI получает .Repository. Потребители (10 файлов), фейк удалён,
тесты 1340 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-12 22:44:59 +03:00
parent 3fb63489d6
commit 1db703c93b
11 changed files with 137 additions and 76 deletions
@@ -18,16 +18,16 @@ public sealed class AuthServiceTests
private static readonly Guid UserTenantId = Guid.NewGuid(); private static readonly Guid UserTenantId = Guid.NewGuid();
private readonly FakeAuthStore _store; private readonly FakeAuthStore _store;
private readonly FakeTenantStore _tenantStore; private readonly TestTenantStore _tenantStore;
private readonly AuthService _service; private readonly AuthService _service;
public AuthServiceTests() public AuthServiceTests()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
_store = new FakeAuthStore(); _store = new FakeAuthStore();
_tenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Тестовый тенант", TenantStatuses.Active, DateTimeOffset.UtcNow)); _tenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Тестовый тенант", TenantStatuses.Active, DateTimeOffset.UtcNow));
_store.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); _store.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
_service = new AuthService(_store, passwordHasher, _tenantStore); _service = new AuthService(_store, passwordHasher, _tenantStore.Repository);
} }
[Fact] [Fact]
@@ -161,12 +161,12 @@ public sealed class AuthServiceTests
string rawToken = "suspended-tenant-session"; string rawToken = "suspended-tenant-session";
_store.AddSession(new SessionDto( _store.AddSession(new SessionDto(
SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30))); SessionTokens.HashToken(rawToken), UserId, UserLogin, DateTimeOffset.UtcNow.AddDays(30)));
await _tenantStore.UpdateStatusAsync(UserTenantId, TenantStatuses.Suspended, CancellationToken.None); await _tenantStore.Repository.UpdateStatusAsync(UserTenantId, TenantStatuses.Suspended, CancellationToken.None);
Assert.Null(await _service.ResolveSessionAsync(rawToken, CancellationToken.None)); Assert.Null(await _service.ResolveSessionAsync(rawToken, CancellationToken.None));
// Строка сессии не удалена: resume возвращает доступ тем же токеном. // Строка сессии не удалена: resume возвращает доступ тем же токеном.
await _tenantStore.UpdateStatusAsync(UserTenantId, TenantStatuses.Active, CancellationToken.None); await _tenantStore.Repository.UpdateStatusAsync(UserTenantId, TenantStatuses.Active, CancellationToken.None);
var resumed = await _service.ResolveSessionAsync(rawToken, CancellationToken.None); var resumed = await _service.ResolveSessionAsync(rawToken, CancellationToken.None);
Assert.NotNull(resumed); Assert.NotNull(resumed);
Assert.Equal(UserId, resumed!.Id); Assert.Equal(UserId, resumed!.Id);
@@ -206,8 +206,8 @@ public sealed class AuthServiceTests
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var suspendedStore = new FakeAuthStore(); var suspendedStore = new FakeAuthStore();
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore); var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository);
var result = await suspendedService.LoginAsync(UserLogin, UserPassword, CancellationToken.None); var result = await suspendedService.LoginAsync(UserLogin, UserPassword, CancellationToken.None);
@@ -227,8 +227,8 @@ public sealed class AuthServiceTests
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var suspendedStore = new FakeAuthStore(); var suspendedStore = new FakeAuthStore();
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore); var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository);
var result = await suspendedService.LoginAsync(UserLogin, "wrong-password", CancellationToken.None); var result = await suspendedService.LoginAsync(UserLogin, "wrong-password", CancellationToken.None);
@@ -271,7 +271,7 @@ public sealed class AuthServiceTests
var secondUserId = Guid.NewGuid(); var secondUserId = Guid.NewGuid();
multiUserStore.AddUser(new StoredUserDto(firstUserId, "first@example.com", UserTenantId, "active", passwordHasher.Hash("p1"))); multiUserStore.AddUser(new StoredUserDto(firstUserId, "first@example.com", UserTenantId, "active", passwordHasher.Hash("p1")));
multiUserStore.AddUser(new StoredUserDto(secondUserId, "second@example.com", UserTenantId, "active", passwordHasher.Hash("p2"))); multiUserStore.AddUser(new StoredUserDto(secondUserId, "second@example.com", UserTenantId, "active", passwordHasher.Hash("p2")));
var multiUserService = new AuthService(multiUserStore, passwordHasher, _tenantStore); var multiUserService = new AuthService(multiUserStore, passwordHasher, _tenantStore.Repository);
// login не задан — берётся первый пользователь тенанта (порядок хранилища = CreatedAt в EF-адаптере). // login не задан — берётся первый пользователь тенанта (порядок хранилища = CreatedAt в EF-адаптере).
var result = await multiUserService.ImpersonateAsync(UserTenantId, targetLogin: null, Guid.NewGuid(), CancellationToken.None); var result = await multiUserService.ImpersonateAsync(UserTenantId, targetLogin: null, Guid.NewGuid(), CancellationToken.None);
@@ -296,7 +296,7 @@ public sealed class AuthServiceTests
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var crossStore = new FakeAuthStore(); var crossStore = new FakeAuthStore();
crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p"))); crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p")));
var crossService = new AuthService(crossStore, passwordHasher, _tenantStore); var crossService = new AuthService(crossStore, passwordHasher, _tenantStore.Repository);
var cross = await crossService.ImpersonateAsync(UserTenantId, "cross@example.com", Guid.NewGuid(), CancellationToken.None); var cross = await crossService.ImpersonateAsync(UserTenantId, "cross@example.com", Guid.NewGuid(), CancellationToken.None);
Assert.False(cross.Ok); Assert.False(cross.Ok);
@@ -318,7 +318,7 @@ public sealed class AuthServiceTests
public async Task ImpersonateAsync_ForTenantWithoutUsers_ReturnsTenantHasNoUsers() public async Task ImpersonateAsync_ForTenantWithoutUsers_ReturnsTenantHasNoUsers()
{ {
var emptyTenantId = Guid.NewGuid(); var emptyTenantId = Guid.NewGuid();
await _tenantStore.CreateAsync(new TenantRecordDto(emptyTenantId, "Пустой тенант", TenantStatuses.Active, DateTimeOffset.UtcNow), CancellationToken.None); await _tenantStore.Repository.CreateAsync(new TenantRecordDto(emptyTenantId, "Пустой тенант", TenantStatuses.Active, DateTimeOffset.UtcNow), CancellationToken.None);
var result = await _service.ImpersonateAsync(emptyTenantId, targetLogin: null, Guid.NewGuid(), CancellationToken.None); var result = await _service.ImpersonateAsync(emptyTenantId, targetLogin: null, Guid.NewGuid(), CancellationToken.None);
@@ -28,7 +28,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, passwordHasher); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, passwordHasher);
@@ -69,7 +69,7 @@ public sealed class JoinFlowTests
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, 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);
@@ -86,7 +86,7 @@ public sealed class JoinFlowTests
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему. // Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow)); var tenantStore = new TestTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -107,7 +107,7 @@ public sealed class JoinFlowTests
var tenantId = Guid.NewGuid(); var tenantId = Guid.NewGuid();
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт var tenantStore = new TestTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
@@ -125,7 +125,7 @@ public sealed class JoinFlowTests
var tenantId = Guid.NewGuid(); var tenantId = Guid.NewGuid();
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
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 TestTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
@@ -141,7 +141,7 @@ public sealed class JoinFlowTests
public async Task ActivateAsync_WithUnknownCode_ReturnsNotFoundWithoutSideEffects() public async Task ActivateAsync_WithUnknownCode_ReturnsNotFoundWithoutSideEffects()
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
@@ -158,7 +158,7 @@ public sealed class JoinFlowTests
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
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 TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
@@ -176,7 +176,7 @@ public sealed class JoinFlowTests
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
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 TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New()); var service = NewService(inviteStore, new TestTenantStore(), 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 TestInviteStore(); var inviteStore = new TestInviteStore();
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 TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New()); var service = NewService(inviteStore, new TestTenantStore(), 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);
@@ -202,7 +202,7 @@ public sealed class JoinFlowTests
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
@@ -220,7 +220,7 @@ public sealed class JoinFlowTests
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
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 TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
@@ -247,7 +247,7 @@ public sealed class JoinFlowTests
{ {
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
@@ -266,7 +266,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -287,7 +287,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
// Конкурент (отзыв) «успел» до нашего CAS — приглашение уже отозвано, активация не выполнится. // Конкурент (отзыв) «успел» до нашего CAS — приглашение уже отозвано, активация не выполнится.
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -310,7 +310,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
// Конкурентная активация «успела» до нашего CAS — приглашение уже активировано. // Конкурентная активация «успела» до нашего CAS — приглашение уже активировано.
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated)); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -356,16 +356,16 @@ public sealed class JoinFlowTests
// Собирает JoinService на общих фейк-хранилищах (все состояния — свежие). // Собирает JoinService на общих фейк-хранилищах (все состояния — свежие).
private static JoinService NewService( private static JoinService NewService(
TestInviteStore inviteStore, TestInviteStore inviteStore,
FakeTenantStore tenantStore, TestTenantStore tenantStore,
ITenantProvisioner provisioner, ITenantProvisioner provisioner,
FakeAuthStore authStore, FakeAuthStore authStore,
IPasswordHasher passwordHasher) => IPasswordHasher passwordHasher) =>
new( new(
new InvitesService(inviteStore.Store), new InvitesService(inviteStore.Store),
new TenantService(tenantStore, provisioner), new TenantService(tenantStore.Repository, provisioner),
authStore, authStore,
passwordHasher, passwordHasher,
tenantStore); tenantStore.Repository);
// Создаёт приглашение-строку для сидирования (дефолт — живой pending без тенанта). // Создаёт приглашение-строку для сидирования (дефолт — живой pending без тенанта).
private static InviteDto NewInvite( private static InviteDto NewInvite(
@@ -16,7 +16,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task CreateAsync_WithName_CreatesTenantAndProvisionsSchema() public async Task CreateAsync_WithName_CreatesTenantAndProvisionsSchema()
{ {
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var service = NewService(tenantStore, new FakeAuthStore(), provisioner); var service = NewService(tenantStore, new FakeAuthStore(), provisioner);
@@ -41,7 +41,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task CreateAsync_WithEmail_CreatesOwnerWithOneTimePassword() public async Task CreateAsync_WithEmail_CreatesOwnerWithOneTimePassword()
{ {
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var hasher = TestHashers.New(); var hasher = TestHashers.New();
var service = NewService(tenantStore, authStore, new TestTenantProvisioner(), hasher); var service = NewService(tenantStore, authStore, new TestTenantProvisioner(), hasher);
@@ -65,7 +65,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task CreateAsync_WithTakenEmail_ReturnsEmailTakenWithoutTenant() public async Task CreateAsync_WithTakenEmail_ReturnsEmailTakenWithoutTenant()
{ {
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
authStore.AddUser(NewUser(Guid.NewGuid(), "owner@example.com")); authStore.AddUser(NewUser(Guid.NewGuid(), "owner@example.com"));
var service = NewService(tenantStore, authStore); var service = NewService(tenantStore, authStore);
@@ -81,7 +81,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task CreateAsync_WithInvalidEmail_ReturnsInvalidEmail() public async Task CreateAsync_WithInvalidEmail_ReturnsInvalidEmail()
{ {
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var service = NewService(tenantStore, new FakeAuthStore()); var service = NewService(tenantStore, new FakeAuthStore());
TenantCreateResultDto result = await service.CreateAsync("Тенант", "not-an-email", CancellationToken.None); TenantCreateResultDto result = await service.CreateAsync("Тенант", "not-an-email", CancellationToken.None);
@@ -94,7 +94,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task CreateAsync_WithEmptyName_ReturnsNameRequired() public async Task CreateAsync_WithEmptyName_ReturnsNameRequired()
{ {
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var service = NewService(tenantStore, new FakeAuthStore()); var service = NewService(tenantStore, new FakeAuthStore());
TenantCreateResultDto result = await service.CreateAsync(" ", email: null, CancellationToken.None); TenantCreateResultDto result = await service.CreateAsync(" ", email: null, CancellationToken.None);
@@ -107,7 +107,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task ListAsync_ReturnsTenantsWithUserCounts() public async Task ListAsync_ReturnsTenantsWithUserCounts()
{ {
var tenantStore = new FakeTenantStore( var tenantStore = new TestTenantStore(
new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)), new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)),
new TenantRecordDto(SecondTenantId, "Второй", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1))); new TenantRecordDto(SecondTenantId, "Второй", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1)));
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
@@ -131,7 +131,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task GetAsync_ForExistingTenant_ReturnsDetailWithUsers() public async Task GetAsync_ForExistingTenant_ReturnsDetailWithUsers()
{ {
var tenantStore = new FakeTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow)); var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow));
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
authStore.AddUser(NewUser(FirstTenantId, "a@example.com")); authStore.AddUser(NewUser(FirstTenantId, "a@example.com"));
authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com")); authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com"));
@@ -150,7 +150,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task GetAsync_ForUnknownTenant_ReturnsNull() public async Task GetAsync_ForUnknownTenant_ReturnsNull()
{ {
var service = NewService(new FakeTenantStore(), new FakeAuthStore()); var service = NewService(new TestTenantStore(), new FakeAuthStore());
Assert.Null(await service.GetAsync(Guid.NewGuid(), CancellationToken.None)); Assert.Null(await service.GetAsync(Guid.NewGuid(), CancellationToken.None));
} }
@@ -158,7 +158,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task ChangeStatusAsync_Suspend_UpdatesTenantAndReportsChanged() public async Task ChangeStatusAsync_Suspend_UpdatesTenantAndReportsChanged()
{ {
var tenantStore = new FakeTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow)); var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow));
var service = NewService(tenantStore, new FakeAuthStore()); var service = NewService(tenantStore, new FakeAuthStore());
TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None); TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None);
@@ -173,7 +173,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task ChangeStatusAsync_WhenAlreadyInStatus_ReturnsOkWithoutChange() public async Task ChangeStatusAsync_WhenAlreadyInStatus_ReturnsOkWithoutChange()
{ {
var tenantStore = new FakeTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
var service = NewService(tenantStore, new FakeAuthStore()); var service = NewService(tenantStore, new FakeAuthStore());
// Повторный suspend приостановленного — Ok без изменения (аудит не дублируется, идемпотентность). // Повторный suspend приостановленного — Ok без изменения (аудит не дублируется, идемпотентность).
@@ -187,7 +187,7 @@ public sealed class TenantAdminServiceTests
[Fact] [Fact]
public async Task ChangeStatusAsync_ForUnknownTenant_ReturnsNotFound() public async Task ChangeStatusAsync_ForUnknownTenant_ReturnsNotFound()
{ {
var service = NewService(new FakeTenantStore(), new FakeAuthStore()); var service = NewService(new TestTenantStore(), new FakeAuthStore());
TenantStatusChangeResultDto result = await service.ChangeStatusAsync(Guid.NewGuid(), TenantStatuses.Suspended, CancellationToken.None); TenantStatusChangeResultDto result = await service.ChangeStatusAsync(Guid.NewGuid(), TenantStatuses.Suspended, CancellationToken.None);
@@ -198,13 +198,13 @@ public sealed class TenantAdminServiceTests
// Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку). // Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку).
private static TenantAdminService NewService( private static TenantAdminService NewService(
FakeTenantStore tenantStore, TestTenantStore tenantStore,
FakeAuthStore authStore, FakeAuthStore authStore,
TestTenantProvisioner? provisioner = null, TestTenantProvisioner? provisioner = null,
IPasswordHasher? hasher = null) IPasswordHasher? hasher = null)
{ {
var tenantService = new TenantService(tenantStore, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner); var tenantService = new TenantService(tenantStore.Repository, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner);
return new TenantAdminService(tenantStore, authStore, tenantService, hasher ?? TestHashers.New()); return new TenantAdminService(tenantStore.Repository, authStore, tenantService, hasher ?? TestHashers.New());
} }
// Пользователь тенанта (активный, без хэша — для чтения). // Пользователь тенанта (активный, без хэша — для чтения).
@@ -38,7 +38,7 @@ public sealed class JoinEndpointHttpTests
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Email)); inviteStore.AddInvite(NewInvite(Email));
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
var tenantStore = new FakeTenantStore(); var tenantStore = new TestTenantStore();
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var auditStore = new FakeAuditLogStore(); var auditStore = new FakeAuditLogStore();
@@ -83,7 +83,7 @@ public sealed class JoinEndpointHttpTests
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
await RunAsync( await RunAsync(
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(), inviteStore, new TestTenantStore(), 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 TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(), inviteStore, new TestTenantStore(), 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 TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(), inviteStore, new TestTenantStore(), 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 TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(), inviteStore, new TestTenantStore(), 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 TestInviteStore(), new FakeTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(), new TestInviteStore(), new TestTenantStore(), 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 TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(), inviteStore, new TestTenantStore(), 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 TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(), inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
async (baseAddress, client) => async (baseAddress, client) =>
{ {
using HttpResponseMessage response = await PostJsonAsync( using HttpResponseMessage response = await PostJsonAsync(
@@ -226,7 +226,7 @@ public sealed class JoinEndpointHttpTests
var inviteStore = new TestInviteStore(); var inviteStore = new TestInviteStore();
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 TestTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var authStore = new FakeAuthStore(); var authStore = new FakeAuthStore();
@@ -250,7 +250,7 @@ public sealed class JoinEndpointHttpTests
// Поднимает in-process Kestrel с /api/join на фейк-хранилищах и прогоняет сценарий. // Поднимает in-process Kestrel с /api/join на фейк-хранилищах и прогоняет сценарий.
private static async Task RunAsync( private static async Task RunAsync(
TestInviteStore inviteStore, TestInviteStore inviteStore,
FakeTenantStore tenantStore, TestTenantStore tenantStore,
ITenantProvisioner provisioner, ITenantProvisioner provisioner,
FakeAuthStore authStore, FakeAuthStore authStore,
FakeAuditLogStore auditStore, FakeAuditLogStore auditStore,
@@ -267,7 +267,7 @@ public sealed class JoinEndpointHttpTests
builder.Services.AddSingleton<IAuthStore>(authStore); builder.Services.AddSingleton<IAuthStore>(authStore);
builder.Services.AddSingleton<IAuditLogStore>(auditStore); builder.Services.AddSingleton<IAuditLogStore>(auditStore);
builder.Services.AddSingleton<IInviteStore>(inviteStore.Store); builder.Services.AddSingleton<IInviteStore>(inviteStore.Store);
builder.Services.AddSingleton<ITenantRepository>(tenantStore); builder.Services.AddSingleton<ITenantRepository>(tenantStore.Repository);
builder.Services.AddSingleton<ITenantProvisioner>(provisioner); builder.Services.AddSingleton<ITenantProvisioner>(provisioner);
WebApplication app = builder.Build(); WebApplication app = builder.Build();
@@ -251,7 +251,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
// ─── Хелперы ───────────────────────────────────────────────────────── // ─── Хелперы ─────────────────────────────────────────────────────────
private static FakeTenantStore NewTenantStore() => private static TestTenantStore NewTenantStore() =>
new( new(
new TenantRecordDto(ActiveTenant, "active-tenant", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)), new TenantRecordDto(ActiveTenant, "active-tenant", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)),
new TenantRecordDto(SuspendedTenant, "suspended-tenant", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1))); new TenantRecordDto(SuspendedTenant, "suspended-tenant", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1)));
@@ -88,10 +88,10 @@ internal static class OperatorAuthHttpHost
public static async Task RunAsync( public static async Task RunAsync(
FakeOperatorAuthStore operatorStore, FakeOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario,
FakeAuditLogStore? auditStore = null, FakeAuditLogStore? auditStore = null,
TestInviteStore? inviteStore = null, TestInviteStore? inviteStore = null,
FakeTenantStore? tenantStore = null) => TestTenantStore? tenantStore = null) =>
await RunCoreAsync( await RunCoreAsync(
operatorStore, operatorStore,
userStore, userStore,
@@ -115,10 +115,10 @@ internal static class OperatorAuthHttpHost
public static async Task RunAsync( public static async Task RunAsync(
FakeOperatorAuthStore operatorStore, FakeOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario,
FakeAuditLogStore? auditStore = null, FakeAuditLogStore? auditStore = null,
TestInviteStore? inviteStore = null, TestInviteStore? inviteStore = null,
FakeTenantStore? tenantStore = null, TestTenantStore? tenantStore = null,
FakeTenantLimitStore? limitStore = null, FakeTenantLimitStore? limitStore = null,
RateLimitOptions? rateLimitOptions = null, RateLimitOptions? rateLimitOptions = null,
FakeTokenUsageEventStore? tokenUsageStore = null) => FakeTokenUsageEventStore? tokenUsageStore = null) =>
@@ -163,16 +163,16 @@ internal static class OperatorAuthHttpHost
FakeAuthStore userStore, FakeAuthStore userStore,
FakeAuditLogStore? auditStore, FakeAuditLogStore? auditStore,
TestInviteStore? inviteStore, TestInviteStore? inviteStore,
FakeTenantStore? tenantStore, TestTenantStore? tenantStore,
FakeTenantLimitStore? limitStore, FakeTenantLimitStore? limitStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario,
RateLimitOptions? rateLimitOptions = null, RateLimitOptions? rateLimitOptions = null,
FakeTokenUsageEventStore? tokenUsageStore = null, FakeTokenUsageEventStore? tokenUsageStore = null,
TestGlobalSettingsStore? globalSettingsStore = null) TestGlobalSettingsStore? globalSettingsStore = null)
{ {
FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore(); FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore();
TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore(); TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore();
FakeTenantStore effectiveTenantStore = tenantStore ?? new FakeTenantStore(); TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore();
FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore(); FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore();
FakeTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new FakeTokenUsageEventStore(); FakeTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new FakeTokenUsageEventStore();
TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore(); TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore();
@@ -190,7 +190,7 @@ internal static class OperatorAuthHttpHost
builder.Services.AddSingleton<IOperatorAuthStore>(operatorStore); builder.Services.AddSingleton<IOperatorAuthStore>(operatorStore);
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore); builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore);
builder.Services.AddSingleton<IInviteStore>(effectiveInviteStore.Store); builder.Services.AddSingleton<IInviteStore>(effectiveInviteStore.Store);
builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore); builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore.Repository);
builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore); builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore);
builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store); builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store);
builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New()); builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New());
@@ -92,7 +92,7 @@ public sealed class OperatorHealthEndpointsHttpTests
} }
// Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые). // Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые).
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario) => private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario) =>
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario); OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario);
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
@@ -284,11 +284,11 @@ public sealed class OperatorLimitsEndpointsHttpTests
$"{baseAddress}/api/operator/tenants/{tenantId}/limit"; $"{baseAddress}/api/operator/tenants/{tenantId}/limit";
// Прогоняет сценарий на хосте с двумя активными тенантами и предзаполненными строками лимитов. // Прогоняет сценарий на хосте с двумя активными тенантами и предзаполненными строками лимитов.
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario, FakeAuditLogStore? auditStore = null) private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario, FakeAuditLogStore? auditStore = null)
{ {
// Первый тенант: расход 80% месячного бюджета (8 000 000 / 10 000 000) + флаг Warned80; // Первый тенант: расход 80% месячного бюджета (8 000 000 / 10 000 000) + флаг Warned80;
// второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает). // второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает).
var tenantStore = new FakeTenantStore( var tenantStore = new TestTenantStore(
new TenantRecordDto(FirstTenantId, FirstTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow), new TenantRecordDto(FirstTenantId, FirstTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow),
new TenantRecordDto(SecondTenantId, SecondTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow)); new TenantRecordDto(SecondTenantId, SecondTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
var limitStore = new FakeTenantLimitStore(); var limitStore = new FakeTenantLimitStore();
@@ -22,7 +22,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
public async Task Migrate_WithoutOperatorSession_Returns401() public async Task Migrate_WithoutOperatorSession_Returns401()
{ {
await RunAsync( await RunAsync(
new FakeTenantStore(), new TestTenantStore(),
async (baseAddress, _, _, _, _, _) => async (baseAddress, _, _, _, _, _) =>
{ {
HttpClient client = CreateClient(baseAddress); HttpClient client = CreateClient(baseAddress);
@@ -38,7 +38,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
{ {
Guid tenantA = Guid.Parse("11111111-1111-1111-1111-111111111111"); Guid tenantA = Guid.Parse("11111111-1111-1111-1111-111111111111");
Guid tenantB = Guid.Parse("22222222-2222-2222-2222-222222222222"); Guid tenantB = Guid.Parse("22222222-2222-2222-2222-222222222222");
var tenantStore = new FakeTenantStore( var tenantStore = new TestTenantStore(
new TenantRecordDto(tenantA, "A", TenantStatuses.Active, DateTimeOffset.UtcNow), new TenantRecordDto(tenantA, "A", TenantStatuses.Active, DateTimeOffset.UtcNow),
new TenantRecordDto(tenantB, "B", TenantStatuses.Active, DateTimeOffset.UtcNow)); new TenantRecordDto(tenantB, "B", TenantStatuses.Active, DateTimeOffset.UtcNow));
@@ -72,7 +72,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
} }
// Прогоняет сценарий на хосте с активным оператором operator/operator и заданным реестром тенантов. // Прогоняет сценарий на хосте с активным оператором operator/operator и заданным реестром тенантов.
private static Task RunAsync(FakeTenantStore tenantStore, Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario) => private static Task RunAsync(TestTenantStore tenantStore, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario) =>
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario, tenantStore: tenantStore); OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario, tenantStore: tenantStore);
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
@@ -301,7 +301,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
public async Task Suspend_AlreadySuspendedTenant_IsOkWithoutDuplicateAudit() public async Task Suspend_AlreadySuspendedTenant_IsOkWithoutDuplicateAudit()
{ {
var auditStore = new FakeAuditLogStore(); var auditStore = new FakeAuditLogStore();
var tenantStore = new FakeTenantStore( var tenantStore = new TestTenantStore(
new TenantRecordDto(TenantId, TenantName, TenantStatuses.Suspended, DateTimeOffset.UtcNow)); new TenantRecordDto(TenantId, TenantName, TenantStatuses.Suspended, DateTimeOffset.UtcNow));
await RunAsync( await RunAsync(
@@ -464,7 +464,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
public async Task Impersonate_WhenTenantHasNoUsers_Returns400() public async Task Impersonate_WhenTenantHasNoUsers_Returns400()
{ {
var emptyTenantId = Guid.NewGuid(); var emptyTenantId = Guid.NewGuid();
var emptyTenantStore = new FakeTenantStore( var emptyTenantStore = new TestTenantStore(
new TenantRecordDto(emptyTenantId, "Пустой тенант", TenantStatuses.Active, DateTimeOffset.UtcNow)); new TenantRecordDto(emptyTenantId, "Пустой тенант", TenantStatuses.Active, DateTimeOffset.UtcNow));
await RunAsync( await RunAsync(
@@ -486,13 +486,13 @@ public sealed class OperatorTenantsEndpointsHttpTests
// Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём. // Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём.
private static Task RunAsync( private static Task RunAsync(
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, FakeAuditLogStore, Task> scenario, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, FakeAuditLogStore, Task> scenario,
FakeAuditLogStore? auditStore = null, FakeAuditLogStore? auditStore = null,
FakeTenantStore? tenantStore = null) TestTenantStore? tenantStore = null)
{ {
// По умолчанию в реестре — активный целевой тенант (пользователь в нём — NewUserStore). // По умолчанию в реестре — активный целевой тенант (пользователь в нём — NewUserStore).
FakeTenantStore effectiveTenantStore = tenantStore ?? TestTenantStore effectiveTenantStore = tenantStore ??
new FakeTenantStore(new TenantRecordDto(TenantId, TenantName, TenantStatuses.Active, DateTimeOffset.UtcNow)); new TestTenantStore(new TenantRecordDto(TenantId, TenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
return OperatorAuthHttpHost.RunAsync( return OperatorAuthHttpHost.RunAsync(
NewOperatorStore(), NewOperatorStore(),
NewUserStore(), NewUserStore(),
@@ -0,0 +1,61 @@
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using NSubstitute;
namespace Deal.Tests.Unit.Support;
/// <summary>
/// Подставка <see cref="ITenantRepository"/> с поддержкой создания тенантов: сервисы получают
/// NSubstitute-подставку (<see cref="Repository"/>), тесты сеют/проверяют реестр через
/// <see cref="Seed"/> и <see cref="Tenants"/>.
/// </summary>
public sealed class TestTenantStore
{
private readonly List<TenantRecordDto> _tenants;
/// <summary>
/// Подставка порта реестра тенантов (создаётся в конструкторе).
/// </summary>
public ITenantRepository Repository { get; }
/// <summary>
/// Записи реестра в порядке добавления.
/// </summary>
public IReadOnlyList<TenantRecordDto> Tenants => _tenants;
/// <summary>
/// Создаёт хранилище, опционально с предзаполненным реестром тенантов.
/// </summary>
/// <param name="seed">Тенанты, которые уже есть в реестре (как строки public.tenants).</param>
public TestTenantStore(params TenantRecordDto[] seed)
{
_tenants = seed.ToList();
Repository = Substitute.For<ITenantRepository>();
Repository.FindByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.Returns(ci => _tenants.FirstOrDefault(tenant => tenant.Id == ci.ArgAt<Guid>(0)));
Repository.When(r => r.CreateAsync(Arg.Any<TenantRecordDto>(), Arg.Any<CancellationToken>()))
.Do(ci => _tenants.Add(ci.Arg<TenantRecordDto>()));
Repository.ListAsync(Arg.Any<CancellationToken>())
.Returns(_tenants);
Repository.ListPageAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(ci => (IReadOnlyList<TenantRecordDto>)_tenants.Skip(ci.ArgAt<int>(0)).Take(ci.ArgAt<int>(1)).ToList());
Repository.UpdateStatusAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
int index = _tenants.FindIndex(tenant => tenant.Id == ci.ArgAt<Guid>(0));
if (index < 0)
{
return false;
}
_tenants[index] = _tenants[index] with { Status = ci.ArgAt<string>(1) };
return true;
});
}
/// <summary>
/// Кладёт тенант в реестр напрямую (псевдоним для читаемости сценариев).
/// </summary>
/// <param name="tenant">Тенант.</param>
public void Seed(TenantRecordDto tenant) => _tenants.Add(tenant);
}