Add Authority Advisory AI and API Lifecycle Configuration
- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
@@ -1,179 +1,492 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
|
||||
public sealed class EntryTraceExecutionServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _tempRoot;
|
||||
|
||||
public EntryTraceExecutionServiceTests()
|
||||
{
|
||||
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-service-{Guid.NewGuid():n}");
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_When_ConfigMetadataMissing()
|
||||
{
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var service = CreateService(analyzer);
|
||||
|
||||
var context = CreateContext(new Dictionary<string, string>());
|
||||
|
||||
await service.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.False(analyzer.Invoked);
|
||||
Assert.False(context.Analysis.TryGet<EntryTraceGraph>(ScanAnalysisKeys.EntryTraceGraph, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_BuildsContext_AndStoresGraph()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "config.json");
|
||||
File.WriteAllText(configPath, """
|
||||
{
|
||||
"config": {
|
||||
"Env": ["PATH=/bin:/usr/bin"],
|
||||
"Entrypoint": ["/entrypoint.sh"],
|
||||
"WorkingDir": "/workspace",
|
||||
"User": "scanner"
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var layerDirectory = Path.Combine(_tempRoot, "layer-1");
|
||||
Directory.CreateDirectory(layerDirectory);
|
||||
File.WriteAllText(Path.Combine(layerDirectory, "entrypoint.sh"), "#!/bin/sh\necho hello\n");
|
||||
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
[ScanMetadataKeys.ImageConfigPath] = configPath,
|
||||
[ScanMetadataKeys.LayerDirectories] = layerDirectory,
|
||||
["image.digest"] = "sha256:test-digest"
|
||||
};
|
||||
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var service = CreateService(analyzer);
|
||||
|
||||
var context = CreateContext(metadata);
|
||||
|
||||
await service.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.True(analyzer.Invoked);
|
||||
Assert.NotNull(analyzer.LastEntrypoint);
|
||||
Assert.Equal("/entrypoint.sh", analyzer.LastEntrypoint!.Entrypoint[0]);
|
||||
Assert.NotNull(analyzer.LastContext);
|
||||
Assert.Equal("scanner", analyzer.LastContext!.User);
|
||||
Assert.Equal("/workspace", analyzer.LastContext.WorkingDirectory);
|
||||
Assert.Contains("/bin", analyzer.LastContext.Path);
|
||||
|
||||
Assert.True(context.Analysis.TryGet(ScanAnalysisKeys.EntryTraceGraph, out EntryTraceGraph stored));
|
||||
Assert.Same(analyzer.Graph, stored);
|
||||
}
|
||||
|
||||
private EntryTraceExecutionService CreateService(IEntryTraceAnalyzer analyzer)
|
||||
{
|
||||
var workerOptions = new ScannerWorkerOptions();
|
||||
var entryTraceOptions = new EntryTraceAnalyzerOptions();
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Trace));
|
||||
return new EntryTraceExecutionService(
|
||||
analyzer,
|
||||
Options.Create(entryTraceOptions),
|
||||
Options.Create(workerOptions),
|
||||
loggerFactory.CreateLogger<EntryTraceExecutionService>(),
|
||||
loggerFactory);
|
||||
}
|
||||
|
||||
private static ScanJobContext CreateContext(IReadOnlyDictionary<string, string> metadata)
|
||||
{
|
||||
var lease = new TestLease(metadata);
|
||||
return new ScanJobContext(lease, TimeProvider.System, DateTimeOffset.UtcNow, CancellationToken.None);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempRoot))
|
||||
{
|
||||
Directory.Delete(_tempRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cleanup failures
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingEntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
{
|
||||
public bool Invoked { get; private set; }
|
||||
|
||||
public EntrypointSpecification? LastEntrypoint { get; private set; }
|
||||
|
||||
public EntryTraceContext? LastContext { get; private set; }
|
||||
|
||||
public EntryTraceGraph Graph { get; } = new(
|
||||
EntryTraceOutcome.Resolved,
|
||||
ImmutableArray<EntryTraceNode>.Empty,
|
||||
ImmutableArray<EntryTraceEdge>.Empty,
|
||||
ImmutableArray<EntryTraceDiagnostic>.Empty);
|
||||
|
||||
public ValueTask<EntryTraceGraph> ResolveAsync(EntrypointSpecification entrypoint, EntryTraceContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Invoked = true;
|
||||
LastEntrypoint = entrypoint;
|
||||
LastContext = context;
|
||||
return ValueTask.FromResult(Graph);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestLease : IScanJobLease
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, string> _metadata;
|
||||
|
||||
public TestLease(IReadOnlyDictionary<string, string> metadata)
|
||||
{
|
||||
_metadata = metadata;
|
||||
EnqueuedAtUtc = DateTimeOffset.UtcNow;
|
||||
LeasedAtUtc = EnqueuedAtUtc;
|
||||
}
|
||||
|
||||
public string JobId { get; } = $"job-{Guid.NewGuid():n}";
|
||||
|
||||
public string ScanId { get; } = $"scan-{Guid.NewGuid():n}";
|
||||
|
||||
public int Attempt => 1;
|
||||
|
||||
public DateTimeOffset EnqueuedAtUtc { get; }
|
||||
|
||||
public DateTimeOffset LeasedAtUtc { get; }
|
||||
|
||||
public TimeSpan LeaseDuration => TimeSpan.FromMinutes(5);
|
||||
|
||||
public IReadOnlyDictionary<string, string> Metadata => _metadata;
|
||||
|
||||
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.EntryTrace.Runtime;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests;
|
||||
|
||||
public sealed class EntryTraceExecutionServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _tempRoot;
|
||||
|
||||
public EntryTraceExecutionServiceTests()
|
||||
{
|
||||
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-service-{Guid.NewGuid():n}");
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_When_ConfigMetadataMissing()
|
||||
{
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var store = new CapturingEntryTraceResultStore();
|
||||
var service = CreateService(analyzer, store);
|
||||
|
||||
var context = CreateContext(new Dictionary<string, string>());
|
||||
|
||||
await service.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.False(analyzer.Invoked);
|
||||
Assert.False(context.Analysis.TryGet<EntryTraceGraph>(ScanAnalysisKeys.EntryTraceGraph, out _));
|
||||
Assert.False(store.Stored);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_BuildsContext_AndStoresGraph()
|
||||
{
|
||||
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var store = new CapturingEntryTraceResultStore();
|
||||
var service = CreateService(analyzer, store);
|
||||
|
||||
var context = CreateContext(metadata);
|
||||
|
||||
await service.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.True(analyzer.Invoked);
|
||||
Assert.NotNull(analyzer.LastEntrypoint);
|
||||
Assert.Equal("/entrypoint.sh", analyzer.LastEntrypoint!.Entrypoint[0]);
|
||||
Assert.NotNull(analyzer.LastContext);
|
||||
Assert.Equal("scanner", analyzer.LastContext!.User);
|
||||
Assert.Equal("/workspace", analyzer.LastContext.WorkingDirectory);
|
||||
Assert.Contains("/bin", analyzer.LastContext.Path);
|
||||
|
||||
Assert.True(context.Analysis.TryGet(ScanAnalysisKeys.EntryTraceGraph, out EntryTraceGraph stored));
|
||||
Assert.Equal(analyzer.Graph.Outcome, stored.Outcome);
|
||||
Assert.Contains(stored.Diagnostics, diagnostic => diagnostic.Reason == EntryTraceUnknownReason.RuntimeSnapshotUnavailable);
|
||||
Assert.True(context.Analysis.TryGet(ScanAnalysisKeys.EntryTraceNdjson, out ImmutableArray<string> ndjsonPayload));
|
||||
Assert.False(ndjsonPayload.IsDefaultOrEmpty);
|
||||
Assert.True(store.Stored);
|
||||
Assert.NotNull(store.LastResult);
|
||||
Assert.Equal(context.ScanId, store.LastResult!.ScanId);
|
||||
Assert.Equal("sha256:test-digest", store.LastResult.ImageDigest);
|
||||
Assert.Equal(stored, store.LastResult.Graph);
|
||||
Assert.Equal(ndjsonPayload, store.LastResult.Ndjson);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_UsesCachedGraphWhenAvailable()
|
||||
{
|
||||
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var store = new CapturingEntryTraceResultStore();
|
||||
var cache = new InMemorySurfaceCache();
|
||||
var service = CreateService(analyzer, store, surfaceCache: cache);
|
||||
|
||||
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
|
||||
Assert.True(analyzer.Invoked);
|
||||
|
||||
analyzer.Reset();
|
||||
store.Reset();
|
||||
|
||||
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
|
||||
|
||||
Assert.False(analyzer.Invoked);
|
||||
Assert.True(store.Stored);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ReplacesSecretReferencesUsingSurfaceSecrets()
|
||||
{
|
||||
var metadata = CreateMetadata("API_KEY=secret://inline/api-key");
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var store = new CapturingEntryTraceResultStore();
|
||||
var secrets = new StubSurfaceSecretProvider(new Dictionary<(string Type, string Name), byte[]>
|
||||
{
|
||||
{("inline", "api-key"), Encoding.UTF8.GetBytes("resolved-value")}
|
||||
});
|
||||
var service = CreateService(analyzer, store, surfaceSecrets: secrets);
|
||||
|
||||
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
|
||||
|
||||
Assert.True(analyzer.Invoked);
|
||||
Assert.Equal("resolved-value", analyzer.LastContext!.Environment["API_KEY"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_FallsBackToBase64ForBinarySecrets()
|
||||
{
|
||||
var metadata = CreateMetadata("BLOB=secret://inline/blob");
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var store = new CapturingEntryTraceResultStore();
|
||||
var payload = new byte[] { 0x00, 0xFF, 0x10 };
|
||||
var secrets = new StubSurfaceSecretProvider(new Dictionary<(string Type, string Name), byte[]>
|
||||
{
|
||||
{("inline", "blob"), payload}
|
||||
});
|
||||
var service = CreateService(analyzer, store, surfaceSecrets: secrets);
|
||||
|
||||
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
|
||||
|
||||
Assert.True(analyzer.Invoked);
|
||||
Assert.Equal(Convert.ToBase64String(payload), analyzer.LastContext!.Environment["BLOB"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SkipsWhenSurfaceValidationFails()
|
||||
{
|
||||
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
|
||||
var analyzer = new CapturingEntryTraceAnalyzer();
|
||||
var store = new CapturingEntryTraceResultStore();
|
||||
var issues = new[]
|
||||
{
|
||||
SurfaceValidationIssue.Error("cache", "unwritable")
|
||||
};
|
||||
var validator = new StaticSurfaceValidatorRunner(SurfaceValidationResult.FromIssues(issues));
|
||||
var service = CreateService(analyzer, store, surfaceValidator: validator);
|
||||
|
||||
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
|
||||
|
||||
Assert.False(analyzer.Invoked);
|
||||
Assert.False(store.Stored);
|
||||
}
|
||||
|
||||
private EntryTraceExecutionService CreateService(
|
||||
IEntryTraceAnalyzer analyzer,
|
||||
IEntryTraceResultStore store,
|
||||
ISurfaceCache? surfaceCache = null,
|
||||
ISurfaceValidatorRunner? surfaceValidator = null,
|
||||
ISurfaceSecretProvider? surfaceSecrets = null,
|
||||
ISurfaceEnvironment? surfaceEnvironment = null)
|
||||
{
|
||||
var workerOptions = new ScannerWorkerOptions();
|
||||
var entryTraceOptions = new EntryTraceAnalyzerOptions();
|
||||
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Trace));
|
||||
surfaceEnvironment ??= new StubSurfaceEnvironment();
|
||||
surfaceCache ??= new InMemorySurfaceCache();
|
||||
surfaceValidator ??= new NoopSurfaceValidatorRunner();
|
||||
surfaceSecrets ??= new StubSurfaceSecretProvider();
|
||||
var serviceProvider = new ServiceCollection()
|
||||
.AddSingleton<ISurfaceEnvironment>(surfaceEnvironment)
|
||||
.BuildServiceProvider();
|
||||
|
||||
return new EntryTraceExecutionService(
|
||||
analyzer,
|
||||
Microsoft.Extensions.Options.Options.Create(entryTraceOptions),
|
||||
Microsoft.Extensions.Options.Options.Create(workerOptions),
|
||||
loggerFactory.CreateLogger<EntryTraceExecutionService>(),
|
||||
loggerFactory,
|
||||
new EntryTraceRuntimeReconciler(),
|
||||
store,
|
||||
surfaceValidator,
|
||||
surfaceEnvironment,
|
||||
surfaceCache,
|
||||
surfaceSecrets,
|
||||
serviceProvider);
|
||||
}
|
||||
|
||||
private static ScanJobContext CreateContext(IReadOnlyDictionary<string, string> metadata)
|
||||
{
|
||||
var lease = new TestLease(metadata);
|
||||
return new ScanJobContext(lease, TimeProvider.System, DateTimeOffset.UtcNow, CancellationToken.None);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> CreateMetadata(params string[] environmentEntries)
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, $"config-{Guid.NewGuid():n}.json");
|
||||
var env = environmentEntries.Length == 0
|
||||
? new[] { "PATH=/bin:/usr/bin" }
|
||||
: environmentEntries;
|
||||
var envJson = string.Join(",", env.Select(value => $"\"{value}\""));
|
||||
File.WriteAllText(configPath,
|
||||
$$"""
|
||||
{
|
||||
"config": {
|
||||
"Env": [{{envJson}}],
|
||||
"Entrypoint": ["/entrypoint.sh"],
|
||||
"WorkingDir": "/workspace",
|
||||
"User": "scanner"
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var rootDirectory = Path.Combine(_tempRoot, $"root-{Guid.NewGuid():n}");
|
||||
Directory.CreateDirectory(rootDirectory);
|
||||
File.WriteAllText(Path.Combine(rootDirectory, "entrypoint.sh"), "#!/bin/sh\necho hello\n");
|
||||
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
[ScanMetadataKeys.ImageConfigPath] = configPath,
|
||||
[ScanMetadataKeys.RootFilesystemPath] = rootDirectory,
|
||||
[ScanMetadataKeys.LayerDirectories] = rootDirectory,
|
||||
["image.digest"] = "sha256:test-digest"
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempRoot))
|
||||
{
|
||||
Directory.Delete(_tempRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cleanup failures
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingEntryTraceAnalyzer : IEntryTraceAnalyzer
|
||||
{
|
||||
public bool Invoked { get; private set; }
|
||||
|
||||
public EntrypointSpecification? LastEntrypoint { get; private set; }
|
||||
|
||||
public EntryTraceContext? LastContext { get; private set; }
|
||||
|
||||
public EntryTraceGraph Graph { get; } = new(
|
||||
EntryTraceOutcome.Resolved,
|
||||
ImmutableArray<EntryTraceNode>.Empty,
|
||||
ImmutableArray<EntryTraceEdge>.Empty,
|
||||
ImmutableArray<EntryTraceDiagnostic>.Empty,
|
||||
ImmutableArray<EntryTracePlan>.Empty,
|
||||
ImmutableArray<EntryTraceTerminal>.Empty);
|
||||
|
||||
public ValueTask<EntryTraceGraph> ResolveAsync(EntrypointSpecification entrypoint, EntryTraceContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Invoked = true;
|
||||
LastEntrypoint = entrypoint;
|
||||
LastContext = context;
|
||||
return ValueTask.FromResult(Graph);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Invoked = false;
|
||||
LastEntrypoint = null;
|
||||
LastContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingEntryTraceResultStore : IEntryTraceResultStore
|
||||
{
|
||||
public bool Stored { get; private set; }
|
||||
public EntryTraceResult? LastResult { get; private set; }
|
||||
|
||||
public Task<EntryTraceResult?> GetAsync(string scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<EntryTraceResult?>(null);
|
||||
}
|
||||
|
||||
public Task StoreAsync(EntryTraceResult result, CancellationToken cancellationToken)
|
||||
{
|
||||
Stored = true;
|
||||
LastResult = result;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Stored = false;
|
||||
LastResult = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestLease : IScanJobLease
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, string> _metadata;
|
||||
|
||||
public TestLease(IReadOnlyDictionary<string, string> metadata)
|
||||
{
|
||||
_metadata = metadata;
|
||||
EnqueuedAtUtc = DateTimeOffset.UtcNow;
|
||||
LeasedAtUtc = EnqueuedAtUtc;
|
||||
}
|
||||
|
||||
public string JobId { get; } = $"job-{Guid.NewGuid():n}";
|
||||
|
||||
public string ScanId { get; } = $"scan-{Guid.NewGuid():n}";
|
||||
|
||||
public int Attempt => 1;
|
||||
|
||||
public DateTimeOffset EnqueuedAtUtc { get; }
|
||||
|
||||
public DateTimeOffset LeasedAtUtc { get; }
|
||||
|
||||
public TimeSpan LeaseDuration => TimeSpan.FromMinutes(5);
|
||||
|
||||
public IReadOnlyDictionary<string, string> Metadata => _metadata;
|
||||
|
||||
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class StubSurfaceEnvironment : ISurfaceEnvironment
|
||||
{
|
||||
public StubSurfaceEnvironment()
|
||||
{
|
||||
var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "surface-cache-tests"));
|
||||
Settings = new SurfaceEnvironmentSettings(
|
||||
new Uri("https://surface.example"),
|
||||
"surface-cache",
|
||||
null,
|
||||
cacheRoot,
|
||||
1024,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("inline", "tenant", null, null, null, AllowInline: true),
|
||||
"tenant",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
RawVariables = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public SurfaceEnvironmentSettings Settings { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> RawVariables { get; }
|
||||
}
|
||||
|
||||
private sealed class NoopSurfaceValidatorRunner : ISurfaceValidatorRunner
|
||||
{
|
||||
public ValueTask<SurfaceValidationResult> RunAllAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.FromResult(SurfaceValidationResult.Success());
|
||||
}
|
||||
|
||||
public ValueTask EnsureAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed class StaticSurfaceValidatorRunner : ISurfaceValidatorRunner
|
||||
{
|
||||
private readonly SurfaceValidationResult _result;
|
||||
|
||||
public StaticSurfaceValidatorRunner(SurfaceValidationResult result)
|
||||
{
|
||||
_result = result;
|
||||
}
|
||||
|
||||
public ValueTask<SurfaceValidationResult> RunAllAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.FromResult(_result);
|
||||
}
|
||||
|
||||
public ValueTask EnsureAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemorySurfaceCache : ISurfaceCache
|
||||
{
|
||||
private readonly Dictionary<string, byte[]> _store = new();
|
||||
private readonly object _gate = new();
|
||||
|
||||
public async Task<T> GetOrCreateAsync<T>(
|
||||
SurfaceCacheKey key,
|
||||
Func<CancellationToken, Task<T>> factory,
|
||||
Func<T, ReadOnlyMemory<byte>> serializer,
|
||||
Func<ReadOnlyMemory<byte>, T> deserializer,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (TryRead(key, deserializer, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var created = await factory(cancellationToken).ConfigureAwait(false);
|
||||
var payload = serializer(created).ToArray();
|
||||
lock (_gate)
|
||||
{
|
||||
_store[key.ToString()] = payload;
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
public Task<T?> TryGetAsync<T>(
|
||||
SurfaceCacheKey key,
|
||||
Func<ReadOnlyMemory<byte>, T> deserializer,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(TryRead(key, deserializer, out var value) ? value : default);
|
||||
}
|
||||
|
||||
public Task SetAsync(
|
||||
SurfaceCacheKey key,
|
||||
ReadOnlyMemory<byte> payload,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_store[key.ToString()] = payload.ToArray();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TryRead<T>(SurfaceCacheKey key, Func<ReadOnlyMemory<byte>, T> deserializer, out T value)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_store.TryGetValue(key.ToString(), out var bytes))
|
||||
{
|
||||
value = deserializer(new ReadOnlyMemory<byte>(bytes));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubSurfaceSecretProvider : ISurfaceSecretProvider
|
||||
{
|
||||
private readonly Dictionary<(string Type, string Name), byte[]> _secrets;
|
||||
private readonly bool _throwOnMissing;
|
||||
|
||||
public StubSurfaceSecretProvider(Dictionary<(string Type, string Name), byte[]>? secrets = null, bool throwOnMissing = false)
|
||||
{
|
||||
_secrets = secrets ?? new Dictionary<(string Type, string Name), byte[]>();
|
||||
_throwOnMissing = throwOnMissing;
|
||||
}
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = (request.SecretType, request.Name ?? string.Empty);
|
||||
if (_secrets.TryGetValue(key, out var payload))
|
||||
{
|
||||
return ValueTask.FromResult(SurfaceSecretHandle.FromBytes(payload));
|
||||
}
|
||||
|
||||
if (_throwOnMissing)
|
||||
{
|
||||
throw new SurfaceSecretNotFoundException(request);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(SurfaceSecretHandle.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public sealed class RedisWorkerSmokeTests
|
||||
public async Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new QueueLeaseRequest(_consumerName, 1, _queueOptions.DefaultLeaseDuration);
|
||||
var leases = await _queue.LeaseAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
var leases = await _queue.LeaseAsync(request, cancellationToken);
|
||||
if (leases.Count == 0)
|
||||
{
|
||||
return null;
|
||||
@@ -221,23 +221,23 @@ public sealed class RedisWorkerSmokeTests
|
||||
|
||||
public async ValueTask RenewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _lease.RenewAsync(_options.DefaultLeaseDuration, cancellationToken).ConfigureAwait(false);
|
||||
await _lease.RenewAsync(_options.DefaultLeaseDuration, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _lease.AcknowledgeAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _lease.AcknowledgeAsync(cancellationToken);
|
||||
_deps.JobCompleted.TrySetResult();
|
||||
}
|
||||
|
||||
public async ValueTask AbandonAsync(string reason, CancellationToken cancellationToken)
|
||||
{
|
||||
await _lease.ReleaseAsync(QueueReleaseDisposition.Retry, cancellationToken).ConfigureAwait(false);
|
||||
await _lease.ReleaseAsync(QueueReleaseDisposition.Retry, cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask PoisonAsync(string reason, CancellationToken cancellationToken)
|
||||
{
|
||||
await _lease.DeadLetterAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||
await _lease.DeadLetterAsync(reason, cancellationToken);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
Reference in New Issue
Block a user