diff --git a/.superpowers/sdd/explicit-interfaces/progress.md b/.superpowers/sdd/explicit-interfaces/progress.md new file mode 100644 index 0000000..f041bd6 --- /dev/null +++ b/.superpowers/sdd/explicit-interfaces/progress.md @@ -0,0 +1,29 @@ +# Ledger: explicit-interfaces (2026-09-11, поздний вечер) + +Решение владельца: вариант A — явные реализации по умолчанию, классы напрямую не вызываем +(исключения: DTO, хелперы, экстеншены), тесты через интерфейсы, моки — NSubstitute, +маркерные классы не используем (маркерные интерфейсы). + +## Итог + +- Codemod `scripts/make_explicit.py` (идемпотентный): таблицы членов интерфейсов (многострочные + сигнатуры), маппинг класс→интерфейсы включая partial-файлы, конвертация `public M(` → `IFoo.M(`. + Применено: 161 член в 30 прод-файлах. +- Исправления компиляторного цикла: недостающие using'и в partial-файлах (скрипт-фиксер), снят + дефолт параметра в явной реализации `ITenantLimitStore.GetOrCreateAsync`, самовызовы + `WTelegramSessionClient` квалифицированы `((ISessionClient)this)`, мусорный using в `LlmHttpClient`. +- Тесты: 17 файлов перетипизированы с конкретных классов на интерфейсы (поля, tuple-деконструкции, + var/target-typed new); DI-регистрации фейков → регистрация интерфейсных инстансов. +- Маркеры: 10 классов → интерфейсы `IKanbanModule`, `ICardsModule`, `IPipelineModule`, + `IDiscoveryModule`, `ISettingsModule`, `ITelegramModule`, `ITenantsModule`, `IContracts`, + `IInfrastructure`, `ISharedKernel`; тесты на `IsInterface`. +- NSubstitute 6.1.0 добавлен в 5 тест-проектов; `FakePasswordHasher` удалён, вместо него + `Support/TestHashers.New()` (Substitute.For + детерминированная семантика «fake-hash:»). +- Правила владельца зафиксированы в §11 код-стайла; план миграции оставшихся ~30 фейков — + `backlog.md` (TD-TESTS-NSUBSTITUTE). + +## Проверка + +- `dotnet build` 5 sln: 0 warnings / 0 errors. +- Тесты: core 1340/1340, telegram 130/130, ai 52/52, ml 38/38, storage 9/9. +- Коммиты: ed25c71 (явные реализации), 93e9100 (маркеры), далее — NSubstitute/доки. diff --git a/backlog.md b/backlog.md index 3edf760..c72b438 100644 --- a/backlog.md +++ b/backlog.md @@ -16,7 +16,8 @@ | BL-RECLASS-SSE | **Сделано (2026-09-11):** пакетная переклассификация отдаёт промежуточный прогресс через SSE `cards_reclassified` (`{progress:true,done,total,moved,kept,trashed,skipped}`) и финальное событие (`{progress:false,reclassified,moved}`); `CardReclassifier.ReclassifyInboxAsync` принимает `IProgress`; в UI — индикатор `done/total` в шапке «Неразобранного» | этап 12, D | P3 | DONE | | TD-CARD-MERGE | Полное слияние внутренних DTO карточки в единый `CardDto`. **Решение (2026-09-11): DEFERRED.** Наружный контракт единый; внутренние DTO (read/write/DB/patch) намеренно разделены по слоям, слияние — риск без пользы | этап 9/11 | P3 | DEFERRED | | TD-PROTO-COMMENTS | **Сделано (2026-09-11):** из комментариев убраны ссылки на процесс/прототип (`Task/Ruling/этап/python L…/main.py/прототип/LEADRADAR_*`), удалены блоки ``, `` сжаты до короткой фразы; `//`-комментарии со ссылками удалены, в `.proto` — тоже. Строк комментариев 27 210 → ~19 100 | запрос владельца 2026-09-11 | P2 | DONE | -| TD-COMMENTS-IFACE | Привести код к правилам код-стайла (`docs/spec/Код-стайл-Дейл.md`). **Сделано (2026-09-11):** (1) `` только блочно — исправлено 5286 шт. в 833 файлах; (2) комментарии только на public/protected — понижено 2028 XML-доков с private/internal (359 файлов), повторный прогон — ещё 12; (3) дедупликация ``→``: **закрыто — дублей нет** (проверено сканами по тексту и по имени члена: 39 интерфейсов, 229 членов, случаев ``+дубль не существует); (4) явная реализация интерфейсов — **остаётся точечным ревью владельца** (54 интерфейса с doc, 43 с реализациями, массовая правка не автоматизируется). Попутно: добавлены 4 недостающих `` членам интерфейсов, переведены 3 англоязычных комментария. Скрипты: `scripts/fix_summary_blocks.py`, `scripts/fix_private_docs.py`, `scripts/dedup_summary_inheritdoc.py`. Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md` | запрос владельца 2026-09-11 | P2 | TECHDEBT (1–3 — DONE; 4 — ревью владельца) | +| TD-COMMENTS-IFACE | Привести код к правилам код-стайла (`docs/spec/Код-стайл-Дейл.md`). **Сделано (2026-09-11):** (1) `` только блочно — 5286 шт.; (2) приватные XML-доки понижены — 2028+12; (3) дедупликация ``→`` — дублей нет (сканы); (4) **явные реализации интерфейсов — сделано (2026-09-11, вечер, вариант A)**: 161 член в 30 прод-файлах конвертирован codemod'ом `scripts/make_explicit.py`, потребители перетипизированы на интерфейсы (8 мест в проде, 17 тест-файлов), Card/ICard-семейство оставлено implicit как DTO; попутно маркерные классы заменены маркерными интерфейсами. Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md` | запрос владельца 2026-09-11 | P2 | DONE | +| TD-TESTS-NSUBSTITUTE | Миграция тестовых фейков на NSubstitute (решение владельца 2026-09-11: моки — через NSubstitute, новых фейк-классов не заводить). **Сделано (2026-09-11):** NSubstitute 6.1.0 подключён к 5 тест-проектам; эталон миграции — `FakePasswordHasher` → хелпер `TestHashers.New()` (NSubstitute, детерминированная семантика сохранена), фейк удалён. **Осталось (по размеру):** FakeDiscoveryPacer (1 файл), FakeRatesListener (2), FakeTenantProvisioner (5), FakeSecretCipher (8), FakeAiTools (4), FakeRatesSource (1), FakeGlobalSettingsStore (4), FakeTenantRegistry/FakeTenantRepository (3+7), FakeAiClassifier (5), FakeSettingsStore (42), FakeRateLimitCounterStore (5), FakeAuditLogStore (13), FakeTenantStore (10), FakeMlLearningStore (5), FakeMlClient (17), FakeOperatorAuthStore (14), FakeInviteStore (7), FakeFileStorage (9), FakeTokenUsageEventStore (10), FakeAuthStore (15), Recording*/Harness* (gRPC-харнессы — оставить как хелперы), крупные stateful: FakeTelegramGateway (5), FakeDiscoveryGateway (2), FakeTelegramStore (7), FakeTenantLimitStore (15), FakePipelineStore (12), FakeDiscoveryStore (7), FakeKanjStore (21). Для каждого: заменить подставку на `Substitute.For<>()` + `Returns`, семантику состояния воспроизвести в конфигурации, тесты перетипизировать на интерфейс | решение владельца 2026-09-11 | P2 | BACKLOG | | TD-STYLE-ANALYZERS | Остаток мягких правил код-стайла. **Закрыто (2026-09-11):** (1) `var` — включён ломающий сборку гейт только для встроенных типов (`csharp_style_var_for_built_in_types = false:warning`), остаток выправлен `dotnet format style --diagnostics IDE0008` по 5 sln; режимы «очевидный/прочий тип» — silent осознанно (~1600 субъективных замен); (2) дедупликация `` — дублей нет (см. TD-COMMENTS-IFACE); (3) переводы строк — **решено: LF** (`.gitattributes` `* text=auto eol=lf`, `.editorconfig` → lf, 1029 файлов нормализовано, `git add --renormalize`; попутно починены 42 CRLF-.sh — до этого первый прогон удалённого CI падал бы). `this.` и именование приватных полей уже закрыты в `.editorconfig` | аудит 2026-09-11 | P3 | DONE | ## 2. Инфраструктура и эксплуатация diff --git a/docs/spec/Код-стайл-Дейл.md b/docs/spec/Код-стайл-Дейл.md index c4d4c95..b7e0d3c 100644 --- a/docs/spec/Код-стайл-Дейл.md +++ b/docs/spec/Код-стайл-Дейл.md @@ -232,11 +232,16 @@ - **Не дублировать `` интерфейса в реализации.** Если член объявлен в интерфейсе с XML-doc, в классе-реализации достаточно `/// ` (или вообще ничего, если doc наследуется настройкой). Текст описания пишется **один раз** — у интерфейса. -- **Явная реализация интерфейсов — где возможно.** Предпочитать явную реализацию - (`Task ICardStore.GetAsync(...)`), если член не является публичным API класса сам по себе. Если тип - реализует член как собственный публичный сервис (нужен в DI/прямых вызовах) — допустима implicit, - но решение осознанное. +- **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11, + решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели + (напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные + реализации (codemod `scripts/make_explicit.py`, идемпотентный). - Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа. +- **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс + (`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]** +- **Тесты: моки — через NSubstitute** (`Substitute.For()`), тестовые переменные + типизируются интерфейсом. Новые hand-written фейк-классы не заводить; существующие мигрируются + поэтапно (план — `backlog.md`, `TD-TESTS-NSUBSTITUTE`). **[изм. 2026-09-11]** ## 12. Приложение: сводная таблица правил именования diff --git a/docs/spec/Код-стайл-аудит-2026-09-11.md b/docs/spec/Код-стайл-аудит-2026-09-11.md index e793247..4cd1283 100644 --- a/docs/spec/Код-стайл-аудит-2026-09-11.md +++ b/docs/spec/Код-стайл-аудит-2026-09-11.md @@ -34,9 +34,11 @@ (запрет только для встроенных типов — как в §4); режимы «очевидный тип» и «прочие» оставлены `silent` осознанно: правка субъективна и потребовала бы ~1600 механических замен. Остаток встроенных типов выправлен `dotnet format style --diagnostics IDE0008` по всем 5 решениям (51 файл); сборка 5 sln — 0/0. -2. **Явная реализация интерфейсов (§11) — остаётся точечным ревью владельца.** Замер: 54 интерфейса с XML-doc, - из них 43 имеют реализации в src (в основном store-порты с одной реализацией). Массовая правка не - автоматизируется сознательно (см. рекомендацию выше). +2. **Явная реализация интерфейсов (§11) — выполнено (вечер, решение владельца, вариант A).** 161 член + в 30 прод-файлах конвертирован codemod'ом `scripts/make_explicit.py` (частичные классы и многострочные + сигнатуры учтены); потребители конкретных типов перетипизированы на интерфейсы (8 мест в проде, + 17 тест-файлов); `Card`/семейство `I*Card` оставлены implicit — это DTO, их члены и есть публичный API. + Правило закреплено в §11 код-стайла: классы напрямую не вызываем (DTO/хелперы/экстеншены — исключения). 3. **Дедупликация `` через `` — закрыто: дублей нет.** Проверено двумя независимыми сканами (сопоставление по тексту и по имени члена интерфейса: 39 интерфейсов, 229 задокументированных членов) — реализаций, дублирующих summary интерфейсного члена, в продакшн-коде нет; случаев diff --git a/docs/superpowers/STATUS.md b/docs/superpowers/STATUS.md index ce2355e..75a31ab 100644 --- a/docs/superpowers/STATUS.md +++ b/docs/superpowers/STATUS.md @@ -31,6 +31,16 @@ > остались точечным ревью владельца (43 интерфейса с реализациями, массовая правка не автоматизируется). > Сборка 5 sln 0/0; тесты: core **1340/1340**, telegram **130/130**, ai **52/52**, ml **38/38**, storage **9/9** — зелёные. > Детали — `docs/spec/Код-стайл-аудит-2026-09-11.md`, §2. +> +> **2026-09-11 (поздний вечер) — явные реализации интерфейсов (вариант A, решение владельца).** Правило +> владельца: классы напрямую не вызываем (исключения — DTO, хелперы, экстеншены), тесты — через +> интерфейсы, моки — NSubstitute, маркерные классы не используем. Сделано: 161 член в 30 прод-файлах +> переведён на явные реализации codemod'ом `scripts/make_explicit.py`; потребители конкретных типов +> перетипизированы на интерфейсы (8 мест в проде, 17 тест-файлов; самовызовы — `((ISessionClient)this)`); +> 10 маркерных классов заменены маркерными интерфейсами (`IKanbanModule`…`ISharedKernel`); NSubstitute 6.1.0 +> подключён к 5 тест-проектам, эталон миграции — `FakePasswordHasher` → `TestHashers.New()` (фейк удалён); +> правила зафиксированы в §11 код-стайла. Card/`I*Card` — implicit (DTO). Оставшиеся 30 фейков — +> поэтапная миграция (`backlog.md`, TD-TESTS-NSUBSTITUTE). Build 5 sln 0/0, тесты зелёные. **Все этапы 0–12 выполнены (100%)** — см. roadmap > `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10 diff --git a/src/ai-service/Deal.Ai.Tests/Deal.Ai.Tests.csproj b/src/ai-service/Deal.Ai.Tests/Deal.Ai.Tests.csproj index c4ac237..6f7543f 100644 --- a/src/ai-service/Deal.Ai.Tests/Deal.Ai.Tests.csproj +++ b/src/ai-service/Deal.Ai.Tests/Deal.Ai.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs index 27d0417..0be3a42 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Api; @@ -128,7 +129,7 @@ public sealed class OperatorBootstrapHostedServiceTests .AddInMemoryCollection(values) .Build(); var store = new FakeOperatorAuthStore(); - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var logger = new ListLogger(); // Hosted-шаг резолвит scoped OperatorBootstrapService из scope (как TenantBootstrapService): // провайдер собирается с реальной регистрацией модуля поверх фейков. @@ -150,7 +151,7 @@ public sealed class OperatorBootstrapHostedServiceTests private sealed record Context( OperatorBootstrapHostedService Hosted, FakeOperatorAuthStore Store, - FakePasswordHasher PasswordHasher, + IPasswordHasher PasswordHasher, ListLogger Logs); // Окружение хоста с фиксированным именем (тестовый IHostEnvironment). diff --git a/src/core/tests/Deal.Tests.Unit/Deal.Tests.Unit.csproj b/src/core/tests/Deal.Tests.Unit/Deal.Tests.Unit.csproj index 69b87bd..c77cb85 100644 --- a/src/core/tests/Deal.Tests.Unit/Deal.Tests.Unit.csproj +++ b/src/core/tests/Deal.Tests.Unit/Deal.Tests.Unit.csproj @@ -10,6 +10,7 @@ + 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 a0298b7..94de77c 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs @@ -1,5 +1,6 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Modules.Tenants; @@ -22,7 +23,7 @@ public sealed class AuthServiceTests public AuthServiceTests() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); _store = new FakeAuthStore(); _tenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Тестовый тенант", TenantStatuses.Active, DateTimeOffset.UtcNow)); _store.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); @@ -202,7 +203,7 @@ public sealed class AuthServiceTests [Fact] public async Task LoginAsync_WhenTenantSuspended_ReturnsSuspendedErrorWithoutSession() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var suspendedStore = new FakeAuthStore(); suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); @@ -223,7 +224,7 @@ public sealed class AuthServiceTests { // Порядок проверок AuthService: учётные данные раньше статуса тенанта — неверный пароль не раскрывает // приостановку (тот же ответ, что для активного тенанта: Login/Token/Error пусты). - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var suspendedStore = new FakeAuthStore(); suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword))); var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); @@ -264,7 +265,7 @@ public sealed class AuthServiceTests [Fact] public async Task ImpersonateAsync_WithoutLogin_TakesFirstTenantUser() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var multiUserStore = new FakeAuthStore(); var firstUserId = Guid.NewGuid(); var secondUserId = Guid.NewGuid(); @@ -292,7 +293,7 @@ public sealed class AuthServiceTests Assert.Empty(_store.Sessions); // Пользователь другого тенанта с тем же логином тоже не резолвится (не раскрываем существование логина). - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var crossStore = new FakeAuthStore(); crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p"))); var crossService = new AuthService(crossStore, passwordHasher, _tenantStore); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakePasswordHasher.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakePasswordHasher.cs deleted file mode 100644 index b4f3685..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakePasswordHasher.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Deal.Modules.Tenants.Application.Abstractions; - -namespace Deal.Tests.Unit.Modules.Tenants; - -/// -/// Детерминированный «хэшер» для тестов AuthService -/// -public sealed class FakePasswordHasher : IPasswordHasher -{ - private const string Prefix = "fake-hash:"; - - /// - public string Hash(string password) => $"{Prefix}{password}"; - - /// - public bool Verify(string password, string encodedHash) => encodedHash == $"{Prefix}{password}"; -} 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 06c5f3b..32a31b4 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs @@ -1,5 +1,7 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Modules.Tenants.Application.Abstractions; namespace Deal.Tests.Unit.Modules.Tenants; @@ -28,7 +30,7 @@ public sealed class JoinFlowTests var provisioner = new FakeTenantProvisioner(); var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var service = NewService(inviteStore, tenantStore, provisioner, authStore, passwordHasher); // Email с регистром/пробелами нормализуется и совпадает с инвайтом; name задаёт имя нового тенанта. @@ -68,7 +70,7 @@ public sealed class JoinFlowTests var inviteStore = new FakeInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new FakeTenantStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), new FakeAuthStore(), new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), new FakeAuthStore(), TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: " ", Password, CancellationToken.None); @@ -86,7 +88,7 @@ public sealed class JoinFlowTests // Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему. var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow)); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, provisioner, authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -107,7 +109,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId)); var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -125,7 +127,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId)); var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow)); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -141,7 +143,7 @@ public sealed class JoinFlowTests var inviteStore = new FakeInviteStore(); var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New()); var result = await service.ActivateAsync(WrongCode, Email, name: null, Password, CancellationToken.None); @@ -158,7 +160,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, expiresAt: DateTimeOffset.UtcNow.AddHours(-1))); var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -174,7 +176,7 @@ public sealed class JoinFlowTests { var inviteStore = new FakeInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated)); - var service = NewService(inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), new FakePasswordHasher()); + var service = NewService(inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -187,7 +189,7 @@ public sealed class JoinFlowTests { var inviteStore = new FakeInviteStore(); inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked)); - var service = NewService(inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), new FakePasswordHasher()); + var service = NewService(inviteStore, new FakeTenantStore(), new FakeTenantProvisioner(), new FakeAuthStore(), TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -202,7 +204,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, OtherEmail, name: null, Password, CancellationToken.None); @@ -222,7 +224,7 @@ public sealed class JoinFlowTests var authStore = new FakeAuthStore(); authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash")); var provisioner = new FakeTenantProvisioner(); - var service = NewService(inviteStore, tenantStore, provisioner, authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -247,7 +249,7 @@ public sealed class JoinFlowTests inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null)); var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, new FakeTenantProvisioner(), authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, password, CancellationToken.None); @@ -266,7 +268,7 @@ public sealed class JoinFlowTests var provisioner = new FakeTenantProvisioner(); var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var service = NewService(inviteStore, tenantStore, provisioner, authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New()); JoinResultDto first = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); JoinResultDto second = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -287,7 +289,7 @@ public sealed class JoinFlowTests var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); var provisioner = new FakeTenantProvisioner(); - var service = NewService(inviteStore, tenantStore, provisioner, authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -309,7 +311,7 @@ public sealed class JoinFlowTests var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); var provisioner = new FakeTenantProvisioner(); - var service = NewService(inviteStore, tenantStore, provisioner, authStore, new FakePasswordHasher()); + var service = NewService(inviteStore, tenantStore, provisioner, authStore, TestHashers.New()); var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None); @@ -355,7 +357,7 @@ public sealed class JoinFlowTests FakeTenantStore tenantStore, FakeTenantProvisioner provisioner, FakeAuthStore authStore, - FakePasswordHasher passwordHasher) => + IPasswordHasher passwordHasher) => new( new InvitesService(inviteStore), new TenantService(tenantStore, provisioner), diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs index 4d575e3..4603707 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs @@ -1,5 +1,6 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Modules.Tenants; @@ -18,7 +19,7 @@ public sealed class OperatorAuthServiceTests public OperatorAuthServiceTests() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); _store = new FakeOperatorAuthStore(); _store.AddOperator(new StoredOperatorDto(OperatorId, OperatorLogin, "active", passwordHasher.Hash(OperatorPassword))); _service = new OperatorAuthService(_store, passwordHasher); @@ -117,7 +118,7 @@ public sealed class OperatorAuthServiceTests public async Task ResolveSessionAsync_WhenOperatorIsNotActive_ReturnsNull() { const string suspendedLogin = "suspended-operator"; - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); _store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), suspendedLogin, "suspended", passwordHasher.Hash("x"))); string rawToken = "suspended-operator-session-raw-token"; _store.AddSession(new OperatorSessionDto( diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs index 09c6d28..c44ed26 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs @@ -1,5 +1,7 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Modules.Tenants.Application.Abstractions; namespace Deal.Tests.Unit.Modules.Tenants; @@ -9,12 +11,12 @@ namespace Deal.Tests.Unit.Modules.Tenants; public sealed class OperatorBootstrapServiceTests { private readonly FakeOperatorAuthStore _store; - private readonly FakePasswordHasher _passwordHasher; + private readonly IPasswordHasher _passwordHasher; private readonly OperatorBootstrapService _service; public OperatorBootstrapServiceTests() { - _passwordHasher = new FakePasswordHasher(); + _passwordHasher = TestHashers.New(); _store = new FakeOperatorAuthStore(); _service = new OperatorBootstrapService(_store, _passwordHasher); } 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 ec8c018..69e1590 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs @@ -1,5 +1,7 @@ using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; +using Deal.Tests.Unit.Support; +using Deal.Modules.Tenants.Application.Abstractions; namespace Deal.Tests.Unit.Modules.Tenants; @@ -41,7 +43,7 @@ public sealed class TenantAdminServiceTests { var tenantStore = new FakeTenantStore(); var authStore = new FakeAuthStore(); - var hasher = new FakePasswordHasher(); + var hasher = TestHashers.New(); var service = NewService(tenantStore, authStore, new FakeTenantProvisioner(), hasher); TenantCreateResultDto result = await service.CreateAsync("Тенант с владельцем", " Owner@Example.COM ", CancellationToken.None); @@ -194,15 +196,15 @@ public sealed class TenantAdminServiceTests Assert.Null(result.Tenant); } - // Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore/FakePasswordHasher). + // Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку). private static TenantAdminService NewService( FakeTenantStore tenantStore, FakeAuthStore authStore, FakeTenantProvisioner? provisioner = null, - FakePasswordHasher? hasher = null) + IPasswordHasher? hasher = null) { var tenantService = new TenantService(tenantStore, provisioner ?? new FakeTenantProvisioner()); - return new TenantAdminService(tenantStore, authStore, tenantService, hasher ?? new FakePasswordHasher()); + return new TenantAdminService(tenantStore, authStore, 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 0bc6bb2..9fed20c 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/JoinEndpointHttpTests.cs @@ -10,6 +10,7 @@ using Deal.Tests.Unit.Modules.Tenants; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -58,7 +59,7 @@ public sealed class JoinEndpointHttpTests // Пользователь создан (login=email, хэш через фейк-хэшер), тенант создан и провижинен один раз, инвайт activated. StoredUserDto user = Assert.Single(authStore.Users); Assert.Equal(Email, user.Login); - Assert.Equal(new FakePasswordHasher().Hash(Password), user.PasswordHash); + Assert.Equal(TestHashers.New().Hash(Password), user.PasswordHash); TenantRecordDto tenant = Assert.Single(tenantStore.Tenants); Assert.Equal("Acme", tenant.Name); Assert.Equal(user.TenantId, tenant.Id); @@ -260,9 +261,9 @@ public sealed class JoinEndpointHttpTests builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port)); builder.Services.AddTenantsModule(); - // FakePasswordHasher регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает + // Хэшер-подставка регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает // последняя регистрация (зеркало OperatorAuthHttpHost). - builder.Services.AddSingleton(); + builder.Services.AddSingleton(TestHashers.New()); builder.Services.AddSingleton(authStore); builder.Services.AddSingleton(auditStore); builder.Services.AddSingleton(inviteStore); diff --git a/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs index 8d76fd5..320be4b 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/LoginAttemptEndpointHttpTests.cs @@ -5,6 +5,7 @@ using Deal.Api.Configuration; using Deal.Api.Services; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -137,7 +138,7 @@ public sealed class LoginAttemptEndpointHttpTests // Фейк-хранилище пользователей с активным пользователем Login/Password. private static FakeAuthStore NewUserStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs index 7ac7dce..eee6861 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAnalyticsEndpointsHttpTests.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -257,7 +258,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), OperatorLogin, "active", passwordHasher.Hash(OperatorPassword))); @@ -266,7 +267,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests private static FakeAuthStore NewUserStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), UserLogin, ActiveTenant, "active", passwordHasher.Hash(UserPassword))); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs index c9bb124..1ee669d 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuditEndpointsHttpTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -280,7 +281,7 @@ public sealed class OperatorAuditEndpointsHttpTests // Фейк-хранилище оператора с активным operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), @@ -293,7 +294,7 @@ public sealed class OperatorAuditEndpointsHttpTests // Фейк-хранилище пользователей с admin/admin в дефолтном тенанте. private static FakeAuthStore NewUserStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs index 06eba7f..cef04fd 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthEndpointsHttpTests.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Deal.Api.Extensions; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -190,7 +191,7 @@ public sealed class OperatorAuthEndpointsHttpTests // Фейк-хранилище оператора с активным (или нет) оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore(bool active = true) { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), @@ -203,7 +204,7 @@ public sealed class OperatorAuthEndpointsHttpTests // Фейк-хранилище пользователей с admin/admin в дефолтном тенанте. private static FakeAuthStore NewUserStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs index f90afd4..10d05ef 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Deal.Tests.Unit.Support; using OperatorCookieOptions = Deal.Api.Configuration.OperatorCookieOptions; using TenantCookieOptions = Deal.Api.Configuration.CookieOptions; @@ -181,9 +182,9 @@ internal static class OperatorAuthHttpHost builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port)); builder.Services.AddTenantsModule(); - // FakePasswordHasher регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает + // Хэшер-подставка регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает // последняя регистрация (зеркало AddTenantsModule в тестах DiscoveryWorkerSchedulerTests). - builder.Services.AddSingleton(); + builder.Services.AddSingleton(TestHashers.New()); builder.Services.AddSingleton(); builder.Services.AddSingleton(userStore); builder.Services.AddSingleton(operatorStore); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs index 8300975..9665900 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorHealthEndpointsHttpTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -97,7 +98,7 @@ public sealed class OperatorHealthEndpointsHttpTests // Фейк-хранилище оператора с активным оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs index e6d9447..51a9050 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorInvitesEndpointsHttpTests.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -263,7 +264,7 @@ public sealed class OperatorInvitesEndpointsHttpTests // Фейк-хранилище оператора с активным оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs index 1a7c597..7fe32cb 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorLimitsEndpointsHttpTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -327,7 +328,7 @@ public sealed class OperatorLimitsEndpointsHttpTests // Фейк-хранилище оператора с активным оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs index a5a8a58..ea573c7 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorMaintenanceEndpointsHttpTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -77,7 +78,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests // Фейк-хранилище оператора с активным оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs index d4037bd..cd1ac47 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs @@ -5,6 +5,7 @@ using Deal.Modules.Settings.Application.Models; using Deal.Modules.Tenants.Application.Models; using Deal.Tests.Unit.Modules.Settings; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -274,7 +275,7 @@ public sealed class OperatorSettingsEndpointsHttpTests // Фейк-хранилище оператора с активным оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs index 17578c9..615a9ce 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorTenantsEndpointsHttpTests.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; using Deal.Tests.Unit.Modules.Tenants; +using Deal.Tests.Unit.Support; namespace Deal.Tests.Unit.Support; @@ -570,7 +571,7 @@ public sealed class OperatorTenantsEndpointsHttpTests // Фейк-хранилище оператора с активным оператором operator/operator. private static FakeOperatorAuthStore NewOperatorStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeOperatorAuthStore(); store.AddOperator(new StoredOperatorDto( Guid.NewGuid(), @@ -583,7 +584,7 @@ public sealed class OperatorTenantsEndpointsHttpTests // Фейк-хранилище пользователей с одним пользователем в целевом тенанте. private static FakeAuthStore NewUserStore() { - var passwordHasher = new FakePasswordHasher(); + var passwordHasher = TestHashers.New(); var store = new FakeAuthStore(); store.AddUser(new StoredUserDto( Guid.NewGuid(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/TestHashers.cs b/src/core/tests/Deal.Tests.Unit/Support/TestHashers.cs new file mode 100644 index 0000000..281471b --- /dev/null +++ b/src/core/tests/Deal.Tests.Unit/Support/TestHashers.cs @@ -0,0 +1,26 @@ +using NSubstitute; + +using Deal.Modules.Tenants.Application.Abstractions; + +namespace Deal.Tests.Unit.Support; + +/// +/// NSubstitute-хэшер пароля с детерминированной семантикой «fake-hash:пароль». +/// +public static class TestHashers +{ + private const string Prefix = "fake-hash:"; + + /// + /// Создаёт хэшер-подставку: добавляет префикс, + /// сравнивает с ним. + /// + public static IPasswordHasher New() + { + IPasswordHasher hasher = Substitute.For(); + hasher.Hash(Arg.Any()).Returns(ci => Prefix + ci.ArgAt(0)); + hasher.Verify(Arg.Any(), Arg.Any()) + .Returns(ci => ci.ArgAt(1) == Prefix + ci.ArgAt(0)); + return hasher; + } +} diff --git a/src/ml-service/Deal.Ml.Tests/Deal.Ml.Tests.csproj b/src/ml-service/Deal.Ml.Tests/Deal.Ml.Tests.csproj index 0a944df..a6ed6c8 100644 --- a/src/ml-service/Deal.Ml.Tests/Deal.Ml.Tests.csproj +++ b/src/ml-service/Deal.Ml.Tests/Deal.Ml.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/storage-service/Deal.Storage.Tests/Deal.Storage.Tests.csproj b/src/storage-service/Deal.Storage.Tests/Deal.Storage.Tests.csproj index fc679cd..8210e81 100644 --- a/src/storage-service/Deal.Storage.Tests/Deal.Storage.Tests.csproj +++ b/src/storage-service/Deal.Storage.Tests/Deal.Storage.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/telegram-service/Deal.Telegram.Tests/Deal.Telegram.Tests.csproj b/src/telegram-service/Deal.Telegram.Tests/Deal.Telegram.Tests.csproj index ece5e57..1bb34c9 100644 --- a/src/telegram-service/Deal.Telegram.Tests/Deal.Telegram.Tests.csproj +++ b/src/telegram-service/Deal.Telegram.Tests/Deal.Telegram.Tests.csproj @@ -13,6 +13,7 @@ +