save progress

This commit is contained in:
StellaOps Bot
2026-01-02 21:06:27 +02:00
parent f46bde5575
commit 3f197814c5
441 changed files with 21545 additions and 4306 deletions

View File

@@ -25,7 +25,7 @@ namespace StellaOps.Auth.Client.Tests;
public class ServiceCollectionExtensionsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public async Task AddStellaOpsAuthClient_ConfiguresRetryPolicy()
{
var services = new ServiceCollection();
@@ -81,7 +81,7 @@ public class ServiceCollectionExtensionsTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void EnsureEgressAllowed_InvokesPolicyWhenAuthorityProvided()
{
var services = new ServiceCollection();
@@ -138,7 +138,7 @@ public class ServiceCollectionExtensionsTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public async Task AddStellaOpsApiAuthentication_AttachesPatAndTenantHeader()
{
var services = new ServiceCollection();
@@ -185,7 +185,7 @@ public class ServiceCollectionExtensionsTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public async Task AddStellaOpsApiAuthentication_UsesClientCredentialsWithCaching()
{
var services = new ServiceCollection();
@@ -217,14 +217,27 @@ public class ServiceCollectionExtensionsTests
options.Tenant = "tenant-oauth";
});
var secondHandler = new RecordingHttpMessageHandler();
services.AddHttpClient("notify2")
.ConfigurePrimaryHttpMessageHandler(() => secondHandler)
.AddStellaOpsApiAuthentication(options =>
{
options.Mode = StellaOpsApiAuthMode.ClientCredentials;
options.Scope = "notify.read";
options.Tenant = "tenant-oauth";
});
using var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<IHttpClientFactory>().CreateClient("notify");
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient("notify");
await client.GetAsync("https://notify.example/api");
await client.GetAsync("https://notify.example/api");
Assert.Equal(2, handler.AuthorizationHistory.Count);
Assert.Equal(1, recordingTokenClient.ClientCredentialsCallCount);
Assert.Equal(1, recordingTokenClient.GetCachedTokenCallCount);
Assert.Equal(1, recordingTokenClient.CacheTokenCallCount);
Assert.All(handler.AuthorizationHistory, header =>
{
Assert.NotNull(header);
@@ -233,6 +246,15 @@ public class ServiceCollectionExtensionsTests
});
Assert.All(handler.TenantHeaders, value => Assert.Equal("tenant-oauth", value));
var clientTwo = factory.CreateClient("notify2");
await clientTwo.GetAsync("https://notify.example/api");
Assert.Equal(1, recordingTokenClient.ClientCredentialsCallCount);
Assert.True(recordingTokenClient.GetCachedTokenCallCount >= 2);
Assert.Equal(1, recordingTokenClient.CacheTokenCallCount);
Assert.Single(secondHandler.AuthorizationHistory);
Assert.Equal("token-1", secondHandler.AuthorizationHistory[0]!.Parameter);
// Advance beyond expiry buffer to force refresh.
fakeTime.Advance(TimeSpan.FromMinutes(2));
await client.GetAsync("https://notify.example/api");
@@ -240,6 +262,89 @@ public class ServiceCollectionExtensionsTests
Assert.Equal(3, handler.AuthorizationHistory.Count);
Assert.Equal("token-2", handler.AuthorizationHistory[^1]!.Parameter);
Assert.Equal(2, recordingTokenClient.ClientCredentialsCallCount);
Assert.True(recordingTokenClient.GetCachedTokenCallCount >= 2);
Assert.True(recordingTokenClient.CacheTokenCallCount >= 2);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AddStellaOpsApiAuthentication_UsesPasswordFlowWithCaching()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddStellaOpsAuthClient(options =>
{
options.Authority = "https://authority.test";
options.DiscoveryCacheLifetime = TimeSpan.FromMinutes(1);
options.JwksCacheLifetime = TimeSpan.FromMinutes(1);
options.AllowOfflineCacheFallback = false;
});
var fakeTime = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T00:00:00Z"));
services.AddSingleton<TimeProvider>(fakeTime);
var recordingTokenClient = new RecordingTokenClient(fakeTime);
services.AddSingleton<IStellaOpsTokenClient>(recordingTokenClient);
var handler = new RecordingHttpMessageHandler();
services.AddHttpClient("vuln")
.ConfigurePrimaryHttpMessageHandler(() => handler)
.AddStellaOpsApiAuthentication(options =>
{
options.Mode = StellaOpsApiAuthMode.Password;
options.Username = "user1";
options.Password = "pass1";
options.Scope = "vuln.view";
});
using var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<IHttpClientFactory>().CreateClient("vuln");
await client.GetAsync("https://vuln.example/api");
await client.GetAsync("https://vuln.example/api");
Assert.Equal(2, handler.AuthorizationHistory.Count);
Assert.Equal(1, recordingTokenClient.PasswordCallCount);
Assert.Equal(1, recordingTokenClient.GetCachedTokenCallCount);
Assert.Equal(1, recordingTokenClient.CacheTokenCallCount);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AddStellaOpsAuthClient_DisablesRetriesWhenConfigured()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddStellaOpsAuthClient(options =>
{
options.Authority = "https://authority.test";
options.EnableRetries = false;
options.DiscoveryCacheLifetime = TimeSpan.FromMinutes(1);
options.JwksCacheLifetime = TimeSpan.FromMinutes(1);
options.AllowOfflineCacheFallback = false;
});
var attemptCount = 0;
services.AddHttpClient<StellaOpsDiscoveryCache>()
.ConfigureHttpMessageHandlerBuilder(builder =>
{
builder.PrimaryHandler = new LambdaHttpMessageHandler((_, _) =>
{
attemptCount++;
return Task.FromResult(CreateResponse(HttpStatusCode.InternalServerError, "{}"));
});
});
using var provider = services.BuildServiceProvider();
var cache = provider.GetRequiredService<StellaOpsDiscoveryCache>();
await Assert.ThrowsAsync<HttpRequestException>(() => cache.GetAsync(CancellationToken.None));
Assert.Equal(1, attemptCount);
}
private sealed class RecordingHttpMessageHandler : HttpMessageHandler
@@ -331,6 +436,7 @@ public class ServiceCollectionExtensionsTests
{
private readonly FakeTimeProvider timeProvider;
private int tokenCounter;
private StellaOpsTokenCacheEntry? cachedEntry;
public RecordingTokenClient(FakeTimeProvider timeProvider)
{
@@ -338,6 +444,9 @@ public class ServiceCollectionExtensionsTests
}
public int ClientCredentialsCallCount { get; private set; }
public int PasswordCallCount { get; private set; }
public int GetCachedTokenCallCount { get; private set; }
public int CacheTokenCallCount { get; private set; }
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
@@ -352,23 +461,46 @@ public class ServiceCollectionExtensionsTests
null,
"{}");
return Task.FromResult(result);
}
return Task.FromResult(result);
}
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
PasswordCallCount++;
var tokenId = Interlocked.Increment(ref tokenCounter);
var result = new StellaOpsTokenResult(
$"token-{tokenId}",
"Bearer",
timeProvider.GetUtcNow().AddMinutes(1),
scope is null ? Array.Empty<string>() : new[] { scope },
null,
null,
"{}");
return Task.FromResult(result);
}
public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new JsonWebKeySet());
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult<StellaOpsTokenCacheEntry?>(null);
{
GetCachedTokenCallCount++;
return ValueTask.FromResult(cachedEntry);
}
public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
{
CacheTokenCallCount++;
cachedEntry = entry;
return ValueTask.CompletedTask;
}
public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
{
cachedEntry = null;
return ValueTask.CompletedTask;
}
}
}