work
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-25 08:01:23 +02:00
parent d92973d6fd
commit 6bee1fdcf5
207 changed files with 12816 additions and 2295 deletions

View File

@@ -0,0 +1,41 @@
using System.IO.Compression;
using StellaOps.PacksRegistry.Core.Services;
using StellaOps.PacksRegistry.Infrastructure.InMemory;
using StellaOps.PacksRegistry.Infrastructure.Verification;
namespace StellaOps.PacksRegistry.Tests;
public sealed class ExportServiceTests
{
[Fact]
public async Task Offline_seed_includes_metadata_and_content_when_requested()
{
var ct = TestContext.Current.CancellationToken;
var packRepo = new InMemoryPackRepository();
var parityRepo = new InMemoryParityRepository();
var lifecycleRepo = new InMemoryLifecycleRepository();
var auditRepo = new InMemoryAuditRepository();
var verifier = new SimpleSignatureVerifier();
var packService = new PackService(packRepo, verifier, auditRepo, null, TimeProvider.System);
var parityService = new ParityService(parityRepo, packRepo, auditRepo, TimeProvider.System);
var lifecycleService = new LifecycleService(lifecycleRepo, packRepo, auditRepo, TimeProvider.System);
var exportService = new ExportService(packRepo, parityRepo, lifecycleRepo, auditRepo, TimeProvider.System);
var content = System.Text.Encoding.UTF8.GetBytes("export-pack");
var provenance = System.Text.Encoding.UTF8.GetBytes("{\"p\":1}");
var record = await packService.UploadAsync("demo", "1.2.3", "tenant-1", content, null, null, provenance, null, ct);
await parityService.SetStatusAsync(record.PackId, record.TenantId, "ready", "seed", ct);
await lifecycleService.SetStateAsync(record.PackId, record.TenantId, "promoted", "seed", ct);
var archiveStream = await exportService.ExportOfflineSeedAsync(record.TenantId, includeContent: true, includeProvenance: true, cancellationToken: ct);
using var archive = new ZipArchive(archiveStream, ZipArchiveMode.Read);
Assert.NotNull(archive.GetEntry("packs.ndjson"));
Assert.NotNull(archive.GetEntry("parity.ndjson"));
Assert.NotNull(archive.GetEntry("lifecycle.ndjson"));
Assert.NotNull(archive.GetEntry("audit.ndjson"));
Assert.NotNull(archive.GetEntry($"content/{record.PackId}.bin"));
Assert.NotNull(archive.GetEntry($"provenance/{record.PackId}.json"));
}
}

View File

@@ -0,0 +1,45 @@
using StellaOps.PacksRegistry.Core.Models;
using StellaOps.PacksRegistry.Infrastructure.FileSystem;
namespace StellaOps.PacksRegistry.Tests;
public sealed class FilePackRepositoryTests
{
[Fact]
public async Task Upsert_and_List_round_trip()
{
var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempPath);
try
{
var ct = TestContext.Current.CancellationToken;
var repo = new FilePackRepository(tempPath);
var record = new PackRecord(
PackId: "demo@1.0.0",
Name: "demo",
Version: "1.0.0",
TenantId: "t1",
Digest: "sha256:abc",
Signature: null,
ProvenanceUri: null,
ProvenanceDigest: null,
CreatedAtUtc: DateTimeOffset.Parse("2025-11-24T00:00:00Z"),
Metadata: new Dictionary<string, string> { ["lang"] = "csharp" });
await repo.UpsertAsync(record, new byte[] { 1, 2, 3 }, null, ct);
var listed = await repo.ListAsync("t1", ct);
Assert.Single(listed);
Assert.Equal(record.PackId, listed[0].PackId);
var fetched = await repo.GetAsync("demo@1.0.0", ct);
Assert.NotNull(fetched);
Assert.Equal(record.Digest, fetched!.Digest);
}
finally
{
Directory.Delete(tempPath, recursive: true);
}
}
}

View File

@@ -0,0 +1,95 @@
using StellaOps.PacksRegistry.Core.Services;
using StellaOps.PacksRegistry.Infrastructure.InMemory;
using StellaOps.PacksRegistry.Infrastructure.Verification;
namespace StellaOps.PacksRegistry.Tests;
public sealed class PackServiceTests
{
private static byte[] SampleContent => System.Text.Encoding.UTF8.GetBytes("sample-pack-content");
[Fact]
public async Task Upload_persists_pack_with_digest()
{
var ct = TestContext.Current.CancellationToken;
var repo = new InMemoryPackRepository();
var verifier = new SimpleSignatureVerifier();
var service = new PackService(repo, verifier, new InMemoryAuditRepository(), null, TimeProvider.System);
var record = await service.UploadAsync(
name: "demo-pack",
version: "1.0.0",
tenantId: "tenant-1",
content: SampleContent,
signature: null,
provenanceUri: "https://example/manifest.json",
provenanceContent: null,
metadata: new Dictionary<string, string> { ["lang"] = "csharp" },
cancellationToken: ct);
Assert.Equal("demo-pack@1.0.0", record.PackId);
Assert.NotNull(record.Digest);
var listed = await service.ListAsync("tenant-1", ct);
Assert.Single(listed);
Assert.Equal(record.PackId, listed[0].PackId);
}
[Fact]
public async Task Upload_rejects_when_digest_mismatch()
{
var ct = TestContext.Current.CancellationToken;
var repo = new InMemoryPackRepository();
var verifier = new AlwaysFailSignatureVerifier();
var service = new PackService(repo, verifier, new InMemoryAuditRepository(), null, TimeProvider.System);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.UploadAsync(
name: "demo-pack",
version: "1.0.0",
tenantId: "tenant-1",
content: SampleContent,
signature: "bogus",
provenanceUri: null,
provenanceContent: null,
metadata: null,
cancellationToken: ct));
}
[Fact]
public async Task Rotate_signature_updates_record_and_audits()
{
var ct = TestContext.Current.CancellationToken;
var repo = new InMemoryPackRepository();
var audit = new InMemoryAuditRepository();
var verifier = new SimpleSignatureVerifier();
var service = new PackService(repo, verifier, audit, null, TimeProvider.System);
var record = await service.UploadAsync(
name: "demo-pack",
version: "1.0.0",
tenantId: "tenant-1",
content: SampleContent,
signature: null,
provenanceUri: null,
provenanceContent: null,
metadata: null,
cancellationToken: ct);
var digest = record.Digest;
var newSignature = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(digest));
var rotated = await service.RotateSignatureAsync(record.PackId, record.TenantId, newSignature, cancellationToken: ct);
Assert.Equal(newSignature, rotated.Signature);
var auditEvents = await audit.ListAsync(record.TenantId, ct);
Assert.Contains(auditEvents, a => a.Event == "signature.rotated" && a.PackId == record.PackId);
}
private sealed class AlwaysFailSignatureVerifier : StellaOps.PacksRegistry.Core.Contracts.IPackSignatureVerifier
{
public Task<bool> VerifyAsync(byte[] content, string digest, string? signature, CancellationToken cancellationToken = default)
=> Task.FromResult(false);
}
}

View File

@@ -0,0 +1,127 @@
using System.Net;
using System.Net.Http.Json;
using System.IO.Compression;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Services;
using StellaOps.PacksRegistry.Infrastructure.InMemory;
using StellaOps.PacksRegistry.WebService.Contracts;
namespace StellaOps.PacksRegistry.Tests;
public sealed class PacksApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public PacksApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IPackRepository>();
services.RemoveAll<IParityRepository>();
services.RemoveAll<ILifecycleRepository>();
services.RemoveAll<IAuditRepository>();
services.AddSingleton<IPackRepository, InMemoryPackRepository>();
services.AddSingleton<IParityRepository, InMemoryParityRepository>();
services.AddSingleton<ILifecycleRepository, InMemoryLifecycleRepository>();
services.AddSingleton<IAuditRepository, InMemoryAuditRepository>();
services.AddSingleton(TimeProvider.System);
services.RemoveAll<PackService>();
services.RemoveAll<ParityService>();
services.RemoveAll<LifecycleService>();
services.RemoveAll<ExportService>();
services.AddSingleton<PackService>();
services.AddSingleton<ParityService>();
services.AddSingleton<LifecycleService>();
services.AddSingleton<ExportService>();
});
});
}
[Fact]
public async Task Upload_and_download_round_trip()
{
var ct = TestContext.Current.CancellationToken;
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "t1");
var auth = _factory.Services.GetRequiredService<StellaOps.PacksRegistry.WebService.Options.AuthOptions>();
if (!string.IsNullOrWhiteSpace(auth.ApiKey))
{
client.DefaultRequestHeaders.Add("X-API-Key", auth.ApiKey);
}
var payload = new PackUploadRequest
{
Name = "demo",
Version = "1.0.0",
TenantId = "t1",
Content = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("hello")),
ProvenanceUri = "https://example/provenance.json",
ProvenanceContent = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"provenance\":true}"))
};
var message = new HttpRequestMessage(HttpMethod.Post, "/api/v1/packs")
{
Content = JsonContent.Create(payload)
};
var response = await client.SendAsync(message, ct);
if (response.StatusCode != HttpStatusCode.Created)
{
var body = await response.Content.ReadAsStringAsync(ct);
throw new InvalidOperationException($"Upload failed with {response.StatusCode}: {body}");
}
var created = await response.Content.ReadFromJsonAsync<PackResponse>(cancellationToken: ct);
Assert.NotNull(created);
Assert.Equal("demo", created!.Name);
Assert.Equal("1.0.0", created.Version);
var get = await client.GetAsync($"/api/v1/packs/{created.PackId}", ct);
Assert.Equal(HttpStatusCode.OK, get.StatusCode);
var content = await client.GetAsync($"/api/v1/packs/{created.PackId}/content", ct);
Assert.Equal(HttpStatusCode.OK, content.StatusCode);
var bytes = await content.Content.ReadAsByteArrayAsync(ct);
Assert.Equal("hello", System.Text.Encoding.UTF8.GetString(bytes));
Assert.True(content.Headers.Contains("X-Content-Digest"));
var prov = await client.GetAsync($"/api/v1/packs/{created.PackId}/provenance", ct);
Assert.Equal(HttpStatusCode.OK, prov.StatusCode);
var provBytes = await prov.Content.ReadAsByteArrayAsync(ct);
Assert.Contains("provenance", System.Text.Encoding.UTF8.GetString(provBytes));
Assert.True(prov.Headers.Contains("X-Provenance-Digest"));
var manifest = await client.GetFromJsonAsync<PackManifestResponse>($"/api/v1/packs/{created.PackId}/manifest", ct);
Assert.NotNull(manifest);
Assert.Equal(created.PackId, manifest!.PackId);
Assert.True(manifest.ContentLength > 0);
Assert.True(manifest.ProvenanceLength > 0);
// parity status
var parityResponse = await client.PostAsJsonAsync($"/api/v1/packs/{created.PackId}/parity", new ParityRequest { Status = "ready", Notes = "tests" }, ct);
Assert.Equal(HttpStatusCode.OK, parityResponse.StatusCode);
var parity = await client.GetFromJsonAsync<ParityResponse>($"/api/v1/packs/{created.PackId}/parity", ct);
Assert.NotNull(parity);
Assert.Equal("ready", parity!.Status);
var newSignature = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(created.Digest));
var rotationResponse = await client.PostAsJsonAsync($"/api/v1/packs/{created.PackId}/signature", new RotateSignatureRequest { Signature = newSignature }, ct);
Assert.Equal(HttpStatusCode.OK, rotationResponse.StatusCode);
var rotated = await rotationResponse.Content.ReadFromJsonAsync<PackResponse>(cancellationToken: ct);
Assert.Equal(newSignature, rotated!.Signature);
var offlineSeed = await client.PostAsJsonAsync("/api/v1/export/offline-seed", new OfflineSeedRequest { TenantId = "t1", IncludeContent = true, IncludeProvenance = true }, ct);
Assert.Equal(HttpStatusCode.OK, offlineSeed.StatusCode);
var bytesZip = await offlineSeed.Content.ReadAsByteArrayAsync(ct);
using var archive = new ZipArchive(new MemoryStream(bytesZip));
Assert.NotNull(archive.GetEntry("packs.ndjson"));
Assert.NotNull(archive.GetEntry($"content/{created.PackId}.bin"));
}
}

View File

@@ -0,0 +1,48 @@
using System.Security.Cryptography;
using System.Text;
using StellaOps.PacksRegistry.Infrastructure.Verification;
namespace StellaOps.PacksRegistry.Tests;
public sealed class RsaSignatureVerifierTests
{
[Fact]
public async Task Verify_succeeds_when_signature_matches_digest()
{
var ct = TestContext.Current.CancellationToken;
using var rsa = RSA.Create(2048);
var publicPem = ExportPublicPem(rsa);
const string digest = "sha256:deadbeef";
var sig = Convert.ToBase64String(rsa.SignData(Encoding.UTF8.GetBytes(digest), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
var verifier = new RsaSignatureVerifier(publicPem);
var ok = await verifier.VerifyAsync(Array.Empty<byte>(), digest, sig, ct);
Assert.True(ok);
}
[Fact]
public async Task Verify_fails_on_invalid_signature()
{
var ct = TestContext.Current.CancellationToken;
using var rsa = RSA.Create(2048);
var publicPem = ExportPublicPem(rsa);
const string digest = "sha256:deadbeef";
var sig = Convert.ToBase64String(Encoding.UTF8.GetBytes("bogus"));
var verifier = new RsaSignatureVerifier(publicPem);
var ok = await verifier.VerifyAsync(Array.Empty<byte>(), digest, sig, ct);
Assert.False(ok);
}
private static string ExportPublicPem(RSA rsa)
{
var builder = new StringBuilder();
builder.AppendLine("-----BEGIN PUBLIC KEY-----");
builder.AppendLine(Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo(), Base64FormattingOptions.InsertLineBreaks));
builder.AppendLine("-----END PUBLIC KEY-----");
return builder.ToString();
}
}

View File

@@ -1,135 +1,34 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit.v3" Version="3.0.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3"/>
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.PacksRegistry.Core\StellaOps.PacksRegistry.Core.csproj"/>
<ProjectReference Include="..\StellaOps.PacksRegistry.Infrastructure\StellaOps.PacksRegistry.Infrastructure.csproj"/>
</ItemGroup>
</Project>
<?xml version="1.0"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="xunit.v3" Version="3.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.PacksRegistry.Core\StellaOps.PacksRegistry.Core.csproj" />
<ProjectReference Include="..\StellaOps.PacksRegistry.Infrastructure\StellaOps.PacksRegistry.Infrastructure.csproj" />
<ProjectReference Include="..\StellaOps.PacksRegistry.WebService\StellaOps.PacksRegistry.WebService.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,10 +0,0 @@
namespace StellaOps.PacksRegistry.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}