Хелпер Support/TestGlobalSettingsStore: словарь состояния + NSubstitute-подставка (GetAsync через Returns, SetAsync через When/Do), DI получает .Store. Потребители (4 файла) перетипизированы, фейк удалён, тесты 1340.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user