save progress
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user