179 lines
7.2 KiB
C#
179 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Vexer.Connectors.SUSE.RancherVEXHub.Authentication;
|
|
using StellaOps.Vexer.Connectors.SUSE.RancherVEXHub.Configuration;
|
|
using StellaOps.Vexer.Connectors.SUSE.RancherVEXHub.Metadata;
|
|
using System.IO.Abstractions.TestingHelpers;
|
|
using System.Threading;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.Vexer.Connectors.SUSE.RancherVEXHub.Tests.Metadata;
|
|
|
|
public sealed class RancherHubMetadataLoaderTests
|
|
{
|
|
private const string SampleDiscovery = """
|
|
{
|
|
"hubId": "vexer:suse.rancher",
|
|
"title": "SUSE Rancher VEX Hub",
|
|
"subscription": {
|
|
"eventsUri": "https://vexhub.suse.com/api/v1/events",
|
|
"checkpointUri": "https://vexhub.suse.com/api/v1/checkpoints",
|
|
"requiresAuthentication": true,
|
|
"channels": ["rke2", "k3s"],
|
|
"scopes": ["hub.read", "hub.events"]
|
|
},
|
|
"authentication": {
|
|
"tokenUri": "https://identity.suse.com/oauth2/token",
|
|
"audience": "https://vexhub.suse.com"
|
|
},
|
|
"offline": {
|
|
"snapshotUri": "https://downloads.suse.com/vexhub/snapshot.json",
|
|
"sha256": "deadbeef",
|
|
"updated": "2025-10-10T12:00:00Z"
|
|
}
|
|
}
|
|
""";
|
|
|
|
[Fact]
|
|
public async Task LoadAsync_FetchesAndCachesMetadata()
|
|
{
|
|
var handler = TestHttpMessageHandler.RespondWith(_ =>
|
|
{
|
|
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(SampleDiscovery, Encoding.UTF8, "application/json"),
|
|
};
|
|
response.Headers.ETag = new EntityTagHeaderValue("\"abc\"");
|
|
return response;
|
|
});
|
|
|
|
var client = new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri("https://vexhub.suse.com"),
|
|
};
|
|
|
|
var factory = new SingleClientHttpClientFactory(client);
|
|
var cache = new MemoryCache(new MemoryCacheOptions());
|
|
var fileSystem = new MockFileSystem();
|
|
var offlinePath = fileSystem.Path.Combine(@"C:\offline", "rancher-hub.json");
|
|
var options = new RancherHubConnectorOptions
|
|
{
|
|
DiscoveryUri = new Uri("https://vexhub.suse.com/.well-known/vex/rancher-hub.json"),
|
|
OfflineSnapshotPath = offlinePath,
|
|
};
|
|
|
|
var tokenProvider = new RancherHubTokenProvider(factory, cache, NullLogger<RancherHubTokenProvider>.Instance);
|
|
var loader = new RancherHubMetadataLoader(factory, cache, tokenProvider, fileSystem, NullLogger<RancherHubMetadataLoader>.Instance);
|
|
|
|
var result = await loader.LoadAsync(options, CancellationToken.None);
|
|
|
|
result.FromCache.Should().BeFalse();
|
|
result.FromOfflineSnapshot.Should().BeFalse();
|
|
result.Metadata.Provider.DisplayName.Should().Be("SUSE Rancher VEX Hub");
|
|
result.Metadata.Subscription.EventsUri.Should().Be(new Uri("https://vexhub.suse.com/api/v1/events"));
|
|
result.Metadata.Authentication.TokenEndpoint.Should().Be(new Uri("https://identity.suse.com/oauth2/token"));
|
|
|
|
// Second call should be served from cache (no additional HTTP invocation).
|
|
handler.ResetInvocationCount();
|
|
await loader.LoadAsync(options, CancellationToken.None);
|
|
handler.InvocationCount.Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadAsync_UsesOfflineSnapshotWhenNetworkFails()
|
|
{
|
|
var handler = TestHttpMessageHandler.RespondWith(_ => throw new HttpRequestException("network down"));
|
|
var client = new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri("https://vexhub.suse.com"),
|
|
};
|
|
|
|
var factory = new SingleClientHttpClientFactory(client);
|
|
var cache = new MemoryCache(new MemoryCacheOptions());
|
|
var fileSystem = new MockFileSystem();
|
|
var offlinePath = fileSystem.Path.Combine(@"C:\offline", "rancher-hub.json");
|
|
fileSystem.AddFile(offlinePath, new MockFileData(SampleDiscovery));
|
|
|
|
var options = new RancherHubConnectorOptions
|
|
{
|
|
DiscoveryUri = new Uri("https://vexhub.suse.com/.well-known/vex/rancher-hub.json"),
|
|
OfflineSnapshotPath = offlinePath,
|
|
};
|
|
|
|
var tokenProvider = new RancherHubTokenProvider(factory, cache, NullLogger<RancherHubTokenProvider>.Instance);
|
|
var loader = new RancherHubMetadataLoader(factory, cache, tokenProvider, fileSystem, NullLogger<RancherHubMetadataLoader>.Instance);
|
|
|
|
var result = await loader.LoadAsync(options, CancellationToken.None);
|
|
result.FromOfflineSnapshot.Should().BeTrue();
|
|
result.Metadata.Subscription.RequiresAuthentication.Should().BeTrue();
|
|
result.Metadata.OfflineSnapshot.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadAsync_ThrowsWhenOfflinePreferredButMissing()
|
|
{
|
|
var handler = TestHttpMessageHandler.RespondWith(_ => throw new HttpRequestException("network down"));
|
|
var client = new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri("https://vexhub.suse.com"),
|
|
};
|
|
|
|
var factory = new SingleClientHttpClientFactory(client);
|
|
var cache = new MemoryCache(new MemoryCacheOptions());
|
|
var fileSystem = new MockFileSystem();
|
|
var options = new RancherHubConnectorOptions
|
|
{
|
|
DiscoveryUri = new Uri("https://vexhub.suse.com/.well-known/vex/rancher-hub.json"),
|
|
OfflineSnapshotPath = "/offline/missing.json",
|
|
PreferOfflineSnapshot = true,
|
|
};
|
|
|
|
var tokenProvider = new RancherHubTokenProvider(factory, cache, NullLogger<RancherHubTokenProvider>.Instance);
|
|
var loader = new RancherHubMetadataLoader(factory, cache, tokenProvider, fileSystem, NullLogger<RancherHubMetadataLoader>.Instance);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => loader.LoadAsync(options, CancellationToken.None));
|
|
}
|
|
|
|
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<HttpRequestMessage, HttpResponseMessage> _responseFactory;
|
|
|
|
private TestHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
|
|
{
|
|
_responseFactory = responseFactory;
|
|
}
|
|
|
|
public int InvocationCount { get; private set; }
|
|
|
|
public static TestHttpMessageHandler RespondWith(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
|
|
=> new(responseFactory);
|
|
|
|
public void ResetInvocationCount() => InvocationCount = 0;
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
InvocationCount++;
|
|
return Task.FromResult(_responseFactory(request));
|
|
}
|
|
}
|
|
}
|