test fixes and new product advisories work
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Infrastructure.Registry.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Infrastructure.Registry.Testing.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// OCI 1.1 Referrers API tests for registry compatibility.
|
||||
/// Tests the native referrers API and fallback tag discovery.
|
||||
/// </summary>
|
||||
[Collection("RegistryCompatibility")]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Category", "RegistryCompatibility")]
|
||||
public class ReferrersApiTests
|
||||
{
|
||||
private readonly RegistryCompatibilityFixture _fixture;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public ReferrersApiTests(RegistryCompatibilityFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RegistryTestData.ReferrersApiRegistryTypes), MemberType = typeof(RegistryTestData))]
|
||||
public async Task Referrers_Endpoint_Returns_OCI_Index(string registryType)
|
||||
{
|
||||
var registry = GetRegistry(registryType);
|
||||
if (registry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var repo = $"test/{registryType}/referrers";
|
||||
var digest = await registry.PushTestImageAsync(repo, "base");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get,
|
||||
$"{registry.RegistryUrl}/v2/{repo}/referrers/{digest}");
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.oci.image.index.v1+json"));
|
||||
ApplyAuth(request, registry);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
// Registries with referrers API should return 200 with OCI index or 404 with empty index
|
||||
response.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NotFound);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("schemaVersion",
|
||||
$"Registry {registryType} should return OCI index structure");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RegistryTestData.FallbackRegistryTypes), MemberType = typeof(RegistryTestData))]
|
||||
public async Task Referrers_Endpoint_Not_Supported_Returns_404_Or_405(string registryType)
|
||||
{
|
||||
var registry = GetRegistry(registryType);
|
||||
if (registry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var repo = $"test/{registryType}/fallback";
|
||||
var digest = await registry.PushTestImageAsync(repo, "base");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get,
|
||||
$"{registry.RegistryUrl}/v2/{repo}/referrers/{digest}");
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.oci.image.index.v1+json"));
|
||||
ApplyAuth(request, registry);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
// Registries without referrers API should return 404 or 405
|
||||
response.StatusCode.Should().BeOneOf(
|
||||
HttpStatusCode.NotFound,
|
||||
HttpStatusCode.MethodNotAllowed,
|
||||
HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RegistryTestData.ReferrersApiRegistryTypes), MemberType = typeof(RegistryTestData))]
|
||||
public async Task Can_Push_Referrer_With_Subject(string registryType)
|
||||
{
|
||||
var registry = GetRegistry(registryType);
|
||||
if (registry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var repo = $"test/{registryType}/with-referrer";
|
||||
var baseDigest = await registry.PushTestImageAsync(repo, "base");
|
||||
|
||||
// Push a referrer artifact with subject field
|
||||
var referrerDigest = await PushReferrerAsync(registry, repo, baseDigest,
|
||||
"application/vnd.stellaops.test+json", "test content");
|
||||
|
||||
// Query referrers
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get,
|
||||
$"{registry.RegistryUrl}/v2/{repo}/referrers/{baseDigest}");
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.oci.image.index.v1+json"));
|
||||
ApplyAuth(request, registry);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain(referrerDigest.Replace("sha256:", ""),
|
||||
$"Registry {registryType} referrers list should contain pushed referrer");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RegistryTestData.AllRegistryTypes), MemberType = typeof(RegistryTestData))]
|
||||
public void Reports_Correct_Referrers_Api_Support(string registryType)
|
||||
{
|
||||
var registry = GetRegistry(registryType);
|
||||
if (registry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var expectedSupport = registryType switch
|
||||
{
|
||||
"generic-oci" => false,
|
||||
"zot" => true,
|
||||
"distribution" => true,
|
||||
"harbor" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
registry.SupportsReferrersApi.Should().Be(expectedSupport,
|
||||
$"Registry {registryType} should report correct referrers API support");
|
||||
}
|
||||
|
||||
private IRegistryTestContainer? GetRegistry(string registryType)
|
||||
{
|
||||
return _fixture.Registries.FirstOrDefault(r => r.RegistryType == registryType);
|
||||
}
|
||||
|
||||
private async Task<string> PushReferrerAsync(
|
||||
IRegistryTestContainer registry,
|
||||
string repo,
|
||||
string subjectDigest,
|
||||
string artifactType,
|
||||
string content)
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
var contentDigest = ComputeDigest(contentBytes);
|
||||
|
||||
// Push blob
|
||||
await PushBlobAsync(registry, repo, contentBytes, contentDigest);
|
||||
|
||||
// Create manifest with subject
|
||||
var manifest = $$"""
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "{{artifactType}}",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.empty.v1+json",
|
||||
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
"size": 2
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "{{artifactType}}",
|
||||
"digest": "{{contentDigest}}",
|
||||
"size": {{contentBytes.Length}}
|
||||
}
|
||||
],
|
||||
"subject": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "{{subjectDigest}}",
|
||||
"size": 0
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var manifestBytes = Encoding.UTF8.GetBytes(manifest);
|
||||
var manifestDigest = ComputeDigest(manifestBytes);
|
||||
|
||||
// Push empty config blob first
|
||||
var emptyConfig = "{}"u8.ToArray();
|
||||
var emptyConfigDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
|
||||
await PushBlobAsync(registry, repo, emptyConfig, emptyConfigDigest);
|
||||
|
||||
// Push manifest
|
||||
using var request = new HttpRequestMessage(HttpMethod.Put,
|
||||
$"{registry.RegistryUrl}/v2/{repo}/manifests/{manifestDigest}");
|
||||
request.Content = new ByteArrayContent(manifestBytes);
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.oci.image.manifest.v1+json");
|
||||
ApplyAuth(request, registry);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new InvalidOperationException($"Failed to push referrer manifest: {response.StatusCode} - {error}");
|
||||
}
|
||||
|
||||
return manifestDigest;
|
||||
}
|
||||
|
||||
private async Task PushBlobAsync(IRegistryTestContainer registry, string repo, byte[] content, string digest)
|
||||
{
|
||||
// Initiate upload
|
||||
using var initiateRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
$"{registry.RegistryUrl}/v2/{repo}/blobs/uploads/");
|
||||
ApplyAuth(initiateRequest, registry);
|
||||
|
||||
var initiateResponse = await _httpClient.SendAsync(initiateRequest);
|
||||
if (initiateResponse.StatusCode != HttpStatusCode.Accepted)
|
||||
{
|
||||
return; // Blob might already exist
|
||||
}
|
||||
|
||||
var location = initiateResponse.Headers.Location?.ToString();
|
||||
if (location == null) return;
|
||||
|
||||
// Complete upload
|
||||
var uploadUrl = location.Contains('?')
|
||||
? $"{location}&digest={digest}"
|
||||
: $"{location}?digest={digest}";
|
||||
|
||||
using var uploadRequest = new HttpRequestMessage(HttpMethod.Put, uploadUrl);
|
||||
uploadRequest.Content = new ByteArrayContent(content);
|
||||
uploadRequest.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
ApplyAuth(uploadRequest, registry);
|
||||
|
||||
await _httpClient.SendAsync(uploadRequest);
|
||||
}
|
||||
|
||||
private static string ComputeDigest(byte[] content)
|
||||
{
|
||||
var hash = SHA256.HashData(content);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static void ApplyAuth(HttpRequestMessage request, IRegistryTestContainer registry)
|
||||
{
|
||||
if (registry.Username != null && registry.Password != null)
|
||||
{
|
||||
var credentials = Convert.ToBase64String(
|
||||
Encoding.UTF8.GetBytes($"{registry.Username}:{registry.Password}"));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user