using System; using System.Net; using System.Net.Http; using System.Text; using System.Threading; using FluentAssertions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Authentication; using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Configuration; using Xunit; namespace StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests.Authentication; public sealed class RancherHubTokenProviderTests { private const string TokenResponse = "{\"access_token\":\"abc123\",\"token_type\":\"Bearer\",\"expires_in\":3600}"; [Fact] public async Task GetAccessTokenAsync_RequestsAndCachesToken() { var handler = TestHttpMessageHandler.RespondWith(request => { request.Headers.Authorization.Should().NotBeNull(); request.Content.Should().NotBeNull(); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TokenResponse, Encoding.UTF8, "application/json"), }; }); var client = new HttpClient(handler) { BaseAddress = new Uri("https://identity.suse.com"), }; var factory = new SingleClientHttpClientFactory(client); var cache = new MemoryCache(new MemoryCacheOptions()); var provider = new RancherHubTokenProvider(factory, cache, NullLogger.Instance); var options = new RancherHubConnectorOptions { ClientId = "client", ClientSecret = "secret", TokenEndpoint = new Uri("https://identity.suse.com/oauth/token"), Audience = "https://vexhub.suse.com", }; options.Scopes.Clear(); options.Scopes.Add("hub.read"); options.Scopes.Add("hub.events"); var token = await provider.GetAccessTokenAsync(options, CancellationToken.None); token.Should().NotBeNull(); token!.Value.Should().Be("abc123"); var cached = await provider.GetAccessTokenAsync(options, CancellationToken.None); cached.Should().NotBeNull(); handler.InvocationCount.Should().Be(1); } [Fact] public async Task GetAccessTokenAsync_ReturnsNullWhenOfflinePreferred() { var handler = TestHttpMessageHandler.RespondWith(_ => new HttpResponseMessage(HttpStatusCode.OK)); var client = new HttpClient(handler) { BaseAddress = new Uri("https://identity.suse.com"), }; var factory = new SingleClientHttpClientFactory(client); var cache = new MemoryCache(new MemoryCacheOptions()); var provider = new RancherHubTokenProvider(factory, cache, NullLogger.Instance); var options = new RancherHubConnectorOptions { PreferOfflineSnapshot = true, ClientId = "client", ClientSecret = "secret", TokenEndpoint = new Uri("https://identity.suse.com/oauth/token"), }; var token = await provider.GetAccessTokenAsync(options, CancellationToken.None); token.Should().BeNull(); handler.InvocationCount.Should().Be(0); } [Fact] public async Task GetAccessTokenAsync_ReturnsNullWithoutCredentials() { var handler = TestHttpMessageHandler.RespondWith(_ => new HttpResponseMessage(HttpStatusCode.OK)); var client = new HttpClient(handler) { BaseAddress = new Uri("https://identity.suse.com"), }; var factory = new SingleClientHttpClientFactory(client); var cache = new MemoryCache(new MemoryCacheOptions()); var provider = new RancherHubTokenProvider(factory, cache, NullLogger.Instance); var options = new RancherHubConnectorOptions(); var token = await provider.GetAccessTokenAsync(options, CancellationToken.None); token.Should().BeNull(); handler.InvocationCount.Should().Be(0); } private sealed class SingleClientHttpClientFactory : IHttpClientFactory { private readonly HttpClient _client; public SingleClientHttpClientFactory(HttpClient client) { _client = client; } public HttpClient CreateClient(string name) => _client; } private sealed class TestHttpMessageHandler : HttpMessageHandler { private readonly Func _responseFactory; private TestHttpMessageHandler(Func responseFactory) { _responseFactory = responseFactory; } public int InvocationCount { get; private set; } public static TestHttpMessageHandler RespondWith(Func responseFactory) => new(responseFactory); protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { InvocationCount++; return Task.FromResult(_responseFactory(request)); } } }