diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs index ef39851..9ed88c7 100644 --- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs +++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs @@ -20,7 +20,7 @@ public sealed class DiscoveryBanGuard /// /// Хранилище Discovery (счётчик DiscLog за сутки). /// KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused). - /// Источник текущего времени (UTC; тесты передают фиксированные «часы», эталон FakeDiscoveryStore). По умолчанию — . + /// Источник текущего времени (UTC). По умолчанию — . public DiscoveryBanGuard( IDiscoveryStore store, ISettingsStore settings, diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs deleted file mode 100644 index 8824bee..0000000 --- a/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Deal.Contracts.Integrations.Abstractions; -using Deal.Contracts.Integrations.Models; - -namespace Deal.Tests.Unit.Contracts; - -/// -/// In-memory реализация для unit-тестов файлов карточки -/// -public sealed class FakeFileStorage : IFileStorage -{ - private readonly Dictionary _objects = new(StringComparer.Ordinal); - private readonly Dictionary _contentTypes = new(StringComparer.Ordinal); - private readonly List _deletedKeys = []; - - /// - /// objectKey всех объектов, сохранённых на данный момент - /// - public IReadOnlyList StoredObjectKeys => _objects.Keys.ToList(); - - /// - /// objectKey всех удалений в порядке вызовов DeleteAsync - /// - public IReadOnlyList DeletedKeys => _deletedKeys.ToList(); - - /// - /// Содержимое сохранённого объекта по ключу либо null — объекта нет - /// - /// Ключ объекта (opaque). - public byte[]? ContentOf(string objectKey) - { - return _objects.TryGetValue(objectKey, out byte[]? content) ? content : null; - } - - /// - public Task PutAsync( - string objectKey, - Stream content, - string contentType, - CancellationToken ct) - { - ArgumentNullException.ThrowIfNull(content); - - using MemoryStream buffer = new(); - if (content.CanSeek && content.Position != 0) - { - content.Position = 0; // контракт порта: Put читает всё содержимое с позиции 0 (Ruling T6) - } - - content.CopyTo(buffer); - _objects[objectKey] = buffer.ToArray(); - _contentTypes[objectKey] = contentType; - return Task.FromResult(objectKey); - } - - /// - public Task GetAsync(string objectKey, CancellationToken ct) - { - if (!_objects.TryGetValue(objectKey, out byte[]? content)) - { - return Task.FromResult(null); - } - - return Task.FromResult(new MemoryStream(content)); - } - - /// - public Task StatAsync(string objectKey, CancellationToken ct) - { - if (!_objects.TryGetValue(objectKey, out byte[]? content)) - { - return Task.FromResult(null); - } - - string contentType = _contentTypes.TryGetValue(objectKey, out string? stored) ? stored : string.Empty; - return Task.FromResult(new FileMeta(objectKey, content.Length, contentType)); - } - - /// - public Task DeleteAsync(string objectKey, CancellationToken ct) - { - _deletedKeys.Add(objectKey); - _objects.Remove(objectKey); - return Task.CompletedTask; - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs deleted file mode 100644 index f5662ef..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs +++ /dev/null @@ -1,614 +0,0 @@ -using Deal.Modules.Discovery.Application.Abstractions; -using Deal.Modules.Discovery.Application.Models; - -namespace Deal.Tests.Unit.Modules.Discovery; - -/// -/// In-memory реализация для unit-тестов Discovery. -/// -public sealed class FakeDiscoveryStore : IDiscoveryStore -{ - private readonly List _tasks = []; - private readonly List _candidates = []; - private readonly List _blacklist = []; - private readonly List _log = []; - private readonly HashSet _monitoredIds = new(StringComparer.Ordinal); - private readonly Func _clock; - - /// - /// Создаёт фейк с «часами» по умолчанию - /// - /// Источник текущего времени (по умолчанию ). - public FakeDiscoveryStore(Func? clock = null) - { - _clock = clock ?? (() => DateTimeOffset.UtcNow); - } - - /// - /// Задачи фейка - /// - public IReadOnlyList Tasks => _tasks.ToList(); - - /// - /// Кандидаты фейка - /// - public IReadOnlyList Candidates => _candidates.ToList(); - - /// - /// Чёрный список фейка - /// - public IReadOnlyList Blacklist => _blacklist.ToList(); - - /// - /// Лог фейка в порядке записи - /// - public IReadOnlyList Log => _log.ToList(); - - /// - /// Кладёт задачу напрямую - /// - /// Задача как если бы была прочитана адаптером. - public void SeedTask(DiscoveryTaskDto task) - { - _tasks.Add(task); - } - - /// - /// Кладёт кандидата напрямую - /// - /// Кандидат как если бы был прочитан адаптером. - public void SeedCandidate(DiscoveryCandidateDto candidate) - { - _candidates.Add(candidate); - } - - /// - /// Помечает источник «уже мониторится» — есть в каталоге Dialogs. - /// - /// Подписанный id источника. - public void SeedMonitored(string dialogId) - { - _monitoredIds.Add(dialogId); - } - - /// - /// Кладёт событие лога напрямую - /// - /// Событие (join_auto/…). - /// Момент события (UTC). - /// Id задачи (по умолчанию — произвольный). - public void SeedLog( - string @event, - DateTimeOffset createdAt, - string taskId = "dt_x") - { - _log.Add(new DiscoveryLogDto("dl_x", taskId, @event, string.Empty, createdAt.ToUnixTimeMilliseconds())); - } - - /// - public Task> ListTasksAsync(CancellationToken ct) - { - return Task.FromResult>(_tasks - .OrderBy(task => task.CreatedAt) - .ToList()); - } - - /// - public Task GetTaskAsync(string taskId, CancellationToken ct) - { - return Task.FromResult(_tasks.FirstOrDefault(task => task.Id == taskId)); - } - - /// - public Task CreateTaskAsync(DiscoveryTaskRow row, CancellationToken ct) - { - long nowMs = _clock().ToUnixTimeMilliseconds(); - _tasks.Add(new DiscoveryTaskDto( - row.Id, - row.Name, - row.Description, - row.Keywords.ToList(), - row.MinSubscribers, - row.Lang, - row.Threshold, - row.SampleSize, - row.PlanJoins, - row.AutoJoin, - DiscoveryTaskStatuses.Draft, - SearchIdx: 0, - SearchDone: false, - Found: 0, - Evaluated: 0, - Joined: 0, - Rejected: 0, - nowMs, - nowMs)); - return Task.CompletedTask; - } - - /// - public Task PatchTaskAsync( - string taskId, - DiscoveryTaskPatch patch, - CancellationToken ct) - { - int index = _tasks.FindIndex(task => task.Id == taskId); - if (index < 0) - { - return Task.FromResult(false); - } - - DiscoveryTaskDto task = _tasks[index]; - if (patch.Name is not null) - { - task = task with { Name = patch.Name }; - } - - if (patch.Description is not null) - { - task = task with { Description = patch.Description }; - } - - if (patch.Keywords is not null) - { - task = task with { Keywords = patch.Keywords.ToList() }; - } - - if (patch.MinSubscribers is int minSubscribers) - { - task = task with { MinSubscribers = minSubscribers }; - } - - if (patch.Lang is not null) - { - task = task with { Lang = patch.Lang }; - } - - if (patch.Threshold is int threshold) - { - task = task with { Threshold = threshold }; - } - - if (patch.SampleSize is int sampleSize) - { - task = task with { SampleSize = sampleSize }; - } - - if (patch.PlanJoins is int planJoins) - { - task = task with { PlanJoins = planJoins }; - } - - if (patch.AutoJoin is bool autoJoin) - { - task = task with { AutoJoin = autoJoin }; - } - - _tasks[index] = task with { UpdatedAt = _clock().ToUnixTimeMilliseconds() }; - return Task.FromResult(true); - } - - /// - public async Task DeleteTaskAsync(string taskId, CancellationToken ct) - { - if (!_tasks.Any(task => task.Id == taskId)) - { - return false; - } - - _tasks.RemoveAll(task => task.Id == taskId); - _candidates.RemoveAll(candidate => candidate.TaskId == taskId); - _log.RemoveAll(entry => entry.TaskId == taskId); - return true; - } - - /// - public Task SetTaskRunningAsync( - string taskId, - bool resetProgress, - CancellationToken ct) - { - int index = _tasks.FindIndex(task => task.Id == taskId); - if (index < 0) - { - return Task.FromResult(false); - } - - DiscoveryTaskDto task = _tasks[index] with { Status = DiscoveryTaskStatuses.Running }; - if (resetProgress) - { - task = task with - { - SearchIdx = 0, - SearchDone = false, - Found = 0, - Evaluated = 0, - Joined = 0, - Rejected = 0, - }; - } - - _tasks[index] = task with { UpdatedAt = _clock().ToUnixTimeMilliseconds() }; - return Task.FromResult(true); - } - - /// - public Task SetTaskPausedAsync(string taskId, CancellationToken ct) - { - int index = _tasks.FindIndex(task => task.Id == taskId); - if (index < 0) - { - return Task.FromResult(false); - } - - _tasks[index] = _tasks[index] with - { - Status = DiscoveryTaskStatuses.Paused, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - return Task.FromResult(true); - } - - /// - public Task SetTaskDoneAsync(string taskId, CancellationToken ct) - { - int index = _tasks.FindIndex(task => task.Id == taskId); - if (index < 0) - { - return Task.FromResult(false); - } - - _tasks[index] = _tasks[index] with - { - Status = DiscoveryTaskStatuses.Done, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - return Task.FromResult(true); - } - - /// - public Task BumpTaskCounterAsync( - string taskId, - DiscoveryCounterField field, - int n, - CancellationToken ct) - { - int index = _tasks.FindIndex(task => task.Id == taskId); - if (index < 0 || n <= 0) - { - return Task.FromResult(index >= 0); - } - - DiscoveryTaskDto task = field switch - { - DiscoveryCounterField.Found => _tasks[index] with { Found = _tasks[index].Found + n }, - DiscoveryCounterField.Evaluated => _tasks[index] with { Evaluated = _tasks[index].Evaluated + n }, - DiscoveryCounterField.Joined => _tasks[index] with { Joined = _tasks[index].Joined + n }, - DiscoveryCounterField.Rejected => _tasks[index] with { Rejected = _tasks[index].Rejected + n }, - _ => throw new ArgumentOutOfRangeException(nameof(field), field, "Неизвестный счётчик задачи"), - }; - _tasks[index] = task with { UpdatedAt = _clock().ToUnixTimeMilliseconds() }; - return Task.FromResult(true); - } - - /// - public Task AdvanceSearchAsync( - string taskId, - int nextIndex, - bool searchDone, - CancellationToken ct) - { - int index = _tasks.FindIndex(task => task.Id == taskId); - if (index < 0) - { - return Task.FromResult(false); - } - - _tasks[index] = _tasks[index] with - { - SearchIdx = nextIndex, - SearchDone = searchDone, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - return Task.FromResult(true); - } - - /// - public Task SumActivePlanAsync(string? excludeTaskId, CancellationToken ct) - { - IEnumerable active = _tasks - .Where(task => !DiscoveryTaskStatuses.IsFinished(task.Status)); - if (excludeTaskId is not null) - { - active = active.Where(task => task.Id != excludeTaskId); - } - - return Task.FromResult(active.Sum(task => task.PlanJoins)); - } - - /// - public Task> ListCandidatesAsync( - string taskId, - string? status, - CancellationToken ct) - { - IEnumerable query = _candidates.Where(candidate => candidate.TaskId == taskId); - if (status is not null) - { - query = query.Where(candidate => candidate.Status == status); - } - - return Task.FromResult>(query - .OrderBy(candidate => candidate.CreatedAt) - .ToList()); - } - - /// - public Task GetCandidateAsync(string dialogId, CancellationToken ct) - { - return Task.FromResult(_candidates.FirstOrDefault(candidate => candidate.DialogId == dialogId)); - } - - /// - public Task IsDialogMonitoredAsync(string dialogId, CancellationToken ct) - { - return Task.FromResult(_monitoredIds.Contains(dialogId)); - } - - /// - public Task IsBlacklistedAsync(string dialogId, CancellationToken ct) - { - return Task.FromResult(_blacklist.Any(entry => entry.DialogId == dialogId)); - } - - /// - public Task CreateCandidateAsync(DiscoveryCandidateRow row, CancellationToken ct) - { - long nowMs = _clock().ToUnixTimeMilliseconds(); - _candidates.Add(new DiscoveryCandidateDto( - row.DialogId, - row.TaskId, - row.Name, - row.Username, - row.Kind, - row.Hue, - Participants: null, - LangRu: null, - new List(), - new List(), - FitRatio: null, - DiscoveryCandidateStatuses.New, - AutoJoined: false, - JoinFailures: 0, - nowMs, - nowMs)); - return Task.CompletedTask; - } - - /// - public Task DeleteCandidateAsync(string dialogId, CancellationToken ct) - { - _candidates.RemoveAll(candidate => candidate.DialogId == dialogId); - return Task.CompletedTask; - } - - /// - public Task PatchCandidateAsync( - string dialogId, - DiscoveryCandidatePatch patch, - CancellationToken ct) - { - int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId); - if (index < 0) - { - return Task.FromResult(false); - } - - DiscoveryCandidateDto candidate = _candidates[index]; - if (patch.Name is not null) - { - candidate = candidate with { Name = patch.Name }; - } - - if (patch.Username is not null) - { - candidate = candidate with { Username = patch.Username }; - } - - if (patch.Kind is not null) - { - candidate = candidate with { Kind = patch.Kind }; - } - - if (patch.Hue is not null) - { - candidate = candidate with { Hue = patch.Hue }; - } - - if (patch.Participants is int participants) - { - candidate = candidate with { Participants = participants }; - } - - if (patch.LangRu is bool langRu) - { - candidate = candidate with { LangRu = langRu }; - } - - if (patch.Marks is not null) - { - candidate = candidate with { Marks = patch.Marks.ToList() }; - } - - if (patch.Topics is not null) - { - candidate = candidate with { Topics = patch.Topics.ToList() }; - } - - if (patch.FitRatio is double fitRatio) - { - candidate = candidate with { FitRatio = fitRatio }; - } - - if (patch.AutoJoined is bool autoJoined) - { - candidate = candidate with { AutoJoined = autoJoined }; - } - - _candidates[index] = candidate with { UpdatedAt = _clock().ToUnixTimeMilliseconds() }; - return Task.FromResult(true); - } - - /// - public Task SetCandidateStatusAsync( - string dialogId, - string status, - CancellationToken ct) - { - int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId); - if (index < 0) - { - return Task.FromResult(false); - } - - _candidates[index] = _candidates[index] with - { - Status = status, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - return Task.FromResult(true); - } - - /// - public Task SetCandidateJoinedAsync( - string dialogId, - bool autoJoined, - CancellationToken ct) - { - int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId); - if (index < 0) - { - return Task.FromResult(false); - } - - _candidates[index] = _candidates[index] with - { - Status = DiscoveryCandidateStatuses.Joined, - AutoJoined = autoJoined, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - return Task.FromResult(true); - } - - /// - public Task IncrementJoinFailuresAsync(string dialogId, CancellationToken ct) - { - int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId); - if (index < 0 || _candidates[index].Status != DiscoveryCandidateStatuses.Review) - { - return Task.FromResult(null); - } - - DiscoveryCandidateDto candidate = _candidates[index] with - { - JoinFailures = _candidates[index].JoinFailures + 1, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - _candidates[index] = candidate; - return Task.FromResult(candidate.JoinFailures); - } - - /// - public Task SetCandidateRejectedAsync(string dialogId, CancellationToken ct) - { - int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId); - if (index < 0) - { - return Task.FromResult(false); - } - - _candidates[index] = _candidates[index] with - { - Status = DiscoveryCandidateStatuses.Rejected, - UpdatedAt = _clock().ToUnixTimeMilliseconds(), - }; - return Task.FromResult(true); - } - - /// - public Task UpsertBlacklistAsync( - string dialogId, - string name, - string reason, - CancellationToken ct) - { - DiscoveryBlacklistDto? existing = _blacklist.FirstOrDefault(entry => entry.DialogId == dialogId); - if (existing is null) - { - _blacklist.Add(new DiscoveryBlacklistDto(dialogId, name, reason, _clock().ToUnixTimeMilliseconds())); - } - else - { - _blacklist[_blacklist.IndexOf(existing)] = existing with { Name = name, Reason = reason }; - } - - return Task.CompletedTask; - } - - /// - public Task RemoveBlacklistAsync(string dialogId, CancellationToken ct) - { - _blacklist.RemoveAll(entry => entry.DialogId == dialogId); - return Task.CompletedTask; - } - - /// - public Task GetBlacklistAsync(string dialogId, CancellationToken ct) - { - return Task.FromResult(_blacklist.FirstOrDefault(entry => entry.DialogId == dialogId)); - } - - /// - public Task> ListBlacklistAsync(CancellationToken ct) - { - return Task.FromResult>(_blacklist - .OrderByDescending(entry => entry.CreatedAt) - .ToList()); - } - - /// - public Task AddLogAsync( - string logId, - string taskId, - string logEvent, - string text, - CancellationToken ct) - { - _log.Add(new DiscoveryLogDto(logId, taskId, logEvent, text, _clock().ToUnixTimeMilliseconds())); - return Task.CompletedTask; - } - - /// - public Task CountLogEventAsync( - string logEvent, - DateTimeOffset sinceUtc, - CancellationToken ct) - { - long sinceMs = sinceUtc.ToUnixTimeMilliseconds(); - return Task.FromResult(_log.Count(entry => entry.Event == logEvent && entry.CreatedAt >= sinceMs)); - } - - /// - public Task> ListTaskLogAsync( - string taskId, - int limit, - CancellationToken ct) - { - IReadOnlyList entries = _log - .Where(entry => entry.TaskId == taskId) - .OrderByDescending(entry => entry.CreatedAt) - .Take(limit) - .ToList(); - return Task.FromResult(entries); - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs index c2b4783..c652716 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs @@ -5,7 +5,7 @@ using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Modules.Tenants; /// -/// Юнит-тесты AuthService на fake-хранилище и детерминированном «хэшере». +/// Юнит-тесты AuthService на подставке хранилища и детерминированном «хэшере». /// public sealed class AuthServiceTests { @@ -17,17 +17,17 @@ public sealed class AuthServiceTests private static readonly Guid UserId = Guid.NewGuid(); private static readonly Guid UserTenantId = Guid.NewGuid(); - private readonly FakeAuthStore _store; + private readonly TestAuthStore _store; private readonly TestTenantStore _tenantStore; private readonly AuthService _service; public AuthServiceTests() { var passwordHasher = TestHashers.New(); - _store = new FakeAuthStore(); + _store = new TestAuthStore(); _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.Repository); + _service = new AuthService(_store.Store, passwordHasher, _tenantStore.Repository); } [Fact] @@ -133,7 +133,7 @@ public sealed class AuthServiceTests var user = await _service.ResolveSessionAsync(rawToken, CancellationToken.None); Assert.Null(user); - // Очистка «протухших» сессий вызвана и удалила сессию из fake-хранилища. + // Очистка «протухших» сессий вызвана и удалила сессию из хранилища. Assert.Contains("delete-expired", _store.Calls); Assert.Empty(_store.Sessions); } @@ -204,10 +204,10 @@ public sealed class AuthServiceTests public async Task LoginAsync_WhenTenantSuspended_ReturnsSuspendedErrorWithoutSession() { var passwordHasher = TestHashers.New(); - var suspendedStore = new FakeAuthStore(); + var suspendedStore = new TestAuthStore(); suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); - var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository); + var suspendedService = new AuthService(suspendedStore.Store, passwordHasher, suspendedTenantStore.Repository); var result = await suspendedService.LoginAsync(UserLogin, UserPassword, CancellationToken.None); @@ -225,10 +225,10 @@ public sealed class AuthServiceTests // Порядок проверок AuthService: учётные данные раньше статуса тенанта — неверный пароль не раскрывает // приостановку (тот же ответ, что для активного тенанта: Login/Token/Error пусты). var passwordHasher = TestHashers.New(); - var suspendedStore = new FakeAuthStore(); + var suspendedStore = new TestAuthStore(); suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); - var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository); + var suspendedService = new AuthService(suspendedStore.Store, passwordHasher, suspendedTenantStore.Repository); var result = await suspendedService.LoginAsync(UserLogin, "wrong-password", CancellationToken.None); @@ -266,12 +266,12 @@ public sealed class AuthServiceTests public async Task ImpersonateAsync_WithoutLogin_TakesFirstTenantUser() { var passwordHasher = TestHashers.New(); - var multiUserStore = new FakeAuthStore(); + var multiUserStore = new TestAuthStore(); var firstUserId = Guid.NewGuid(); 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.Repository); + var multiUserService = new AuthService(multiUserStore.Store, passwordHasher, _tenantStore.Repository); // login не задан — берётся первый пользователь тенанта (порядок хранилища = CreatedAt в EF-адаптере). var result = await multiUserService.ImpersonateAsync(UserTenantId, targetLogin: null, Guid.NewGuid(), CancellationToken.None); @@ -294,9 +294,9 @@ public sealed class AuthServiceTests // Пользователь другого тенанта с тем же логином тоже не резолвится (не раскрываем существование логина). var passwordHasher = TestHashers.New(); - var crossStore = new FakeAuthStore(); + var crossStore = new TestAuthStore(); crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p"))); - var crossService = new AuthService(crossStore, passwordHasher, _tenantStore.Repository); + var crossService = new AuthService(crossStore.Store, passwordHasher, _tenantStore.Repository); var cross = await crossService.ImpersonateAsync(UserTenantId, "cross@example.com", Guid.NewGuid(), CancellationToken.None); Assert.False(cross.Ok); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs deleted file mode 100644 index 518f7b6..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Deal.Modules.Tenants.Application.Abstractions; -using Deal.Modules.Tenants.Application.Models; - -namespace Deal.Tests.Unit.Modules.Tenants; - -/// -/// In-memory реализация для юнит-тестов AuthService. -/// -public sealed class FakeAuthStore : IAuthStore -{ - private readonly List _users = []; - private readonly List _sessions = []; - private readonly List _calls = []; - - /// - /// Пользователи хранилища. - /// - public IReadOnlyList Users => _users; - - /// - /// Сессии хранилища. - /// - public IReadOnlyList Sessions => _sessions; - - /// - /// Журнал мутирующих вызовов в порядке их совершения. - /// - public IReadOnlyList Calls => _calls; - - /// - /// Добавляет пользователя - /// - /// Пользователь. - public void AddUser(StoredUserDto user) => _users.Add(user); - - /// - /// Добавляет сессию - /// - /// Сессия. - public void AddSession(SessionDto session) => _sessions.Add(session); - - /// - public Task FindUserByLoginAsync(string login, CancellationToken ct) => - Task.FromResult(_users.SingleOrDefault(u => u.Login == login)); - - /// - public Task FindSessionByTokenHashAsync(string tokenHash, CancellationToken ct) => - Task.FromResult(_sessions.SingleOrDefault(s => s.TokenHash == tokenHash)); - - /// - public Task FindUserByIdAsync(Guid userId, CancellationToken ct) - { - var user = _users.SingleOrDefault(u => u.Id == userId); - return Task.FromResult( - user is null - ? null - : new UserIdentityDto(user.Id, user.Login, user.TenantId, user.Status)); - } - - /// - public Task> ListUsersByTenantIdAsync(Guid tenantId, CancellationToken ct) => - Task.FromResult>(_users - .Where(u => u.TenantId == tenantId) - .Select(u => new UserIdentityDto(u.Id, u.Login, u.TenantId, u.Status)) - .ToList()); - - /// - public Task CreateUserAsync(StoredUserDto user, CancellationToken ct) - { - _users.Add(user); - _calls.Add($"create-user:{user.Id}"); - return Task.CompletedTask; - } - - /// - public Task CreateSessionAsync(SessionDto session, CancellationToken ct) - { - _sessions.Add(session); - _calls.Add($"create-session:{session.UserId}"); - return Task.CompletedTask; - } - - /// - public Task DeleteSessionAsync(string tokenHash, CancellationToken ct) - { - _sessions.RemoveAll(s => s.TokenHash == tokenHash); - _calls.Add($"delete-session:{tokenHash}"); - return Task.CompletedTask; - } - - /// - public Task DeleteSessionsByUserIdAsync(Guid userId, CancellationToken ct) - { - _sessions.RemoveAll(s => s.UserId == userId); - _calls.Add($"delete-user-sessions:{userId}"); - return Task.CompletedTask; - } - - /// - public Task UpdatePasswordHashAsync( - Guid userId, - string passwordHash, - CancellationToken ct) - { - int index = _users.FindIndex(u => u.Id == userId); - if (index >= 0) - { - var user = _users[index]; - _users[index] = user with { PasswordHash = passwordHash }; - } - - _calls.Add($"update-password-hash:{userId}"); - return Task.CompletedTask; - } - - /// - public Task DeleteExpiredSessionsAsync(CancellationToken ct) - { - _sessions.RemoveAll(s => s.ExpiresAt <= DateTimeOffset.UtcNow); - _calls.Add("delete-expired"); - return Task.CompletedTask; - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs deleted file mode 100644 index 91b350d..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Deal.Modules.Tenants.Application.Abstractions; -using Deal.Modules.Tenants.Application.Models; - -namespace Deal.Tests.Unit.Modules.Tenants; - -/// -/// In-memory реализация для юнит-тестов OperatorAuthService. -/// -public sealed class FakeOperatorAuthStore : IOperatorAuthStore -{ - private readonly List _operators = []; - private readonly List _sessions = []; - private readonly List _calls = []; - - /// - /// Операторы хранилища. - /// - public IReadOnlyList Operators => _operators; - - /// - /// Сессии операторов хранилища. - /// - public IReadOnlyList Sessions => _sessions; - - /// - /// Журнал мутирующих вызовов в порядке их совершения. - /// - public IReadOnlyList Calls => _calls; - - /// - /// Добавляет оператора - /// - /// Оператор. - public void AddOperator(StoredOperatorDto operatorRecord) => _operators.Add(operatorRecord); - - /// - /// Добавляет сессию - /// - /// Сессия. - public void AddSession(OperatorSessionDto session) => _sessions.Add(session); - - /// - public Task FindByLoginAsync(string login, CancellationToken ct) => - Task.FromResult(_operators.SingleOrDefault(o => o.Login == login)); - - /// - public Task FindSessionByTokenHashAsync(string tokenHash, CancellationToken ct) => - Task.FromResult(_sessions.SingleOrDefault(s => s.TokenHash == tokenHash)); - - /// - public Task CreateAsync(StoredOperatorDto operatorRecord, CancellationToken ct) - { - _operators.Add(operatorRecord); - _calls.Add($"create-operator:{operatorRecord.Id}"); - return Task.CompletedTask; - } - - /// - public Task CreateSessionAsync(OperatorSessionDto session, CancellationToken ct) - { - _sessions.Add(session); - _calls.Add($"create-session:{session.OperatorId}"); - return Task.CompletedTask; - } - - /// - public Task DeleteSessionAsync(string tokenHash, CancellationToken ct) - { - _sessions.RemoveAll(s => s.TokenHash == tokenHash); - _calls.Add($"delete-session:{tokenHash}"); - return Task.CompletedTask; - } - - /// - public Task DeleteExpiredSessionsAsync(CancellationToken ct) - { - _sessions.RemoveAll(s => s.ExpiresAt <= DateTimeOffset.UtcNow); - _calls.Add("delete-expired"); - return Task.CompletedTask; - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs deleted file mode 100644 index 86a4192..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Deal.Modules.Tenants.Application.Abstractions; -using Deal.Modules.Tenants.Application.Models; - -namespace Deal.Tests.Unit.Modules.Tenants; - -/// -/// In-memory реализация для unit-тестов -/// -public sealed class FakeTenantRepository : ITenantRepository -{ - private readonly IReadOnlyList _tenants; - private readonly List<(int Offset, int Limit)> _pageRequests = []; - - /// - /// Создаёт реестр с фиксированным списком тенантов. - /// - /// Тенанты реестра (как строки public.tenants). - public FakeTenantRepository(params TenantRecordDto[] tenants) - { - _tenants = tenants.ToList(); - } - - /// - /// Запрошенные страницы (offset, limit) в порядке обращений. - /// - public IReadOnlyList<(int Offset, int Limit)> PageRequests => _pageRequests; - - /// - public Task> ListAsync(CancellationToken ct) => - Task.FromResult>(_tenants.ToList()); - - /// - public Task> ListPageAsync( - int offset, - int limit, - CancellationToken ct) - { - _pageRequests.Add((offset, limit)); - IReadOnlyList page = _tenants.Skip(offset).Take(limit).ToList(); - return Task.FromResult(page); - } - - /// - public Task FindByIdAsync(Guid id, CancellationToken ct) => - throw new NotSupportedException("FindByIdAsync не используется тестами реестра"); - - /// - public Task UpdateStatusAsync( - Guid id, - string status, - CancellationToken ct) => - throw new NotSupportedException("UpdateStatusAsync не используется тестами реестра"); - - /// - public Task CreateAsync(TenantRecordDto tenant, CancellationToken ct) => - throw new NotSupportedException("CreateAsync не используется тестами реестра"); -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs deleted file mode 100644 index 24e12b6..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Deal.Modules.Tenants.Application.Abstractions; -using Deal.Modules.Tenants.Application.Models; - -namespace Deal.Tests.Unit.Modules.Tenants; - -/// -/// In-memory реализация с поддержкой создания тенантов. -/// -public sealed class FakeTenantStore : ITenantRepository -{ - private readonly List _tenants; - - /// - /// Создаёт хранилище, опционально с предзаполненным реестром тенантов. - /// - /// Тенанты, которые уже есть в реестре (как строки public.tenants). - public FakeTenantStore(params TenantRecordDto[] seed) - { - _tenants = seed.ToList(); - } - - /// - /// Записи реестра в порядке добавления. - /// - public IReadOnlyList Tenants => _tenants; - - /// - public Task FindByIdAsync(Guid id, CancellationToken ct) => - Task.FromResult(_tenants.FirstOrDefault(tenant => tenant.Id == id)); - - /// - public Task CreateAsync(TenantRecordDto tenant, CancellationToken ct) - { - _tenants.Add(tenant); - return Task.CompletedTask; - } - - /// - public Task> ListAsync(CancellationToken ct) => - Task.FromResult>(_tenants.ToList()); - - /// - public Task> ListPageAsync( - int offset, - int limit, - CancellationToken ct) - { - IReadOnlyList page = _tenants.Skip(offset).Take(limit).ToList(); - return Task.FromResult(page); - } - - /// - public Task UpdateStatusAsync( - Guid id, - string status, - CancellationToken ct) - { - int index = _tenants.FindIndex(tenant => tenant.Id == id); - if (index < 0) - { - return Task.FromResult(false); - } - - var current = _tenants[index]; - _tenants[index] = current with { Status = status }; - return Task.FromResult(true); - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs deleted file mode 100644 index 6394a0b..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Deal.Modules.Tenants.Application.Abstractions; -using Deal.Modules.Tenants.Application.Models; - -namespace Deal.Tests.Unit.Modules.Tenants; - -/// -/// In-memory реализация для юнит/HTTP-тестов. -/// -public sealed class FakeTokenUsageEventStore : ITokenUsageEventStore -{ - private readonly List _records = []; - - /// - /// Записи хранилища в порядке добавления. - /// - public IReadOnlyList Records => _records; - - /// - public Task AppendAsync(TokenUsageEventDto record, CancellationToken ct) - { - _records.Add(record); - return Task.CompletedTask; - } - - /// - public Task> AggregateAsync(TokenUsageEventQueryDto query, CancellationToken ct) - { - IEnumerable filtered = _records.Where(record => - (query.TenantId is null || record.TenantId == query.TenantId) && - (string.IsNullOrWhiteSpace(query.Provider) || record.Provider == query.Provider) && - (string.IsNullOrWhiteSpace(query.Model) || record.Model == query.Model) && - (string.IsNullOrWhiteSpace(query.Kind) || record.Kind == query.Kind) && - (query.From is null || record.At >= query.From.Value) && - (query.To is null || record.At <= query.To.Value)); - - IReadOnlyList result = query.GroupBy switch - { - TokenUsageGroupBys.Day => GroupByDay(filtered), - TokenUsageGroupBys.Tenant => GroupByString(filtered, record => record.TenantId.ToString("D")), - TokenUsageGroupBys.Provider => GroupByString(filtered, record => record.Provider), - TokenUsageGroupBys.Model => GroupByString(filtered, record => record.Model), - _ => throw new ArgumentException($"Неизвестная группировка: '{query.GroupBy}'.", nameof(query)), - }; - return Task.FromResult(result); - } - - private static IReadOnlyList GroupByDay(IEnumerable source) => - source - .GroupBy(record => record.At.UtcDateTime.Date) - .OrderBy(group => group.Key) - .Select(group => ToAggregate(group.Key.ToString("yyyy-MM-dd"), group)) - .ToList(); - - private static IReadOnlyList GroupByString(IEnumerable source, Func keySelector) => - source - .GroupBy(keySelector) - .Select(group => ToAggregate(group.Key, group)) - .OrderByDescending(row => row.TotalTokens) - .ToList(); - - private static TokenUsageAggregateDto ToAggregate(string key, IEnumerable group) - { - List rows = group.ToList(); - return new TokenUsageAggregateDto( - key, - rows.Sum(row => row.PromptTokens), - rows.Sum(row => row.CompletionTokens), - rows.Sum(row => row.TotalTokens), - rows.Count); - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs index 2b4bc8e..6ac3d1b 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs @@ -29,7 +29,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var provisioner = new TestTenantProvisioner(); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var passwordHasher = TestHashers.New(); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, passwordHasher); @@ -70,7 +70,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); 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 TestAuthStore(), TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: " ", Password, CancellationToken.None); @@ -87,7 +87,7 @@ public sealed class JoinFlowTests var provisioner = new TestTenantProvisioner(); // Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему. var tenantStore = new TestTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -108,7 +108,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId)); var tenantStore = new TestTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -126,7 +126,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId)); var tenantStore = new TestTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -142,7 +142,7 @@ public sealed class JoinFlowTests { var inviteStore = new TestInviteStore(); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(WrongCode, Email, name: null, Password, CancellationToken.None); @@ -159,7 +159,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, expiresAt: DateTimeOffset.UtcNow.AddHours(-1))); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -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 TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New()); + var service = NewService(inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), 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 TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New()); + var service = NewService(inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -203,7 +203,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, OtherEmail, name: null, Password, CancellationToken.None); @@ -221,7 +221,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash")); var provisioner = new TestTenantProvisioner(); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); @@ -248,7 +248,7 @@ public sealed class JoinFlowTests var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, password, CancellationToken.None); @@ -267,7 +267,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var provisioner = new TestTenantProvisioner(); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); JoinResultDto first = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -288,7 +288,7 @@ public sealed class JoinFlowTests // Конкурент (отзыв) «успел» до нашего CAS — приглашение уже отозвано, активация не выполнится. inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked)); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var provisioner = new TestTenantProvisioner(); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); @@ -311,7 +311,7 @@ public sealed class JoinFlowTests // Конкурентная активация «успела» до нашего CAS — приглашение уже активировано. inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated)); var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var provisioner = new TestTenantProvisioner(); var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New()); @@ -353,17 +353,17 @@ public sealed class JoinFlowTests // ─── Хелперы ───────────────────────────────────────────────────────── - // Собирает JoinService на общих фейк-хранилищах (все состояния — свежие). + // Собирает JoinService на общих подставках хранилищ (все состояния — свежие). private static JoinService NewService( TestInviteStore inviteStore, TestTenantStore tenantStore, ITenantProvisioner provisioner, - FakeAuthStore authStore, + TestAuthStore authStore, IPasswordHasher passwordHasher) => new( new InvitesService(inviteStore.Store), new TenantService(tenantStore.Repository, provisioner), - authStore, + authStore.Store, passwordHasher, tenantStore.Repository); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs index b56cfb3..e0cb343 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs @@ -18,7 +18,7 @@ public sealed class TenantAdminServiceTests { var tenantStore = new TestTenantStore(); var provisioner = new TestTenantProvisioner(); - var service = NewService(tenantStore, new FakeAuthStore(), provisioner); + var service = NewService(tenantStore, new TestAuthStore(), provisioner); TenantCreateResultDto result = await service.CreateAsync(" Новый тенант ", email: null, CancellationToken.None); @@ -42,7 +42,7 @@ public sealed class TenantAdminServiceTests public async Task CreateAsync_WithEmail_CreatesOwnerWithOneTimePassword() { var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var hasher = TestHashers.New(); var service = NewService(tenantStore, authStore, new TestTenantProvisioner(), hasher); @@ -66,7 +66,7 @@ public sealed class TenantAdminServiceTests public async Task CreateAsync_WithTakenEmail_ReturnsEmailTakenWithoutTenant() { var tenantStore = new TestTenantStore(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); authStore.AddUser(NewUser(Guid.NewGuid(), "owner@example.com")); var service = NewService(tenantStore, authStore); @@ -82,7 +82,7 @@ public sealed class TenantAdminServiceTests public async Task CreateAsync_WithInvalidEmail_ReturnsInvalidEmail() { var tenantStore = new TestTenantStore(); - var service = NewService(tenantStore, new FakeAuthStore()); + var service = NewService(tenantStore, new TestAuthStore()); TenantCreateResultDto result = await service.CreateAsync("Тенант", "not-an-email", CancellationToken.None); @@ -95,7 +95,7 @@ public sealed class TenantAdminServiceTests public async Task CreateAsync_WithEmptyName_ReturnsNameRequired() { var tenantStore = new TestTenantStore(); - var service = NewService(tenantStore, new FakeAuthStore()); + var service = NewService(tenantStore, new TestAuthStore()); TenantCreateResultDto result = await service.CreateAsync(" ", email: null, CancellationToken.None); @@ -110,7 +110,7 @@ public sealed class TenantAdminServiceTests 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(); + var authStore = new TestAuthStore(); authStore.AddUser(NewUser(FirstTenantId, "a@example.com")); authStore.AddUser(NewUser(FirstTenantId, "b@example.com")); authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com")); @@ -132,7 +132,7 @@ public sealed class TenantAdminServiceTests public async Task GetAsync_ForExistingTenant_ReturnsDetailWithUsers() { var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); authStore.AddUser(NewUser(FirstTenantId, "a@example.com")); authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com")); var service = NewService(tenantStore, authStore); @@ -150,7 +150,7 @@ public sealed class TenantAdminServiceTests [Fact] public async Task GetAsync_ForUnknownTenant_ReturnsNull() { - var service = NewService(new TestTenantStore(), new FakeAuthStore()); + var service = NewService(new TestTenantStore(), new TestAuthStore()); Assert.Null(await service.GetAsync(Guid.NewGuid(), CancellationToken.None)); } @@ -159,7 +159,7 @@ public sealed class TenantAdminServiceTests public async Task ChangeStatusAsync_Suspend_UpdatesTenantAndReportsChanged() { var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow)); - var service = NewService(tenantStore, new FakeAuthStore()); + var service = NewService(tenantStore, new TestAuthStore()); TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None); @@ -174,7 +174,7 @@ public sealed class TenantAdminServiceTests public async Task ChangeStatusAsync_WhenAlreadyInStatus_ReturnsOkWithoutChange() { var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); - var service = NewService(tenantStore, new FakeAuthStore()); + var service = NewService(tenantStore, new TestAuthStore()); // Повторный suspend приостановленного — Ok без изменения (аудит не дублируется, идемпотентность). TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None); @@ -187,7 +187,7 @@ public sealed class TenantAdminServiceTests [Fact] public async Task ChangeStatusAsync_ForUnknownTenant_ReturnsNotFound() { - var service = NewService(new TestTenantStore(), new FakeAuthStore()); + var service = NewService(new TestTenantStore(), new TestAuthStore()); TenantStatusChangeResultDto result = await service.ChangeStatusAsync(Guid.NewGuid(), TenantStatuses.Suspended, CancellationToken.None); @@ -196,15 +196,15 @@ public sealed class TenantAdminServiceTests Assert.Null(result.Tenant); } - // Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку). + // Сервис на подставках хранилищ (владелец-пользователи создаются через TestAuthStore и хэшер-подставку). private static TenantAdminService NewService( TestTenantStore tenantStore, - FakeAuthStore authStore, + TestAuthStore authStore, TestTenantProvisioner? provisioner = null, IPasswordHasher? hasher = null) { var tenantService = new TenantService(tenantStore.Repository, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner); - return new TenantAdminService(tenantStore.Repository, authStore, tenantService, hasher ?? TestHashers.New()); + return new TenantAdminService(tenantStore.Repository, authStore.Store, tenantService, hasher ?? TestHashers.New()); } // Пользователь тенанта (активный, без хэша — для чтения). diff --git a/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs index dfaa5c8..220e1cf 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs @@ -37,7 +37,7 @@ public sealed class JoinEndpointHttpTests { var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Email)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); var tenantStore = new TestTenantStore(); var provisioner = new TestTenantProvisioner(); var auditStore = new TestAuditLogStore(); @@ -56,7 +56,7 @@ public sealed class JoinEndpointHttpTests Assert.False(response.Headers.Contains("Set-Cookie")); }); - // Пользователь создан (login=email, хэш через фейк-хэшер), тенант создан и провижинен один раз, инвайт activated. + // Пользователь создан (login=email, хэш через подставку-хэшер), тенант создан и провижинен один раз, инвайт activated. StoredUserDto user = Assert.Single(authStore.Users); Assert.Equal(Email, user.Login); Assert.Equal(TestHashers.New().Hash(Password), user.PasswordHash); @@ -80,7 +80,7 @@ public sealed class JoinEndpointHttpTests { var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Email)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); await RunAsync( inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new TestAuditLogStore(), @@ -108,7 +108,7 @@ public sealed class JoinEndpointHttpTests { var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Email)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); await RunAsync( inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new TestAuditLogStore(), @@ -132,7 +132,7 @@ public sealed class JoinEndpointHttpTests inviteStore.AddInvite(NewInvite(Email, status: InviteStatuses.Revoked)); await RunAsync( - inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(), + inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), new TestAuditLogStore(), 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 TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(), + inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), new TestAuditLogStore(), 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 TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(), + new TestInviteStore(), new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), new TestAuditLogStore(), async (baseAddress, client) => { using HttpResponseMessage response = await PostJsonAsync( @@ -183,7 +183,7 @@ public sealed class JoinEndpointHttpTests inviteStore.AddInvite(NewInvite(Email)); await RunAsync( - inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(), + inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), new TestAuditLogStore(), async (baseAddress, client) => { using HttpResponseMessage response = await PostJsonAsync( @@ -201,7 +201,7 @@ public sealed class JoinEndpointHttpTests { var inviteStore = new TestInviteStore(); inviteStore.AddInvite(NewInvite(Email)); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: "active", "hash")); await RunAsync( @@ -228,7 +228,7 @@ public sealed class JoinEndpointHttpTests // Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему. var tenantStore = new TestTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow)); var provisioner = new TestTenantProvisioner(); - var authStore = new FakeAuthStore(); + var authStore = new TestAuthStore(); await RunAsync( inviteStore, tenantStore, provisioner.Provisioner, authStore, new TestAuditLogStore(), @@ -247,12 +247,12 @@ public sealed class JoinEndpointHttpTests // ─── Хелперы ───────────────────────────────────────────────────────── - // Поднимает in-process Kestrel с /api/join на фейк-хранилищах и прогоняет сценарий. + // Поднимает in-process Kestrel с /api/join на подставках хранилищ и прогоняет сценарий. private static async Task RunAsync( TestInviteStore inviteStore, TestTenantStore tenantStore, ITenantProvisioner provisioner, - FakeAuthStore authStore, + TestAuthStore authStore, TestAuditLogStore auditStore, Func scenario) { @@ -264,7 +264,7 @@ public sealed class JoinEndpointHttpTests // Хэшер-подставка регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает // последняя регистрация (зеркало OperatorAuthHttpHost). builder.Services.AddSingleton(TestHashers.New()); - builder.Services.AddSingleton(authStore); + builder.Services.AddSingleton(authStore.Store); builder.Services.AddSingleton(auditStore.Store); builder.Services.AddSingleton(inviteStore.Store); builder.Services.AddSingleton(tenantStore.Repository); diff --git a/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs index 6b1120c..d7dc66f 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs @@ -136,10 +136,10 @@ public sealed class LoginAttemptEndpointHttpTests new() { Enabled = true }; // Фейк-хранилище пользователей с активным пользователем Login/Password. - private static FakeAuthStore NewUserStore() + private static TestAuthStore NewUserStore() { var passwordHasher = TestHashers.New(); - var store = new FakeAuthStore(); + var store = new TestAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), Login, diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs index 5ee10e0..d4379b6 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs @@ -265,10 +265,10 @@ public sealed class OperatorAnalyticsEndpointsHttpTests return store; } - private static FakeAuthStore NewUserStore() + private static TestAuthStore NewUserStore() { var passwordHasher = TestHashers.New(); - var store = new FakeAuthStore(); + var store = new TestAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), UserLogin, ActiveTenant, "active", passwordHasher.Hash(UserPassword))); return store; diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs index 52b1fab..4c80ebe 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs @@ -292,10 +292,10 @@ public sealed class OperatorAuditEndpointsHttpTests } // Фейк-хранилище пользователей с admin/admin в дефолтном тенанте. - private static FakeAuthStore NewUserStore() + private static TestAuthStore NewUserStore() { var passwordHasher = TestHashers.New(); - var store = new FakeAuthStore(); + var store = new TestAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), UserLogin, diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs index 19c9c97..336b7e7 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs @@ -202,10 +202,10 @@ public sealed class OperatorAuthEndpointsHttpTests } // Фейк-хранилище пользователей с admin/admin в дефолтном тенанте. - private static FakeAuthStore NewUserStore() + private static TestAuthStore NewUserStore() { var passwordHasher = TestHashers.New(); - var store = new FakeAuthStore(); + var store = new TestAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), UserLogin, diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs index 4a5f1dc..79c569f 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs @@ -33,16 +33,16 @@ internal static class OperatorAuthHttpHost "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password;Timeout=3"; /// - /// Прогоняет сценарий против поднятого хоста с фейк-хранилищами оператора и пользователей. + /// Прогоняет сценарий против поднятого хоста с подставками хранилищ оператора и пользователей. /// - /// Фейк-хранилище оператора (IOperatorAuthStore). - /// Фейк-хранилище пользователей (IAuthStore) для тенантных ручек /api/auth. + /// Подставка хранилища оператора (IOperatorAuthStore). + /// Подставка хранилища пользователей (IAuthStore) для тенантных ручек /api/auth. /// Сценарий: получает base-адрес и хранилища для проверок/досидирования. - /// Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри. + /// Подставка хранилища аудита (IAuditLogStore); null — создаётся пустое внутри. public static async Task RunAsync( TestOperatorAuthStore operatorStore, - FakeAuthStore userStore, - Func scenario, + TestAuthStore userStore, + Func scenario, TestAuditLogStore? auditStore = null) => await RunCoreAsync( operatorStore, @@ -56,15 +56,15 @@ internal static class OperatorAuthHttpHost /// /// Прогоняет сценарий /// - /// Фейк-хранилище оператора (IOperatorAuthStore). - /// Фейк-хранилище пользователей (IAuthStore) для тенантных ручек /api/auth. + /// Подставка хранилища оператора (IOperatorAuthStore). + /// Подставка хранилища пользователей (IAuthStore) для тенантных ручек /api/auth. /// Сценарий: получает base-адрес и хранилища (без реестра тенантов). - /// Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри. - /// Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри. + /// Подставка хранилища аудита (IAuditLogStore); null — создаётся пустое внутри. + /// Подставка хранилища приглашений (IInviteStore); null — создаётся пустое внутри. public static async Task RunAsync( TestOperatorAuthStore operatorStore, - FakeAuthStore userStore, - Func scenario, + TestAuthStore userStore, + Func scenario, TestAuditLogStore? auditStore = null, TestInviteStore? inviteStore = null) => await RunCoreAsync( @@ -79,16 +79,16 @@ internal static class OperatorAuthHttpHost /// /// Прогоняет сценарий /// - /// Фейк-хранилище оператора (IOperatorAuthStore). - /// Фейк-хранилище пользователей (IAuthStore) для тенантных ручек /api/auth. + /// Подставка хранилища оператора (IOperatorAuthStore). + /// Подставка хранилища пользователей (IAuthStore) для тенантных ручек /api/auth. /// Сценарий: получает base-адрес и все хранилища, кроме лимитов. - /// Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри. - /// Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри. - /// Фейк-хранилище реестра тенантов (ITenantRepository); null — создаётся пустое внутри. + /// Подставка хранилища аудита (IAuditLogStore); null — создаётся пустое внутри. + /// Подставка хранилища приглашений (IInviteStore); null — создаётся пустое внутри. + /// Подставка хранилища реестра тенантов (ITenantRepository); null — создаётся пустое внутри. public static async Task RunAsync( TestOperatorAuthStore operatorStore, - FakeAuthStore userStore, - Func scenario, + TestAuthStore userStore, + Func scenario, TestAuditLogStore? auditStore = null, TestInviteStore? inviteStore = null, TestTenantStore? tenantStore = null) => @@ -104,18 +104,18 @@ internal static class OperatorAuthHttpHost /// /// Прогоняет сценарий /// - /// Фейк-хранилище оператора (IOperatorAuthStore). - /// Фейк-хранилище пользователей (IAuthStore) для тенантных ручек /api/auth. + /// Подставка хранилища оператора (IOperatorAuthStore). + /// Подставка хранилища пользователей (IAuthStore) для тенантных ручек /api/auth. /// Сценарий: получает base-адрес и все хранилища, включая лимиты. - /// Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри. - /// Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри. - /// Фейк-хранилище реестра тенантов (ITenantRepository); null — создаётся пустое внутри. - /// Фейк-хранилище лимитов (ITenantLimitStore); null — создаётся пустое внутри. + /// Подставка хранилища аудита (IAuditLogStore); null — создаётся пустое внутри. + /// Подставка хранилища приглашений (IInviteStore); null — создаётся пустое внутри. + /// Подставка хранилища реестра тенантов (ITenantRepository); null — создаётся пустое внутри. + /// Подставка хранилища лимитов (ITenantLimitStore); null — создаётся пустое внутри. /// Опции rate limiting: null — выключенные по умолчанию (LoginAttemptGuard no-op, существующие сценарии не режутся); Enabled-опции — сценарии защиты входа. public static async Task RunAsync( TestOperatorAuthStore operatorStore, - FakeAuthStore userStore, - Func scenario, + TestAuthStore userStore, + Func scenario, TestAuditLogStore? auditStore = null, TestInviteStore? inviteStore = null, TestTenantStore? tenantStore = null, @@ -136,15 +136,15 @@ internal static class OperatorAuthHttpHost /// /// Прогоняет сценарий с глобальными настройками /// - /// Фейк-хранилище оператора (IOperatorAuthStore). - /// Фейк-хранилище пользователей (IAuthStore) для тенантных ручек /api/auth. + /// Подставка хранилища оператора (IOperatorAuthStore). + /// Подставка хранилища пользователей (IAuthStore) для тенантных ручек /api/auth. /// Сценарий: base-адрес, хранилища оператора/пользователей/глобальных настроек и аудита. - /// Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри. - /// Фейк глобального KV-хранилища (IGlobalSettingsStore); null — создаётся пустое. + /// Подставка хранилища аудита (IAuditLogStore); null — создаётся пустое внутри. + /// Подставка глобального KV-хранилища (IGlobalSettingsStore); null — создаётся пустое. public static async Task RunWithGlobalSettingsAsync( TestOperatorAuthStore operatorStore, - FakeAuthStore userStore, - Func scenario, + TestAuthStore userStore, + Func scenario, TestAuditLogStore? auditStore = null, TestGlobalSettingsStore? globalSettingsStore = null) => await RunCoreAsync( @@ -160,12 +160,12 @@ internal static class OperatorAuthHttpHost // Поднимает in-process Kestrel-хост и прогоняет сценарий (см. публичные перегрузки). private static async Task RunCoreAsync( TestOperatorAuthStore operatorStore, - FakeAuthStore userStore, + TestAuthStore userStore, TestAuditLogStore? auditStore, TestInviteStore? inviteStore, TestTenantStore? tenantStore, TestTenantLimitStore? limitStore, - Func scenario, + Func scenario, RateLimitOptions? rateLimitOptions = null, TestTokenUsageEventStore? tokenUsageStore = null, TestGlobalSettingsStore? globalSettingsStore = null) @@ -186,7 +186,7 @@ internal static class OperatorAuthHttpHost // последняя регистрация (зеркало AddTenantsModule в тестах DiscoveryWorkerSchedulerTests). builder.Services.AddSingleton(TestHashers.New()); builder.Services.AddSingleton(); - builder.Services.AddSingleton(userStore); + builder.Services.AddSingleton(userStore.Store); builder.Services.AddSingleton(operatorStore.Store); builder.Services.AddSingleton(effectiveAuditStore.Store); builder.Services.AddSingleton(effectiveInviteStore.Store); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs index 127a6aa..d04fbd8 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs @@ -92,8 +92,8 @@ public sealed class OperatorHealthEndpointsHttpTests } // Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые). - private static Task RunAsync(Func scenario) => - OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario); + private static Task RunAsync(Func scenario) => + OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new TestAuthStore(), scenario); // Фейк-хранилище оператора с активным оператором operator/operator. private static TestOperatorAuthStore NewOperatorStore() diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs index 4667f87..4c99114 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs @@ -275,7 +275,7 @@ public sealed class OperatorInvitesEndpointsHttpTests } // Пустое фейк-хранилище пользователей (тенантные ручки в этих сценариях не используются). - private static FakeAuthStore NewUserStore() => new(); + private static TestAuthStore NewUserStore() => new(); // HTTP-клиент с собственным CookieContainer (изоляция сценариев кук). private static HttpClient CreateClient(string baseAddress) => diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs index b3c389f..7bee4dd 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs @@ -284,7 +284,7 @@ public sealed class OperatorLimitsEndpointsHttpTests $"{baseAddress}/api/operator/tenants/{tenantId}/limit"; // Прогоняет сценарий на хосте с двумя активными тенантами и предзаполненными строками лимитов. - private static Task RunAsync(Func scenario, TestAuditLogStore? auditStore = null) + private static Task RunAsync(Func scenario, TestAuditLogStore? auditStore = null) { // Первый тенант: расход 80% месячного бюджета (8 000 000 / 10 000 000) + флаг Warned80; // второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает). @@ -296,7 +296,7 @@ public sealed class OperatorLimitsEndpointsHttpTests limitStore.Preload(SecondTenantId, 100_000, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 0); return OperatorAuthHttpHost.RunAsync( NewOperatorStore(), - new FakeAuthStore(), + new TestAuthStore(), scenario, auditStore, tenantStore: tenantStore, diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs index 4d36a43..1d580de 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs @@ -72,8 +72,8 @@ public sealed class OperatorMaintenanceEndpointsHttpTests } // Прогоняет сценарий на хосте с активным оператором operator/operator и заданным реестром тенантов. - private static Task RunAsync(TestTenantStore tenantStore, Func scenario) => - OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario, tenantStore: tenantStore); + private static Task RunAsync(TestTenantStore tenantStore, Func scenario) => + OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new TestAuthStore(), scenario, tenantStore: tenantStore); // Фейк-хранилище оператора с активным оператором operator/operator. private static TestOperatorAuthStore NewOperatorStore() diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs index c482cbd..4280c87 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs @@ -257,10 +257,10 @@ public sealed class OperatorSettingsEndpointsHttpTests $"{baseAddress}/api/operator/settings/telegram-keys"; // Прогоняет сценарий на хосте с фейком глобального хранилища и аудита. - private static Task RunAsync(Func scenario, TestAuditLogStore? auditStore = null) => + private static Task RunAsync(Func scenario, TestAuditLogStore? auditStore = null) => OperatorAuthHttpHost.RunWithGlobalSettingsAsync( NewOperatorStore(), - new FakeAuthStore(), + new TestAuthStore(), scenario, auditStore); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs index 3376462..5579f7f 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs @@ -486,7 +486,7 @@ public sealed class OperatorTenantsEndpointsHttpTests // Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём. private static Task RunAsync( - Func scenario, + Func scenario, TestAuditLogStore? auditStore = null, TestTenantStore? tenantStore = null) { @@ -582,10 +582,10 @@ public sealed class OperatorTenantsEndpointsHttpTests } // Фейк-хранилище пользователей с одним пользователем в целевом тенанте. - private static FakeAuthStore NewUserStore() + private static TestAuthStore NewUserStore() { var passwordHasher = TestHashers.New(); - var store = new FakeAuthStore(); + var store = new TestAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), TenantUserLogin, diff --git a/src/core/tests/Deal.Tests.Unit/Support/TestAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Support/TestAuthStore.cs new file mode 100644 index 0000000..50925f6 --- /dev/null +++ b/src/core/tests/Deal.Tests.Unit/Support/TestAuthStore.cs @@ -0,0 +1,127 @@ +using Deal.Modules.Tenants.Application.Abstractions; +using Deal.Modules.Tenants.Application.Models; +using NSubstitute; + +namespace Deal.Tests.Unit.Support; + +/// +/// Подставка на списках: сервисы получают NSubstitute-подставку +/// (), тесты сеют/проверяют пользователей и сессии через +/// , , , , +/// журнал мутирующих вызовов — . +/// +public sealed class TestAuthStore +{ + private readonly List _users = []; + private readonly List _sessions = []; + private readonly List _calls = []; + + /// + /// Подставка порта аутентификации (создаётся в конструкторе). + /// + public IAuthStore Store { get; } + + /// + /// Пользователи хранилища. + /// + public IReadOnlyList Users => _users; + + /// + /// Сессии хранилища. + /// + public IReadOnlyList Sessions => _sessions; + + /// + /// Журнал мутирующих вызовов в порядке их совершения. + /// + public IReadOnlyList Calls => _calls; + + /// + /// Создаёт подставку с пустыми списками. + /// + public TestAuthStore() + { + Store = Substitute.For(); + Store.FindUserByLoginAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _users.SingleOrDefault(u => u.Login == ci.ArgAt(0))); + Store.FindSessionByTokenHashAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _sessions.SingleOrDefault(s => s.TokenHash == ci.ArgAt(0))); + Store.FindUserByIdAsync(Arg.Any(), Arg.Any()) + .Returns(ci => FindById(ci.ArgAt(0))); + Store.ListUsersByTenantIdAsync(Arg.Any(), Arg.Any()) + .Returns(ci => ListByTenantId(ci.ArgAt(0))); + Store.When(s => s.CreateUserAsync(Arg.Any(), Arg.Any())) + .Do(ci => + { + StoredUserDto user = ci.Arg(); + _users.Add(user); + _calls.Add($"create-user:{user.Id}"); + }); + Store.When(s => s.CreateSessionAsync(Arg.Any(), Arg.Any())) + .Do(ci => + { + SessionDto session = ci.Arg(); + _sessions.Add(session); + _calls.Add($"create-session:{session.UserId}"); + }); + Store.When(s => s.DeleteSessionAsync(Arg.Any(), Arg.Any())) + .Do(ci => + { + _sessions.RemoveAll(s => s.TokenHash == ci.ArgAt(0)); + _calls.Add($"delete-session:{ci.ArgAt(0)}"); + }); + Store.When(s => s.DeleteSessionsByUserIdAsync(Arg.Any(), Arg.Any())) + .Do(ci => + { + _sessions.RemoveAll(s => s.UserId == ci.ArgAt(0)); + _calls.Add($"delete-user-sessions:{ci.ArgAt(0)}"); + }); + Store.When(s => s.UpdatePasswordHashAsync( + Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => UpdatePasswordHash(ci.ArgAt(0), ci.ArgAt(1))); + Store.When(s => s.DeleteExpiredSessionsAsync(Arg.Any())) + .Do(_ => + { + _sessions.RemoveAll(s => s.ExpiresAt <= DateTimeOffset.UtcNow); + _calls.Add("delete-expired"); + }); + } + + /// + /// Добавляет пользователя + /// + /// Пользователь. + public void AddUser(StoredUserDto user) => _users.Add(user); + + /// + /// Добавляет сессию + /// + /// Сессия. + public void AddSession(SessionDto session) => _sessions.Add(session); + + private UserIdentityDto? FindById(Guid userId) + { + StoredUserDto? user = _users.SingleOrDefault(u => u.Id == userId); + return user is null ? null : new UserIdentityDto(user.Id, user.Login, user.TenantId, user.Status); + } + + private IReadOnlyList ListByTenantId(Guid tenantId) + { + return _users + .Where(u => u.TenantId == tenantId) + .Select(u => new UserIdentityDto(u.Id, u.Login, u.TenantId, u.Status)) + .ToList(); + } + + private void UpdatePasswordHash(Guid userId, string passwordHash) + { + int index = _users.FindIndex(u => u.Id == userId); + if (index >= 0) + { + StoredUserDto user = _users[index]; + _users[index] = user with { PasswordHash = passwordHash }; + } + + _calls.Add($"update-password-hash:{userId}"); + } +}