up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -15,37 +15,37 @@ using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using StellaOps.Attestor.Infrastructure.Storage;
|
||||
using StellaOps.Attestor.Infrastructure.Submission;
|
||||
using StellaOps.Attestor.Tests.Support;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorSubmissionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ReturnsDeterministicUuid_OnDuplicateBundle()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions
|
||||
{
|
||||
Url = string.Empty
|
||||
},
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.stellaops.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorSubmissionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ReturnsDeterministicUuid_OnDuplicateBundle()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions
|
||||
{
|
||||
Url = string.Empty
|
||||
},
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.stellaops.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
||||
var auditSink = new InMemoryAttestorAuditSink();
|
||||
@@ -66,21 +66,21 @@ public sealed class AttestorSubmissionServiceTests
|
||||
logger,
|
||||
TimeProvider.System,
|
||||
metrics);
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default",
|
||||
ClientCertificate = null,
|
||||
MtlsThumbprint = "00"
|
||||
};
|
||||
|
||||
var first = await service.SubmitAsync(request, context);
|
||||
var second = await service.SubmitAsync(request, context);
|
||||
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default",
|
||||
ClientCertificate = null,
|
||||
MtlsThumbprint = "00"
|
||||
};
|
||||
|
||||
var first = await service.SubmitAsync(request, context);
|
||||
var second = await service.SubmitAsync(request, context);
|
||||
|
||||
Assert.NotNull(first.Uuid);
|
||||
Assert.Equal(first.Uuid, second.Uuid);
|
||||
|
||||
@@ -89,43 +89,43 @@ public sealed class AttestorSubmissionServiceTests
|
||||
Assert.Equal(first.Uuid, stored!.RekorUuid);
|
||||
Assert.Single(verificationCache.InvalidatedSubjects);
|
||||
Assert.Equal(request.Meta.Artifact.Sha256, verificationCache.InvalidatedSubjects[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validator_ThrowsWhenModeNotAllowed()
|
||||
{
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer, new[] { "kms" });
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Bundle.Mode = "keyless";
|
||||
|
||||
await Assert.ThrowsAsync<AttestorValidationException>(() => validator.ValidateAsync(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_Throws_WhenMirrorDisabledButRequested()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Validator_ThrowsWhenModeNotAllowed()
|
||||
{
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer, new[] { "kms" });
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Bundle.Mode = "keyless";
|
||||
|
||||
await Assert.ThrowsAsync<AttestorValidationException>(() => validator.ValidateAsync(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_Throws_WhenMirrorDisabledButRequested()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
||||
var auditSink = new InMemoryAttestorAuditSink();
|
||||
var witnessClient = new TestTransparencyWitnessClient();
|
||||
@@ -145,53 +145,53 @@ public sealed class AttestorSubmissionServiceTests
|
||||
logger,
|
||||
TimeProvider.System,
|
||||
metrics);
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Meta.LogPreference = "mirror";
|
||||
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default"
|
||||
};
|
||||
|
||||
var ex = await Assert.ThrowsAsync<AttestorValidationException>(() => service.SubmitAsync(request, context));
|
||||
Assert.Equal("mirror_disabled", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ReturnsMirrorMetadata_WhenPreferenceBoth()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
},
|
||||
Mirror = new AttestorOptions.RekorMirrorOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Url = "https://rekor.mirror.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Meta.LogPreference = "mirror";
|
||||
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default"
|
||||
};
|
||||
|
||||
var ex = await Assert.ThrowsAsync<AttestorValidationException>(() => service.SubmitAsync(request, context));
|
||||
Assert.Equal("mirror_disabled", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ReturnsMirrorMetadata_WhenPreferenceBoth()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
},
|
||||
Mirror = new AttestorOptions.RekorMirrorOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Url = "https://rekor.mirror.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
||||
var auditSink = new InMemoryAttestorAuditSink();
|
||||
var witnessClient = new TestTransparencyWitnessClient();
|
||||
@@ -211,56 +211,56 @@ public sealed class AttestorSubmissionServiceTests
|
||||
logger,
|
||||
TimeProvider.System,
|
||||
metrics);
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Meta.LogPreference = "both";
|
||||
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default"
|
||||
};
|
||||
|
||||
var result = await service.SubmitAsync(request, context);
|
||||
|
||||
Assert.NotNull(result.Mirror);
|
||||
Assert.False(string.IsNullOrEmpty(result.Mirror!.Uuid));
|
||||
Assert.Equal("included", result.Mirror.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_UsesMirrorAsCanonical_WhenPreferenceMirror()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
},
|
||||
Mirror = new AttestorOptions.RekorMirrorOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Url = "https://rekor.mirror.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Meta.LogPreference = "both";
|
||||
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default"
|
||||
};
|
||||
|
||||
var result = await service.SubmitAsync(request, context);
|
||||
|
||||
Assert.NotNull(result.Mirror);
|
||||
Assert.False(string.IsNullOrEmpty(result.Mirror!.Uuid));
|
||||
Assert.Equal("included", result.Mirror.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_UsesMirrorAsCanonical_WhenPreferenceMirror()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
{
|
||||
Redis = new AttestorOptions.RedisOptions { Url = string.Empty },
|
||||
Rekor = new AttestorOptions.RekorOptions
|
||||
{
|
||||
Primary = new AttestorOptions.RekorBackendOptions
|
||||
{
|
||||
Url = "https://rekor.primary.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
},
|
||||
Mirror = new AttestorOptions.RekorMirrorOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Url = "https://rekor.mirror.test",
|
||||
ProofTimeoutMs = 1000,
|
||||
PollIntervalMs = 50,
|
||||
MaxAttempts = 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
var validator = new AttestorSubmissionValidator(canonicalizer);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var dedupeStore = new InMemoryAttestorDedupeStore();
|
||||
var rekorClient = new StubRekorClient(new NullLogger<StubRekorClient>());
|
||||
var archiveStore = new NullAttestorArchiveStore(new NullLogger<NullAttestorArchiveStore>());
|
||||
var auditSink = new InMemoryAttestorAuditSink();
|
||||
var witnessClient = new TestTransparencyWitnessClient();
|
||||
@@ -280,24 +280,24 @@ public sealed class AttestorSubmissionServiceTests
|
||||
logger,
|
||||
TimeProvider.System,
|
||||
metrics);
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Meta.LogPreference = "mirror";
|
||||
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default"
|
||||
};
|
||||
|
||||
var result = await service.SubmitAsync(request, context);
|
||||
|
||||
Assert.NotNull(result.Uuid);
|
||||
var stored = await repository.GetByBundleShaAsync(request.Meta.BundleSha256);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("mirror", stored!.Log.Backend);
|
||||
|
||||
var request = CreateValidRequest(canonicalizer);
|
||||
request.Meta.LogPreference = "mirror";
|
||||
|
||||
var context = new SubmissionContext
|
||||
{
|
||||
CallerSubject = "urn:stellaops:signer",
|
||||
CallerAudience = "attestor",
|
||||
CallerClientId = "signer-service",
|
||||
CallerTenant = "default"
|
||||
};
|
||||
|
||||
var result = await service.SubmitAsync(request, context);
|
||||
|
||||
Assert.NotNull(result.Uuid);
|
||||
var stored = await repository.GetByBundleShaAsync(request.Meta.BundleSha256);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("mirror", stored!.Log.Backend);
|
||||
Assert.Null(result.Mirror);
|
||||
}
|
||||
|
||||
@@ -323,36 +323,36 @@ public sealed class AttestorSubmissionServiceTests
|
||||
var request = new AttestorSubmissionRequest
|
||||
{
|
||||
Bundle = new AttestorSubmissionRequest.SubmissionBundle
|
||||
{
|
||||
Mode = "keyless",
|
||||
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
||||
{
|
||||
PayloadType = "application/vnd.in-toto+json",
|
||||
PayloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures =
|
||||
{
|
||||
new AttestorSubmissionRequest.DsseSignature
|
||||
{
|
||||
KeyId = "test",
|
||||
Signature = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Meta = new AttestorSubmissionRequest.SubmissionMeta
|
||||
{
|
||||
Artifact = new AttestorSubmissionRequest.ArtifactInfo
|
||||
{
|
||||
Sha256 = new string('a', 64),
|
||||
Kind = "sbom"
|
||||
},
|
||||
LogPreference = "primary",
|
||||
Archive = false
|
||||
}
|
||||
};
|
||||
|
||||
var canonical = canonicalizer.CanonicalizeAsync(request).GetAwaiter().GetResult();
|
||||
request.Meta.BundleSha256 = Convert.ToHexString(SHA256.HashData(canonical)).ToLowerInvariant();
|
||||
return request;
|
||||
}
|
||||
}
|
||||
{
|
||||
Mode = "keyless",
|
||||
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
||||
{
|
||||
PayloadType = "application/vnd.in-toto+json",
|
||||
PayloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures =
|
||||
{
|
||||
new AttestorSubmissionRequest.DsseSignature
|
||||
{
|
||||
KeyId = "test",
|
||||
Signature = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Meta = new AttestorSubmissionRequest.SubmissionMeta
|
||||
{
|
||||
Artifact = new AttestorSubmissionRequest.ArtifactInfo
|
||||
{
|
||||
Sha256 = new string('a', 64),
|
||||
Kind = "sbom"
|
||||
},
|
||||
LogPreference = "primary",
|
||||
Archive = false
|
||||
}
|
||||
};
|
||||
|
||||
var canonical = canonicalizer.CanonicalizeAsync(request).GetAwaiter().GetResult();
|
||||
request.Meta.BundleSha256 = Convert.ToHexString(SHA256.HashData(canonical)).ToLowerInvariant();
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class HttpRekorClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ParsesResponse()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
uuid = "123",
|
||||
index = 42,
|
||||
logURL = "https://rekor.example/api/v2/log/entries/123",
|
||||
status = "included",
|
||||
proof = new
|
||||
{
|
||||
checkpoint = new { origin = "rekor", size = 10, rootHash = "abc", timestamp = "2025-10-19T00:00:00Z" },
|
||||
inclusion = new { leafHash = "leaf", path = new[] { "p1", "p2" } }
|
||||
}
|
||||
};
|
||||
|
||||
var client = CreateClient(HttpStatusCode.Created, payload);
|
||||
var rekorClient = new HttpRekorClient(client, NullLogger<HttpRekorClient>.Instance);
|
||||
|
||||
var request = new AttestorSubmissionRequest
|
||||
{
|
||||
Bundle = new AttestorSubmissionRequest.SubmissionBundle
|
||||
{
|
||||
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
||||
{
|
||||
PayloadType = "application/json",
|
||||
PayloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures = { new AttestorSubmissionRequest.DsseSignature { Signature = "sig" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example/"),
|
||||
ProofTimeout = TimeSpan.FromSeconds(1),
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
MaxAttempts = 1
|
||||
};
|
||||
|
||||
var response = await rekorClient.SubmitAsync(request, backend);
|
||||
|
||||
Assert.Equal("123", response.Uuid);
|
||||
Assert.Equal(42, response.Index);
|
||||
Assert.Equal("included", response.Status);
|
||||
Assert.NotNull(response.Proof);
|
||||
Assert.Equal("leaf", response.Proof!.Inclusion!.LeafHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ThrowsOnConflict()
|
||||
{
|
||||
var client = CreateClient(HttpStatusCode.Conflict, new { error = "duplicate" });
|
||||
var rekorClient = new HttpRekorClient(client, NullLogger<HttpRekorClient>.Instance);
|
||||
|
||||
var request = new AttestorSubmissionRequest
|
||||
{
|
||||
Bundle = new AttestorSubmissionRequest.SubmissionBundle
|
||||
{
|
||||
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
||||
{
|
||||
PayloadType = "application/json",
|
||||
PayloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures = { new AttestorSubmissionRequest.DsseSignature { Signature = "sig" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example/"),
|
||||
ProofTimeout = TimeSpan.FromSeconds(1),
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
MaxAttempts = 1
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => rekorClient.SubmitAsync(request, backend));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetProofAsync_ReturnsNullOnNotFound()
|
||||
{
|
||||
var client = CreateClient(HttpStatusCode.NotFound, new { });
|
||||
var rekorClient = new HttpRekorClient(client, NullLogger<HttpRekorClient>.Instance);
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example/"),
|
||||
ProofTimeout = TimeSpan.FromSeconds(1),
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
MaxAttempts = 1
|
||||
};
|
||||
|
||||
var proof = await rekorClient.GetProofAsync("abc", backend);
|
||||
Assert.Null(proof);
|
||||
}
|
||||
|
||||
private static HttpClient CreateClient(HttpStatusCode statusCode, object payload)
|
||||
{
|
||||
var handler = new StubHandler(statusCode, payload);
|
||||
return new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://rekor.example/")
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpStatusCode _statusCode;
|
||||
private readonly object _payload;
|
||||
|
||||
public StubHandler(HttpStatusCode statusCode, object payload)
|
||||
{
|
||||
_statusCode = statusCode;
|
||||
_payload = payload;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_payload);
|
||||
var response = new HttpResponseMessage(_statusCode)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class HttpRekorClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ParsesResponse()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
uuid = "123",
|
||||
index = 42,
|
||||
logURL = "https://rekor.example/api/v2/log/entries/123",
|
||||
status = "included",
|
||||
proof = new
|
||||
{
|
||||
checkpoint = new { origin = "rekor", size = 10, rootHash = "abc", timestamp = "2025-10-19T00:00:00Z" },
|
||||
inclusion = new { leafHash = "leaf", path = new[] { "p1", "p2" } }
|
||||
}
|
||||
};
|
||||
|
||||
var client = CreateClient(HttpStatusCode.Created, payload);
|
||||
var rekorClient = new HttpRekorClient(client, NullLogger<HttpRekorClient>.Instance);
|
||||
|
||||
var request = new AttestorSubmissionRequest
|
||||
{
|
||||
Bundle = new AttestorSubmissionRequest.SubmissionBundle
|
||||
{
|
||||
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
||||
{
|
||||
PayloadType = "application/json",
|
||||
PayloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures = { new AttestorSubmissionRequest.DsseSignature { Signature = "sig" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example/"),
|
||||
ProofTimeout = TimeSpan.FromSeconds(1),
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
MaxAttempts = 1
|
||||
};
|
||||
|
||||
var response = await rekorClient.SubmitAsync(request, backend);
|
||||
|
||||
Assert.Equal("123", response.Uuid);
|
||||
Assert.Equal(42, response.Index);
|
||||
Assert.Equal("included", response.Status);
|
||||
Assert.NotNull(response.Proof);
|
||||
Assert.Equal("leaf", response.Proof!.Inclusion!.LeafHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ThrowsOnConflict()
|
||||
{
|
||||
var client = CreateClient(HttpStatusCode.Conflict, new { error = "duplicate" });
|
||||
var rekorClient = new HttpRekorClient(client, NullLogger<HttpRekorClient>.Instance);
|
||||
|
||||
var request = new AttestorSubmissionRequest
|
||||
{
|
||||
Bundle = new AttestorSubmissionRequest.SubmissionBundle
|
||||
{
|
||||
Dsse = new AttestorSubmissionRequest.DsseEnvelope
|
||||
{
|
||||
PayloadType = "application/json",
|
||||
PayloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")),
|
||||
Signatures = { new AttestorSubmissionRequest.DsseSignature { Signature = "sig" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example/"),
|
||||
ProofTimeout = TimeSpan.FromSeconds(1),
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
MaxAttempts = 1
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => rekorClient.SubmitAsync(request, backend));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetProofAsync_ReturnsNullOnNotFound()
|
||||
{
|
||||
var client = CreateClient(HttpStatusCode.NotFound, new { });
|
||||
var rekorClient = new HttpRekorClient(client, NullLogger<HttpRekorClient>.Instance);
|
||||
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary",
|
||||
Url = new Uri("https://rekor.example/"),
|
||||
ProofTimeout = TimeSpan.FromSeconds(1),
|
||||
PollInterval = TimeSpan.FromMilliseconds(100),
|
||||
MaxAttempts = 1
|
||||
};
|
||||
|
||||
var proof = await rekorClient.GetProofAsync("abc", backend);
|
||||
Assert.Null(proof);
|
||||
}
|
||||
|
||||
private static HttpClient CreateClient(HttpStatusCode statusCode, object payload)
|
||||
{
|
||||
var handler = new StubHandler(statusCode, payload);
|
||||
return new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri("https://rekor.example/")
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpStatusCode _statusCode;
|
||||
private readonly object _payload;
|
||||
|
||||
public StubHandler(HttpStatusCode statusCode, object payload)
|
||||
{
|
||||
_statusCode = statusCode;
|
||||
_payload = payload;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_payload);
|
||||
var response = new HttpResponseMessage(_statusCode)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user