Хелпер Support/TestCiphers.New: детерминированная семантика enc:base64 через Returns-лямбды. Потребители (8 файлов) перетипизированы на ISecretCipher, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Deal.Api.Telegram;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Tests.Unit.Modules.Settings;
|
||||
using Deal.Tests.Unit.Support;
|
||||
@@ -11,7 +12,7 @@ namespace Deal.Tests.Unit.Api;
|
||||
public sealed class TelegramKeysServiceTests
|
||||
{
|
||||
private readonly TestGlobalSettingsStore _store = new();
|
||||
private readonly FakeSecretCipher _cipher = new();
|
||||
private readonly ISecretCipher _cipher = TestCiphers.New();
|
||||
private readonly TelegramKeysService _service;
|
||||
|
||||
public TelegramKeysServiceTests()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Deal.Api.Telegram;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Telegram.Application;
|
||||
using Deal.Modules.Telegram.Application.Models;
|
||||
@@ -104,13 +105,13 @@ public sealed class TgStatusServiceTests
|
||||
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + FakeSecretCipher).
|
||||
private static (TgStatusService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, FakeSecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + ISecretCipher).
|
||||
private static (TgStatusService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, ISecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||
{
|
||||
var store = new TestTelegramStore();
|
||||
var settings = new FakeSettingsStore();
|
||||
var gateway = new TestTelegramGateway();
|
||||
var cipher = new FakeSecretCipher();
|
||||
var cipher = TestCiphers.New();
|
||||
var globalSettings = new TestGlobalSettingsStore();
|
||||
var keys = new TelegramKeysService(globalSettings.Store, cipher);
|
||||
var dialogs = new DialogsService(store.Store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance);
|
||||
@@ -127,7 +128,7 @@ public sealed class TgStatusServiceTests
|
||||
string apiId,
|
||||
string apiHash)
|
||||
{
|
||||
string encrypted = new FakeSecretCipher().Encrypt(apiHash);
|
||||
string encrypted = TestCiphers.New().Encrypt(apiHash);
|
||||
settings.Preload(GlobalSettingsKeys.TelegramKeys, $"{{\"apiId\":\"{apiId}\",\"apiHash\":\"{encrypted}\"}}");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Deal.Infrastructure.Integrations.Exceptions;
|
||||
using Deal.Infrastructure.Integrations.Models;
|
||||
using Deal.Infrastructure.Integrations.Options;
|
||||
using Deal.Infrastructure.Integrations.Services;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
@@ -39,7 +40,7 @@ public sealed class GrpcAiToolsTests
|
||||
reply.Keywords.Add("telegram каналы");
|
||||
service.GenerateKeywordsReply = reply;
|
||||
FakeSettingsStore settings = new();
|
||||
FakeSecretCipher cipher = new();
|
||||
ISecretCipher cipher = TestCiphers.New();
|
||||
FakeTenantLimitStore limits = new();
|
||||
|
||||
IAiTools tools = CreateTools(port, settings, cipher, limits);
|
||||
@@ -65,7 +66,7 @@ public sealed class GrpcAiToolsTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.KeywordsUnavailable = true;
|
||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher());
|
||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
|
||||
|
||||
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None);
|
||||
|
||||
@@ -88,7 +89,7 @@ public sealed class GrpcAiToolsTests
|
||||
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
||||
};
|
||||
FakeTenantLimitStore limits = new();
|
||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher(), limits);
|
||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New(), limits);
|
||||
|
||||
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
||||
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
|
||||
@@ -112,7 +113,7 @@ public sealed class GrpcAiToolsTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.FitUnavailable = true;
|
||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), new FakeSecretCipher());
|
||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
|
||||
|
||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
|
||||
@@ -128,7 +129,7 @@ public sealed class GrpcAiToolsTests
|
||||
private static GrpcAiTools CreateTools(
|
||||
int port,
|
||||
FakeSettingsStore settings,
|
||||
FakeSecretCipher cipher,
|
||||
ISecretCipher cipher,
|
||||
FakeTenantLimitStore? limits = null)
|
||||
{
|
||||
ITenantContext tenantContext = new TenantContext();
|
||||
|
||||
@@ -145,7 +145,7 @@ public sealed class IntegrationsDiTests
|
||||
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
|
||||
services.AddSingleton<ITenantContext>(tenantContext);
|
||||
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
||||
services.AddScoped<ISecretCipher>(_ => new FakeSecretCipher());
|
||||
services.AddScoped<ISecretCipher>(_ => TestCiphers.New());
|
||||
services.AddScoped<ICardStore>(_ => new FakeKanjStore());
|
||||
services.AddScoped<IMlLearningStore>(_ => new FakeMlLearningStore());
|
||||
services.AddScoped<ITenantLimitStore>(_ => new FakeTenantLimitStore());
|
||||
|
||||
@@ -9,6 +9,7 @@ using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Parse;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
@@ -178,7 +179,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
||||
var grpcClassifier = new GrpcAiClassifier(
|
||||
tenantContext,
|
||||
connection,
|
||||
new AiProviderConfigBuilder(settings, new FakeSecretCipher()),
|
||||
new AiProviderConfigBuilder(settings, TestCiphers.New()),
|
||||
new AiClassifyContextBuilder(settings, kanjStore),
|
||||
new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(new FakeTokenUsageEventStore())),
|
||||
NullLogger<GrpcAiClassifier>.Instance);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Deal.Tests.Unit.Support;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
@@ -9,6 +10,7 @@ using Deal.Infrastructure.Integrations.Options;
|
||||
using Deal.Infrastructure.Integrations.Services;
|
||||
using Deal.Modules.Kanban.Application.Models;
|
||||
using Deal.Modules.Pipeline.Application.Services;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
@@ -50,7 +52,7 @@ public sealed class GrpcAiClassifierTests
|
||||
Reason = "реклама",
|
||||
Usage = new Usage { Prompt = 500, Completion = 40, Total = 540 },
|
||||
};
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Фильтруй. {domain} | {keywords}."));
|
||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
||||
@@ -86,7 +88,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.FilterUnavailable = true;
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||
@@ -100,7 +102,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
string text = new string('а', 5000);
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await classifier.FilterAsync(text, CancellationToken.None);
|
||||
@@ -129,7 +131,7 @@ public sealed class GrpcAiClassifierTests
|
||||
""",
|
||||
Usage = new Usage { Prompt = 3000, Completion = 700, Total = 3700 },
|
||||
};
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
settings.Preload(SettingsKeys.AiPrompt, Json("Разбор: {domain}."));
|
||||
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке»."));
|
||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||
@@ -189,7 +191,7 @@ public sealed class GrpcAiClassifierTests
|
||||
Ok = false,
|
||||
Usage = new Usage { Prompt = 900, Completion = 0, Total = 900 },
|
||||
};
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
|
||||
FakeTenantLimitStore limits = new();
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||
@@ -210,7 +212,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.ClassifyUnavailable = true;
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await Assert.ThrowsAsync<AiUnavailableException>(
|
||||
@@ -224,7 +226,7 @@ public sealed class GrpcAiClassifierTests
|
||||
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
|
||||
{
|
||||
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Заголовок","stack":[],"is_spam":false}""" };
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
settings.Preload(SettingsKeys.AiProvider, Json("anthropic"));
|
||||
settings.Preload(
|
||||
SettingsKeys.AiConfigs,
|
||||
@@ -261,7 +263,7 @@ public sealed class GrpcAiClassifierTests
|
||||
{
|
||||
service.ClassifyReply = new ClassifyReply { Ok = true, Json = """{"title":"Т","stack":[],"is_spam":false}""" };
|
||||
string text = new string('б', 6000);
|
||||
(FakeSettingsStore settings, FakeSecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore);
|
||||
|
||||
await classifier.ClassifyAsync(text, CancellationToken.None);
|
||||
@@ -283,7 +285,7 @@ public sealed class GrpcAiClassifierTests
|
||||
private static GrpcAiClassifier CreateClassifier(
|
||||
int port,
|
||||
FakeSettingsStore settings,
|
||||
FakeSecretCipher cipher,
|
||||
ISecretCipher cipher,
|
||||
FakeKanjStore kanjStore,
|
||||
FakeTenantLimitStore? limits = null)
|
||||
{
|
||||
@@ -302,8 +304,8 @@ public sealed class GrpcAiClassifierTests
|
||||
// Создаёт контекст сценария (пустые фейки; сценарий переопределяет настройки/доски).
|
||||
// port: Порт хоста (не используется контекстом — единый вид хелперов).
|
||||
// Возвращает: Кортеж фейков (настройки, шифр, канбан).
|
||||
private static (FakeSettingsStore Settings, FakeSecretCipher Cipher, FakeKanjStore Kanj) Context(int port)
|
||||
=> (new FakeSettingsStore(), new FakeSecretCipher(), new FakeKanjStore());
|
||||
private static (FakeSettingsStore Settings, ISecretCipher Cipher, FakeKanjStore Kanj) Context(int port)
|
||||
=> (new FakeSettingsStore(), TestCiphers.New(), new FakeKanjStore());
|
||||
|
||||
// Сериализует значение настройки в JSON-строку (как пишет SettingsStore).
|
||||
// value: Значение (строка/список).
|
||||
|
||||
@@ -193,7 +193,7 @@ internal static class OperatorAuthHttpHost
|
||||
builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore);
|
||||
builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore);
|
||||
builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store);
|
||||
builder.Services.AddSingleton<ISecretCipher>(new FakeSecretCipher());
|
||||
builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New());
|
||||
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
|
||||
builder.Services.AddScoped<TelegramKeysService>();
|
||||
builder.Services.AddSingleton<ITokenUsageEventStore>(effectiveTokenUsageStore);
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Deal.Tests.Unit.Support;
|
||||
public sealed class SettingsServiceTests
|
||||
{
|
||||
private readonly FakeSettingsStore _store = new();
|
||||
private readonly FakeSecretCipher _cipher = new();
|
||||
private readonly ISecretCipher _cipher = TestCiphers.New();
|
||||
private readonly SettingsService _service;
|
||||
|
||||
public SettingsServiceTests()
|
||||
|
||||
+17
-6
@@ -1,17 +1,29 @@
|
||||
using System.Text;
|
||||
using Deal.Modules.Settings.Application.Abstractions;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Детерминированный шифр для тестов SettingsService
|
||||
/// Подставка <see cref="ISecretCipher"/> с детерминированной семантикой «enc:base64»
|
||||
/// (как раньше — фейк): шифрование/расшифровка взаимно обратимы в тестах.
|
||||
/// </summary>
|
||||
public sealed class FakeSecretCipher : ISecretCipher
|
||||
public static class TestCiphers
|
||||
{
|
||||
private const string EncryptedPrefix = "enc:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Encrypt(string plainText)
|
||||
/// <summary>
|
||||
/// Создаёт подставку шифра.
|
||||
/// </summary>
|
||||
public static ISecretCipher New()
|
||||
{
|
||||
ISecretCipher cipher = Substitute.For<ISecretCipher>();
|
||||
cipher.Encrypt(Arg.Any<string>()).Returns(ci => Encrypt(ci.ArgAt<string>(0)));
|
||||
cipher.Decrypt(Arg.Any<string>()).Returns(ci => Decrypt(ci.ArgAt<string>(0)));
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static string Encrypt(string plainText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plainText))
|
||||
{
|
||||
@@ -21,8 +33,7 @@ public sealed class FakeSecretCipher : ISecretCipher
|
||||
return EncryptedPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(plainText));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Decrypt(string cipherText)
|
||||
private static string Decrypt(string cipherText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cipherText))
|
||||
{
|
||||
Reference in New Issue
Block a user