Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Gateway.Tests/PolicyEngineClientTests.cs

218 lines
8.6 KiB
C#

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<PolicyGatewayDpopProofGenerator>.Instance);
var tokenProvider = new PolicyEngineTokenProvider(tokenClient, optionsMonitor, dpopGenerator, TimeProvider.System, NullLogger<PolicyEngineTokenProvider>.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<PolicyEngineClient>.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<long>((instrument, value, tags, state) =>
{
if (instrument.Name != "policy_gateway_activation_requests_total")
{
return;
}
measurements.Add((value, GetTag(tags, "outcome"), GetTag(tags, "source")));
});
listener.SetMeasurementEventCallback<double>((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<KeyValuePair<string, object?>> 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<PolicyGatewayOptions>
{
public TestOptionsMonitor(PolicyGatewayOptions current)
{
CurrentValue = current;
}
public PolicyGatewayOptions CurrentValue { get; }
public PolicyGatewayOptions Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<PolicyGatewayOptions, string?> 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<string, string>? LastAdditionalParameters { get; private set; }
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
RequestCount++;
LastAdditionalParameters = additionalParameters;
return Task.FromResult(new StellaOpsTokenResult("service-token", "Bearer", DateTimeOffset.UtcNow.AddMinutes(5), Array.Empty<string>()));
}
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult<StellaOpsTokenCacheEntry?>(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<HttpResponseMessage> 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<PolicyActivationApprovalDto>())));
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();
}
}