Rename Vexer to Excititor

This commit is contained in:
master
2025-10-18 20:00:46 +03:00
parent e2672ba968
commit dd66f58b00
263 changed files with 848 additions and 848 deletions

View File

@@ -0,0 +1,138 @@
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<RancherHubTokenProvider>.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<RancherHubTokenProvider>.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<RancherHubTokenProvider>.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<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);
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
InvocationCount++;
return Task.FromResult(_responseFactory(request));
}
}
}

View File

@@ -0,0 +1,178 @@
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.Excititor.Connectors.SUSE.RancherVEXHub.Authentication;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Configuration;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Metadata;
using System.IO.Abstractions.TestingHelpers;
using System.Threading;
using Xunit;
namespace StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Tests.Metadata;
public sealed class RancherHubMetadataLoaderTests
{
private const string SampleDiscovery = """
{
"hubId": "excititor: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));
}
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Excititor.Connectors.SUSE.RancherVEXHub\StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.csproj" />
<ProjectReference Include="..\StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.28" />
</ItemGroup>
</Project>