Перевести FakeSecretCipher на NSubstitute
ci / build-test (push) Successful in 4m45s

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