Подключить NSubstitute и мигрировать FakePasswordHasher, зафиксировать правила тестов
ci / build-test (push) Has been cancelled
ci / build-test (push) Has been cancelled
NSubstitute 6.1.0 в 5 тест-проектов; эталон миграции: FakePasswordHasher удалён, вместо него Support/TestHashers.New() (Substitute.For с детерминированной семантикой fake-hash:, DI-регистрации переведены на инстанс хелпера). Правила владельца в §11 код-стайла: классы напрямую не вызываем (DTO/хелперы/ экстеншены — исключения), моки через NSubstitute, маркерные классы не используем. План миграции оставшихся фейков — backlog TD-TESTS-NSUBSTITUTE. Build 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
@@ -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/доки.
|
||||||
+2
-1
@@ -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<ReclassifyProgressDto>`; в UI — индикатор `done/total` в шапке «Неразобранного» | этап 12, D | P3 | DONE |
|
| BL-RECLASS-SSE | **Сделано (2026-09-11):** пакетная переклассификация отдаёт промежуточный прогресс через SSE `cards_reclassified` (`{progress:true,done,total,moved,kept,trashed,skipped}`) и финальное событие (`{progress:false,reclassified,moved}`); `CardReclassifier.ReclassifyInboxAsync` принимает `IProgress<ReclassifyProgressDto>`; в 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-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_*`), удалены блоки `<remarks>`, `<summary>` сжаты до короткой фразы; `//`-комментарии со ссылками удалены, в `.proto` — тоже. Строк комментариев 27 210 → ~19 100 | запрос владельца 2026-09-11 | P2 | DONE |
|
| TD-PROTO-COMMENTS | **Сделано (2026-09-11):** из комментариев убраны ссылки на процесс/прототип (`Task/Ruling/этап/python L…/main.py/прототип/LEADRADAR_*`), удалены блоки `<remarks>`, `<summary>` сжаты до короткой фразы; `//`-комментарии со ссылками удалены, в `.proto` — тоже. Строк комментариев 27 210 → ~19 100 | запрос владельца 2026-09-11 | P2 | DONE |
|
||||||
| TD-COMMENTS-IFACE | Привести код к правилам код-стайла (`docs/spec/Код-стайл-Дейл.md`). **Сделано (2026-09-11):** (1) `<summary>` только блочно — исправлено 5286 шт. в 833 файлах; (2) комментарии только на public/protected — понижено 2028 XML-доков с private/internal (359 файлов), повторный прогон — ещё 12; (3) дедупликация `<summary>`→`<inheritdoc/>`: **закрыто — дублей нет** (проверено сканами по тексту и по имени члена: 39 интерфейсов, 229 членов, случаев `<param>`+дубль не существует); (4) явная реализация интерфейсов — **остаётся точечным ревью владельца** (54 интерфейса с doc, 43 с реализациями, массовая правка не автоматизируется). Попутно: добавлены 4 недостающих `<summary>` членам интерфейсов, переведены 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) `<summary>` только блочно — 5286 шт.; (2) приватные XML-доки понижены — 2028+12; (3) дедупликация `<summary>`→`<inheritdoc/>` — дублей нет (сканы); (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) дедупликация `<summary>` — дублей нет (см. 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 |
|
| 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) дедупликация `<summary>` — дублей нет (см. 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. Инфраструктура и эксплуатация
|
## 2. Инфраструктура и эксплуатация
|
||||||
|
|||||||
@@ -232,11 +232,16 @@
|
|||||||
- **Не дублировать `<summary>` интерфейса в реализации.** Если член объявлен в интерфейсе с XML-doc,
|
- **Не дублировать `<summary>` интерфейса в реализации.** Если член объявлен в интерфейсе с XML-doc,
|
||||||
в классе-реализации достаточно `/// <inheritdoc/>` (или вообще ничего, если doc наследуется настройкой).
|
в классе-реализации достаточно `/// <inheritdoc/>` (или вообще ничего, если doc наследуется настройкой).
|
||||||
Текст описания пишется **один раз** — у интерфейса.
|
Текст описания пишется **один раз** — у интерфейса.
|
||||||
- **Явная реализация интерфейсов — где возможно.** Предпочитать явную реализацию
|
- **Явная реализация интерфейсов — по умолчанию** (`Task ICardStore.GetAsync(...)`). **[изм. 2026-09-11,
|
||||||
(`Task ICardStore.GetAsync(...)`), если член не является публичным API класса сам по себе. Если тип
|
решение владельца]** Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели
|
||||||
реализует член как собственный публичный сервис (нужен в DI/прямых вызовах) — допустима implicit,
|
(напр. `Card` и семейство `I*Card`), хелперы, extension-классы. Весь прод-код уже переведён на явные
|
||||||
но решение осознанное.
|
реализации (codemod `scripts/make_explicit.py`, идемпотентный).
|
||||||
- Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
|
- Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
|
||||||
|
- **Маркерные классы не используются** — если нужен маркер, это маркерный интерфейс
|
||||||
|
(`IKanbanModule`, `ISharedKernel` и т.п.). **[изм. 2026-09-11]**
|
||||||
|
- **Тесты: моки — через NSubstitute** (`Substitute.For<IPasswordHasher>()`), тестовые переменные
|
||||||
|
типизируются интерфейсом. Новые hand-written фейк-классы не заводить; существующие мигрируются
|
||||||
|
поэтапно (план — `backlog.md`, `TD-TESTS-NSUBSTITUTE`). **[изм. 2026-09-11]**
|
||||||
|
|
||||||
## 12. Приложение: сводная таблица правил именования
|
## 12. Приложение: сводная таблица правил именования
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,11 @@
|
|||||||
(запрет только для встроенных типов — как в §4); режимы «очевидный тип» и «прочие» оставлены `silent`
|
(запрет только для встроенных типов — как в §4); режимы «очевидный тип» и «прочие» оставлены `silent`
|
||||||
осознанно: правка субъективна и потребовала бы ~1600 механических замен. Остаток встроенных типов
|
осознанно: правка субъективна и потребовала бы ~1600 механических замен. Остаток встроенных типов
|
||||||
выправлен `dotnet format style --diagnostics IDE0008` по всем 5 решениям (51 файл); сборка 5 sln — 0/0.
|
выправлен `dotnet format style --diagnostics IDE0008` по всем 5 решениям (51 файл); сборка 5 sln — 0/0.
|
||||||
2. **Явная реализация интерфейсов (§11) — остаётся точечным ревью владельца.** Замер: 54 интерфейса с XML-doc,
|
2. **Явная реализация интерфейсов (§11) — выполнено (вечер, решение владельца, вариант A).** 161 член
|
||||||
из них 43 имеют реализации в src (в основном store-порты с одной реализацией). Массовая правка не
|
в 30 прод-файлах конвертирован codemod'ом `scripts/make_explicit.py` (частичные классы и многострочные
|
||||||
автоматизируется сознательно (см. рекомендацию выше).
|
сигнатуры учтены); потребители конкретных типов перетипизированы на интерфейсы (8 мест в проде,
|
||||||
|
17 тест-файлов); `Card`/семейство `I*Card` оставлены implicit — это DTO, их члены и есть публичный API.
|
||||||
|
Правило закреплено в §11 код-стайла: классы напрямую не вызываем (DTO/хелперы/экстеншены — исключения).
|
||||||
3. **Дедупликация `<summary>` через `<inheritdoc/>` — закрыто: дублей нет.** Проверено двумя независимыми
|
3. **Дедупликация `<summary>` через `<inheritdoc/>` — закрыто: дублей нет.** Проверено двумя независимыми
|
||||||
сканами (сопоставление по тексту и по имени члена интерфейса: 39 интерфейсов, 229 задокументированных
|
сканами (сопоставление по тексту и по имени члена интерфейса: 39 интерфейсов, 229 задокументированных
|
||||||
членов) — реализаций, дублирующих summary интерфейсного члена, в продакшн-коде нет; случаев
|
членов) — реализаций, дублирующих summary интерфейсного члена, в продакшн-коде нет; случаев
|
||||||
|
|||||||
@@ -31,6 +31,16 @@
|
|||||||
> остались точечным ревью владельца (43 интерфейса с реализациями, массовая правка не автоматизируется).
|
> остались точечным ревью владельца (43 интерфейса с реализациями, массовая правка не автоматизируется).
|
||||||
> Сборка 5 sln 0/0; тесты: core **1340/1340**, telegram **130/130**, ai **52/52**, ml **38/38**, storage **9/9** — зелёные.
|
> Сборка 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.
|
> Детали — `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
|
**Все этапы 0–12 выполнены (100%)** — см. roadmap
|
||||||
> `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10
|
> `docs/superpowers/plans/2026-09-05-deal-roadmap.md`, план этапа 10
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="6.1.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Api;
|
namespace Deal.Tests.Unit.Api;
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ public sealed class OperatorBootstrapHostedServiceTests
|
|||||||
.AddInMemoryCollection(values)
|
.AddInMemoryCollection(values)
|
||||||
.Build();
|
.Build();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var logger = new ListLogger();
|
var logger = new ListLogger();
|
||||||
// Hosted-шаг резолвит scoped OperatorBootstrapService из scope (как TenantBootstrapService):
|
// Hosted-шаг резолвит scoped OperatorBootstrapService из scope (как TenantBootstrapService):
|
||||||
// провайдер собирается с реальной регистрацией модуля поверх фейков.
|
// провайдер собирается с реальной регистрацией модуля поверх фейков.
|
||||||
@@ -150,7 +151,7 @@ public sealed class OperatorBootstrapHostedServiceTests
|
|||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
OperatorBootstrapHostedService Hosted,
|
OperatorBootstrapHostedService Hosted,
|
||||||
FakeOperatorAuthStore Store,
|
FakeOperatorAuthStore Store,
|
||||||
FakePasswordHasher PasswordHasher,
|
IPasswordHasher PasswordHasher,
|
||||||
ListLogger Logs);
|
ListLogger Logs);
|
||||||
|
|
||||||
// Окружение хоста с фиксированным именем (тестовый IHostEnvironment).
|
// Окружение хоста с фиксированным именем (тестовый IHostEnvironment).
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="6.1.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ public sealed class AuthServiceTests
|
|||||||
|
|
||||||
public AuthServiceTests()
|
public AuthServiceTests()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
_store = new FakeAuthStore();
|
_store = new FakeAuthStore();
|
||||||
_tenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Тестовый тенант", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
_tenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Тестовый тенант", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||||
_store.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
_store.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
||||||
@@ -202,7 +203,7 @@ public sealed class AuthServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoginAsync_WhenTenantSuspended_ReturnsSuspendedErrorWithoutSession()
|
public async Task LoginAsync_WhenTenantSuspended_ReturnsSuspendedErrorWithoutSession()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var suspendedStore = new FakeAuthStore();
|
var suspendedStore = new FakeAuthStore();
|
||||||
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
||||||
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||||
@@ -223,7 +224,7 @@ public sealed class AuthServiceTests
|
|||||||
{
|
{
|
||||||
// Порядок проверок AuthService: учётные данные раньше статуса тенанта — неверный пароль не раскрывает
|
// Порядок проверок AuthService: учётные данные раньше статуса тенанта — неверный пароль не раскрывает
|
||||||
// приостановку (тот же ответ, что для активного тенанта: Login/Token/Error пусты).
|
// приостановку (тот же ответ, что для активного тенанта: Login/Token/Error пусты).
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var suspendedStore = new FakeAuthStore();
|
var suspendedStore = new FakeAuthStore();
|
||||||
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
|
||||||
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
var suspendedTenantStore = new FakeTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||||
@@ -264,7 +265,7 @@ public sealed class AuthServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ImpersonateAsync_WithoutLogin_TakesFirstTenantUser()
|
public async Task ImpersonateAsync_WithoutLogin_TakesFirstTenantUser()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var multiUserStore = new FakeAuthStore();
|
var multiUserStore = new FakeAuthStore();
|
||||||
var firstUserId = Guid.NewGuid();
|
var firstUserId = Guid.NewGuid();
|
||||||
var secondUserId = Guid.NewGuid();
|
var secondUserId = Guid.NewGuid();
|
||||||
@@ -292,7 +293,7 @@ public sealed class AuthServiceTests
|
|||||||
Assert.Empty(_store.Sessions);
|
Assert.Empty(_store.Sessions);
|
||||||
|
|
||||||
// Пользователь другого тенанта с тем же логином тоже не резолвится (не раскрываем существование логина).
|
// Пользователь другого тенанта с тем же логином тоже не резолвится (не раскрываем существование логина).
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var crossStore = new FakeAuthStore();
|
var crossStore = new FakeAuthStore();
|
||||||
crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p")));
|
crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p")));
|
||||||
var crossService = new AuthService(crossStore, passwordHasher, _tenantStore);
|
var crossService = new AuthService(crossStore, passwordHasher, _tenantStore);
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Детерминированный «хэшер» для тестов AuthService
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FakePasswordHasher : IPasswordHasher
|
|
||||||
{
|
|
||||||
private const string Prefix = "fake-hash:";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string Hash(string password) => $"{Prefix}{password}";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Verify(string password, string encodedHash) => encodedHash == $"{Prefix}{password}";
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ public sealed class JoinFlowTests
|
|||||||
var provisioner = new FakeTenantProvisioner();
|
var provisioner = new FakeTenantProvisioner();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var service = NewService(inviteStore, tenantStore, provisioner, authStore, passwordHasher);
|
var service = NewService(inviteStore, tenantStore, provisioner, authStore, passwordHasher);
|
||||||
|
|
||||||
// Email с регистром/пробелами нормализуется и совпадает с инвайтом; name задаёт имя нового тенанта.
|
// Email с регистром/пробелами нормализуется и совпадает с инвайтом; name задаёт имя нового тенанта.
|
||||||
@@ -68,7 +70,7 @@ public sealed class JoinFlowTests
|
|||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
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);
|
var result = await service.ActivateAsync(Code, Email, name: " ", Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ public sealed class JoinFlowTests
|
|||||||
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
|
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
|
||||||
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
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));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
||||||
var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
|
var tenantStore = new FakeTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
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));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
|
||||||
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
var tenantStore = new FakeTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
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 inviteStore = new FakeInviteStore();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
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)));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -174,7 +176,7 @@ public sealed class JoinFlowTests
|
|||||||
{
|
{
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
|
||||||
var service = NewService(inviteStore, new FakeTenantStore(), new 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);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -187,7 +189,7 @@ public sealed class JoinFlowTests
|
|||||||
{
|
{
|
||||||
var inviteStore = new FakeInviteStore();
|
var inviteStore = new FakeInviteStore();
|
||||||
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
|
||||||
var service = NewService(inviteStore, new FakeTenantStore(), new 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);
|
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));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
var result = await service.ActivateAsync(Code, OtherEmail, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ public sealed class JoinFlowTests
|
|||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash"));
|
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash"));
|
||||||
var provisioner = new FakeTenantProvisioner();
|
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);
|
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));
|
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
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);
|
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 provisioner = new FakeTenantProvisioner();
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
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 first = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
JoinResultDto second = 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 tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var provisioner = new FakeTenantProvisioner();
|
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);
|
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 tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var provisioner = new FakeTenantProvisioner();
|
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);
|
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
|
||||||
|
|
||||||
@@ -355,7 +357,7 @@ public sealed class JoinFlowTests
|
|||||||
FakeTenantStore tenantStore,
|
FakeTenantStore tenantStore,
|
||||||
FakeTenantProvisioner provisioner,
|
FakeTenantProvisioner provisioner,
|
||||||
FakeAuthStore authStore,
|
FakeAuthStore authStore,
|
||||||
FakePasswordHasher passwordHasher) =>
|
IPasswordHasher passwordHasher) =>
|
||||||
new(
|
new(
|
||||||
new InvitesService(inviteStore),
|
new InvitesService(inviteStore),
|
||||||
new TenantService(tenantStore, provisioner),
|
new TenantService(tenantStore, provisioner),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ public sealed class OperatorAuthServiceTests
|
|||||||
|
|
||||||
public OperatorAuthServiceTests()
|
public OperatorAuthServiceTests()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
_store = new FakeOperatorAuthStore();
|
_store = new FakeOperatorAuthStore();
|
||||||
_store.AddOperator(new StoredOperatorDto(OperatorId, OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
|
_store.AddOperator(new StoredOperatorDto(OperatorId, OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
|
||||||
_service = new OperatorAuthService(_store, passwordHasher);
|
_service = new OperatorAuthService(_store, passwordHasher);
|
||||||
@@ -117,7 +118,7 @@ public sealed class OperatorAuthServiceTests
|
|||||||
public async Task ResolveSessionAsync_WhenOperatorIsNotActive_ReturnsNull()
|
public async Task ResolveSessionAsync_WhenOperatorIsNotActive_ReturnsNull()
|
||||||
{
|
{
|
||||||
const string suspendedLogin = "suspended-operator";
|
const string suspendedLogin = "suspended-operator";
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
_store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), suspendedLogin, "suspended", passwordHasher.Hash("x")));
|
_store.AddOperator(new StoredOperatorDto(Guid.NewGuid(), suspendedLogin, "suspended", passwordHasher.Hash("x")));
|
||||||
string rawToken = "suspended-operator-session-raw-token";
|
string rawToken = "suspended-operator-session-raw-token";
|
||||||
_store.AddSession(new OperatorSessionDto(
|
_store.AddSession(new OperatorSessionDto(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
|
||||||
@@ -9,12 +11,12 @@ namespace Deal.Tests.Unit.Modules.Tenants;
|
|||||||
public sealed class OperatorBootstrapServiceTests
|
public sealed class OperatorBootstrapServiceTests
|
||||||
{
|
{
|
||||||
private readonly FakeOperatorAuthStore _store;
|
private readonly FakeOperatorAuthStore _store;
|
||||||
private readonly FakePasswordHasher _passwordHasher;
|
private readonly IPasswordHasher _passwordHasher;
|
||||||
private readonly OperatorBootstrapService _service;
|
private readonly OperatorBootstrapService _service;
|
||||||
|
|
||||||
public OperatorBootstrapServiceTests()
|
public OperatorBootstrapServiceTests()
|
||||||
{
|
{
|
||||||
_passwordHasher = new FakePasswordHasher();
|
_passwordHasher = TestHashers.New();
|
||||||
_store = new FakeOperatorAuthStore();
|
_store = new FakeOperatorAuthStore();
|
||||||
_service = new OperatorBootstrapService(_store, _passwordHasher);
|
_service = new OperatorBootstrapService(_store, _passwordHasher);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ public sealed class TenantAdminServiceTests
|
|||||||
{
|
{
|
||||||
var tenantStore = new FakeTenantStore();
|
var tenantStore = new FakeTenantStore();
|
||||||
var authStore = new FakeAuthStore();
|
var authStore = new FakeAuthStore();
|
||||||
var hasher = new FakePasswordHasher();
|
var hasher = TestHashers.New();
|
||||||
var service = NewService(tenantStore, authStore, new FakeTenantProvisioner(), hasher);
|
var service = NewService(tenantStore, authStore, new FakeTenantProvisioner(), hasher);
|
||||||
|
|
||||||
TenantCreateResultDto result = await service.CreateAsync("Тенант с владельцем", " Owner@Example.COM ", CancellationToken.None);
|
TenantCreateResultDto result = await service.CreateAsync("Тенант с владельцем", " Owner@Example.COM ", CancellationToken.None);
|
||||||
@@ -194,15 +196,15 @@ public sealed class TenantAdminServiceTests
|
|||||||
Assert.Null(result.Tenant);
|
Assert.Null(result.Tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore/FakePasswordHasher).
|
// Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку).
|
||||||
private static TenantAdminService NewService(
|
private static TenantAdminService NewService(
|
||||||
FakeTenantStore tenantStore,
|
FakeTenantStore tenantStore,
|
||||||
FakeAuthStore authStore,
|
FakeAuthStore authStore,
|
||||||
FakeTenantProvisioner? provisioner = null,
|
FakeTenantProvisioner? provisioner = null,
|
||||||
FakePasswordHasher? hasher = null)
|
IPasswordHasher? hasher = null)
|
||||||
{
|
{
|
||||||
var tenantService = new TenantService(tenantStore, provisioner ?? new FakeTenantProvisioner());
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пользователь тенанта (активный, без хэша — для чтения).
|
// Пользователь тенанта (активный, без хэша — для чтения).
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Deal.Tests.Unit.Modules.Tenants;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ public sealed class JoinEndpointHttpTests
|
|||||||
// Пользователь создан (login=email, хэш через фейк-хэшер), тенант создан и провижинен один раз, инвайт activated.
|
// Пользователь создан (login=email, хэш через фейк-хэшер), тенант создан и провижинен один раз, инвайт activated.
|
||||||
StoredUserDto user = Assert.Single(authStore.Users);
|
StoredUserDto user = Assert.Single(authStore.Users);
|
||||||
Assert.Equal(Email, user.Login);
|
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);
|
TenantRecordDto tenant = Assert.Single(tenantStore.Tenants);
|
||||||
Assert.Equal("Acme", tenant.Name);
|
Assert.Equal("Acme", tenant.Name);
|
||||||
Assert.Equal(user.TenantId, tenant.Id);
|
Assert.Equal(user.TenantId, tenant.Id);
|
||||||
@@ -260,9 +261,9 @@ public sealed class JoinEndpointHttpTests
|
|||||||
builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port));
|
builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port));
|
||||||
|
|
||||||
builder.Services.AddTenantsModule();
|
builder.Services.AddTenantsModule();
|
||||||
// FakePasswordHasher регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает
|
// Хэшер-подставка регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает
|
||||||
// последняя регистрация (зеркало OperatorAuthHttpHost).
|
// последняя регистрация (зеркало OperatorAuthHttpHost).
|
||||||
builder.Services.AddSingleton<IPasswordHasher, FakePasswordHasher>();
|
builder.Services.AddSingleton(TestHashers.New());
|
||||||
builder.Services.AddSingleton<IAuthStore>(authStore);
|
builder.Services.AddSingleton<IAuthStore>(authStore);
|
||||||
builder.Services.AddSingleton<IAuditLogStore>(auditStore);
|
builder.Services.AddSingleton<IAuditLogStore>(auditStore);
|
||||||
builder.Services.AddSingleton<IInviteStore>(inviteStore);
|
builder.Services.AddSingleton<IInviteStore>(inviteStore);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Deal.Api.Configuration;
|
|||||||
using Deal.Api.Services;
|
using Deal.Api.Services;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ public sealed class LoginAttemptEndpointHttpTests
|
|||||||
// Фейк-хранилище пользователей с активным пользователем Login/Password.
|
// Фейк-хранилище пользователей с активным пользователем Login/Password.
|
||||||
private static FakeAuthStore NewUserStore()
|
private static FakeAuthStore NewUserStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeAuthStore();
|
var store = new FakeAuthStore();
|
||||||
store.AddUser(new StoredUserDto(
|
store.AddUser(new StoredUserDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -257,7 +258,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
|||||||
|
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(), OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
|
Guid.NewGuid(), OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
|
||||||
@@ -266,7 +267,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
|||||||
|
|
||||||
private static FakeAuthStore NewUserStore()
|
private static FakeAuthStore NewUserStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeAuthStore();
|
var store = new FakeAuthStore();
|
||||||
store.AddUser(new StoredUserDto(
|
store.AddUser(new StoredUserDto(
|
||||||
Guid.NewGuid(), UserLogin, ActiveTenant, "active", passwordHasher.Hash(UserPassword)));
|
Guid.NewGuid(), UserLogin, ActiveTenant, "active", passwordHasher.Hash(UserPassword)));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -280,7 +281,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным operator/operator.
|
// Фейк-хранилище оператора с активным operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
@@ -293,7 +294,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
|||||||
// Фейк-хранилище пользователей с admin/admin в дефолтном тенанте.
|
// Фейк-хранилище пользователей с admin/admin в дефолтном тенанте.
|
||||||
private static FakeAuthStore NewUserStore()
|
private static FakeAuthStore NewUserStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeAuthStore();
|
var store = new FakeAuthStore();
|
||||||
store.AddUser(new StoredUserDto(
|
store.AddUser(new StoredUserDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using Deal.Api.Extensions;
|
using Deal.Api.Extensions;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ public sealed class OperatorAuthEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным (или нет) оператором operator/operator.
|
// Фейк-хранилище оператора с активным (или нет) оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore(bool active = true)
|
private static FakeOperatorAuthStore NewOperatorStore(bool active = true)
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
@@ -203,7 +204,7 @@ public sealed class OperatorAuthEndpointsHttpTests
|
|||||||
// Фейк-хранилище пользователей с admin/admin в дефолтном тенанте.
|
// Фейк-хранилище пользователей с admin/admin в дефолтном тенанте.
|
||||||
private static FakeAuthStore NewUserStore()
|
private static FakeAuthStore NewUserStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeAuthStore();
|
var store = new FakeAuthStore();
|
||||||
store.AddUser(new StoredUserDto(
|
store.AddUser(new StoredUserDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using OperatorCookieOptions = Deal.Api.Configuration.OperatorCookieOptions;
|
using OperatorCookieOptions = Deal.Api.Configuration.OperatorCookieOptions;
|
||||||
using TenantCookieOptions = Deal.Api.Configuration.CookieOptions;
|
using TenantCookieOptions = Deal.Api.Configuration.CookieOptions;
|
||||||
|
|
||||||
@@ -181,9 +182,9 @@ internal static class OperatorAuthHttpHost
|
|||||||
builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port));
|
builder.WebHost.ConfigureKestrel(kestrel => kestrel.Listen(IPAddress.Loopback, port));
|
||||||
|
|
||||||
builder.Services.AddTenantsModule();
|
builder.Services.AddTenantsModule();
|
||||||
// FakePasswordHasher регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает
|
// Хэшер-подставка регистрируется ПОСЛЕ AddTenantsModule (DefaultPasswordHasher) — побеждает
|
||||||
// последняя регистрация (зеркало AddTenantsModule в тестах DiscoveryWorkerSchedulerTests).
|
// последняя регистрация (зеркало AddTenantsModule в тестах DiscoveryWorkerSchedulerTests).
|
||||||
builder.Services.AddSingleton<IPasswordHasher, FakePasswordHasher>();
|
builder.Services.AddSingleton(TestHashers.New());
|
||||||
builder.Services.AddSingleton<ITenantContext, TenantContext>();
|
builder.Services.AddSingleton<ITenantContext, TenantContext>();
|
||||||
builder.Services.AddSingleton<IAuthStore>(userStore);
|
builder.Services.AddSingleton<IAuthStore>(userStore);
|
||||||
builder.Services.AddSingleton<IOperatorAuthStore>(operatorStore);
|
builder.Services.AddSingleton<IOperatorAuthStore>(operatorStore);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ public sealed class OperatorHealthEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -263,7 +264,7 @@ public sealed class OperatorInvitesEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -327,7 +328,7 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Deal.Modules.Settings.Application.Models;
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ public sealed class OperatorSettingsEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
|||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Support;
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
@@ -570,7 +571,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
|||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
private static FakeOperatorAuthStore NewOperatorStore()
|
private static FakeOperatorAuthStore NewOperatorStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeOperatorAuthStore();
|
var store = new FakeOperatorAuthStore();
|
||||||
store.AddOperator(new StoredOperatorDto(
|
store.AddOperator(new StoredOperatorDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
@@ -583,7 +584,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
|||||||
// Фейк-хранилище пользователей с одним пользователем в целевом тенанте.
|
// Фейк-хранилище пользователей с одним пользователем в целевом тенанте.
|
||||||
private static FakeAuthStore NewUserStore()
|
private static FakeAuthStore NewUserStore()
|
||||||
{
|
{
|
||||||
var passwordHasher = new FakePasswordHasher();
|
var passwordHasher = TestHashers.New();
|
||||||
var store = new FakeAuthStore();
|
var store = new FakeAuthStore();
|
||||||
store.AddUser(new StoredUserDto(
|
store.AddUser(new StoredUserDto(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NSubstitute-хэшер пароля с детерминированной семантикой «fake-hash:пароль».
|
||||||
|
/// </summary>
|
||||||
|
public static class TestHashers
|
||||||
|
{
|
||||||
|
private const string Prefix = "fake-hash:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт хэшер-подставку: <see cref="IPasswordHasher.Hash"/> добавляет префикс,
|
||||||
|
/// <see cref="IPasswordHasher.Verify"/> сравнивает с ним.
|
||||||
|
/// </summary>
|
||||||
|
public static IPasswordHasher New()
|
||||||
|
{
|
||||||
|
IPasswordHasher hasher = Substitute.For<IPasswordHasher>();
|
||||||
|
hasher.Hash(Arg.Any<string>()).Returns(ci => Prefix + ci.ArgAt<string>(0));
|
||||||
|
hasher.Verify(Arg.Any<string>(), Arg.Any<string>())
|
||||||
|
.Returns(ci => ci.ArgAt<string>(1) == Prefix + ci.ArgAt<string>(0));
|
||||||
|
return hasher;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="6.1.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="6.1.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="6.1.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user