Хелпер 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 readonly FakeAuthStore _store;
|
||||
private readonly FakeTenantStore _tenantStore;
|
||||
private readonly TestTenantStore _tenantStore;
|
||||
private readonly AuthService _service;
|
||||
|
||||
public AuthServiceTests()
|
||||
{
|
||||
var passwordHasher = TestHashers.New();
|
||||
_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)));
|
||||
_service = new AuthService(_store, passwordHasher, _tenantStore);
|
||||
_service = new AuthService(_store, passwordHasher, _tenantStore.Repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -161,12 +161,12 @@ public sealed class AuthServiceTests
|
||||
string rawToken = "suspended-tenant-session";
|
||||
_store.AddSession(new SessionDto(
|
||||
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));
|
||||
|
||||
// Строка сессии не удалена: 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);
|
||||
Assert.NotNull(resumed);
|
||||
Assert.Equal(UserId, resumed!.Id);
|
||||
@@ -206,8 +206,8 @@ public sealed class AuthServiceTests
|
||||
var passwordHasher = TestHashers.New();
|
||||
var suspendedStore = new FakeAuthStore();
|
||||
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
||||
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||
var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore);
|
||||
var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||
var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository);
|
||||
|
||||
var result = await suspendedService.LoginAsync(UserLogin, UserPassword, CancellationToken.None);
|
||||
|
||||
@@ -227,8 +227,8 @@ public sealed class AuthServiceTests
|
||||
var passwordHasher = TestHashers.New();
|
||||
var suspendedStore = new FakeAuthStore();
|
||||
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
||||
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||
var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore);
|
||||
var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||
var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository);
|
||||
|
||||
var result = await suspendedService.LoginAsync(UserLogin, "wrong-password", CancellationToken.None);
|
||||
|
||||
@@ -271,7 +271,7 @@ public sealed class AuthServiceTests
|
||||
var secondUserId = Guid.NewGuid();
|
||||
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")));
|
||||
var multiUserService = new AuthService(multiUserStore, passwordHasher, _tenantStore);
|
||||
var multiUserService = new AuthService(multiUserStore, passwordHasher, _tenantStore.Repository);
|
||||
|
||||
// login не задан — берётся первый пользователь тенанта (порядок хранилища = CreatedAt в EF-адаптере).
|
||||
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 crossStore = new FakeAuthStore();
|
||||
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);
|
||||
|
||||
Assert.False(cross.Ok);
|
||||
@@ -318,7 +318,7 @@ public sealed class AuthServiceTests
|
||||
public async Task ImpersonateAsync_ForTenantWithoutUsers_ReturnsTenantHasNoUsers()
|
||||
{
|
||||
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);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class JoinFlowTests
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var passwordHasher = TestHashers.New();
|
||||
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, passwordHasher);
|
||||
@@ -69,7 +69,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
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 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));
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
// Активный тенант уже существует (создан оператором ранее) — 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 service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -107,7 +107,7 @@ public sealed class JoinFlowTests
|
||||
var tenantId = Guid.NewGuid();
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
||||
var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
|
||||
var tenantStore = new TestTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
|
||||
var authStore = new FakeAuthStore();
|
||||
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -125,7 +125,7 @@ public sealed class JoinFlowTests
|
||||
var tenantId = Guid.NewGuid();
|
||||
var inviteStore = new TestInviteStore();
|
||||
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 service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -141,7 +141,7 @@ public sealed class JoinFlowTests
|
||||
public async Task ActivateAsync_WithUnknownCode_ReturnsNotFoundWithoutSideEffects()
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -158,7 +158,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
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 service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -176,7 +176,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
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);
|
||||
|
||||
@@ -189,7 +189,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
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);
|
||||
|
||||
@@ -202,7 +202,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -220,7 +220,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash"));
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
@@ -247,7 +247,7 @@ public sealed class JoinFlowTests
|
||||
{
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -266,7 +266,7 @@ public sealed class JoinFlowTests
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||
|
||||
@@ -287,7 +287,7 @@ public sealed class JoinFlowTests
|
||||
var inviteStore = new TestInviteStore();
|
||||
// Конкурент (отзыв) «успел» до нашего CAS — приглашение уже отозвано, активация не выполнится.
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||
@@ -310,7 +310,7 @@ public sealed class JoinFlowTests
|
||||
var inviteStore = new TestInviteStore();
|
||||
// Конкурентная активация «успела» до нашего CAS — приглашение уже активировано.
|
||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
|
||||
@@ -356,16 +356,16 @@ public sealed class JoinFlowTests
|
||||
// Собирает JoinService на общих фейк-хранилищах (все состояния — свежие).
|
||||
private static JoinService NewService(
|
||||
TestInviteStore inviteStore,
|
||||
FakeTenantStore tenantStore,
|
||||
TestTenantStore tenantStore,
|
||||
ITenantProvisioner provisioner,
|
||||
FakeAuthStore authStore,
|
||||
IPasswordHasher passwordHasher) =>
|
||||
new(
|
||||
new InvitesService(inviteStore.Store),
|
||||
new TenantService(tenantStore, provisioner),
|
||||
new TenantService(tenantStore.Repository, provisioner),
|
||||
authStore,
|
||||
passwordHasher,
|
||||
tenantStore);
|
||||
tenantStore.Repository);
|
||||
|
||||
// Создаёт приглашение-строку для сидирования (дефолт — живой pending без тенанта).
|
||||
private static InviteDto NewInvite(
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithName_CreatesTenantAndProvisionsSchema()
|
||||
{
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var service = NewService(tenantStore, new FakeAuthStore(), provisioner);
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithEmail_CreatesOwnerWithOneTimePassword()
|
||||
{
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
var hasher = TestHashers.New();
|
||||
var service = NewService(tenantStore, authStore, new TestTenantProvisioner(), hasher);
|
||||
@@ -65,7 +65,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithTakenEmail_ReturnsEmailTakenWithoutTenant()
|
||||
{
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var authStore = new FakeAuthStore();
|
||||
authStore.AddUser(NewUser(Guid.NewGuid(), "owner@example.com"));
|
||||
var service = NewService(tenantStore, authStore);
|
||||
@@ -81,7 +81,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithInvalidEmail_ReturnsInvalidEmail()
|
||||
{
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var service = NewService(tenantStore, new FakeAuthStore());
|
||||
|
||||
TenantCreateResultDto result = await service.CreateAsync("Тенант", "not-an-email", CancellationToken.None);
|
||||
@@ -94,7 +94,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
public async Task CreateAsync_WithEmptyName_ReturnsNameRequired()
|
||||
{
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var service = NewService(tenantStore, new FakeAuthStore());
|
||||
|
||||
TenantCreateResultDto result = await service.CreateAsync(" ", email: null, CancellationToken.None);
|
||||
@@ -107,7 +107,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
public async Task ListAsync_ReturnsTenantsWithUserCounts()
|
||||
{
|
||||
var tenantStore = new FakeTenantStore(
|
||||
var tenantStore = new TestTenantStore(
|
||||
new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)),
|
||||
new TenantRecordDto(SecondTenantId, "Второй", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1)));
|
||||
var authStore = new FakeAuthStore();
|
||||
@@ -131,7 +131,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
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();
|
||||
authStore.AddUser(NewUser(FirstTenantId, "a@example.com"));
|
||||
authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com"));
|
||||
@@ -150,7 +150,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
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));
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
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());
|
||||
|
||||
TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None);
|
||||
@@ -173,7 +173,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
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());
|
||||
|
||||
// Повторный suspend приостановленного — Ok без изменения (аудит не дублируется, идемпотентность).
|
||||
@@ -187,7 +187,7 @@ public sealed class TenantAdminServiceTests
|
||||
[Fact]
|
||||
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);
|
||||
|
||||
@@ -198,13 +198,13 @@ public sealed class TenantAdminServiceTests
|
||||
|
||||
// Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку).
|
||||
private static TenantAdminService NewService(
|
||||
FakeTenantStore tenantStore,
|
||||
TestTenantStore tenantStore,
|
||||
FakeAuthStore authStore,
|
||||
TestTenantProvisioner? provisioner = null,
|
||||
IPasswordHasher? hasher = null)
|
||||
{
|
||||
var tenantService = new TenantService(tenantStore, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner);
|
||||
return new TenantAdminService(tenantStore, authStore, tenantService, hasher ?? TestHashers.New());
|
||||
var tenantService = new TenantService(tenantStore.Repository, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner);
|
||||
return new TenantAdminService(tenantStore.Repository, authStore, tenantService, hasher ?? TestHashers.New());
|
||||
}
|
||||
|
||||
// Пользователь тенанта (активный, без хэша — для чтения).
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Email));
|
||||
var authStore = new FakeAuthStore();
|
||||
var tenantStore = new FakeTenantStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var authStore = new FakeAuthStore();
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using (HttpResponseMessage first = await PostJsonAsync(
|
||||
@@ -111,7 +111,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var authStore = new FakeAuthStore();
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -132,7 +132,7 @@ public sealed class JoinEndpointHttpTests
|
||||
inviteStore.AddInvite(NewInvite(Email, status: InviteStatuses.Revoked));
|
||||
|
||||
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) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -150,7 +150,7 @@ public sealed class JoinEndpointHttpTests
|
||||
inviteStore.AddInvite(NewInvite(Email, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
||||
|
||||
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) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -165,7 +165,7 @@ public sealed class JoinEndpointHttpTests
|
||||
public async Task Join_WithUnknownCode_Returns400NotFound()
|
||||
{
|
||||
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) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -183,7 +183,7 @@ public sealed class JoinEndpointHttpTests
|
||||
inviteStore.AddInvite(NewInvite(Email));
|
||||
|
||||
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) =>
|
||||
{
|
||||
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"));
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new FakeTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -226,7 +226,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var inviteStore = new TestInviteStore();
|
||||
inviteStore.AddInvite(NewInvite(Email, tenantId: tenantId));
|
||||
// Активный тенант уже существует (создан оператором ранее) — 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 authStore = new FakeAuthStore();
|
||||
|
||||
@@ -250,7 +250,7 @@ public sealed class JoinEndpointHttpTests
|
||||
// Поднимает in-process Kestrel с /api/join на фейк-хранилищах и прогоняет сценарий.
|
||||
private static async Task RunAsync(
|
||||
TestInviteStore inviteStore,
|
||||
FakeTenantStore tenantStore,
|
||||
TestTenantStore tenantStore,
|
||||
ITenantProvisioner provisioner,
|
||||
FakeAuthStore authStore,
|
||||
FakeAuditLogStore auditStore,
|
||||
@@ -267,7 +267,7 @@ public sealed class JoinEndpointHttpTests
|
||||
builder.Services.AddSingleton<IAuthStore>(authStore);
|
||||
builder.Services.AddSingleton<IAuditLogStore>(auditStore);
|
||||
builder.Services.AddSingleton<IInviteStore>(inviteStore.Store);
|
||||
builder.Services.AddSingleton<ITenantRepository>(tenantStore);
|
||||
builder.Services.AddSingleton<ITenantRepository>(tenantStore.Repository);
|
||||
builder.Services.AddSingleton<ITenantProvisioner>(provisioner);
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
@@ -251,7 +251,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
||||
|
||||
// ─── Хелперы ─────────────────────────────────────────────────────────
|
||||
|
||||
private static FakeTenantStore NewTenantStore() =>
|
||||
private static TestTenantStore NewTenantStore() =>
|
||||
new(
|
||||
new TenantRecordDto(ActiveTenant, "active-tenant", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)),
|
||||
new TenantRecordDto(SuspendedTenant, "suspended-tenant", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1)));
|
||||
|
||||
@@ -88,10 +88,10 @@ internal static class OperatorAuthHttpHost
|
||||
public static async Task RunAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
TestInviteStore? inviteStore = null,
|
||||
FakeTenantStore? tenantStore = null) =>
|
||||
TestTenantStore? tenantStore = null) =>
|
||||
await RunCoreAsync(
|
||||
operatorStore,
|
||||
userStore,
|
||||
@@ -115,10 +115,10 @@ internal static class OperatorAuthHttpHost
|
||||
public static async Task RunAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
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,
|
||||
TestInviteStore? inviteStore = null,
|
||||
FakeTenantStore? tenantStore = null,
|
||||
TestTenantStore? tenantStore = null,
|
||||
FakeTenantLimitStore? limitStore = null,
|
||||
RateLimitOptions? rateLimitOptions = null,
|
||||
FakeTokenUsageEventStore? tokenUsageStore = null) =>
|
||||
@@ -163,16 +163,16 @@ internal static class OperatorAuthHttpHost
|
||||
FakeAuthStore userStore,
|
||||
FakeAuditLogStore? auditStore,
|
||||
TestInviteStore? inviteStore,
|
||||
FakeTenantStore? tenantStore,
|
||||
TestTenantStore? tenantStore,
|
||||
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,
|
||||
FakeTokenUsageEventStore? tokenUsageStore = null,
|
||||
TestGlobalSettingsStore? globalSettingsStore = null)
|
||||
{
|
||||
FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore();
|
||||
TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore();
|
||||
FakeTenantStore effectiveTenantStore = tenantStore ?? new FakeTenantStore();
|
||||
TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore();
|
||||
FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore();
|
||||
FakeTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new FakeTokenUsageEventStore();
|
||||
TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore();
|
||||
@@ -190,7 +190,7 @@ internal static class OperatorAuthHttpHost
|
||||
builder.Services.AddSingleton<IOperatorAuthStore>(operatorStore);
|
||||
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore);
|
||||
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<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store);
|
||||
builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New());
|
||||
|
||||
@@ -92,7 +92,7 @@ public sealed class OperatorHealthEndpointsHttpTests
|
||||
}
|
||||
|
||||
// Прогоняет сценарий на хосте с активным оператором 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);
|
||||
|
||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||
|
||||
@@ -284,11 +284,11 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
||||
$"{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;
|
||||
// второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает).
|
||||
var tenantStore = new FakeTenantStore(
|
||||
var tenantStore = new TestTenantStore(
|
||||
new TenantRecordDto(FirstTenantId, FirstTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow),
|
||||
new TenantRecordDto(SecondTenantId, SecondTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||
var limitStore = new FakeTenantLimitStore();
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
|
||||
public async Task Migrate_WithoutOperatorSession_Returns401()
|
||||
{
|
||||
await RunAsync(
|
||||
new FakeTenantStore(),
|
||||
new TestTenantStore(),
|
||||
async (baseAddress, _, _, _, _, _) =>
|
||||
{
|
||||
HttpClient client = CreateClient(baseAddress);
|
||||
@@ -38,7 +38,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
|
||||
{
|
||||
Guid tenantA = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
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(tenantB, "B", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||
|
||||
@@ -72,7 +72,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
|
||||
}
|
||||
|
||||
// Прогоняет сценарий на хосте с активным оператором 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);
|
||||
|
||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||
|
||||
@@ -301,7 +301,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
public async Task Suspend_AlreadySuspendedTenant_IsOkWithoutDuplicateAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var tenantStore = new FakeTenantStore(
|
||||
var tenantStore = new TestTenantStore(
|
||||
new TenantRecordDto(TenantId, TenantName, TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||
|
||||
await RunAsync(
|
||||
@@ -464,7 +464,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
public async Task Impersonate_WhenTenantHasNoUsers_Returns400()
|
||||
{
|
||||
var emptyTenantId = Guid.NewGuid();
|
||||
var emptyTenantStore = new FakeTenantStore(
|
||||
var emptyTenantStore = new TestTenantStore(
|
||||
new TenantRecordDto(emptyTenantId, "Пустой тенант", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||
|
||||
await RunAsync(
|
||||
@@ -486,13 +486,13 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
|
||||
// Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём.
|
||||
private static Task RunAsync(
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, FakeAuditLogStore, Task> scenario,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, FakeAuditLogStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
FakeTenantStore? tenantStore = null)
|
||||
TestTenantStore? tenantStore = null)
|
||||
{
|
||||
// По умолчанию в реестре — активный целевой тенант (пользователь в нём — NewUserStore).
|
||||
FakeTenantStore effectiveTenantStore = tenantStore ??
|
||||
new FakeTenantStore(new TenantRecordDto(TenantId, TenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||
TestTenantStore effectiveTenantStore = tenantStore ??
|
||||
new TestTenantStore(new TenantRecordDto(TenantId, TenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||
return OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
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