Хелпер Support/TestTenantStore: реестр на списке с Create/UpdateStatus, DI получает .Repository. Потребители (10 файлов), фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user