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}");
+ }
+}