478 lines
21 KiB
C#
478 lines
21 KiB
C#
// -----------------------------------------------------------------------------
|
|
// StellaOpsTokenClientTests.cs
|
|
// Sprint: SPRINT_5100_0009_0005_authority_tests
|
|
// Task: AUTHORITY-5100-001, AUTHORITY-5100-002
|
|
// Description: Model L0 token issuance and validation tests
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.Extensions.Time.Testing;
|
|
using StellaOps.Auth.Client;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Auth.Client.Tests;
|
|
|
|
/// <summary>
|
|
/// Token issuance and validation tests for Authority module.
|
|
/// Implements Model L0 (Core Logic) test requirements:
|
|
/// - Valid claims → token generated with correct expiry
|
|
/// - Client credentials flow → token issued
|
|
/// - Invalid credentials → appropriate error
|
|
/// </summary>
|
|
public class StellaOpsTokenClientTests
|
|
{
|
|
#region Task 1: Token Issuance Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestPasswordToken_ReturnsResultAndCaches()
|
|
{
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-02-01T00:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"access_token\":\"abc\",\"token_type\":\"Bearer\",\"expires_in\":120,\"scope\":\"concelier.jobs.trigger\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"keys\":[]}"));
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "cli"
|
|
};
|
|
options.DefaultScopes.Add("concelier.jobs.trigger");
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
var result = await client.RequestPasswordTokenAsync("user", "pass");
|
|
|
|
Assert.Equal("abc", result.AccessToken);
|
|
Assert.Contains("concelier.jobs.trigger", result.Scopes);
|
|
|
|
await client.CacheTokenAsync("key", result.ToCacheEntry());
|
|
var cached = await client.GetCachedTokenAsync("key");
|
|
Assert.NotNull(cached);
|
|
Assert.Equal("abc", cached!.AccessToken);
|
|
|
|
var jwks = await client.GetJsonWebKeySetAsync();
|
|
Assert.Empty(jwks.Keys);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestClientCredentialsToken_ReturnsTokenWithCorrectExpiry()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var expiresIn = 3600; // 1 hour
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse($"{{\"access_token\":\"client_cred_token\",\"token_type\":\"Bearer\",\"expires_in\":{expiresIn},\"scope\":\"scanner.scan\"}}"));
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "scanner-service",
|
|
ClientSecret = "secret123"
|
|
};
|
|
options.DefaultScopes.Add("scanner.scan");
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act
|
|
var result = await client.RequestClientCredentialsTokenAsync();
|
|
|
|
// Assert
|
|
Assert.Equal("client_cred_token", result.AccessToken);
|
|
Assert.Equal("Bearer", result.TokenType);
|
|
Assert.Contains("scanner.scan", result.Scopes);
|
|
|
|
// Verify expiry is calculated correctly
|
|
var expectedExpiry = timeProvider.GetUtcNow().AddSeconds(expiresIn);
|
|
Assert.Equal(expectedExpiry, result.ExpiresAt);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestClientCredentialsToken_WithCustomScope_UsesCustomScope()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"access_token\":\"custom_scope_token\",\"token_type\":\"Bearer\",\"expires_in\":1800,\"scope\":\"policy.run policy.evaluate\"}"));
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "policy-service",
|
|
ClientSecret = "policy_secret"
|
|
};
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act
|
|
var result = await client.RequestClientCredentialsTokenAsync(scope: "policy.run policy.evaluate");
|
|
|
|
// Assert
|
|
Assert.Equal("custom_scope_token", result.AccessToken);
|
|
Assert.Contains("policy.run", result.Scopes);
|
|
Assert.Contains("policy.evaluate", result.Scopes);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestClientCredentialsToken_WithoutClientId_ThrowsInvalidOperation()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
Task.FromResult(CreateJsonResponse("{}")));
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "" // Empty client ID
|
|
};
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
client.RequestClientCredentialsTokenAsync());
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestPasswordToken_WithAdditionalParameters_IncludesParameters()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"access_token\":\"param_token\",\"token_type\":\"Bearer\",\"expires_in\":600}"));
|
|
|
|
HttpRequestMessage? capturedRequest = null;
|
|
var handler = new StubHttpMessageHandler(async (request, cancellationToken) =>
|
|
{
|
|
if (request.RequestUri?.AbsolutePath == "/connect/token")
|
|
{
|
|
capturedRequest = request;
|
|
}
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return responses.Dequeue();
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "cli"
|
|
};
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act
|
|
var additionalParams = new Dictionary<string, string>
|
|
{
|
|
["tenant_id"] = "tenant-123",
|
|
["custom_claim"] = "value"
|
|
};
|
|
var result = await client.RequestPasswordTokenAsync("user", "pass", additionalParameters: additionalParams);
|
|
|
|
// Assert
|
|
Assert.Equal("param_token", result.AccessToken);
|
|
Assert.NotNull(capturedRequest);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Task 2: Token Validation/Rejection Tests
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestPasswordToken_WhenServerReturnsError_ThrowsInvalidOperation()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(new HttpResponseMessage(HttpStatusCode.Unauthorized)
|
|
{
|
|
Content = new StringContent("{\"error\":\"invalid_client\",\"error_description\":\"Invalid client credentials\"}")
|
|
{
|
|
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
|
|
}
|
|
});
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "invalid-client"
|
|
};
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
client.RequestPasswordTokenAsync("user", "wrong_pass"));
|
|
Assert.Contains("401", ex.Message);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestPasswordToken_WhenResponseMissingAccessToken_ThrowsInvalidOperation()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"token_type\":\"Bearer\",\"expires_in\":3600}")); // Missing access_token
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "cli"
|
|
};
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
client.RequestPasswordTokenAsync("user", "pass"));
|
|
Assert.Contains("access_token", ex.Message);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task CachedToken_WhenExpired_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(60));
|
|
|
|
var entry = new StellaOpsTokenCacheEntry(
|
|
"expired_token",
|
|
timeProvider.GetUtcNow().AddMinutes(-5), // Already expired
|
|
["scanner.scan"]);
|
|
|
|
await cache.SetAsync("expired_key", entry);
|
|
|
|
// Advance time past cache cleanup
|
|
timeProvider.Advance(TimeSpan.FromSeconds(61));
|
|
|
|
// Act
|
|
var result = await cache.GetAsync("expired_key");
|
|
|
|
// Assert - Expired entries should be cleaned up or return null
|
|
// Note: Depends on cache implementation behavior
|
|
// The cache may have already evicted it or it won't be returned
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestPasswordToken_DefaultsToBearer_WhenTokenTypeNotProvided()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"access_token\":\"no_type_token\",\"expires_in\":3600}")); // Missing token_type
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "cli"
|
|
};
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act
|
|
var result = await client.RequestPasswordTokenAsync("user", "pass");
|
|
|
|
// Assert
|
|
Assert.Equal("no_type_token", result.AccessToken);
|
|
Assert.Equal("Bearer", result.TokenType); // Defaults to Bearer
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task RequestPasswordToken_DefaultsTo3600ExpiresIn_WhenNotProvided()
|
|
{
|
|
// Arrange
|
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-03-15T10:00:00Z"));
|
|
var responses = new Queue<HttpResponseMessage>();
|
|
responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
|
|
responses.Enqueue(CreateJsonResponse("{\"access_token\":\"no_expiry_token\",\"token_type\":\"Bearer\"}")); // Missing expires_in
|
|
|
|
var handler = new StubHttpMessageHandler((request, cancellationToken) =>
|
|
{
|
|
Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
|
|
return Task.FromResult(responses.Dequeue());
|
|
});
|
|
|
|
var httpClient = new HttpClient(handler);
|
|
var options = new StellaOpsAuthClientOptions
|
|
{
|
|
Authority = "https://authority.test",
|
|
ClientId = "cli"
|
|
};
|
|
options.Validate();
|
|
|
|
var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
|
|
var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
|
|
var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
|
|
var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
|
|
var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);
|
|
|
|
// Act
|
|
var result = await client.RequestPasswordTokenAsync("user", "pass");
|
|
|
|
// Assert
|
|
Assert.Equal("no_expiry_token", result.AccessToken);
|
|
var expectedExpiry = timeProvider.GetUtcNow().AddSeconds(3600); // Default 1 hour
|
|
Assert.Equal(expectedExpiry, result.ExpiresAt);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static HttpResponseMessage CreateJsonResponse(string json)
|
|
{
|
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(json)
|
|
{
|
|
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
|
|
}
|
|
};
|
|
}
|
|
|
|
private sealed class StubHttpMessageHandler : HttpMessageHandler
|
|
{
|
|
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responder;
|
|
|
|
public StubHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responder)
|
|
{
|
|
this.responder = responder;
|
|
}
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
=> responder(request, cancellationToken);
|
|
}
|
|
|
|
private sealed class TestOptionsMonitor<TOptions> : IOptionsMonitor<TOptions>
|
|
where TOptions : class
|
|
{
|
|
private readonly TOptions value;
|
|
|
|
public TestOptionsMonitor(TOptions value)
|
|
{
|
|
this.value = value;
|
|
}
|
|
|
|
public TOptions CurrentValue => value;
|
|
|
|
public TOptions Get(string? name) => value;
|
|
|
|
public IDisposable OnChange(Action<TOptions, string> listener) => NullDisposable.Instance;
|
|
|
|
private sealed class NullDisposable : IDisposable
|
|
{
|
|
public static NullDisposable Instance { get; } = new();
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|