Перевести тестовые фейки на моки NSubstitute #13
@@ -28,7 +28,7 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 20))), CancellationToken.None);
|
await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 20))), CancellationToken.None);
|
||||||
await audit.Store.AppendAsync(AuditRecord(AuditEvents.TenantLoginOk, now.AddDays(-1)), CancellationToken.None);
|
await audit.Store.AppendAsync(AuditRecord(AuditEvents.TenantLoginOk, now.AddDays(-1)), CancellationToken.None);
|
||||||
|
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
|
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
|
||||||
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 700, warned80: true);
|
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 700, warned80: true);
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
var audit = new TestAuditLogStore();
|
var audit = new TestAuditLogStore();
|
||||||
await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
|
await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 500);
|
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 500);
|
||||||
await using ServiceProvider provider = BuildProvider(audit, limits, new TestRateLimitCounterStore());
|
await using ServiceProvider provider = BuildProvider(audit, limits, new TestRateLimitCounterStore());
|
||||||
DataRetentionScheduler scheduler = new(
|
DataRetentionScheduler scheduler = new(
|
||||||
@@ -87,12 +87,12 @@ public sealed class DataRetentionSchedulerTests
|
|||||||
// Возвращает: Провайдер с сервисами цикла.
|
// Возвращает: Провайдер с сервисами цикла.
|
||||||
private static ServiceProvider BuildProvider(
|
private static ServiceProvider BuildProvider(
|
||||||
TestAuditLogStore audit,
|
TestAuditLogStore audit,
|
||||||
FakeTenantLimitStore limits,
|
TestTenantLimitStore limits,
|
||||||
TestRateLimitCounterStore counters)
|
TestRateLimitCounterStore counters)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddScoped<IAuditLogStore>(_ => audit.Store);
|
services.AddScoped<IAuditLogStore>(_ => audit.Store);
|
||||||
services.AddScoped<ITenantLimitStore>(_ => limits);
|
services.AddScoped<ITenantLimitStore>(_ => limits.Store);
|
||||||
services.AddScoped<IRateLimitCounterStore>(_ => counters.Store);
|
services.AddScoped<IRateLimitCounterStore>(_ => counters.Store);
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ public sealed class MlOutboxFlushSchedulerTests
|
|||||||
// Tenant-scoped адаптеры: выбирают фейк по тому же ITenantContext, который планировщик заполняет SetTenant.
|
// Tenant-scoped адаптеры: выбирают фейк по тому же ITenantContext, который планировщик заполняет SetTenant.
|
||||||
services.AddScoped<IMlLearningStore>(provider => storesByTenant[TenantOf(provider)]);
|
services.AddScoped<IMlLearningStore>(provider => storesByTenant[TenantOf(provider)]);
|
||||||
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
||||||
services.AddScoped<ITenantLimitStore>(_ => new FakeTenantLimitStore());
|
services.AddScoped<ITenantLimitStore>(_ => new TestTenantLimitStore().Store);
|
||||||
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
||||||
services.AddScoped<TokenUsageRecorder>();
|
services.AddScoped<TokenUsageRecorder>();
|
||||||
services.AddLogging();
|
services.AddLogging();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CollectAsync_CollectsBudgetRatiosPerTenant()
|
public async Task CollectAsync_CollectsBudgetRatiosPerTenant()
|
||||||
{
|
{
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
DateTimeOffset periodStart = DateTimeOffset.UtcNow;
|
DateTimeOffset periodStart = DateTimeOffset.UtcNow;
|
||||||
limits.Preload(TenantA, 100, TenantLimitPeriods.Month, periodStart, 80);
|
limits.Preload(TenantA, 100, TenantLimitPeriods.Month, periodStart, 80);
|
||||||
limits.Preload(TenantB, 0, TenantLimitPeriods.Month, periodStart, 55);
|
limits.Preload(TenantB, 0, TenantLimitPeriods.Month, periodStart, 55);
|
||||||
@@ -69,7 +69,7 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
PipelineStores(TenantA, TenantB, TenantC),
|
PipelineStores(TenantA, TenantB, TenantC),
|
||||||
OutboxStores(TenantA, TenantB, TenantC),
|
OutboxStores(TenantA, TenantB, TenantC),
|
||||||
TenantRecords(TenantA, TenantB, TenantC),
|
TenantRecords(TenantA, TenantB, TenantC),
|
||||||
limits);
|
limits.Store);
|
||||||
|
|
||||||
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
|
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CollectAsync_BudgetReadFailure_SkipsFailedTenant()
|
public async Task CollectAsync_BudgetReadFailure_SkipsFailedTenant()
|
||||||
{
|
{
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
limits.FailStateReads.Add(TenantA);
|
limits.FailStateReads.Add(TenantA);
|
||||||
limits.Preload(TenantB, 100, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 80);
|
limits.Preload(TenantB, 100, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 80);
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
PipelineStores(TenantA, TenantB),
|
PipelineStores(TenantA, TenantB),
|
||||||
OutboxStores(TenantA, TenantB),
|
OutboxStores(TenantA, TenantB),
|
||||||
TenantRecords(TenantA, TenantB),
|
TenantRecords(TenantA, TenantB),
|
||||||
limits);
|
limits.Store);
|
||||||
|
|
||||||
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
|
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
services.AddScoped<PipelineIngestService>();
|
services.AddScoped<PipelineIngestService>();
|
||||||
services.AddScoped<PipelineProcessingService>();
|
services.AddScoped<PipelineProcessingService>();
|
||||||
services.AddScoped<IMlLearningStore>(provider => outboxByTenant[CurrentTenant(provider)]);
|
services.AddScoped<IMlLearningStore>(provider => outboxByTenant[CurrentTenant(provider)]);
|
||||||
services.AddSingleton<ITenantLimitStore>(limitStore ?? new FakeTenantLimitStore());
|
services.AddSingleton<ITenantLimitStore>(limitStore ?? new TestTenantLimitStore().Store);
|
||||||
|
|
||||||
ServiceProvider provider = services.BuildServiceProvider();
|
ServiceProvider provider = services.BuildServiceProvider();
|
||||||
return new RuntimeDepthsCollector(
|
return new RuntimeDepthsCollector(
|
||||||
@@ -172,4 +172,4 @@ public sealed class RuntimeDepthsCollectorTests
|
|||||||
ITenantContext context = provider.GetRequiredService<ITenantContext>();
|
ITenantContext context = provider.GetRequiredService<ITenantContext>();
|
||||||
return context.TenantId!.Value.Value;
|
return context.TenantId!.Value.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ namespace Deal.Tests.Unit.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BudgetedAiClassifierTests
|
public sealed class BudgetedAiClassifierTests
|
||||||
{
|
{
|
||||||
// Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита FakeTenantLimitStore.
|
// Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита TestTenantLimitStore.
|
||||||
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
|
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
|
||||||
|
|
||||||
// Guid того же тенанта — ключ строки лимита.
|
// Guid того же тенанта — ключ строки лимита.
|
||||||
@@ -158,10 +158,10 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
// (зеркало регистрации AddDealIntegrations при UseLocal=false).
|
// (зеркало регистрации AddDealIntegrations при UseLocal=false).
|
||||||
// configure: Настройка строки лимита сценария (null — строки нет, ленивый дефолт-бюджет).
|
// configure: Настройка строки лимита сценария (null — строки нет, ленивый дефолт-бюджет).
|
||||||
// Возвращает: Контекст теста.
|
// Возвращает: Контекст теста.
|
||||||
private static Context Create(Action<FakeTenantLimitStore>? configure = null)
|
private static Context Create(Action<TestTenantLimitStore>? configure = null)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
configure?.Invoke(limits);
|
configure?.Invoke(limits);
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
@@ -169,7 +169,7 @@ public sealed class BudgetedAiClassifierTests
|
|||||||
IAiClassifier decorator = new BudgetedAiClassifier(
|
IAiClassifier decorator = new BudgetedAiClassifier(
|
||||||
paid.Classifier,
|
paid.Classifier,
|
||||||
new LocalAiClassifier(new LocalFieldsParser(settings)),
|
new LocalAiClassifier(new LocalFieldsParser(settings)),
|
||||||
limits,
|
limits.Store,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
NullLogger<BudgetedAiClassifier>.Instance);
|
NullLogger<BudgetedAiClassifier>.Instance);
|
||||||
return new Context(decorator, paid, settings);
|
return new Context(decorator, paid, settings);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Deal.Tests.Unit.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BudgetedAiToolsTests
|
public sealed class BudgetedAiToolsTests
|
||||||
{
|
{
|
||||||
// Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита FakeTenantLimitStore.
|
// Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита TestTenantLimitStore.
|
||||||
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
|
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
|
||||||
|
|
||||||
// Guid того же тенанта — ключ строки лимита.
|
// Guid того же тенанта — ключ строки лимита.
|
||||||
@@ -139,16 +139,16 @@ public sealed class BudgetedAiToolsTests
|
|||||||
// AddDealIntegrations при UseLocal=false).
|
// AddDealIntegrations при UseLocal=false).
|
||||||
// configure: Настройка строки лимита сценария (null — строки нет, ленивый дефолт-бюджет).
|
// configure: Настройка строки лимита сценария (null — строки нет, ленивый дефолт-бюджет).
|
||||||
// Возвращает: Контекст теста.
|
// Возвращает: Контекст теста.
|
||||||
private static Context Create(Action<FakeTenantLimitStore>? configure = null)
|
private static Context Create(Action<TestTenantLimitStore>? configure = null)
|
||||||
{
|
{
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
configure?.Invoke(limits);
|
configure?.Invoke(limits);
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var paid = TestAiTools.New();
|
var paid = TestAiTools.New();
|
||||||
IAiTools decorator = new BudgetedAiTools(
|
IAiTools decorator = new BudgetedAiTools(
|
||||||
paid,
|
paid,
|
||||||
limits,
|
limits.Store,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
NullLogger<BudgetedAiTools>.Instance);
|
NullLogger<BudgetedAiTools>.Instance);
|
||||||
return new Context(decorator, paid);
|
return new Context(decorator, paid);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
service.GenerateKeywordsReply = reply;
|
service.GenerateKeywordsReply = reply;
|
||||||
FakeSettingsStore settings = new();
|
FakeSettingsStore settings = new();
|
||||||
ISecretCipher cipher = TestCiphers.New();
|
ISecretCipher cipher = TestCiphers.New();
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
|
|
||||||
IAiTools tools = CreateTools(port, settings, cipher, limits);
|
IAiTools tools = CreateTools(port, settings, cipher, limits);
|
||||||
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("Бэкенд-разработка на Python", CancellationToken.None);
|
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("Бэкенд-разработка на Python", CancellationToken.None);
|
||||||
@@ -88,7 +88,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
Reason = "другая сфера",
|
Reason = "другая сфера",
|
||||||
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
|
||||||
};
|
};
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New(), limits);
|
IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New(), limits);
|
||||||
|
|
||||||
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
|
||||||
@@ -130,8 +130,9 @@ public sealed class GrpcAiToolsTests
|
|||||||
int port,
|
int port,
|
||||||
FakeSettingsStore settings,
|
FakeSettingsStore settings,
|
||||||
ISecretCipher cipher,
|
ISecretCipher cipher,
|
||||||
FakeTenantLimitStore? limits = null)
|
TestTenantLimitStore? limits = null)
|
||||||
{
|
{
|
||||||
|
limits ??= new TestTenantLimitStore();
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
||||||
@@ -139,7 +140,7 @@ public sealed class GrpcAiToolsTests
|
|||||||
tenantContext,
|
tenantContext,
|
||||||
connection,
|
connection,
|
||||||
new AiProviderConfigBuilder(settings, cipher),
|
new AiProviderConfigBuilder(settings, cipher),
|
||||||
new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcAiTools>.Instance);
|
NullLogger<GrpcAiTools>.Instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ public sealed class GrpcMlClientTests
|
|||||||
cache ?? new MlStatusCache(),
|
cache ?? new MlStatusCache(),
|
||||||
new TokenUsageRecorder(
|
new TokenUsageRecorder(
|
||||||
settings ?? new FakeSettingsStore(),
|
settings ?? new FakeSettingsStore(),
|
||||||
new FakeTenantLimitStore(),
|
new TestTenantLimitStore().Store,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcMlClient>.Instance);
|
NullLogger<GrpcMlClient>.Instance);
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public sealed class IntegrationsDiTests
|
|||||||
services.AddScoped<ISecretCipher>(_ => TestCiphers.New());
|
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 TestTenantLimitStore().Store);
|
||||||
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
services.AddScoped<TokenUsageEventService>(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
||||||
services.AddScoped<LocalFieldsParser>();
|
services.AddScoped<LocalFieldsParser>();
|
||||||
services.AddScoped<AiClassifyContextBuilder>();
|
services.AddScoped<AiClassifyContextBuilder>();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
// Id тенанта сценария строкой (формат N) — metadata tenant-id вызовов ai-service.
|
// Id тенанта сценария строкой (формат N) — metadata tenant-id вызовов ai-service.
|
||||||
private const string TenantIdValue = "abcdefabcdefabcdefabcdefabcdefab";
|
private const string TenantIdValue = "abcdefabcdefabcdefabcdefabcdefab";
|
||||||
|
|
||||||
// Guid того же тенанта — ключ строки лимита в FakeTenantLimitStore (списание usage).
|
// Guid того же тенанта — ключ строки лимита в TestTenantLimitStore (списание usage).
|
||||||
private static readonly Guid TenantGuid = Guid.Parse(TenantIdValue);
|
private static readonly Guid TenantGuid = Guid.Parse(TenantIdValue);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -127,7 +127,7 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
Usage = new Usage { Prompt = 2000, Completion = 400, Total = 2400 },
|
Usage = new Usage { Prompt = 2000, Completion = 400, Total = 2400 },
|
||||||
};
|
};
|
||||||
|
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
limits.Preload(TenantGuid, budgetTokens: 1000, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, usedTokens: 1000);
|
limits.Preload(TenantGuid, budgetTokens: 1000, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, usedTokens: 1000);
|
||||||
Context ctx = CreateContext(port, limits, budgeted: true);
|
Context ctx = CreateContext(port, limits, budgeted: true);
|
||||||
ctx.PipelineStore.SeedQueue(QueueRowFiltered("p_1", "Нужен middle Python-разработчик в команду, оплата 2000$ в месяц"));
|
ctx.PipelineStore.SeedQueue(QueueRowFiltered("p_1", "Нужен middle Python-разработчик в команду, оплата 2000$ в месяц"));
|
||||||
@@ -157,11 +157,11 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
TestPipelineStore PipelineStore,
|
TestPipelineStore PipelineStore,
|
||||||
FakeKanjStore KanjStore,
|
FakeKanjStore KanjStore,
|
||||||
FakeSettingsStore Settings,
|
FakeSettingsStore Settings,
|
||||||
FakeTenantLimitStore Limits);
|
TestTenantLimitStore Limits);
|
||||||
|
|
||||||
private static Context CreateContext(
|
private static Context CreateContext(
|
||||||
int port,
|
int port,
|
||||||
FakeTenantLimitStore? limits = null,
|
TestTenantLimitStore? limits = null,
|
||||||
bool budgeted = false)
|
bool budgeted = false)
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
@@ -175,19 +175,19 @@ public sealed class PipelineWorkerGrpcAiTests
|
|||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var connection = new AiGrpcConnection(
|
var connection = new AiGrpcConnection(
|
||||||
new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
||||||
limits ??= new FakeTenantLimitStore();
|
limits ??= new TestTenantLimitStore();
|
||||||
var grpcClassifier = new GrpcAiClassifier(
|
var grpcClassifier = new GrpcAiClassifier(
|
||||||
tenantContext,
|
tenantContext,
|
||||||
connection,
|
connection,
|
||||||
new AiProviderConfigBuilder(settings, TestCiphers.New()),
|
new AiProviderConfigBuilder(settings, TestCiphers.New()),
|
||||||
new AiClassifyContextBuilder(settings, kanjStore),
|
new AiClassifyContextBuilder(settings, kanjStore),
|
||||||
new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcAiClassifier>.Instance);
|
NullLogger<GrpcAiClassifier>.Instance);
|
||||||
IAiClassifier aiClassifier = budgeted
|
IAiClassifier aiClassifier = budgeted
|
||||||
? new BudgetedAiClassifier(
|
? new BudgetedAiClassifier(
|
||||||
grpcClassifier,
|
grpcClassifier,
|
||||||
new LocalAiClassifier(fieldsParser),
|
new LocalAiClassifier(fieldsParser),
|
||||||
limits,
|
limits.Store,
|
||||||
tenantContext,
|
tenantContext,
|
||||||
NullLogger<BudgetedAiClassifier>.Instance)
|
NullLogger<BudgetedAiClassifier>.Instance)
|
||||||
: grpcClassifier;
|
: grpcClassifier;
|
||||||
|
|||||||
+38
-23
@@ -1,13 +1,15 @@
|
|||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
using Deal.Modules.Tenants.Application.Services;
|
using Deal.Modules.Tenants.Application.Services;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// In-memory реализация <see cref="ITenantLimitStore"/> для юнит-тестов recorder'а и бюджетного гейта/алертов
|
/// Подставка <see cref="ITenantLimitStore"/> для юнит-тестов recorder'а и бюджетного гейта/алертов:
|
||||||
|
/// сервисы получают NSubstitute-подставку (<see cref="Store"/>), тесты управляют сценарием через свойства
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FakeTenantLimitStore : ITenantLimitStore
|
public sealed class TestTenantLimitStore
|
||||||
{
|
{
|
||||||
private sealed class Row
|
private sealed class Row
|
||||||
{
|
{
|
||||||
@@ -28,26 +30,46 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
|
|
||||||
private readonly Dictionary<Guid, Row> _rows = new();
|
private readonly Dictionary<Guid, Row> _rows = new();
|
||||||
private readonly TokenBudgetService _budgetService = new();
|
private readonly TokenBudgetService _budgetService = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта лимитов (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public ITenantLimitStore Store { get; }
|
||||||
private readonly Func<DateTimeOffset> _utcNow;
|
private readonly Func<DateTimeOffset> _utcNow;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создаёт фейк с системными часами и статусом тенанта active.
|
/// Создаёт подставку с системными часами и статусом тенанта active.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FakeTenantLimitStore()
|
public TestTenantLimitStore()
|
||||||
: this(() => DateTimeOffset.UtcNow, TenantStatuses.Active)
|
: this(() => DateTimeOffset.UtcNow, TenantStatuses.Active)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создаёт фейк с заданными часами и статусом тенанта
|
/// Создаёт подставку с заданными часами и статусом тенанта
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="utcNow">Источник текущего времени (UTC).</param>
|
/// <param name="utcNow">Источник текущего времени (UTC).</param>
|
||||||
/// <param name="tenantStatus">Статус тенанта для всех строк (константа <see cref="TenantStatuses"/>).</param>
|
/// <param name="tenantStatus">Статус тенанта для всех строк (константа <see cref="TenantStatuses"/>).</param>
|
||||||
public FakeTenantLimitStore(Func<DateTimeOffset> utcNow, string tenantStatus = TenantStatuses.Active)
|
public TestTenantLimitStore(Func<DateTimeOffset> utcNow, string tenantStatus = TenantStatuses.Active)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(utcNow);
|
ArgumentNullException.ThrowIfNull(utcNow);
|
||||||
_utcNow = utcNow;
|
_utcNow = utcNow;
|
||||||
TenantStatus = tenantStatus;
|
TenantStatus = tenantStatus;
|
||||||
|
Store = Substitute.For<ITenantLimitStore>();
|
||||||
|
Store.GetOrCreateAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>(), Arg.Any<TokenLimitDefaults?>())
|
||||||
|
.Returns(ci => GetOrCreateAsync(ci.ArgAt<Guid>(0), ci.ArgAt<CancellationToken>(1), ci.ArgAt<TokenLimitDefaults?>(2)));
|
||||||
|
Store.GetStateAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => GetStateAsync(ci.ArgAt<Guid>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.AddUsageAsync(Arg.Any<Guid>(), Arg.Any<long>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => AddUsageAsync(ci.ArgAt<Guid>(0), ci.ArgAt<long>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.UpdateBudgetAsync(Arg.Any<Guid>(), Arg.Any<long>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => UpdateBudgetAsync(ci.ArgAt<Guid>(0), ci.ArgAt<long>(1), ci.ArgAt<string>(2), ci.ArgAt<CancellationToken>(3)));
|
||||||
|
Store.TryMarkWarnedAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => TryMarkWarnedAsync(ci.ArgAt<Guid>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.TryMarkNotifiedExhaustedAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => TryMarkNotifiedExhaustedAsync(ci.ArgAt<Guid>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.ResetExpiredPeriodsAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => ResetExpiredPeriodsAsync(ci.ArgAt<DateTimeOffset>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,12 +78,12 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
public string TenantStatus { get; set; }
|
public string TenantStatus { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Тенанты, для которых <see cref="GetStateAsync"/> бросает исключение (устойчивость сборщиков)
|
/// Тенанты, для которых <see cref="GetStateAsync"/> подставки бросает исключение (устойчивость сборщиков)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<Guid> FailStateReads { get; } = new();
|
public HashSet<Guid> FailStateReads { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Кладёт готовую строку лимита
|
/// Кладёт готовую строку лимита (в состояние подставки)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tenantId">Тенант.</param>
|
/// <param name="tenantId">Тенант.</param>
|
||||||
/// <param name="budgetTokens">Бюджет периода.</param>
|
/// <param name="budgetTokens">Бюджет периода.</param>
|
||||||
@@ -116,18 +138,16 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
/// <returns>True — строка существует.</returns>
|
/// <returns>True — строка существует.</returns>
|
||||||
public bool Exists(Guid tenantId) => _rows.ContainsKey(tenantId);
|
public bool Exists(Guid tenantId) => _rows.ContainsKey(tenantId);
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<TenantLimitDto> GetOrCreateAsync(
|
||||||
public Task<TenantLimitDto> GetOrCreateAsync(
|
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
TokenLimitDefaults? defaults = null)
|
TokenLimitDefaults? defaults)
|
||||||
{
|
{
|
||||||
Row row = Ensure(tenantId, defaults ?? TokenBudgetDefaults.Default);
|
Row row = Ensure(tenantId, defaults ?? TokenBudgetDefaults.Default);
|
||||||
return Task.FromResult(ToLimitDto(row, tenantId));
|
return Task.FromResult(ToLimitDto(row, tenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<BudgetStateDto> GetStateAsync(Guid tenantId, CancellationToken ct)
|
||||||
public Task<BudgetStateDto> GetStateAsync(Guid tenantId, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
if (FailStateReads.Contains(tenantId))
|
if (FailStateReads.Contains(tenantId))
|
||||||
{
|
{
|
||||||
@@ -139,8 +159,7 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
return Task.FromResult(ToStateDto(row, tenantId));
|
return Task.FromResult(ToStateDto(row, tenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<BudgetStateDto> AddUsageAsync(
|
||||||
public Task<BudgetStateDto> AddUsageAsync(
|
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
long tokens,
|
long tokens,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
@@ -155,8 +174,7 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
return Task.FromResult(ToStateDto(row, tenantId));
|
return Task.FromResult(ToStateDto(row, tenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<BudgetStateDto> UpdateBudgetAsync(
|
||||||
public Task<BudgetStateDto> UpdateBudgetAsync(
|
|
||||||
Guid tenantId,
|
Guid tenantId,
|
||||||
long budgetTokens,
|
long budgetTokens,
|
||||||
string period,
|
string period,
|
||||||
@@ -170,8 +188,7 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
return Task.FromResult(ToStateDto(row, tenantId));
|
return Task.FromResult(ToStateDto(row, tenantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<bool> TryMarkWarnedAsync(Guid tenantId, CancellationToken ct)
|
||||||
public Task<bool> TryMarkWarnedAsync(Guid tenantId, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
Row row = Ensure(tenantId, TokenBudgetDefaults.Default);
|
Row row = Ensure(tenantId, TokenBudgetDefaults.Default);
|
||||||
ResetIfPeriodExpired(row);
|
ResetIfPeriodExpired(row);
|
||||||
@@ -184,8 +201,7 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<bool> TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct)
|
||||||
public Task<bool> TryMarkNotifiedExhaustedAsync(Guid tenantId, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
Row row = Ensure(tenantId, TokenBudgetDefaults.Default);
|
Row row = Ensure(tenantId, TokenBudgetDefaults.Default);
|
||||||
ResetIfPeriodExpired(row);
|
ResetIfPeriodExpired(row);
|
||||||
@@ -198,8 +214,7 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
|
|||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private Task<int> ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
|
||||||
public Task<int> ResetExpiredPeriodsAsync(DateTimeOffset now, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
int reset = 0;
|
int reset = 0;
|
||||||
foreach (Row row in _rows.Values)
|
foreach (Row row in _rows.Values)
|
||||||
@@ -3,6 +3,7 @@ using Deal.Api.Events;
|
|||||||
using Deal.Api.Hosting;
|
using Deal.Api.Hosting;
|
||||||
using Deal.Modules.Tenants.Application.Abstractions;
|
using Deal.Modules.Tenants.Application.Abstractions;
|
||||||
using Deal.Modules.Tenants.Application.Models;
|
using Deal.Modules.Tenants.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Tests.Unit.Modules.Tenants;
|
using Deal.Tests.Unit.Modules.Tenants;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
@@ -27,7 +28,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunCycle_TwoCycles_PublishesToastOncePerThresholdPerTenant()
|
public async Task RunCycle_TwoCycles_PublishesToastOncePerThresholdPerTenant()
|
||||||
{
|
{
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 800);
|
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 800);
|
||||||
limits.Preload(TenantB, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 1000);
|
limits.Preload(TenantB, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 1000);
|
||||||
await using ServiceProvider provider = BuildProvider(
|
await using ServiceProvider provider = BuildProvider(
|
||||||
@@ -54,7 +55,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunCycle_TenantBelowThreshold_NoToastAndNoFlagSet()
|
public async Task RunCycle_TenantBelowThreshold_NoToastAndNoFlagSet()
|
||||||
{
|
{
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 700);
|
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 700);
|
||||||
await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
|
await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
|
|
||||||
Assert.Empty(ReadToasts(subscriptionA));
|
Assert.Empty(ReadToasts(subscriptionA));
|
||||||
// Порог не достигнут — TryMark* не выставил флаг (повторный проход после роста расхода даст тост).
|
// Порог не достигнут — TryMark* не выставил флаг (повторный проход после роста расхода даст тост).
|
||||||
Assert.False(await limits.TryMarkWarnedAsync(TenantA, CancellationToken.None));
|
Assert.False(await limits.Store.TryMarkWarnedAsync(TenantA, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -74,7 +75,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
{
|
{
|
||||||
// Строка «уже исчерпан, но ни один флаг не стоял» (сценарий: оператор уменьшил бюджет — флаги сброшены,
|
// Строка «уже исчерпан, но ни один флаг не стоял» (сценарий: оператор уменьшил бюджет — флаги сброшены,
|
||||||
// расход ≥ бюджета): за один проход выходят оба порога ровно по одному разу.
|
// расход ≥ бюджета): за один проход выходят оба порога ровно по одному разу.
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
limits.Preload(TenantA, budgetTokens: 500, TenantLimitPeriods.Month, Now(), usedTokens: 700);
|
limits.Preload(TenantA, budgetTokens: 500, TenantLimitPeriods.Month, Now(), usedTokens: 700);
|
||||||
await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
|
await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunCycle_NaturalSpendCrossing80Then100_PublishesOneToastPerThreshold()
|
public async Task RunCycle_NaturalSpendCrossing80Then100_PublishesOneToastPerThreshold()
|
||||||
{
|
{
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 0);
|
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 0);
|
||||||
await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
|
await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
SseSubscription subscriptionA = broker.Subscribe(TenantA);
|
SseSubscription subscriptionA = broker.Subscribe(TenantA);
|
||||||
BudgetAlertScheduler scheduler = CreateScheduler(provider);
|
BudgetAlertScheduler scheduler = CreateScheduler(provider);
|
||||||
|
|
||||||
await limits.AddUsageAsync(TenantA, tokens: 850, CancellationToken.None);
|
await limits.Store.AddUsageAsync(TenantA, tokens: 850, CancellationToken.None);
|
||||||
|
|
||||||
await scheduler.RunCycleAsync(CancellationToken.None);
|
await scheduler.RunCycleAsync(CancellationToken.None);
|
||||||
Assert.Equal(new[] { (Warned80ToastText, "bell") }, ReadToasts(subscriptionA));
|
Assert.Equal(new[] { (Warned80ToastText, "bell") }, ReadToasts(subscriptionA));
|
||||||
@@ -110,7 +111,7 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
Assert.Empty(ReadToasts(subscriptionA)); // повторный проход — флаг уже стоит, тост не дублируется
|
Assert.Empty(ReadToasts(subscriptionA)); // повторный проход — флаг уже стоит, тост не дублируется
|
||||||
|
|
||||||
// Расход до исчерпания (850 + 200 = 1050 ≥ 1000) → ещё ровно один тост (100%); 80% уже отмечен.
|
// Расход до исчерпания (850 + 200 = 1050 ≥ 1000) → ещё ровно один тост (100%); 80% уже отмечен.
|
||||||
await limits.AddUsageAsync(TenantA, tokens: 200, CancellationToken.None);
|
await limits.Store.AddUsageAsync(TenantA, tokens: 200, CancellationToken.None);
|
||||||
await scheduler.RunCycleAsync(CancellationToken.None);
|
await scheduler.RunCycleAsync(CancellationToken.None);
|
||||||
Assert.Equal(new[] { (ExhaustedToastText, "bell") }, ReadToasts(subscriptionA));
|
Assert.Equal(new[] { (ExhaustedToastText, "bell") }, ReadToasts(subscriptionA));
|
||||||
|
|
||||||
@@ -124,11 +125,11 @@ public sealed class BudgetAlertSchedulerTests
|
|||||||
// tenants: Фейк реестра тенантов (обход прохода).
|
// tenants: Фейк реестра тенантов (обход прохода).
|
||||||
// limits: Фейк-хранилище лимитов (строки посеяны сценарием до прохода).
|
// limits: Фейк-хранилище лимитов (строки посеяны сценарием до прохода).
|
||||||
// Возвращает: Провайдер с сервисами цикла.
|
// Возвращает: Провайдер с сервисами цикла.
|
||||||
private static ServiceProvider BuildProvider(ITenantRepository tenants, FakeTenantLimitStore limits)
|
private static ServiceProvider BuildProvider(ITenantRepository tenants, TestTenantLimitStore limits)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton(tenants);
|
services.AddSingleton(tenants);
|
||||||
services.AddScoped<ITenantLimitStore>(_ => limits);
|
services.AddScoped<ITenantLimitStore>(_ => limits.Store);
|
||||||
services.AddSingleton<SseBroker>();
|
services.AddSingleton<SseBroker>();
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
settings.Preload(SettingsKeys.DomainDescription, Json(TestDomain));
|
||||||
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
settings.Preload(SettingsKeys.DomainKeywords, Json(TestKeywords));
|
||||||
|
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||||
AiFilterResultDto result = await classifier.FilterAsync("Купите телеграм-канал", CancellationToken.None);
|
AiFilterResultDto result = await classifier.FilterAsync("Купите телеграм-канал", CancellationToken.None);
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
Order = 0,
|
Order = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||||
AiParsedCardDto parsed = await classifier.ClassifyAsync("Нужен Python-разработчик, оплата от 2000$", CancellationToken.None);
|
AiParsedCardDto parsed = await classifier.ClassifyAsync("Нужен Python-разработчик, оплата от 2000$", CancellationToken.None);
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
};
|
};
|
||||||
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
(FakeSettingsStore settings, ISecretCipher cipher, FakeKanjStore kanjStore) = Context(port);
|
||||||
|
|
||||||
FakeTenantLimitStore limits = new();
|
TestTenantLimitStore limits = new();
|
||||||
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
IAiClassifier classifier = CreateClassifier(port, settings, cipher, kanjStore, limits);
|
||||||
|
|
||||||
// ok=false (модель без JSON) → AiUnavailableException: воркер падает в локальный разбор (aiFail).
|
// ok=false (модель без JSON) → AiUnavailableException: воркер падает в локальный разбор (aiFail).
|
||||||
@@ -287,8 +287,9 @@ public sealed class GrpcAiClassifierTests
|
|||||||
FakeSettingsStore settings,
|
FakeSettingsStore settings,
|
||||||
ISecretCipher cipher,
|
ISecretCipher cipher,
|
||||||
FakeKanjStore kanjStore,
|
FakeKanjStore kanjStore,
|
||||||
FakeTenantLimitStore? limits = null)
|
TestTenantLimitStore? limits = null)
|
||||||
{
|
{
|
||||||
|
limits ??= new TestTenantLimitStore();
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
|
||||||
@@ -297,7 +298,7 @@ public sealed class GrpcAiClassifierTests
|
|||||||
connection,
|
connection,
|
||||||
new AiProviderConfigBuilder(settings, cipher),
|
new AiProviderConfigBuilder(settings, cipher),
|
||||||
new AiClassifyContextBuilder(settings, kanjStore),
|
new AiClassifyContextBuilder(settings, kanjStore),
|
||||||
new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
|
||||||
NullLogger<GrpcAiClassifier>.Instance);
|
NullLogger<GrpcAiClassifier>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,11 @@ internal static class OperatorAuthHttpHost
|
|||||||
public static async Task RunAsync(
|
public static async Task RunAsync(
|
||||||
TestOperatorAuthStore operatorStore,
|
TestOperatorAuthStore operatorStore,
|
||||||
FakeAuthStore userStore,
|
FakeAuthStore userStore,
|
||||||
Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario,
|
Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, TestTenantLimitStore, Task> scenario,
|
||||||
TestAuditLogStore? auditStore = null,
|
TestAuditLogStore? auditStore = null,
|
||||||
TestInviteStore? inviteStore = null,
|
TestInviteStore? inviteStore = null,
|
||||||
TestTenantStore? tenantStore = null,
|
TestTenantStore? tenantStore = null,
|
||||||
FakeTenantLimitStore? limitStore = null,
|
TestTenantLimitStore? limitStore = null,
|
||||||
RateLimitOptions? rateLimitOptions = null,
|
RateLimitOptions? rateLimitOptions = null,
|
||||||
TestTokenUsageEventStore? tokenUsageStore = null) =>
|
TestTokenUsageEventStore? tokenUsageStore = null) =>
|
||||||
await RunCoreAsync(
|
await RunCoreAsync(
|
||||||
@@ -164,8 +164,8 @@ internal static class OperatorAuthHttpHost
|
|||||||
TestAuditLogStore? auditStore,
|
TestAuditLogStore? auditStore,
|
||||||
TestInviteStore? inviteStore,
|
TestInviteStore? inviteStore,
|
||||||
TestTenantStore? tenantStore,
|
TestTenantStore? tenantStore,
|
||||||
FakeTenantLimitStore? limitStore,
|
TestTenantLimitStore? limitStore,
|
||||||
Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario,
|
Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, TestTenantLimitStore, TestGlobalSettingsStore, Task> scenario,
|
||||||
RateLimitOptions? rateLimitOptions = null,
|
RateLimitOptions? rateLimitOptions = null,
|
||||||
TestTokenUsageEventStore? tokenUsageStore = null,
|
TestTokenUsageEventStore? tokenUsageStore = null,
|
||||||
TestGlobalSettingsStore? globalSettingsStore = null)
|
TestGlobalSettingsStore? globalSettingsStore = null)
|
||||||
@@ -173,7 +173,7 @@ internal static class OperatorAuthHttpHost
|
|||||||
TestAuditLogStore effectiveAuditStore = auditStore ?? new TestAuditLogStore();
|
TestAuditLogStore effectiveAuditStore = auditStore ?? new TestAuditLogStore();
|
||||||
TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore();
|
TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore();
|
||||||
TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore();
|
TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore();
|
||||||
FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore();
|
TestTenantLimitStore effectiveLimitStore = limitStore ?? new TestTenantLimitStore();
|
||||||
TestTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new TestTokenUsageEventStore();
|
TestTokenUsageEventStore effectiveTokenUsageStore = tokenUsageStore ?? new TestTokenUsageEventStore();
|
||||||
TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore();
|
TestGlobalSettingsStore effectiveGlobalSettingsStore = globalSettingsStore ?? new TestGlobalSettingsStore();
|
||||||
RateLimitOptions effectiveRateLimitOptions = rateLimitOptions ?? new RateLimitOptions();
|
RateLimitOptions effectiveRateLimitOptions = rateLimitOptions ?? new RateLimitOptions();
|
||||||
@@ -191,7 +191,7 @@ internal static class OperatorAuthHttpHost
|
|||||||
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore.Store);
|
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore.Store);
|
||||||
builder.Services.AddSingleton<IInviteStore>(effectiveInviteStore.Store);
|
builder.Services.AddSingleton<IInviteStore>(effectiveInviteStore.Store);
|
||||||
builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore.Repository);
|
builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore.Repository);
|
||||||
builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore);
|
builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore.Store);
|
||||||
builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store);
|
builder.Services.AddSingleton<IGlobalSettingsStore>(effectiveGlobalSettingsStore.Store);
|
||||||
builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New());
|
builder.Services.AddSingleton<ISecretCipher>(TestCiphers.New());
|
||||||
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
|
// Сервис глобальных ключей Telegram (операторские ручки /api/operator/settings/telegram-keys).
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public sealed class OperatorHealthEndpointsHttpTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые).
|
// Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые).
|
||||||
private static Task RunAsync(Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario) =>
|
private static Task RunAsync(Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, TestTenantLimitStore, Task> scenario) =>
|
||||||
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario);
|
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario);
|
||||||
|
|
||||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||||
|
|||||||
@@ -284,14 +284,14 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
|||||||
$"{baseAddress}/api/operator/tenants/{tenantId}/limit";
|
$"{baseAddress}/api/operator/tenants/{tenantId}/limit";
|
||||||
|
|
||||||
// Прогоняет сценарий на хосте с двумя активными тенантами и предзаполненными строками лимитов.
|
// Прогоняет сценарий на хосте с двумя активными тенантами и предзаполненными строками лимитов.
|
||||||
private static Task RunAsync(Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario, TestAuditLogStore? auditStore = null)
|
private static Task RunAsync(Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, TestTenantLimitStore, Task> scenario, TestAuditLogStore? auditStore = null)
|
||||||
{
|
{
|
||||||
// Первый тенант: расход 80% месячного бюджета (8 000 000 / 10 000 000) + флаг Warned80;
|
// Первый тенант: расход 80% месячного бюджета (8 000 000 / 10 000 000) + флаг Warned80;
|
||||||
// второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает).
|
// второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает).
|
||||||
var tenantStore = new TestTenantStore(
|
var tenantStore = new TestTenantStore(
|
||||||
new TenantRecordDto(FirstTenantId, FirstTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow),
|
new TenantRecordDto(FirstTenantId, FirstTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow),
|
||||||
new TenantRecordDto(SecondTenantId, SecondTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
|
new TenantRecordDto(SecondTenantId, SecondTenantName, TenantStatuses.Active, DateTimeOffset.UtcNow));
|
||||||
var limitStore = new FakeTenantLimitStore();
|
var limitStore = new TestTenantLimitStore();
|
||||||
limitStore.Preload(FirstTenantId, DefaultBudgetTokens, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 8_000_000, warned80: true);
|
limitStore.Preload(FirstTenantId, DefaultBudgetTokens, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 8_000_000, warned80: true);
|
||||||
limitStore.Preload(SecondTenantId, 100_000, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 0);
|
limitStore.Preload(SecondTenantId, 100_000, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 0);
|
||||||
return OperatorAuthHttpHost.RunAsync(
|
return OperatorAuthHttpHost.RunAsync(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Deal.Tests.Unit.Support;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class TokenUsageRecorderTests
|
public sealed class TokenUsageRecorderTests
|
||||||
{
|
{
|
||||||
// Id тенанта сценариев строкой (формат N) — как в GrpcAi* тестах; Guid для FakeTenantLimitStore.
|
// Id тенанта сценариев строкой (формат N) — как в GrpcAi* тестах; Guid для TestTenantLimitStore.
|
||||||
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
|
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
|
||||||
|
|
||||||
private static readonly Guid TenantGuid = Guid.Parse(TenantIdValue);
|
private static readonly Guid TenantGuid = Guid.Parse(TenantIdValue);
|
||||||
@@ -26,7 +26,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_ChargesTenantLimitsAccumulatesLifetimeKvAndWritesEvent()
|
public async Task AddAsync_ChargesTenantLimitsAccumulatesLifetimeKvAndWritesEvent()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "deepseek-chat", CancellationToken.None);
|
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "deepseek-chat", CancellationToken.None);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_AccumulatesAcrossCalls()
|
public async Task AddAsync_AccumulatesAcrossCalls()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "m", CancellationToken.None);
|
await recorder.AddAsync(new Usage { Prompt = 500, Completion = 40, Total = 540 }, "deepseek", "m", CancellationToken.None);
|
||||||
await recorder.AddAsync(new Usage { Prompt = 300, Completion = 60, Total = 360 }, "deepseek", "m", CancellationToken.None);
|
await recorder.AddAsync(new Usage { Prompt = 300, Completion = 60, Total = 360 }, "deepseek", "m", CancellationToken.None);
|
||||||
@@ -68,7 +68,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_NullUsage_IsNoop()
|
public async Task AddAsync_NullUsage_IsNoop()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
await recorder.AddAsync(null, "deepseek", "m", CancellationToken.None);
|
await recorder.AddAsync(null, "deepseek", "m", CancellationToken.None);
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddAsync_ZeroTotal_AccumulatesKvAndWritesEventButSkipsTenantLimits()
|
public async Task AddAsync_ZeroTotal_AccumulatesKvAndWritesEventButSkipsTenantLimits()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
// Ответ модели без оценки (total=0): lifetime-KV и событие пишутся (вызов состоялся), строка лимита
|
// Ответ модели без оценки (total=0): lifetime-KV и событие пишутся (вызов состоялся), строка лимита
|
||||||
// нулевым расходом не заводится — ленивый GetOrCreate остаётся первому реальному списанию/чтению.
|
// нулевым расходом не заводится — ленивый GetOrCreate остаётся первому реальному списанию/чтению.
|
||||||
@@ -97,10 +97,10 @@ public sealed class TokenUsageRecorderTests
|
|||||||
public async Task AddAsync_OutsideTenantContext_Throws()
|
public async Task AddAsync_OutsideTenantContext_Throws()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
|
ITenantContext tenantContext = new TenantContext(); // без SetTenant — списание вне tenant-контекста невозможно.
|
||||||
var recorder = new TokenUsageRecorder(
|
var recorder = new TokenUsageRecorder(
|
||||||
settings, limits, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
settings, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => recorder.AddAsync(new Usage { Prompt = 1, Completion = 1, Total = 2 }, "deepseek", "m", CancellationToken.None));
|
() => recorder.AddAsync(new Usage { Prompt = 1, Completion = 1, Total = 2 }, "deepseek", "m", CancellationToken.None));
|
||||||
@@ -111,7 +111,7 @@ public sealed class TokenUsageRecorderTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddEstimatedAsync_MlCall_WritesMlEventOnlyWithCharEstimate()
|
public async Task AddEstimatedAsync_MlCall_WritesMlEventOnlyWithCharEstimate()
|
||||||
{
|
{
|
||||||
(FakeSettingsStore settings, FakeTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
(FakeSettingsStore settings, TestTenantLimitStore limits, TestTokenUsageEventStore events, TokenUsageRecorder recorder) = CreateRecorder();
|
||||||
|
|
||||||
// 20 символов → 5 токенов (≈chars/4); бюджет и lifetime AI-счётчик не трогаются.
|
// 20 символов → 5 токенов (≈chars/4); бюджет и lifetime AI-счётчик не трогаются.
|
||||||
long estimated = await recorder.AddEstimatedAsync(
|
long estimated = await recorder.AddEstimatedAsync(
|
||||||
@@ -141,14 +141,14 @@ public sealed class TokenUsageRecorderTests
|
|||||||
|
|
||||||
// Создаёт recorder сценария: tenant-контекст + фейки KV/лимитов/истории.
|
// Создаёт recorder сценария: tenant-контекст + фейки KV/лимитов/истории.
|
||||||
// Возвращает: Кортеж (настройки, лимиты, история, recorder).
|
// Возвращает: Кортеж (настройки, лимиты, история, recorder).
|
||||||
private static (FakeSettingsStore Settings, FakeTenantLimitStore Limits, TestTokenUsageEventStore Events, TokenUsageRecorder Recorder) CreateRecorder()
|
private static (FakeSettingsStore Settings, TestTenantLimitStore Limits, TestTokenUsageEventStore Events, TokenUsageRecorder Recorder) CreateRecorder()
|
||||||
{
|
{
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var limits = new FakeTenantLimitStore();
|
var limits = new TestTenantLimitStore();
|
||||||
var events = new TestTokenUsageEventStore();
|
var events = new TestTokenUsageEventStore();
|
||||||
ITenantContext tenantContext = new TenantContext();
|
ITenantContext tenantContext = new TenantContext();
|
||||||
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
tenantContext.SetTenant(new TenantId(TenantIdValue));
|
||||||
var recorder = new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(events.Store));
|
var recorder = new TokenUsageRecorder(settings, limits.Store, tenantContext, new TokenUsageEventService(events.Store));
|
||||||
return (settings, limits, events, recorder);
|
return (settings, limits, events, recorder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user