Перевести FakeGlobalSettingsStore на NSubstitute
ci / build-test (push) Canceled after 0s

Хелпер Support/TestGlobalSettingsStore: словарь состояния + NSubstitute-подставка
(GetAsync через Returns, SetAsync через When/Do), DI получает .Store.
Потребители (4 файла) перетипизированы, фейк удалён, тесты 1340.
This commit is contained in:
Rustam Khalimov
2026-09-12 20:28:08 +03:00
parent 28b9777fa0
commit 76a705a1d8
6 changed files with 67 additions and 65 deletions
@@ -10,13 +10,13 @@ namespace Deal.Tests.Unit.Api;
/// </summary>
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]
@@ -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<DialogsService>.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)
{
@@ -1,50 +0,0 @@
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
namespace Deal.Tests.Unit.Modules.Settings;
/// <summary>
/// In-memory реализация <see cref="IGlobalSettingsStore"/> для юнит-тестов глобальных настроек.
/// </summary>
public sealed class FakeGlobalSettingsStore : IGlobalSettingsStore
{
private readonly Dictionary<string, SettingValue> _rows = new(StringComparer.Ordinal);
public IReadOnlyCollection<string> Keys => _rows.Keys.ToList();
/// <summary>
/// Кладёт готовую строку
/// </summary>
/// <param name="key">Ключ настройки.</param>
/// <param name="valueJson">Значение, сериализованное в JSON.</param>
public void Preload(string key, string valueJson)
{
_rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
}
/// <summary>
/// Возвращает JSON сохранённого значения или null, если ключа нет.
/// </summary>
/// <param name="key">Ключ настройки.</param>
/// <returns>JSON-строка значения или null.</returns>
public string? GetStoredJson(string key)
{
return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null;
}
/// <inheritdoc />
public Task<SettingValue?> GetAsync(string key, CancellationToken ct)
{
return Task.FromResult(_rows.TryGetValue(key, out SettingValue? row) ? row : null);
}
/// <inheritdoc />
public Task SetAsync(
string key,
string valueJson,
CancellationToken ct)
{
_rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
return Task.CompletedTask;
}
}
@@ -144,9 +144,9 @@ internal static class OperatorAuthHttpHost
public static async Task RunWithGlobalSettingsAsync(
FakeOperatorAuthStore operatorStore,
FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeGlobalSettingsStore, FakeAuditLogStore, Task> scenario,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, FakeAuditLogStore, Task> 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<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, FakeInviteStore, FakeAuditLogStore, FakeTenantLimitStore, FakeGlobalSettingsStore, Task> scenario,
Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeTenantStore, FakeInviteStore, FakeAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> 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<IInviteStore>(effectiveInviteStore);
builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore);
builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore);
builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore);
builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store);
builder.Services.AddSingleton<ISecretCipher>(new FakeSecretCipher());
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
builder.Services.AddScoped<TelegramKeysService>();
@@ -257,7 +257,7 @@ public sealed class OperatorSettingsEndpointsHttpTests
$"{baseAddress}/api/operator/settings/telegram-keys";
// Прогоняет сценарий на хосте с фейком глобального хранилища и аудита.
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, FakeGlobalSettingsStore, FakeAuditLogStore, Task> scenario, FakeAuditLogStore? auditStore = null) =>
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, FakeAuditLogStore, Task> scenario, FakeAuditLogStore? auditStore = null) =>
OperatorAuthHttpHost.RunWithGlobalSettingsAsync(
NewOperatorStore(),
new FakeAuthStore(),
@@ -0,0 +1,52 @@
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
using NSubstitute;
namespace Deal.Tests.Unit.Support;
/// <summary>
/// Подставка <see cref="IGlobalSettingsStore"/> на словаре: в DI уходит NSubstitute-подставка
/// (<see cref="Store"/>), а тесты читают/сеют состояние через <see cref="Preload"/> и <see cref="GetStoredJson"/>.
/// </summary>
public sealed class TestGlobalSettingsStore
{
private readonly Dictionary<string, SettingValue> _rows = new(StringComparer.Ordinal);
/// <summary>
/// Подставка порта глобальных настроек (создаётся в конструкторе).
/// </summary>
public IGlobalSettingsStore Store { get; }
/// <summary>
/// Создаёт подставку с пустым словарём.
/// </summary>
public TestGlobalSettingsStore()
{
Store = Substitute.For<IGlobalSettingsStore>();
Store.GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(ci => _rows.TryGetValue(ci.ArgAt<string>(0), out SettingValue? row) ? row : null);
Store.When(s => s.SetAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>()))
.Do(ci => _rows[ci.ArgAt<string>(0)] =
new SettingValue(ci.ArgAt<string>(0), ci.ArgAt<string>(1), DateTimeOffset.UtcNow));
}
/// <summary>
/// Кладёт готовую строку настройки.
/// </summary>
/// <param name="key">Ключ настройки.</param>
/// <param name="valueJson">Значение, сериализованное в JSON.</param>
public void Preload(string key, string valueJson)
{
_rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
}
/// <summary>
/// Возвращает JSON сохранённого значения или null, если ключа нет.
/// </summary>
/// <param name="key">Ключ настройки.</param>
/// <returns>JSON-строка значения или null.</returns>
public string? GetStoredJson(string key)
{
return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null;
}
}