using System; using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Net; using System.Net.Http; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using StellaOps.Auth.Client; using StellaOps.Policy.Gateway.Clients; using StellaOps.Policy.Gateway.Contracts; using StellaOps.Policy.Gateway.Options; using StellaOps.Policy.Gateway.Services; using Xunit; using StellaOps.TestKit; namespace StellaOps.Policy.Gateway.Tests; public class PolicyEngineClientTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task ActivateRevision_UsesServiceTokenWhenForwardingContextMissing() { var options = CreateGatewayOptions(); options.PolicyEngine.ClientCredentials.Enabled = true; options.PolicyEngine.ClientCredentials.ClientId = "policy-gateway"; options.PolicyEngine.ClientCredentials.ClientSecret = "secret"; options.PolicyEngine.ClientCredentials.Scopes.Clear(); options.PolicyEngine.ClientCredentials.Scopes.Add("policy:activate"); options.PolicyEngine.BaseAddress = "https://policy-engine.test/"; var optionsMonitor = new TestOptionsMonitor(options); var tokenClient = new StubTokenClient(); var dpopGenerator = new PolicyGatewayDpopProofGenerator(new StubHostEnvironment(), optionsMonitor, TimeProvider.System, NullLogger.Instance); var tokenProvider = new PolicyEngineTokenProvider(tokenClient, optionsMonitor, dpopGenerator, TimeProvider.System, NullLogger.Instance); using var recordingHandler = new RecordingHandler(); using var httpClient = new HttpClient(recordingHandler) { BaseAddress = new Uri(options.PolicyEngine.BaseAddress) }; var client = new PolicyEngineClient(httpClient, Microsoft.Extensions.Options.Options.Create(options), tokenProvider, NullLogger.Instance); var request = new ActivatePolicyRevisionRequest("comment"); var result = await client.ActivatePolicyRevisionAsync(null, "pack-123", 7, request, CancellationToken.None); Assert.True(result.IsSuccess); Assert.NotNull(recordingHandler.LastRequest); var authorization = recordingHandler.LastRequest!.Headers.Authorization; Assert.NotNull(authorization); Assert.Equal("Bearer", authorization!.Scheme); Assert.Equal("service-token", authorization.Parameter); Assert.Equal(1, tokenClient.RequestCount); } [Trait("Category", TestCategories.Unit)] [Fact] public void Metrics_RecordActivation_EmitsExpectedTags() { using var metrics = new PolicyGatewayMetrics(); using var listener = new MeterListener(); using StellaOps.TestKit; var measurements = new List<(long Value, string Outcome, string Source)>(); var latencies = new List<(double Value, string Outcome, string Source)>(); listener.InstrumentPublished += (instrument, meterListener) => { if (!string.Equals(instrument.Meter.Name, "StellaOps.Policy.Gateway", StringComparison.Ordinal)) { return; } meterListener.EnableMeasurementEvents(instrument); }; listener.SetMeasurementEventCallback((instrument, value, tags, state) => { if (instrument.Name != "policy_gateway_activation_requests_total") { return; } measurements.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source"))); }); listener.SetMeasurementEventCallback((instrument, value, tags, state) => { if (instrument.Name != "policy_gateway_activation_latency_ms") { return; } latencies.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source"))); }); listener.Start(); metrics.RecordActivation("activated", "service", 42.5); listener.Dispose(); Assert.Contains(measurements, entry => entry.Value == 1 && entry.Outcome == "activated" && entry.Source == "service"); Assert.Contains(latencies, entry => entry.Outcome == "activated" && entry.Source == "service" && entry.Value == 42.5); } private static string GetTag(ReadOnlySpan> tags, string key) { foreach (var tag in tags) { if (string.Equals(tag.Key, key, StringComparison.Ordinal)) { return tag.Value?.ToString() ?? string.Empty; } } return string.Empty; } private static PolicyGatewayOptions CreateGatewayOptions() { return new PolicyGatewayOptions { PolicyEngine = { BaseAddress = "https://policy-engine.test/" } }; } private sealed class TestOptionsMonitor : IOptionsMonitor { public TestOptionsMonitor(PolicyGatewayOptions current) { CurrentValue = current; } public PolicyGatewayOptions CurrentValue { get; } public PolicyGatewayOptions Get(string? name) => CurrentValue; public IDisposable OnChange(Action listener) => EmptyDisposable.Instance; private sealed class EmptyDisposable : IDisposable { public static readonly EmptyDisposable Instance = new(); public void Dispose() { } } } private sealed class StubTokenClient : IStellaOpsTokenClient { public int RequestCount { get; private set; } public IReadOnlyDictionary? LastAdditionalParameters { get; private set; } public Task RequestClientCredentialsTokenAsync(string? scope = null, IReadOnlyDictionary? additionalParameters = null, CancellationToken cancellationToken = default) { RequestCount++; LastAdditionalParameters = additionalParameters; return Task.FromResult(new StellaOpsTokenResult("service-token", "Bearer", DateTimeOffset.UtcNow.AddMinutes(5), Array.Empty())); } public Task RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary? additionalParameters = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetJsonWebKeySetAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(); public ValueTask GetCachedTokenAsync(string key, CancellationToken cancellationToken = default) => ValueTask.FromResult(null); public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; } private sealed class RecordingHandler : HttpMessageHandler { public HttpRequestMessage? LastRequest { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { LastRequest = request; var payload = JsonSerializer.Serialize(new PolicyRevisionActivationDto("activated", new PolicyRevisionDto(7, "Activated", false, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, Array.Empty()))); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(payload, Encoding.UTF8, "application/json") }; return Task.FromResult(response); } } private sealed class StubHostEnvironment : IHostEnvironment { public string EnvironmentName { get; set; } = "Development"; public string ApplicationName { get; set; } = "PolicyGatewayTests"; public string ContentRootPath { get; set; } = AppContext.BaseDirectory; public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); } }