diff --git a/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs
new file mode 100644
index 0000000..d6646b0
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiClassifierFactory.cs
@@ -0,0 +1,15 @@
+using Deal.Contracts.Integrations.Abstractions;
+
+namespace Deal.Infrastructure.Integrations.Abstractions;
+
+///
+/// Фабрика ИИ-классификатора с бюджетным гейтом
+///
+public interface IAiClassifierFactory
+{
+ ///
+ /// Создаёт ИИ-классификатор
+ ///
+ /// Готовый порт классификации.
+ public IAiClassifier Create();
+}
diff --git a/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs
new file mode 100644
index 0000000..0952945
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Abstractions/IAiToolsFactory.cs
@@ -0,0 +1,15 @@
+using Deal.Contracts.Integrations.Abstractions;
+
+namespace Deal.Infrastructure.Integrations.Abstractions;
+
+///
+/// Фабрика ИИ-инструментов с бюджетным гейтом
+///
+public interface IAiToolsFactory
+{
+ ///
+ /// Создаёт ИИ-инструменты
+ ///
+ /// Готовый порт ИИ-инструментов.
+ public IAiTools Create();
+}
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs
new file mode 100644
index 0000000..fc0f698
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Services/AiClassifierFactory.cs
@@ -0,0 +1,50 @@
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Infrastructure.Integrations.Abstractions;
+using Deal.Modules.Tenants.Application.Abstractions;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Microsoft.Extensions.Logging;
+
+namespace Deal.Infrastructure.Integrations.Services;
+
+///
+/// Фабрика декоратора бюджетного гейта классификатора
+///
+public sealed class AiClassifierFactory : IAiClassifierFactory
+{
+ private readonly GrpcAiClassifier _paidClassifier;
+ private readonly LocalAiClassifier _localClassifier;
+ private readonly ITenantLimitStore _tenantLimits;
+ private readonly ITenantContext _tenantContext;
+ private readonly ILogger _logger;
+
+ ///
+ /// Создаёт фабрику классификатора
+ ///
+ /// Платный исполнитель (gRPC-адаптер ai-service).
+ /// Бесплатный локальный разбор/фильтр (fallback).
+ /// Хранилище лимитов бюджета.
+ /// Контекст текущего тенанта.
+ /// Логгер переходов на локальный путь.
+ public AiClassifierFactory(
+ GrpcAiClassifier paidClassifier,
+ LocalAiClassifier localClassifier,
+ ITenantLimitStore tenantLimits,
+ ITenantContext tenantContext,
+ ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(paidClassifier);
+ ArgumentNullException.ThrowIfNull(localClassifier);
+ ArgumentNullException.ThrowIfNull(tenantLimits);
+ ArgumentNullException.ThrowIfNull(tenantContext);
+ ArgumentNullException.ThrowIfNull(logger);
+ _paidClassifier = paidClassifier;
+ _localClassifier = localClassifier;
+ _tenantLimits = tenantLimits;
+ _tenantContext = tenantContext;
+ _logger = logger;
+ }
+
+ ///
+ public IAiClassifier Create() =>
+ new BudgetedAiClassifier(_paidClassifier, _localClassifier, _tenantLimits, _tenantContext, _logger);
+}
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs
new file mode 100644
index 0000000..3815c42
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Services/AiToolsFactory.cs
@@ -0,0 +1,44 @@
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Infrastructure.Integrations.Abstractions;
+using Deal.Modules.Tenants.Application.Abstractions;
+using Deal.SharedKernel.Tenants.Abstractions;
+using Microsoft.Extensions.Logging;
+
+namespace Deal.Infrastructure.Integrations.Services;
+
+///
+/// Фабрика декоратора бюджетного гейта ИИ-инструментов
+///
+public sealed class AiToolsFactory : IAiToolsFactory
+{
+ private readonly GrpcAiTools _paidTools;
+ private readonly ITenantLimitStore _tenantLimits;
+ private readonly ITenantContext _tenantContext;
+ private readonly ILogger _logger;
+
+ ///
+ /// Создаёт фабрику ИИ-инструментов
+ ///
+ /// Платный исполнитель (gRPC-адаптер ai-service).
+ /// Хранилище лимитов бюджета.
+ /// Контекст текущего тенанта.
+ /// Логгер переходов на локальный путь.
+ public AiToolsFactory(
+ GrpcAiTools paidTools,
+ ITenantLimitStore tenantLimits,
+ ITenantContext tenantContext,
+ ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(paidTools);
+ ArgumentNullException.ThrowIfNull(tenantLimits);
+ ArgumentNullException.ThrowIfNull(tenantContext);
+ ArgumentNullException.ThrowIfNull(logger);
+ _paidTools = paidTools;
+ _tenantLimits = tenantLimits;
+ _tenantContext = tenantContext;
+ _logger = logger;
+ }
+
+ ///
+ public IAiTools Create() => new BudgetedAiTools(_paidTools, _tenantLimits, _tenantContext, _logger);
+}
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs
new file mode 100644
index 0000000..1760129
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Abstractions/IFileStorageFactory.cs
@@ -0,0 +1,15 @@
+using Deal.Contracts.Integrations.Abstractions;
+
+namespace Deal.Infrastructure.Integrations.Storage.Abstractions;
+
+///
+/// Фабрика файлового хранилища вложений (Local или MinIO по конфигурации)
+///
+public interface IFileStorageFactory
+{
+ ///
+ /// Создаёт файловое хранилище
+ ///
+ /// Готовый порт файлового хранилища.
+ public IFileStorage Create();
+}
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs
new file mode 100644
index 0000000..363fc4b
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Options/LocalStorageRoot.cs
@@ -0,0 +1,7 @@
+namespace Deal.Infrastructure.Integrations.Storage.Options;
+
+///
+/// Абсолютный путь корня локального файлового хранилища
+///
+/// Абсолютный путь каталога вложений.
+public sealed record LocalStorageRoot(string Path);
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs
new file mode 100644
index 0000000..42f73cd
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageFactory.cs
@@ -0,0 +1,42 @@
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Infrastructure.Integrations.Storage.Abstractions;
+using Deal.Infrastructure.Integrations.Storage.Extensions;
+using Deal.Infrastructure.Integrations.Storage.Options;
+using Microsoft.Extensions.Logging;
+
+namespace Deal.Infrastructure.Integrations.Storage.Services;
+
+///
+/// Фабрика файлового хранилища: MinIO при заполненной секции, иначе локальный каталог
+///
+public sealed class FileStorageFactory : IFileStorageFactory
+{
+ private readonly StorageOptions _options;
+ private readonly LocalStorageRoot _localRoot;
+ private readonly ILogger _minioLogger;
+
+ ///
+ /// Создаёт фабрику файлового хранилища
+ ///
+ /// Настройки секции Storage.
+ /// Абсолютный путь корня локального режима.
+ /// Логгер MinIO-адаптера.
+ public FileStorageFactory(
+ StorageOptions options,
+ LocalStorageRoot localRoot,
+ ILogger minioLogger)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(localRoot);
+ ArgumentNullException.ThrowIfNull(minioLogger);
+ _options = options;
+ _localRoot = localRoot;
+ _minioLogger = minioLogger;
+ }
+
+ ///
+ public IFileStorage Create() =>
+ _options.Minio.IsConfigured()
+ ? new MinioFileStorage(_options.Minio, _minioLogger)
+ : new LocalFileStorage(_localRoot.Path);
+}
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
index 007b84c..b993afb 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
@@ -1,4 +1,5 @@
using Deal.Contracts.Integrations.Abstractions;
+using Deal.Infrastructure.Integrations.Storage.Abstractions;
using Deal.Infrastructure.Integrations.Storage.Extensions;
using Deal.Infrastructure.Integrations.Storage.Options;
using Microsoft.Extensions.Configuration;
@@ -48,16 +49,12 @@ public static class FileStorageRegistrar
ArgumentNullException.ThrowIfNull(configuration);
StorageOptions options = ReadOptions(configuration);
-
- if (options.Minio.IsConfigured())
- {
- services.AddSingleton(serviceProvider =>
- new MinioFileStorage(options.Minio, serviceProvider.GetRequiredService>()));
- return services;
- }
-
string rootPath = ResolveLocalRoot(options.Local, contentRootPath);
- services.AddSingleton(new LocalFileStorage(rootPath));
+ services.AddSingleton(options);
+ services.AddSingleton(new LocalStorageRoot(rootPath));
+ services.AddSingleton();
+ services.AddSingleton(serviceProvider =>
+ serviceProvider.GetRequiredService().Create());
return services;
}
diff --git a/src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs
new file mode 100644
index 0000000..f8fadbd
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Persistence/Abstractions/ITenantLimitStoreFactory.cs
@@ -0,0 +1,15 @@
+using Deal.Modules.Tenants.Application.Abstractions;
+
+namespace Deal.Infrastructure.Persistence.Abstractions;
+
+///
+/// Фабрика хранилища лимитов ИИ-бюджета
+///
+public interface ITenantLimitStoreFactory
+{
+ ///
+ /// Создаёт хранилище лимитов
+ ///
+ /// Готовый порт лимитов тенанта.
+ public ITenantLimitStore Create();
+}
diff --git a/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs
new file mode 100644
index 0000000..f6a9cd3
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Persistence/Services/TenantLimitStoreFactory.cs
@@ -0,0 +1,31 @@
+using Deal.Infrastructure.Persistence.Abstractions;
+using Deal.Infrastructure.Persistence.Repositories;
+using Deal.Modules.Tenants.Application.Abstractions;
+using Deal.Modules.Tenants.Application.Models;
+
+namespace Deal.Infrastructure.Persistence.Services;
+
+///
+/// Фабрика EF-адаптера хранилища лимитов с дефолт-бюджетом из конфигурации
+///
+public sealed class TenantLimitStoreFactory : ITenantLimitStoreFactory
+{
+ private readonly DealDbContext _dbContext;
+ private readonly TokenLimitDefaults _defaults;
+
+ ///
+ /// Создаёт фабрику хранилища лимитов
+ ///
+ /// Системный контекст (public-схема).
+ /// Дефолт-параметры лениво создаваемой строки.
+ public TenantLimitStoreFactory(DealDbContext dbContext, TokenLimitDefaults defaults)
+ {
+ ArgumentNullException.ThrowIfNull(dbContext);
+ ArgumentNullException.ThrowIfNull(defaults);
+ _dbContext = dbContext;
+ _defaults = defaults;
+ }
+
+ ///
+ public ITenantLimitStore Create() => new TenantLimitStore(_dbContext, _defaults);
+}
diff --git a/src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs
new file mode 100644
index 0000000..e79b491
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Security/Abstractions/ISecretCipherFactory.cs
@@ -0,0 +1,15 @@
+using Deal.Modules.Settings.Application.Abstractions;
+
+namespace Deal.Infrastructure.Security.Abstractions;
+
+///
+/// Фабрика шифра секретов тенанта
+///
+public interface ISecretCipherFactory
+{
+ ///
+ /// Создаёт шифр секретов
+ ///
+ /// Готовый порт симметричного шифрования.
+ public ISecretCipher Create();
+}
diff --git a/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs
new file mode 100644
index 0000000..b6aeaad
--- /dev/null
+++ b/src/core/Deal.Infrastructure/Security/Services/SecretCipherFactory.cs
@@ -0,0 +1,25 @@
+using Deal.Infrastructure.Security.Abstractions;
+using Deal.Modules.Settings.Application.Abstractions;
+
+namespace Deal.Infrastructure.Security.Services;
+
+///
+/// Фабрика AES-256-GCM-шифра по ключу приложения
+///
+public sealed class SecretCipherFactory : ISecretCipherFactory
+{
+ private readonly EncryptionKeyProvider _keyProvider;
+
+ ///
+ /// Создаёт фабрику шифра
+ ///
+ /// Источник ключа шифрования приложения.
+ public SecretCipherFactory(EncryptionKeyProvider keyProvider)
+ {
+ ArgumentNullException.ThrowIfNull(keyProvider);
+ _keyProvider = keyProvider;
+ }
+
+ ///
+ public ISecretCipher Create() => new AesGcmSecretCipher(_keyProvider.GetKey());
+}
diff --git a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
index 84c5734..7d466c2 100644
--- a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
+++ b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
@@ -5,8 +5,12 @@ using Deal.Infrastructure.Integrations.Options;
using Deal.Infrastructure.Integrations.Services;
using Deal.Infrastructure.Integrations.Sources;
using Deal.Infrastructure.Persistence;
+using Deal.Infrastructure.Persistence.Abstractions;
using Deal.Infrastructure.Persistence.Repositories;
+using Deal.Infrastructure.Persistence.Services;
using Deal.Infrastructure.Security;
+using Deal.Infrastructure.Security.Abstractions;
+using Deal.Infrastructure.Security.Services;
using Deal.Infrastructure.Services;
using Deal.Infrastructure.Tenancy;
using Deal.Modules.Cards.Application.Abstractions;
@@ -20,7 +24,6 @@ using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using Deal.SharedKernel.Tenants.Abstractions;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure;
@@ -46,9 +49,9 @@ public static class ServiceCollectionExtensions
services.AddScoped();
- services.AddScoped(provider => new TenantLimitStore(
- provider.GetRequiredService(),
- tenantLimitDefaults ?? TokenBudgetDefaults.Default));
+ services.AddSingleton(tenantLimitDefaults ?? TokenBudgetDefaults.Default);
+ services.AddScoped();
+ services.AddScoped(provider => provider.GetRequiredService().Create());
services.AddScoped();
@@ -117,18 +120,11 @@ public static class ServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
- services.AddScoped(provider => new BudgetedAiClassifier(
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService>()));
+ services.AddScoped();
+ services.AddScoped(provider => provider.GetRequiredService().Create());
services.AddScoped();
- services.AddScoped(provider => new BudgetedAiTools(
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService>()));
+ services.AddScoped();
+ services.AddScoped(provider => provider.GetRequiredService().Create());
}
if (telegramOptions.UseLocal)
@@ -152,8 +148,9 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddDealSecurity(this IServiceCollection services, string contentRootPath)
{
EncryptionKeyProvider keyProvider = new(contentRootPath);
- byte[] key = keyProvider.GetKey();
- services.AddSingleton(new AesGcmSecretCipher(key));
+ services.AddSingleton(keyProvider);
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetRequiredService().Create());
return services;
}
}
diff --git a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs
index d87810f..848908b 100644
--- a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs
@@ -21,8 +21,7 @@ public static class DiscoveryModuleRegistrar
services.AddScoped();
services.AddScoped();
- services.AddSingleton();
- services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton();
services.AddScoped();
services.AddScoped();
services.AddScoped(sp => new DiscoveryBanGuard(
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
index a5e66ea..13a9291 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
@@ -31,7 +31,7 @@ public sealed partial class DiscoveryWorkerService
/// Квоты дня/flood/стоп-кран (DiscoveryBanGuard).
/// Паузы между авто-вступлениями (интерфейс — фейк в тестах).
/// Гейт telegram-service (Search/Info/ReadForEval/Join/SetMonitor/Backfill).
- /// Singleton-счётчик ошибок ключей поиска; null — локальный (на инстанс воркера).
+ /// Singleton-счётчик ошибок ключей поиска.
public DiscoveryWorkerService(
IDiscoveryStore store,
DiscoveryTasksService tasks,
@@ -41,7 +41,7 @@ public sealed partial class DiscoveryWorkerService
DiscoveryBanGuard banGuard,
IDiscoveryPacer pacer,
ITelegramGateway gateway,
- IDiscoverySearchErrorCounter? searchErrors = null)
+ IDiscoverySearchErrorCounter searchErrors)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(tasks);
@@ -51,6 +51,7 @@ public sealed partial class DiscoveryWorkerService
ArgumentNullException.ThrowIfNull(banGuard);
ArgumentNullException.ThrowIfNull(pacer);
ArgumentNullException.ThrowIfNull(gateway);
+ ArgumentNullException.ThrowIfNull(searchErrors);
_store = store;
_tasks = tasks;
_candidates = candidates;
@@ -59,7 +60,7 @@ public sealed partial class DiscoveryWorkerService
_banGuard = banGuard;
_pacer = pacer;
_gateway = gateway;
- _searchErrors = searchErrors ?? new DiscoverySearchErrorCounter();
+ _searchErrors = searchErrors;
}
///
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs
index fea2453..ec6eb0a 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs
@@ -443,7 +443,8 @@ public sealed class DiscoveryWorkerServiceTests
var pacer = Substitute.For();
var gateway = new TestDiscoveryGateway();
var worker = new DiscoveryWorkerService(
- store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings.Store, ml.Client, ai), banGuard, pacer, gateway.Gateway);
+ store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings.Store, ml.Client, ai), banGuard, pacer, gateway.Gateway,
+ new DiscoverySearchErrorCounter());
return new Fixture(store, settings, gateway, pacer, worker, tasks);
}
diff --git a/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs b/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs
new file mode 100644
index 0000000..094310b
--- /dev/null
+++ b/src/core/tests/Deal.Tests.Unit/Support/FileStorageFactoryTests.cs
@@ -0,0 +1,49 @@
+using Deal.Contracts.Integrations.Abstractions;
+using Deal.Infrastructure.Integrations.Storage.Options;
+using Deal.Infrastructure.Integrations.Storage.Services;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Deal.Tests.Unit.Support;
+
+///
+/// Тесты FileStorageFactory — выбор Local/MinIO по заполненности секции Storage:Minio.
+///
+public sealed class FileStorageFactoryTests
+{
+ // Локальный режим: относительно пустая секция Minio → LocalFileStorage.
+ [Fact]
+ public void Create_MinioNotConfigured_ReturnsLocalFileStorage()
+ {
+ var factory = new FileStorageFactory(
+ new StorageOptions(),
+ new LocalStorageRoot("/tmp/deal-attachments"),
+ NullLogger.Instance);
+
+ IFileStorage storage = factory.Create();
+
+ Assert.IsType(storage);
+ }
+
+ // MinIO-режим: заполнены Endpoint/AccessKey/SecretKey → MinioFileStorage.
+ [Fact]
+ public void Create_MinioConfigured_ReturnsMinioFileStorage()
+ {
+ var options = new StorageOptions
+ {
+ Minio = new MinioStorageOptions
+ {
+ Endpoint = "localhost:9000",
+ AccessKey = "deal",
+ SecretKey = "deal-secret",
+ },
+ };
+ var factory = new FileStorageFactory(
+ options,
+ new LocalStorageRoot("/tmp/deal-attachments"),
+ NullLogger.Instance);
+
+ IFileStorage storage = factory.Create();
+
+ Assert.IsType(storage);
+ }
+}