diff --git a/src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs index 39451c4..8daca6e 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/TelegramKeysServiceTests.cs @@ -10,13 +10,13 @@ namespace Deal.Tests.Unit.Api; /// public sealed class TelegramKeysServiceTests { - private readonly FakeGlobalSettingsStore _store = new(); + private readonly TestGlobalSettingsStore _store = new(); private readonly FakeSecretCipher _cipher = new(); private readonly TelegramKeysService _service; public TelegramKeysServiceTests() { - _service = new TelegramKeysService(_store, _cipher); + _service = new TelegramKeysService(_store.Store, _cipher); } [Fact] diff --git a/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs index 806ec82..8e8b081 100644 --- a/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs @@ -42,7 +42,7 @@ public sealed class TgStatusServiceTests [Fact] public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys() { - (TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, FakeGlobalSettingsStore globalSettings) = Create(); + (TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create(); store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true)); store.Seed(Dialog("d_2", "Группа", "group", Monitor: true)); store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false)); @@ -68,7 +68,7 @@ public sealed class TgStatusServiceTests [Fact] public async Task GetAsync_ReadyGateway_ComposesLiveFieldsWithKvAccountMonitoredAndKeys() { - (TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, FakeGlobalSettingsStore globalSettings) = Create(); + (TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create(); store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true)); settings.Preload(SettingsKeys.TgAccount, "\"@realuser\""); PreloadKeys(globalSettings, "123456", "abcdefghijklmnop"); @@ -105,14 +105,14 @@ public sealed class TgStatusServiceTests // ─── Хелперы ──────────────────────────────────────────────────────────── // Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + FakeSecretCipher). - private static (TgStatusService Service, FakeTelegramStore Store, FakeSettingsStore Settings, FakeTelegramGateway Gateway, FakeSecretCipher Cipher, FakeGlobalSettingsStore GlobalSettings) Create() + private static (TgStatusService Service, FakeTelegramStore Store, FakeSettingsStore Settings, FakeTelegramGateway Gateway, FakeSecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create() { var store = new FakeTelegramStore(); var settings = new FakeSettingsStore(); var gateway = new FakeTelegramGateway(); var cipher = new FakeSecretCipher(); - var globalSettings = new FakeGlobalSettingsStore(); - var keys = new TelegramKeysService(globalSettings, cipher); + var globalSettings = new TestGlobalSettingsStore(); + var keys = new TelegramKeysService(globalSettings.Store, cipher); var dialogs = new DialogsService(store, settings, gateway, NullLogger.Instance); var service = new TgStatusService(gateway, dialogs, settings, keys); return (service, store, settings, gateway, cipher, globalSettings); @@ -123,7 +123,7 @@ public sealed class TgStatusServiceTests // apiId: apiId приложения (5..9 цифр). // apiHash: Открытый api_hash (ядро шифрует при сохранении). private static void PreloadKeys( - FakeGlobalSettingsStore settings, + TestGlobalSettingsStore settings, string apiId, string apiHash) { diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeGlobalSettingsStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeGlobalSettingsStore.cs deleted file mode 100644 index a3f97c3..0000000 --- a/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeGlobalSettingsStore.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Deal.Modules.Settings.Application.Abstractions; -using Deal.Modules.Settings.Application.Models; - -namespace Deal.Tests.Unit.Modules.Settings; - -/// -/// In-memory реализация для юнит-тестов глобальных настроек. -/// -public sealed class FakeGlobalSettingsStore : IGlobalSettingsStore -{ - private readonly Dictionary _rows = new(StringComparer.Ordinal); - - public IReadOnlyCollection Keys => _rows.Keys.ToList(); - - /// - /// Кладёт готовую строку - /// - /// Ключ настройки. - /// Значение, сериализованное в JSON. - public void Preload(string key, string valueJson) - { - _rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow); - } - - /// - /// Возвращает JSON сохранённого значения или null, если ключа нет. - /// - /// Ключ настройки. - /// JSON-строка значения или null. - public string? GetStoredJson(string key) - { - return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null; - } - - /// - public Task GetAsync(string key, CancellationToken ct) - { - return Task.FromResult(_rows.TryGetValue(key, out SettingValue? row) ? row : null); - } - - /// - public Task SetAsync( - string key, - string valueJson, - CancellationToken ct) - { - _rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow); - return Task.CompletedTask; - } -} diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs index 10d05ef..52007b7 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorAuthHttpHost.cs @@ -144,9 +144,9 @@ internal static class OperatorAuthHttpHost public static async Task RunWithGlobalSettingsAsync( FakeOperatorAuthStore operatorStore, FakeAuthStore userStore, - Func scenario, + Func scenario, FakeAuditLogStore? auditStore = null, - FakeGlobalSettingsStore? globalSettingsStore = null) => + TestGlobalSettingsStore? globalSettingsStore = null) => await RunCoreAsync( operatorStore, userStore, @@ -165,17 +165,17 @@ internal static class OperatorAuthHttpHost FakeInviteStore? inviteStore, FakeTenantStore? tenantStore, FakeTenantLimitStore? limitStore, - Func scenario, + Func scenario, RateLimitOptions? rateLimitOptions = null, FakeTokenUsageEventStore? tokenUsageStore = null, - FakeGlobalSettingsStore? globalSettingsStore = null) + TestGlobalSettingsStore? globalSettingsStore = null) { FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore(); FakeInviteStore effectiveInviteStore = inviteStore ?? new FakeInviteStore(); FakeTenantStore effectiveTenantStore = tenantStore ?? new FakeTenantStore(); FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore(); FakeTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new FakeTokenUsageEventStore(); - FakeGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new FakeGlobalSettingsStore(); + TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore(); RateLimitOptions effectiveRateLimitOptions = rateLimitOptions ?? new RateLimitOptions(); int port = TestPort.Allocate(); WebApplicationBuilder builder = WebApplication.CreateBuilder(); @@ -192,7 +192,7 @@ internal static class OperatorAuthHttpHost builder.Services.AddSingleton(effectiveInviteStore); builder.Services.AddSingleton(effectiveTenantStore); builder.Services.AddSingleton(effectiveLimitStore); - builder.Services.AddSingleton(effectiveGlobalSettingsStore); + builder.Services.AddSingleton(effectiveGlobalSettingsStore.Store); builder.Services.AddSingleton(new FakeSecretCipher()); // Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys). builder.Services.AddScoped(); diff --git a/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs index cd1ac47..ef1d402 100644 --- a/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Support/OperatorSettingsEndpointsHttpTests.cs @@ -257,7 +257,7 @@ public sealed class OperatorSettingsEndpointsHttpTests $"{baseAddress}/api/operator/settings/telegram-keys"; // Прогоняет сценарий на хосте с фейком глобального хранилища и аудита. - private static Task RunAsync(Func scenario, FakeAuditLogStore? auditStore = null) => + private static Task RunAsync(Func scenario, FakeAuditLogStore? auditStore = null) => OperatorAuthHttpHost.RunWithGlobalSettingsAsync( NewOperatorStore(), new FakeAuthStore(), diff --git a/src/core/tests/Deal.Tests.Unit/Support/TestGlobalSettingsStore.cs b/src/core/tests/Deal.Tests.Unit/Support/TestGlobalSettingsStore.cs new file mode 100644 index 0000000..3a50ce5 --- /dev/null +++ b/src/core/tests/Deal.Tests.Unit/Support/TestGlobalSettingsStore.cs @@ -0,0 +1,52 @@ +using Deal.Modules.Settings.Application.Abstractions; +using Deal.Modules.Settings.Application.Models; +using NSubstitute; + +namespace Deal.Tests.Unit.Support; + +/// +/// Подставка на словаре: в DI уходит NSubstitute-подставка +/// (), а тесты читают/сеют состояние через и . +/// +public sealed class TestGlobalSettingsStore +{ + private readonly Dictionary _rows = new(StringComparer.Ordinal); + + /// + /// Подставка порта глобальных настроек (создаётся в конструкторе). + /// + public IGlobalSettingsStore Store { get; } + + /// + /// Создаёт подставку с пустым словарём. + /// + public TestGlobalSettingsStore() + { + Store = Substitute.For(); + Store.GetAsync(Arg.Any(), Arg.Any()) + .Returns(ci => _rows.TryGetValue(ci.ArgAt(0), out SettingValue? row) ? row : null); + Store.When(s => s.SetAsync(Arg.Any(), Arg.Any(), Arg.Any())) + .Do(ci => _rows[ci.ArgAt(0)] = + new SettingValue(ci.ArgAt(0), ci.ArgAt(1), DateTimeOffset.UtcNow)); + } + + /// + /// Кладёт готовую строку настройки. + /// + /// Ключ настройки. + /// Значение, сериализованное в JSON. + public void Preload(string key, string valueJson) + { + _rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow); + } + + /// + /// Возвращает JSON сохранённого значения или null, если ключа нет. + /// + /// Ключ настройки. + /// JSON-строка значения или null. + public string? GetStoredJson(string key) + { + return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null; + } +}