Refactor code structure and optimize performance across multiple modules

This commit is contained in:
StellaOps Bot
2025-12-26 20:03:22 +02:00
parent c786faae84
commit b4fc66feb6
3353 changed files with 88254 additions and 1590657 deletions

View File

@@ -48,17 +48,48 @@ Before working in this directory:
## Test Categories
When writing tests, use appropriate xUnit traits:
Use the standardized test categories from `StellaOps.TestKit.TestCategories`:
```csharp
[Trait("Category", "Unit")] // Fast, isolated unit tests
[Trait("Category", "Integration")] // Tests requiring infrastructure
[Trait("Category", "E2E")] // Full end-to-end workflows
[Trait("Category", "AirGap")] // Must work without network
[Trait("Category", "Interop")] // Third-party tool compatibility
[Trait("Category", "Performance")] // Performance benchmarks
[Trait("Category", "Chaos")] // Failure injection tests
[Trait("Category", "Security")] // Security-focused tests
using StellaOps.TestKit;
// PR-GATING TESTS (run on every push/PR)
[Trait("Category", TestCategories.Unit)] // Fast, in-memory, no external dependencies
[Trait("Category", TestCategories.Architecture)] // Module dependency rules, naming conventions
[Trait("Category", TestCategories.Contract)] // API/WebService contract verification
[Trait("Category", TestCategories.Integration)] // Testcontainers, PostgreSQL, Valkey
[Trait("Category", TestCategories.Security)] // Cryptographic validation, vulnerability scanning
[Trait("Category", TestCategories.Golden)] // Output comparison against known-good references
// SCHEDULED/ON-DEMAND TESTS
[Trait("Category", TestCategories.Performance)] // Performance measurements, SLO enforcement
[Trait("Category", TestCategories.Benchmark)] // BenchmarkDotNet measurements
[Trait("Category", TestCategories.AirGap)] // Offline/air-gapped environment validation
[Trait("Category", TestCategories.Chaos)] // Fault injection, failure recovery
[Trait("Category", TestCategories.Determinism)] // Reproducibility, stable ordering, idempotency
[Trait("Category", TestCategories.Resilience)] // Retry policies, circuit breakers, timeouts
[Trait("Category", TestCategories.Observability)] // OpenTelemetry traces, metrics, logging
// OTHER CATEGORIES
[Trait("Category", TestCategories.Property)] // FsCheck/generative testing for invariants
[Trait("Category", TestCategories.Snapshot)] // Golden master regression testing
[Trait("Category", TestCategories.Live)] // Require external services (Rekor, feeds)
```
### CI/CD Integration
Tests are discovered dynamically by `.gitea/workflows/test-matrix.yml` which runs all `*.Tests.csproj` files with Category filtering:
- **PR-Gating:** Unit, Architecture, Contract, Integration, Security, Golden
- **Scheduled:** Performance, Benchmark (daily)
- **On-Demand:** AirGap, Chaos, Determinism, Resilience, Observability
### Validation
Run the validation script to ensure all tests have Category traits:
```bash
python devops/scripts/validate-test-traits.py # Report coverage
python devops/scripts/validate-test-traits.py --fix # Add default Unit trait
python devops/scripts/validate-test-traits.py --json # JSON output for CI
```
## Key Patterns

View File

@@ -9,11 +9,13 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Controller.Tests;
public class AirGapStartupDiagnosticsHostedServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Blocks_when_allowlist_missing_for_sealed_state()
{
var now = DateTimeOffset.UtcNow;
@@ -37,7 +39,8 @@ public class AirGapStartupDiagnosticsHostedServiceTests
Assert.Contains("egress-allowlist-missing", ex.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Passes_when_materials_present_and_anchor_fresh()
{
var now = DateTimeOffset.UtcNow;
@@ -59,7 +62,8 @@ public class AirGapStartupDiagnosticsHostedServiceTests
await service.StartAsync(CancellationToken.None); // should not throw
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Blocks_when_anchor_is_stale()
{
var now = DateTimeOffset.UtcNow;
@@ -82,7 +86,8 @@ public class AirGapStartupDiagnosticsHostedServiceTests
Assert.Contains("time-anchor-stale", ex.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Blocks_when_rotation_pending_without_dual_approval()
{
var now = DateTimeOffset.UtcNow;

View File

@@ -4,6 +4,7 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Controller.Tests;
public class AirGapStateServiceTests
@@ -17,7 +18,8 @@ public class AirGapStateServiceTests
_service = new AirGapStateService(_store, _calculator);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Seal_sets_state_and_computes_staleness()
{
var now = DateTimeOffset.UtcNow;
@@ -35,7 +37,8 @@ public class AirGapStateServiceTests
Assert.Equal(120 - status.Staleness.AgeSeconds, status.Staleness.SecondsRemaining);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Unseal_clears_sealed_flag_and_updates_timestamp()
{
var now = DateTimeOffset.UtcNow;
@@ -49,7 +52,8 @@ public class AirGapStateServiceTests
Assert.Equal(later, status.State.LastTransitionAt);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Seal_persists_drift_baseline_seconds()
{
var now = DateTimeOffset.UtcNow;
@@ -61,7 +65,8 @@ public class AirGapStateServiceTests
Assert.Equal(300, state.DriftBaselineSeconds); // 5 minutes = 300 seconds
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Seal_creates_default_content_budgets_when_not_provided()
{
var now = DateTimeOffset.UtcNow;
@@ -76,7 +81,8 @@ public class AirGapStateServiceTests
Assert.Equal(budget, state.ContentBudgets["advisories"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Seal_uses_provided_content_budgets()
{
var now = DateTimeOffset.UtcNow;
@@ -95,7 +101,8 @@ public class AirGapStateServiceTests
Assert.Equal(budget, state.ContentBudgets["policy"]); // Falls back to default
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetStatus_returns_per_content_staleness()
{
var now = DateTimeOffset.UtcNow;

View File

@@ -3,13 +3,15 @@ using StellaOps.AirGap.Controller.Stores;
using StellaOps.AirGap.Time.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Controller.Tests;
public class InMemoryAirGapStateStoreTests
{
private readonly InMemoryAirGapStateStore _store = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Upsert_and_read_state_by_tenant()
{
var state = new AirGapState
@@ -32,7 +34,8 @@ public class InMemoryAirGapStateStoreTests
Assert.Equal(10, stored.StalenessBudget.WarningSeconds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Enforces_singleton_per_tenant()
{
var first = new AirGapState { TenantId = "tenant-y", Sealed = true, PolicyHash = "h1" };
@@ -46,7 +49,8 @@ public class InMemoryAirGapStateStoreTests
Assert.False(stored.Sealed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Defaults_to_unknown_when_missing()
{
var stored = await _store.GetAsync("absent");
@@ -54,7 +58,8 @@ public class InMemoryAirGapStateStoreTests
Assert.Equal("absent", stored.TenantId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Parallel_upserts_keep_single_document()
{
var tasks = Enumerable.Range(0, 20).Select(i =>
@@ -74,7 +79,8 @@ public class InMemoryAirGapStateStoreTests
Assert.StartsWith("hash-", stored.PolicyHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Multi_tenant_updates_do_not_collide()
{
var tenants = Enumerable.Range(0, 5).Select(i => $"t-{i}").ToArray();
@@ -95,7 +101,8 @@ public class InMemoryAirGapStateStoreTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Staleness_round_trip_matches_budget()
{
var anchor = new TimeAnchor(DateTimeOffset.UtcNow.AddMinutes(-3), "roughtime", "roughtime", "fp", "digest");
@@ -116,7 +123,8 @@ public class InMemoryAirGapStateStoreTests
Assert.Equal(budget.BreachSeconds, stored.StalenessBudget.BreachSeconds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Multi_tenant_states_preserve_transition_times()
{
var tenants = new[] { "a", "b", "c" };

View File

@@ -7,6 +7,7 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Controller.Tests;
public class ReplayVerificationServiceTests
@@ -22,7 +23,8 @@ public class ReplayVerificationServiceTests
_service = new ReplayVerificationService(_stateService, new ReplayVerifier());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Passes_full_recompute_when_hashes_match()
{
var now = DateTimeOffset.Parse("2025-12-02T01:00:00Z");
@@ -46,7 +48,8 @@ public class ReplayVerificationServiceTests
Assert.Equal("full-recompute-passed", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Detects_stale_manifest()
{
var now = DateTimeOffset.UtcNow;
@@ -67,7 +70,8 @@ public class ReplayVerificationServiceTests
Assert.Equal("manifest-stale", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Policy_freeze_requires_matching_policy()
{
var now = DateTimeOffset.UtcNow;

View File

@@ -1,11 +1,13 @@
using StellaOps.AirGap.Importer.Contracts;
using StellaOps.AirGap.Importer.Planning;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class BundleImportPlannerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFailureWhenBundlePathMissing()
{
var planner = new BundleImportPlanner();
@@ -15,7 +17,8 @@ public class BundleImportPlannerTests
Assert.Equal("bundle-path-required", result.InitialState.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsFailureWhenTrustRootsMissing()
{
var planner = new BundleImportPlanner();
@@ -25,7 +28,8 @@ public class BundleImportPlannerTests
Assert.Equal("trust-roots-required", result.InitialState.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReturnsDefaultPlanWhenInputsProvided()
{
var planner = new BundleImportPlanner();

View File

@@ -2,11 +2,14 @@ using System.Security.Cryptography;
using StellaOps.AirGap.Importer.Contracts;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class DsseVerifierTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FailsWhenUntrustedKey()
{
var verifier = new DsseVerifier();
@@ -18,7 +21,8 @@ public class DsseVerifierTests
Assert.False(result.IsValid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void VerifiesRsaPssSignature()
{
using var rsa = RSA.Create(2048);

View File

@@ -6,11 +6,14 @@ using StellaOps.AirGap.Importer.Quarantine;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.AirGap.Importer.Versioning;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public sealed class ImportValidatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_WhenTufInvalid_ShouldFailAndQuarantine()
{
var quarantine = new CapturingQuarantineService();
@@ -54,7 +57,8 @@ public sealed class ImportValidatorTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_WhenAllChecksPass_ShouldSucceedAndRecordActivation()
{
var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}";

View File

@@ -1,11 +1,13 @@
using StellaOps.AirGap.Importer.Models;
using StellaOps.AirGap.Importer.Repositories;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class InMemoryBundleRepositoriesTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CatalogUpsertOverwritesPerTenant()
{
var repo = new InMemoryBundleCatalogRepository();
@@ -20,7 +22,8 @@ public class InMemoryBundleRepositoriesTests
Assert.Equal("d2", list[0].Digest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CatalogIsTenantIsolated()
{
var repo = new InMemoryBundleCatalogRepository();
@@ -32,7 +35,8 @@ public class InMemoryBundleRepositoriesTests
Assert.Equal("d1", t1[0].Digest);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ItemsOrderedByPath()
{
var repo = new InMemoryBundleItemRepository();
@@ -46,7 +50,8 @@ public class InMemoryBundleRepositoriesTests
Assert.Equal(new[] { "a.txt", "b.txt" }, list.Select(i => i.Path).ToArray());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ItemsTenantIsolated()
{
var repo = new InMemoryBundleItemRepository();

View File

@@ -1,10 +1,12 @@
using StellaOps.AirGap.Importer.Validation;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class MerkleRootCalculatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EmptySetProducesEmptyRoot()
{
var calc = new MerkleRootCalculator();
@@ -12,7 +14,8 @@ public class MerkleRootCalculatorTests
Assert.Equal(string.Empty, root);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DeterministicAcrossOrder()
{
var calc = new MerkleRootCalculator();

View File

@@ -1,6 +1,8 @@
using System.Diagnostics.Metrics;
using StellaOps.AirGap.Importer.Telemetry;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public sealed class OfflineKitMetricsTests : IDisposable
@@ -32,7 +34,8 @@ public sealed class OfflineKitMetricsTests : IDisposable
public void Dispose() => _listener.Dispose();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordImport_EmitsCounterWithLabels()
{
using var metrics = new OfflineKitMetrics();
@@ -47,7 +50,8 @@ public sealed class OfflineKitMetricsTests : IDisposable
m.HasTag(OfflineKitMetrics.TagNames.TenantId, "tenant-a"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordAttestationVerifyLatency_EmitsHistogramWithLabels()
{
using var metrics = new OfflineKitMetrics();
@@ -62,7 +66,8 @@ public sealed class OfflineKitMetricsTests : IDisposable
m.HasTag(OfflineKitMetrics.TagNames.Success, "true"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordRekorSuccess_EmitsCounterWithLabels()
{
using var metrics = new OfflineKitMetrics();
@@ -76,7 +81,8 @@ public sealed class OfflineKitMetricsTests : IDisposable
m.HasTag(OfflineKitMetrics.TagNames.Mode, "offline"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordRekorRetry_EmitsCounterWithLabels()
{
using var metrics = new OfflineKitMetrics();
@@ -90,7 +96,8 @@ public sealed class OfflineKitMetricsTests : IDisposable
m.HasTag(OfflineKitMetrics.TagNames.Reason, "stale_snapshot"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RecordRekorInclusionLatency_EmitsHistogramWithLabels()
{
using var metrics = new OfflineKitMetrics();

View File

@@ -1,13 +1,15 @@
using StellaOps.AirGap.Importer.Contracts;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class ReplayVerifierTests
{
private readonly ReplayVerifier _verifier = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FullRecompute_succeeds_when_hashes_match_and_fresh()
{
var now = DateTimeOffset.Parse("2025-12-02T01:00:00Z");
@@ -28,7 +30,8 @@ public class ReplayVerifierTests
Assert.Equal("full-recompute-passed", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Detects_hash_drift()
{
var now = DateTimeOffset.UtcNow;
@@ -49,7 +52,8 @@ public class ReplayVerifierTests
Assert.Equal("manifest-hash-drift", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PolicyFreeze_requires_matching_policy_hash()
{
var now = DateTimeOffset.UtcNow;

View File

@@ -1,10 +1,12 @@
using StellaOps.AirGap.Importer.Validation;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class RootRotationPolicyTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RequiresTwoApprovers()
{
var policy = new RootRotationPolicy();
@@ -13,7 +15,8 @@ public class RootRotationPolicyTests
Assert.Equal("rotation-dual-approval-required", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RejectsNoChange()
{
var policy = new RootRotationPolicy();
@@ -25,7 +28,8 @@ public class RootRotationPolicyTests
Assert.Equal("rotation-no-change", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AcceptsRotationWithDualApproval()
{
var policy = new RootRotationPolicy();

View File

@@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Reconciliation/Fixtures/**/*">

View File

@@ -1,10 +1,12 @@
using StellaOps.AirGap.Importer.Validation;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests;
public class TufMetadataValidatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RejectsInvalidJson()
{
var validator = new TufMetadataValidator();
@@ -12,7 +14,8 @@ public class TufMetadataValidatorTests
Assert.False(result.IsValid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AcceptsConsistentSnapshotHash()
{
var validator = new TufMetadataValidator();
@@ -26,7 +29,8 @@ public class TufMetadataValidatorTests
Assert.Equal("tuf-metadata-valid", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DetectsHashMismatch()
{
var validator = new TufMetadataValidator();

View File

@@ -2,11 +2,13 @@ using Microsoft.Extensions.Options;
using StellaOps.AirGap.Time.Config;
using StellaOps.AirGap.Time.Models;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class AirGapOptionsValidatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FailsWhenTenantMissing()
{
var opts = new AirGapOptions { TenantId = "" };
@@ -15,7 +17,8 @@ public class AirGapOptionsValidatorTests
Assert.True(result.Failed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FailsWhenWarningExceedsBreach()
{
var opts = new AirGapOptions { TenantId = "t", Staleness = new StalenessOptions { WarningSeconds = 20, BreachSeconds = 10 } };
@@ -24,7 +27,8 @@ public class AirGapOptionsValidatorTests
Assert.True(result.Failed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SucceedsForValidOptions()
{
var opts = new AirGapOptions { TenantId = "t", Staleness = new StalenessOptions { WarningSeconds = 10, BreachSeconds = 20 } };

View File

@@ -1,6 +1,7 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
/// <summary>
@@ -11,7 +12,8 @@ public class Rfc3161VerifierTests
{
private readonly Rfc3161Verifier _verifier = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenTrustRootsEmpty()
{
var token = new byte[] { 0x01, 0x02, 0x03 };
@@ -23,7 +25,8 @@ public class Rfc3161VerifierTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenTokenEmpty()
{
var trust = new[] { new TimeTrustRoot("tsa-root", new byte[] { 0x01 }, "rsa") };
@@ -35,7 +38,8 @@ public class Rfc3161VerifierTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenInvalidAsn1Structure()
{
var token = new byte[] { 0x01, 0x02, 0x03 }; // Invalid ASN.1
@@ -48,7 +52,8 @@ public class Rfc3161VerifierTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ProducesTokenDigest()
{
var token = new byte[] { 0x30, 0x00 }; // Empty SEQUENCE (minimal valid ASN.1)
@@ -61,7 +66,8 @@ public class Rfc3161VerifierTests
Assert.Contains("rfc3161-", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_HandlesExceptionsGracefully()
{
// Create bytes that might cause internal exceptions
@@ -77,7 +83,8 @@ public class Rfc3161VerifierTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReportsDecodeErrorForMalformedCms()
{
// Create something that looks like CMS but isn't valid

View File

@@ -1,6 +1,7 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
/// <summary>
@@ -11,7 +12,8 @@ public class RoughtimeVerifierTests
{
private readonly RoughtimeVerifier _verifier = new();
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenTrustRootsEmpty()
{
var token = new byte[] { 0x01, 0x02, 0x03, 0x04 };
@@ -23,7 +25,8 @@ public class RoughtimeVerifierTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenTokenEmpty()
{
var trust = new[] { new TimeTrustRoot("root1", new byte[32], "ed25519") };
@@ -35,7 +38,8 @@ public class RoughtimeVerifierTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenTokenTooShort()
{
var token = new byte[] { 0x01, 0x02, 0x03 };
@@ -47,7 +51,8 @@ public class RoughtimeVerifierTests
Assert.Equal("roughtime-message-too-short", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenInvalidTagCount()
{
// Create a minimal wire format with invalid tag count
@@ -63,7 +68,8 @@ public class RoughtimeVerifierTests
Assert.Equal("roughtime-invalid-tag-count", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenNonEd25519Algorithm()
{
// Create a minimal valid-looking wire format
@@ -77,7 +83,8 @@ public class RoughtimeVerifierTests
Assert.Contains("roughtime-", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ReturnsFailure_WhenKeyLengthWrong()
{
var token = CreateMinimalRoughtimeToken();
@@ -89,7 +96,8 @@ public class RoughtimeVerifierTests
Assert.Contains("roughtime-", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_ProducesTokenDigest()
{
var token = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };

View File

@@ -2,11 +2,13 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.AirGap.Time.Stores;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class SealedStartupValidatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FailsWhenAnchorMissing()
{
var validator = Build(out var statusService);
@@ -15,7 +17,8 @@ public class SealedStartupValidatorTests
Assert.Equal("time-anchor-missing", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FailsWhenBreach()
{
var validator = Build(out var statusService);
@@ -30,7 +33,8 @@ public class SealedStartupValidatorTests
Assert.Equal("time-anchor-stale", validation.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SucceedsWhenFresh()
{
var validator = Build(out var statusService);
@@ -41,7 +45,8 @@ public class SealedStartupValidatorTests
Assert.True(validation.IsValid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FailsOnBudgetMismatch()
{
var validator = Build(out var statusService);

View File

@@ -1,11 +1,13 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class StalenessCalculatorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UnknownWhenNoAnchor()
{
var calc = new StalenessCalculator();
@@ -14,7 +16,8 @@ public class StalenessCalculatorTests
Assert.False(result.IsBreach);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BreachWhenBeyondBudget()
{
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest");
@@ -28,7 +31,8 @@ public class StalenessCalculatorTests
Assert.Equal(25, result.AgeSeconds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void WarningWhenBetweenWarningAndBreach()
{
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest");

View File

@@ -12,5 +12,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Time/StellaOps.AirGap.Time.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,11 +3,13 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Parsing;
using StellaOps.AirGap.Time.Services;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class TimeAnchorLoaderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RejectsInvalidHex()
{
var loader = Build();
@@ -17,7 +19,8 @@ public class TimeAnchorLoaderTests
Assert.Equal("token-hex-invalid", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void LoadsHexToken()
{
var loader = Build();
@@ -29,7 +32,8 @@ public class TimeAnchorLoaderTests
Assert.Equal("Roughtime", anchor.Format);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RejectsIncompatibleTrustRoots()
{
var loader = Build();
@@ -43,7 +47,8 @@ public class TimeAnchorLoaderTests
Assert.Equal("trust-roots-incompatible-format", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RejectsWhenTrustRootsMissing()
{
var loader = Build();

View File

@@ -4,6 +4,7 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.AirGap.Time.Stores;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
/// <summary>
@@ -42,7 +43,8 @@ public class TimeAnchorPolicyServiceTests
_fixedTimeProvider);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateTimeAnchorAsync_ReturnsFailure_WhenNoAnchor()
{
var service = CreateService();
@@ -54,7 +56,8 @@ public class TimeAnchorPolicyServiceTests
Assert.NotNull(result.Remediation);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateTimeAnchorAsync_ReturnsSuccess_WhenAnchorValid()
{
var service = CreateService();
@@ -76,7 +79,8 @@ public class TimeAnchorPolicyServiceTests
Assert.False(result.Staleness.IsBreach);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateTimeAnchorAsync_ReturnsWarning_WhenAnchorStale()
{
var service = CreateService();
@@ -98,7 +102,8 @@ public class TimeAnchorPolicyServiceTests
Assert.Contains("warning", result.Reason, StringComparison.OrdinalIgnoreCase);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateTimeAnchorAsync_ReturnsFailure_WhenAnchorBreached()
{
var service = CreateService();
@@ -120,7 +125,8 @@ public class TimeAnchorPolicyServiceTests
Assert.True(result.Staleness.IsBreach);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnforceBundleImportPolicyAsync_AllowsImport_WhenAnchorValid()
{
var service = CreateService();
@@ -142,7 +148,8 @@ public class TimeAnchorPolicyServiceTests
Assert.True(result.Allowed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnforceBundleImportPolicyAsync_BlocksImport_WhenDriftExceeded()
{
var options = new TimeAnchorPolicyOptions { MaxDriftSeconds = 3600 }; // 1 hour max
@@ -168,7 +175,8 @@ public class TimeAnchorPolicyServiceTests
Assert.Equal(TimeAnchorPolicyErrorCodes.DriftExceeded, result.ErrorCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnforceOperationPolicyAsync_BlocksStrictOperations_WhenNoAnchor()
{
var options = new TimeAnchorPolicyOptions
@@ -183,7 +191,8 @@ public class TimeAnchorPolicyServiceTests
Assert.Equal(TimeAnchorPolicyErrorCodes.AnchorMissing, result.ErrorCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnforceOperationPolicyAsync_AllowsNonStrictOperations_InNonStrictMode()
{
var options = new TimeAnchorPolicyOptions
@@ -198,7 +207,8 @@ public class TimeAnchorPolicyServiceTests
Assert.True(result.Allowed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CalculateDriftAsync_ReturnsNoDrift_WhenNoAnchor()
{
var service = CreateService();
@@ -210,7 +220,8 @@ public class TimeAnchorPolicyServiceTests
Assert.Null(result.AnchorTime);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CalculateDriftAsync_ReturnsDrift_WhenAnchorExists()
{
var service = CreateService(new TimeAnchorPolicyOptions { MaxDriftSeconds = 3600 });
@@ -229,7 +240,8 @@ public class TimeAnchorPolicyServiceTests
Assert.False(result.DriftExceedsThreshold);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CalculateDriftAsync_DetectsExcessiveDrift()
{
var service = CreateService(new TimeAnchorPolicyOptions { MaxDriftSeconds = 60 }); // 1 minute max

View File

@@ -1,10 +1,12 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class TimeStatusDtoTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SerializesDeterministically()
{
var status = new TimeStatus(

View File

@@ -2,11 +2,13 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.AirGap.Time.Stores;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class TimeStatusServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReturnsUnknownWhenNoAnchor()
{
var svc = Build(out var telemetry);
@@ -16,7 +18,8 @@ public class TimeStatusServiceTests
Assert.Equal(0, telemetry.GetLatest("t1")?.AgeSeconds ?? 0);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PersistsAnchorAndBudget()
{
var svc = Build(out var telemetry);

View File

@@ -1,11 +1,13 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class TimeTelemetryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Records_latest_snapshot_per_tenant()
{
var telemetry = new TimeTelemetry();

View File

@@ -1,11 +1,13 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Parsing;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class TimeTokenParserTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EmptyTokenFails()
{
var parser = new TimeTokenParser();
@@ -16,7 +18,8 @@ public class TimeTokenParserTests
Assert.Equal(TimeAnchor.Unknown, anchor);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RoughtimeTokenProducesDigest()
{
var parser = new TimeTokenParser();

View File

@@ -2,11 +2,13 @@ using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Parsing;
using StellaOps.AirGap.Time.Services;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Time.Tests;
public class TimeVerificationServiceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FailsWithoutTrustRoots()
{
var svc = new TimeVerificationService();
@@ -15,7 +17,8 @@ public class TimeVerificationServiceTests
Assert.Equal("trust-roots-required", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void SucceedsForRoughtimeWithTrustRoot()
{
var svc = new TimeVerificationService();

View File

@@ -0,0 +1,79 @@
# CI/CD Template Tests
This directory contains validation tests for the StellaOps CI/CD signing templates.
## Running Tests
```bash
# Run all validation tests
./validate-templates.sh
# Run with verbose output
bash -x validate-templates.sh
```
## Test Coverage
| Test Suite | Description |
|------------|-------------|
| File Existence | Verifies all template files exist |
| YAML Syntax | Validates YAML syntax using yq |
| Workflow Structure | Checks required fields in workflows |
| Documentation | Validates documentation content |
| Cross-Platform | Ensures consistent patterns across platforms |
| actionlint | GitHub Actions specific linting |
## Prerequisites
For full test coverage, install:
```bash
# yq - YAML processor
brew install yq # macOS
# or
apt-get install yq # Debian/Ubuntu
# actionlint - GitHub Actions linter
go install github.com/rhysd/actionlint/cmd/actionlint@latest
# or
brew install actionlint
```
## Templates Tested
### GitHub Actions
- `stellaops-sign.yml` - Reusable signing workflow
- `stellaops-verify.yml` - Reusable verification workflow
- Example workflows for containers, SBOMs, verdicts
### GitLab CI
- `.gitlab-ci-stellaops.yml` - Include-able templates
- `example-pipeline.gitlab-ci.yml` - Full pipeline example
### Gitea
- `release-keyless-sign.yml` - Release signing
- `deploy-keyless-verify.yml` - Deploy verification
## Expected Output
```
========================================
CI/CD Template Validation Tests
Sprint: SPRINT_20251226_004_BE
========================================
Checking required tools
✓ PASS: yq is installed
✓ PASS: actionlint is installed
Testing GitHub Actions templates exist
✓ PASS: .github/workflows/examples/stellaops-sign.yml exists
...
Test Summary
========================================
Passed: 25
Failed: 0
All tests passed!
```

View File

@@ -0,0 +1,424 @@
#!/bin/bash
# CI/CD Template Validation Tests
# Sprint: SPRINT_20251226_004_BE_cicd_signing_templates
# Tasks: 0020-0024
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
PASS_COUNT=0
FAIL_COUNT=0
log_pass() {
echo -e "${GREEN}✓ PASS${NC}: $1"
((PASS_COUNT++))
}
log_fail() {
echo -e "${RED}✗ FAIL${NC}: $1"
((FAIL_COUNT++))
}
log_skip() {
echo -e "${YELLOW}○ SKIP${NC}: $1"
}
log_section() {
echo ""
echo "========================================"
echo "$1"
echo "========================================"
}
# Check for required tools
check_tools() {
log_section "Checking required tools"
if command -v yq &> /dev/null; then
log_pass "yq is installed"
else
log_skip "yq not installed - YAML structure tests will be skipped"
fi
if command -v actionlint &> /dev/null; then
log_pass "actionlint is installed"
else
log_skip "actionlint not installed - GitHub Actions lint tests will be skipped"
fi
}
# Test: GitHub Actions templates exist
test_github_templates_exist() {
log_section "Testing GitHub Actions templates exist"
local templates=(
".github/workflows/examples/stellaops-sign.yml"
".github/workflows/examples/stellaops-verify.yml"
".github/workflows/examples/example-container-sign.yml"
".github/workflows/examples/example-sbom-sign.yml"
".github/workflows/examples/example-verdict-sign.yml"
".github/workflows/examples/example-verification-gate.yml"
)
for template in "${templates[@]}"; do
if [[ -f "$ROOT_DIR/$template" ]]; then
log_pass "$template exists"
else
log_fail "$template not found"
fi
done
}
# Test: GitLab CI templates exist
test_gitlab_templates_exist() {
log_section "Testing GitLab CI templates exist"
local templates=(
"deploy/gitlab/examples/.gitlab-ci-stellaops.yml"
"deploy/gitlab/examples/example-pipeline.gitlab-ci.yml"
"deploy/gitlab/README.md"
)
for template in "${templates[@]}"; do
if [[ -f "$ROOT_DIR/$template" ]]; then
log_pass "$template exists"
else
log_fail "$template not found"
fi
done
}
# Test: Gitea workflows exist
test_gitea_workflows_exist() {
log_section "Testing Gitea workflows exist"
local workflows=(
".gitea/workflows/release-keyless-sign.yml"
".gitea/workflows/deploy-keyless-verify.yml"
)
for workflow in "${workflows[@]}"; do
if [[ -f "$ROOT_DIR/$workflow" ]]; then
log_pass "$workflow exists"
else
log_fail "$workflow not found"
fi
done
}
# Test: Documentation exists
test_documentation_exists() {
log_section "Testing documentation exists"
local docs=(
"docs/guides/identity-constraints.md"
"docs/guides/keyless-signing-troubleshooting.md"
"docs/guides/keyless-signing-quickstart.md"
)
for doc in "${docs[@]}"; do
if [[ -f "$ROOT_DIR/$doc" ]]; then
log_pass "$doc exists"
else
log_fail "$doc not found"
fi
done
}
# Test: YAML syntax validation
test_yaml_syntax() {
log_section "Testing YAML syntax"
if ! command -v yq &> /dev/null; then
log_skip "yq not available - skipping YAML syntax tests"
return
fi
local yaml_files=(
".github/workflows/examples/stellaops-sign.yml"
".github/workflows/examples/stellaops-verify.yml"
".github/workflows/examples/example-container-sign.yml"
"deploy/gitlab/examples/.gitlab-ci-stellaops.yml"
".gitea/workflows/release-keyless-sign.yml"
)
for yaml_file in "${yaml_files[@]}"; do
local full_path="$ROOT_DIR/$yaml_file"
if [[ -f "$full_path" ]]; then
if yq eval '.' "$full_path" > /dev/null 2>&1; then
log_pass "$yaml_file has valid YAML syntax"
else
log_fail "$yaml_file has invalid YAML syntax"
fi
fi
done
}
# Test: GitHub Actions workflow structure
test_github_workflow_structure() {
log_section "Testing GitHub Actions workflow structure"
if ! command -v yq &> /dev/null; then
log_skip "yq not available - skipping structure tests"
return
fi
local sign_workflow="$ROOT_DIR/.github/workflows/examples/stellaops-sign.yml"
if [[ -f "$sign_workflow" ]]; then
# Check for required fields
if yq eval '.on.workflow_call' "$sign_workflow" | grep -q "inputs"; then
log_pass "stellaops-sign.yml has workflow_call inputs"
else
log_fail "stellaops-sign.yml missing workflow_call inputs"
fi
if yq eval '.jobs.sign.permissions' "$sign_workflow" | grep -q "id-token"; then
log_pass "stellaops-sign.yml has id-token permission"
else
log_fail "stellaops-sign.yml missing id-token permission"
fi
if yq eval '.jobs.sign.outputs' "$sign_workflow" > /dev/null 2>&1; then
log_pass "stellaops-sign.yml has job outputs"
else
log_fail "stellaops-sign.yml missing job outputs"
fi
fi
}
# Test: GitLab CI template structure
test_gitlab_template_structure() {
log_section "Testing GitLab CI template structure"
if ! command -v yq &> /dev/null; then
log_skip "yq not available - skipping structure tests"
return
fi
local gitlab_template="$ROOT_DIR/deploy/gitlab/examples/.gitlab-ci-stellaops.yml"
if [[ -f "$gitlab_template" ]]; then
# Check for hidden job templates
if grep -q "\.stellaops-sign:" "$gitlab_template"; then
log_pass "GitLab template has .stellaops-sign hidden job"
else
log_fail "GitLab template missing .stellaops-sign hidden job"
fi
if grep -q "\.stellaops-verify:" "$gitlab_template"; then
log_pass "GitLab template has .stellaops-verify hidden job"
else
log_fail "GitLab template missing .stellaops-verify hidden job"
fi
if grep -q "id_tokens:" "$gitlab_template"; then
log_pass "GitLab template has id_tokens configuration"
else
log_fail "GitLab template missing id_tokens configuration"
fi
fi
}
# Test: Identity constraint documentation content
test_identity_docs_content() {
log_section "Testing identity constraint documentation content"
local doc="$ROOT_DIR/docs/guides/identity-constraints.md"
if [[ -f "$doc" ]]; then
if grep -q "GitHub Actions" "$doc"; then
log_pass "Identity docs cover GitHub Actions"
else
log_fail "Identity docs missing GitHub Actions coverage"
fi
if grep -q "GitLab" "$doc"; then
log_pass "Identity docs cover GitLab CI"
else
log_fail "Identity docs missing GitLab CI coverage"
fi
if grep -q "certificate-identity" "$doc"; then
log_pass "Identity docs explain certificate-identity"
else
log_fail "Identity docs missing certificate-identity explanation"
fi
if grep -q "certificate-oidc-issuer" "$doc"; then
log_pass "Identity docs explain certificate-oidc-issuer"
else
log_fail "Identity docs missing certificate-oidc-issuer explanation"
fi
fi
}
# Test: Troubleshooting guide content
test_troubleshooting_content() {
log_section "Testing troubleshooting guide content"
local doc="$ROOT_DIR/docs/guides/keyless-signing-troubleshooting.md"
if [[ -f "$doc" ]]; then
if grep -q "OIDC" "$doc"; then
log_pass "Troubleshooting covers OIDC issues"
else
log_fail "Troubleshooting missing OIDC coverage"
fi
if grep -q "Fulcio" "$doc"; then
log_pass "Troubleshooting covers Fulcio issues"
else
log_fail "Troubleshooting missing Fulcio coverage"
fi
if grep -q "Rekor" "$doc"; then
log_pass "Troubleshooting covers Rekor issues"
else
log_fail "Troubleshooting missing Rekor coverage"
fi
if grep -q "verification" "$doc"; then
log_pass "Troubleshooting covers verification issues"
else
log_fail "Troubleshooting missing verification coverage"
fi
fi
}
# Test: Quick-start guide content
test_quickstart_content() {
log_section "Testing quick-start guide content"
local doc="$ROOT_DIR/docs/guides/keyless-signing-quickstart.md"
if [[ -f "$doc" ]]; then
if grep -q "GitHub Actions" "$doc"; then
log_pass "Quick-start covers GitHub Actions"
else
log_fail "Quick-start missing GitHub Actions"
fi
if grep -q "GitLab" "$doc"; then
log_pass "Quick-start covers GitLab CI"
else
log_fail "Quick-start missing GitLab CI"
fi
if grep -q "id-token: write" "$doc"; then
log_pass "Quick-start shows id-token permission"
else
log_fail "Quick-start missing id-token permission example"
fi
if grep -q "stella attest" "$doc"; then
log_pass "Quick-start shows stella CLI usage"
else
log_fail "Quick-start missing stella CLI examples"
fi
fi
}
# Test: GitHub Actions linting (if actionlint available)
test_actionlint() {
log_section "Testing GitHub Actions with actionlint"
if ! command -v actionlint &> /dev/null; then
log_skip "actionlint not available - skipping lint tests"
return
fi
local workflows=(
".github/workflows/examples/stellaops-sign.yml"
".github/workflows/examples/stellaops-verify.yml"
".github/workflows/examples/example-container-sign.yml"
)
for workflow in "${workflows[@]}"; do
local full_path="$ROOT_DIR/$workflow"
if [[ -f "$full_path" ]]; then
if actionlint "$full_path" 2>&1 | grep -q "error"; then
log_fail "$workflow has actionlint errors"
else
log_pass "$workflow passes actionlint"
fi
fi
done
}
# Test: Cross-platform verification pattern
test_cross_platform_pattern() {
log_section "Testing cross-platform verification patterns"
local github_verify="$ROOT_DIR/.github/workflows/examples/stellaops-verify.yml"
local gitlab_template="$ROOT_DIR/deploy/gitlab/examples/.gitlab-ci-stellaops.yml"
# Check that both platforms use the same verification parameters
if [[ -f "$github_verify" ]] && [[ -f "$gitlab_template" ]]; then
if grep -q "certificate-identity" "$github_verify" && grep -q "CERTIFICATE_IDENTITY" "$gitlab_template"; then
log_pass "Cross-platform: Both use certificate-identity pattern"
else
log_fail "Cross-platform: Missing consistent certificate-identity pattern"
fi
if grep -q "certificate-oidc-issuer" "$github_verify" && grep -q "CERTIFICATE_OIDC_ISSUER" "$gitlab_template"; then
log_pass "Cross-platform: Both use certificate-oidc-issuer pattern"
else
log_fail "Cross-platform: Missing consistent certificate-oidc-issuer pattern"
fi
if grep -q "require-rekor" "$github_verify" && grep -q "REQUIRE_REKOR" "$gitlab_template"; then
log_pass "Cross-platform: Both support Rekor requirement"
else
log_fail "Cross-platform: Missing consistent Rekor requirement support"
fi
fi
}
# Run all tests
main() {
echo "============================================"
echo "CI/CD Template Validation Tests"
echo "Sprint: SPRINT_20251226_004_BE"
echo "============================================"
check_tools
test_github_templates_exist
test_gitlab_templates_exist
test_gitea_workflows_exist
test_documentation_exists
test_yaml_syntax
test_github_workflow_structure
test_gitlab_template_structure
test_identity_docs_content
test_troubleshooting_content
test_quickstart_content
test_actionlint
test_cross_platform_pattern
echo ""
echo "============================================"
echo "Test Summary"
echo "============================================"
echo -e "${GREEN}Passed: $PASS_COUNT${NC}"
echo -e "${RED}Failed: $FAIL_COUNT${NC}"
echo ""
if [[ $FAIL_COUNT -gt 0 ]]; then
echo -e "${RED}Some tests failed!${NC}"
exit 1
else
echo -e "${GREEN}All tests passed!${NC}"
exit 0
fi
}
main "$@"

View File

@@ -7,11 +7,13 @@ using StellaOps.Graph.Indexer.Ingestion.Advisory;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class AdvisoryLinksetProcessorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_persists_batch_and_records_success()
{
var snapshot = CreateSnapshot();
@@ -34,7 +36,8 @@ public sealed class AdvisoryLinksetProcessorTests
metrics.LastRecord.EdgeCount.Should().Be(writer.LastBatch!.Edges.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_records_failure_when_writer_throws()
{
var snapshot = CreateSnapshot();

View File

@@ -9,6 +9,7 @@ using StellaOps.Graph.Indexer.Ingestion.Advisory;
using Xunit;
using Xunit.Abstractions;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class AdvisoryLinksetTransformerTests
@@ -33,7 +34,8 @@ public sealed class AdvisoryLinksetTransformerTests
"AFFECTED_BY"
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_projects_advisory_nodes_and_affected_by_edges()
{
var snapshot = LoadSnapshot("concelier-linkset.json");

View File

@@ -4,13 +4,15 @@ using FluentAssertions;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class FileSystemSnapshotFileWriterTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), $"graph-snapshots-{Guid.NewGuid():N}");
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WriteJsonAsync_writes_canonical_json()
{
var writer = new FileSystemSnapshotFileWriter(_root);
@@ -26,7 +28,8 @@ public sealed class FileSystemSnapshotFileWriterTests : IDisposable
content.Should().Be("{\"a\":\"value1\",\"b\":\"value2\"}");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WriteJsonLinesAsync_writes_each_object_on_new_line()
{
var writer = new FileSystemSnapshotFileWriter(_root);

View File

@@ -4,6 +4,7 @@ using FluentAssertions;
using StellaOps.Graph.Indexer.Schema;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class GraphIdentityTests
@@ -11,7 +12,8 @@ public sealed class GraphIdentityTests
private static readonly string FixturesRoot =
Path.Combine(AppContext.BaseDirectory, "Fixtures", "v1");
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void NodeIds_are_stable()
{
var nodes = LoadArray("nodes.json");
@@ -40,7 +42,8 @@ public sealed class GraphIdentityTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EdgeIds_are_stable()
{
var edges = LoadArray("edges.json");
@@ -69,7 +72,8 @@ public sealed class GraphIdentityTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AttributeCoverage_matches_matrix()
{
var matrix = LoadObject("schema-matrix.json");

View File

@@ -14,6 +14,7 @@ using StellaOps.Graph.Indexer.Ingestion.Vex;
using StellaOps.Graph.Indexer.Schema;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class GraphSnapshotBuilderTests
@@ -21,7 +22,8 @@ public sealed class GraphSnapshotBuilderTests
private static readonly string FixturesRoot =
Path.Combine(AppContext.BaseDirectory, "Fixtures", "v1");
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Build_creates_manifest_and_adjacency_with_lineage()
{
var sbomSnapshot = Load<SbomSnapshot>("sbom-snapshot.json");

View File

@@ -7,11 +7,13 @@ using StellaOps.Graph.Indexer.Ingestion.Policy;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class PolicyOverlayProcessorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_persists_overlay_and_records_success_metrics()
{
var snapshot = CreateSnapshot();
@@ -33,7 +35,8 @@ public sealed class PolicyOverlayProcessorTests
metrics.LastRecord.EdgeCount.Should().Be(writer.LastBatch!.Edges.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_records_failure_when_writer_throws()
{
var snapshot = CreateSnapshot();

View File

@@ -9,6 +9,7 @@ using StellaOps.Graph.Indexer.Ingestion.Policy;
using Xunit;
using Xunit.Abstractions;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class PolicyOverlayTransformerTests
@@ -33,7 +34,8 @@ public sealed class PolicyOverlayTransformerTests
"GOVERNS_WITH"
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_projects_policy_nodes_and_governs_with_edges()
{
var snapshot = LoadSnapshot("policy-overlay.json");

View File

@@ -5,11 +5,13 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class SbomIngestProcessorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_writes_batch_and_records_success_metrics()
{
var snapshot = CreateSnapshot();
@@ -30,7 +32,8 @@ public sealed class SbomIngestProcessorTests
snapshotExporter.LastBatch.Should().BeSameAs(writer.LastBatch);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProcessAsync_records_failure_when_writer_throws()
{
var snapshot = CreateSnapshot();

View File

@@ -8,6 +8,8 @@ using Microsoft.Extensions.DependencyInjection;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class SbomIngestServiceCollectionExtensionsTests : IDisposable
@@ -23,7 +25,8 @@ public sealed class SbomIngestServiceCollectionExtensionsTests : IDisposable
Directory.CreateDirectory(_tempDirectory);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AddSbomIngestPipeline_exports_snapshots_to_configured_directory()
{
var services = new ServiceCollection();
@@ -42,7 +45,8 @@ public sealed class SbomIngestServiceCollectionExtensionsTests : IDisposable
writer!.LastBatch.Should().NotBeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AddSbomIngestPipeline_uses_environment_variable_when_not_configured()
{
var previous = Environment.GetEnvironmentVariable("STELLAOPS_GRAPH_SNAPSHOT_DIR");

View File

@@ -7,6 +7,7 @@ using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
using Xunit.Abstractions;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class SbomIngestTransformerTests
@@ -36,7 +37,8 @@ public sealed class SbomIngestTransformerTests
"BUILT_FROM"
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_produces_expected_nodes_and_edges()
{
var snapshot = LoadSnapshot("sbom-snapshot.json");
@@ -92,7 +94,8 @@ public sealed class SbomIngestTransformerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_deduplicates_license_nodes_case_insensitive()
{
var baseCollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:00Z");
@@ -130,7 +133,8 @@ public sealed class SbomIngestTransformerTests
canonicalKey["source_digest"]!.GetValue<string>().Should().Be("sha256:license001");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_emits_built_from_edge_with_provenance()
{
var snapshot = LoadSnapshot("sbom-snapshot.json");
@@ -155,7 +159,8 @@ public sealed class SbomIngestTransformerTests
canonicalKey.ContainsKey("child_artifact_digest").Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_normalizes_valid_from_to_utc()
{
var componentCollectedAt = new DateTimeOffset(2025, 11, 1, 15, 30, 45, TimeSpan.FromHours(2));

View File

@@ -14,6 +14,7 @@ using StellaOps.Graph.Indexer.Ingestion.Sbom;
using StellaOps.Graph.Indexer.Ingestion.Vex;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class SbomSnapshotExporterTests
@@ -21,7 +22,8 @@ public sealed class SbomSnapshotExporterTests
private static readonly string FixturesRoot =
Path.Combine(AppContext.BaseDirectory, "Fixtures", "v1");
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExportAsync_writes_manifest_adjacency_nodes_and_edges()
{
var sbomSnapshot = Load<SbomSnapshot>("sbom-snapshot.json");

View File

@@ -9,6 +9,7 @@ using StellaOps.Graph.Indexer.Ingestion.Vex;
using Xunit;
using Xunit.Abstractions;
using StellaOps.TestKit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class VexOverlayTransformerTests
@@ -33,7 +34,8 @@ public sealed class VexOverlayTransformerTests
"VEX_EXEMPTS"
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Transform_projects_vex_nodes_and_exempt_edges()
{
var snapshot = LoadSnapshot("excititor-vex.json");

View File

@@ -0,0 +1,714 @@
// -----------------------------------------------------------------------------
// BinaryEvidenceDeterminismTests.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-23 - Determinism tests for binary verdict reproducibility
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Canonical.Json;
using StellaOps.Testing.Determinism;
using Xunit;
namespace StellaOps.Integration.Determinism;
/// <summary>
/// Determinism validation tests for binary vulnerability evidence.
/// Ensures identical binary inputs produce identical verdicts across:
/// - Binary identity extraction
/// - Vulnerability match computation
/// - Fix status determination
/// - Proof segment generation
/// - Multiple runs with frozen time
/// - Parallel execution
/// </summary>
public class BinaryEvidenceDeterminismTests
{
#region Binary Identity Determinism Tests
[Fact]
public void BinaryIdentity_WithIdenticalInput_ProducesDeterministicOutput()
{
// Arrange
var binaryData = CreateSampleBinaryData();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act - Extract identity multiple times
var identity1 = ExtractBinaryIdentity(binaryData, frozenTime);
var identity2 = ExtractBinaryIdentity(binaryData, frozenTime);
var identity3 = ExtractBinaryIdentity(binaryData, frozenTime);
// Assert - All outputs should be identical
identity1.Should().Be(identity2);
identity2.Should().Be(identity3);
}
[Fact]
public void BinaryIdentity_BuildId_IsStable()
{
// Arrange
var binaryData = CreateSampleBinaryData();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var identity1 = ExtractBinaryIdentity(binaryData, frozenTime);
var identity2 = ExtractBinaryIdentity(binaryData, frozenTime);
// Assert
identity1.BuildId.Should().Be(identity2.BuildId);
identity1.BuildId.Should().MatchRegex("^[0-9a-f]{40}$");
}
[Fact]
public void BinaryIdentity_BinaryKey_IsStable()
{
// Arrange
var binaryData = CreateSampleBinaryData();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var identity1 = ExtractBinaryIdentity(binaryData, frozenTime);
var identity2 = ExtractBinaryIdentity(binaryData, frozenTime);
// Assert
identity1.BinaryKey.Should().Be(identity2.BinaryKey);
}
[Fact]
public async Task BinaryIdentity_ParallelExtraction_ProducesDeterministicOutput()
{
// Arrange
var binaryData = CreateSampleBinaryData();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act - Extract in parallel 20 times
var tasks = Enumerable.Range(0, 20)
.Select(_ => Task.Run(() => ExtractBinaryIdentity(binaryData, frozenTime)))
.ToArray();
var identities = await Task.WhenAll(tasks);
// Assert - All outputs should be identical
identities.Should().AllBe(identities[0]);
}
#endregion
#region Vulnerability Match Determinism Tests
[Fact]
public void VulnMatch_WithIdenticalBinary_ProducesDeterministicMatches()
{
// Arrange
var identity = CreateSampleBinaryIdentity();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act - Look up matches multiple times
var matches1 = LookupVulnerabilities(identity, frozenTime);
var matches2 = LookupVulnerabilities(identity, frozenTime);
var matches3 = LookupVulnerabilities(identity, frozenTime);
// Assert - All results should be identical
var json1 = SerializeMatches(matches1);
var json2 = SerializeMatches(matches2);
var json3 = SerializeMatches(matches3);
json1.Should().Be(json2);
json2.Should().Be(json3);
}
[Fact]
public void VulnMatch_Ordering_IsDeterministic()
{
// Arrange
var identity = CreateSampleBinaryIdentityWithMultipleCves();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var matches1 = LookupVulnerabilities(identity, frozenTime);
var matches2 = LookupVulnerabilities(identity, frozenTime);
// Assert - CVEs should be in same order
var cves1 = matches1.Select(m => m.CveId).ToList();
var cves2 = matches2.Select(m => m.CveId).ToList();
cves1.Should().Equal(cves2);
}
[Fact]
public void VulnMatch_Confidence_IsDeterministic()
{
// Arrange
var identity = CreateSampleBinaryIdentity();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var matches1 = LookupVulnerabilities(identity, frozenTime);
var matches2 = LookupVulnerabilities(identity, frozenTime);
// Assert - Confidence scores should be identical
for (int i = 0; i < matches1.Length; i++)
{
matches1[i].Confidence.Should().Be(matches2[i].Confidence);
}
}
[Fact]
public void VulnMatch_CanonicalHash_IsStable()
{
// Arrange
var identity = CreateSampleBinaryIdentity();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var matches1 = LookupVulnerabilities(identity, frozenTime);
var json1 = SerializeMatches(matches1);
var hash1 = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(json1));
var matches2 = LookupVulnerabilities(identity, frozenTime);
var json2 = SerializeMatches(matches2);
var hash2 = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(json2));
// Assert
hash1.Should().Be(hash2);
hash1.Should().MatchRegex("^[0-9a-f]{64}$");
}
#endregion
#region Fix Status Determinism Tests
[Fact]
public void FixStatus_WithIdenticalInput_ProducesDeterministicOutput()
{
// Arrange
var input = CreateFixStatusInput();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var status1 = GetFixStatus(input, frozenTime);
var status2 = GetFixStatus(input, frozenTime);
var status3 = GetFixStatus(input, frozenTime);
// Assert
SerializeFixStatus(status1).Should().Be(SerializeFixStatus(status2));
SerializeFixStatus(status2).Should().Be(SerializeFixStatus(status3));
}
[Fact]
public void FixStatus_BackportDetection_IsDeterministic()
{
// Arrange
var input = CreateBackportedCveInput();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var status1 = GetFixStatus(input, frozenTime);
var status2 = GetFixStatus(input, frozenTime);
// Assert - Both should detect as fixed
status1.State.Should().Be("fixed");
status2.State.Should().Be("fixed");
status1.FixedVersion.Should().Be(status2.FixedVersion);
status1.Confidence.Should().Be(status2.Confidence);
}
[Fact]
public void FixStatus_Method_IsConsistent()
{
// Arrange
var input = CreateFixStatusInput();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var status = GetFixStatus(input, frozenTime);
// Assert - Method should be one of known values
status.Method.Should().BeOneOf("changelog", "patch_analysis", "advisory");
}
#endregion
#region Proof Segment Determinism Tests
[Fact]
public void ProofSegment_WithIdenticalEvidence_ProducesDeterministicOutput()
{
// Arrange
var evidence = CreateSampleBinaryEvidence();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
var deterministicId = GenerateDeterministicProofId(evidence, frozenTime);
// Act
var proof1 = CreateBinaryProofSegment(evidence, frozenTime, deterministicId);
var proof2 = CreateBinaryProofSegment(evidence, frozenTime, deterministicId);
var proof3 = CreateBinaryProofSegment(evidence, frozenTime, deterministicId);
// Assert
proof1.Should().Be(proof2);
proof2.Should().Be(proof3);
}
[Fact]
public void ProofSegment_CanonicalHash_IsStable()
{
// Arrange
var evidence = CreateSampleBinaryEvidence();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
var deterministicId = GenerateDeterministicProofId(evidence, frozenTime);
// Act
var proof1 = CreateBinaryProofSegment(evidence, frozenTime, deterministicId);
var hash1 = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(proof1));
var proof2 = CreateBinaryProofSegment(evidence, frozenTime, deterministicId);
var hash2 = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(proof2));
// Assert
hash1.Should().Be(hash2);
}
[Fact]
public void ProofSegment_PredicateType_IsConsistent()
{
// Arrange
var evidence = CreateSampleBinaryEvidence();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
var deterministicId = GenerateDeterministicProofId(evidence, frozenTime);
// Act
var proof = CreateBinaryProofSegment(evidence, frozenTime, deterministicId);
// Assert
proof.Should().Contain("\"predicateType\"");
proof.Should().Contain("https://stellaops.dev/predicates/binary-fingerprint-evidence@v1");
}
[Fact]
public async Task ProofSegment_ParallelGeneration_ProducesDeterministicOutput()
{
// Arrange
var evidence = CreateSampleBinaryEvidence();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
var deterministicId = GenerateDeterministicProofId(evidence, frozenTime);
// Act - Generate in parallel
var tasks = Enumerable.Range(0, 20)
.Select(_ => Task.Run(() => CreateBinaryProofSegment(evidence, frozenTime, deterministicId)))
.ToArray();
var proofs = await Task.WhenAll(tasks);
// Assert
proofs.Should().AllBe(proofs[0]);
}
#endregion
#region End-to-End Verdict Determinism Tests
[Fact]
public void FullBinaryVerdict_WithIdenticalInput_ProducesDeterministicOutput()
{
// Arrange
var scanInput = CreateSampleScanInput();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act - Process scan multiple times
var verdict1 = ProcessBinaryScan(scanInput, frozenTime);
var verdict2 = ProcessBinaryScan(scanInput, frozenTime);
var verdict3 = ProcessBinaryScan(scanInput, frozenTime);
// Assert
var json1 = SerializeVerdict(verdict1);
var json2 = SerializeVerdict(verdict2);
var json3 = SerializeVerdict(verdict3);
json1.Should().Be(json2);
json2.Should().Be(json3);
}
[Fact]
public void FullBinaryVerdict_CanonicalHash_IsStable()
{
// Arrange
var scanInput = CreateSampleScanInput();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
// Act
var verdict1 = ProcessBinaryScan(scanInput, frozenTime);
var json1 = SerializeVerdict(verdict1);
var hash1 = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(json1));
var verdict2 = ProcessBinaryScan(scanInput, frozenTime);
var json2 = SerializeVerdict(verdict2);
var hash2 = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(json2));
// Assert
hash1.Should().Be(hash2);
}
[Fact]
public void FullBinaryVerdict_DeterminismManifest_CanBeCreated()
{
// Arrange
var scanInput = CreateSampleScanInput();
var frozenTime = DateTimeOffset.Parse("2025-12-23T18:00:00Z");
var verdict = ProcessBinaryScan(scanInput, frozenTime);
var verdictBytes = Encoding.UTF8.GetBytes(SerializeVerdict(verdict));
var artifactInfo = new ArtifactInfo
{
Type = "binary-evidence",
Name = "binary-vulnerability-verdict",
Version = "1.0.0",
Format = "BinaryEvidence JSON"
};
var toolchain = new ToolchainInfo
{
Platform = ".NET 10.0",
Components = new[]
{
new ComponentInfo { Name = "StellaOps.BinaryIndex", Version = "1.0.0" }
}
};
// Act
var manifest = DeterminismManifestWriter.CreateManifest(
verdictBytes,
artifactInfo,
toolchain);
// Assert
manifest.SchemaVersion.Should().Be("1.0");
manifest.Artifact.Format.Should().Be("BinaryEvidence JSON");
manifest.CanonicalHash.Algorithm.Should().Be("SHA-256");
}
#endregion
#region Helper Methods
private static byte[] CreateSampleBinaryData()
{
// Simulated ELF binary data with Build-ID
var data = new byte[1024];
var random = new Random(42); // Deterministic seed
random.NextBytes(data);
// Add ELF magic header
data[0] = 0x7f;
data[1] = 0x45; // E
data[2] = 0x4c; // L
data[3] = 0x46; // F
return data;
}
private static BinaryIdentityResult ExtractBinaryIdentity(byte[] data, DateTimeOffset timestamp)
{
// Compute deterministic Build-ID from data
var buildId = ComputeDeterministicBuildId(data);
var fileSha256 = CanonJson.Sha256Hex(data);
return new BinaryIdentityResult
{
Format = "elf",
BuildId = buildId,
FileSha256 = $"sha256:{fileSha256}",
Architecture = "x86_64",
BinaryKey = $"test-binary:{buildId[..8]}"
};
}
private static BinaryIdentityResult CreateSampleBinaryIdentity()
{
return new BinaryIdentityResult
{
Format = "elf",
BuildId = "8d8f09a0d7e2c1b3a5f4e6d8c0b2a4e6f8d0c2b4",
FileSha256 = "sha256:abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
Architecture = "x86_64",
BinaryKey = "openssl:1.1.1w-1"
};
}
private static BinaryIdentityResult CreateSampleBinaryIdentityWithMultipleCves()
{
return new BinaryIdentityResult
{
Format = "elf",
BuildId = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
FileSha256 = "sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff",
Architecture = "x86_64",
BinaryKey = "curl:7.74.0-1"
};
}
private static VulnMatch[] LookupVulnerabilities(BinaryIdentityResult identity, DateTimeOffset timestamp)
{
// Deterministic vulnerability lookup based on binary key
var matches = new List<VulnMatch>();
if (identity.BinaryKey.Contains("openssl"))
{
matches.Add(new VulnMatch
{
CveId = "CVE-2023-5678",
Method = "buildid_catalog",
Confidence = 0.95m,
VulnerablePurl = "pkg:deb/debian/openssl@1.1.1n-0+deb11u4"
});
}
if (identity.BinaryKey.Contains("curl"))
{
matches.Add(new VulnMatch
{
CveId = "CVE-2023-38545",
Method = "buildid_catalog",
Confidence = 0.98m,
VulnerablePurl = "pkg:deb/debian/curl@7.74.0-1.3+deb11u5"
});
matches.Add(new VulnMatch
{
CveId = "CVE-2024-2398",
Method = "buildid_catalog",
Confidence = 0.96m,
VulnerablePurl = "pkg:deb/debian/curl@7.74.0-1.3+deb11u6"
});
}
// Sort by CVE ID for deterministic ordering
return matches.OrderBy(m => m.CveId, StringComparer.Ordinal).ToArray();
}
private static FixStatusInput CreateFixStatusInput()
{
return new FixStatusInput
{
Distro = "debian",
Release = "bookworm",
SourcePkg = "openssl",
CveId = "CVE-2023-5678"
};
}
private static FixStatusInput CreateBackportedCveInput()
{
return new FixStatusInput
{
Distro = "debian",
Release = "bookworm",
SourcePkg = "openssl",
CveId = "CVE-2023-4807"
};
}
private static FixStatusResult GetFixStatus(FixStatusInput input, DateTimeOffset timestamp)
{
// Deterministic fix status based on input
return new FixStatusResult
{
State = "fixed",
FixedVersion = "1.1.1w-1",
Method = "changelog",
Confidence = 0.98m
};
}
private static BinaryEvidence CreateSampleBinaryEvidence()
{
return new BinaryEvidence
{
Identity = CreateSampleBinaryIdentity(),
LayerDigest = "sha256:layer1abc123def456789012345678901234567890abcdef12345678901234",
Matches = LookupVulnerabilities(CreateSampleBinaryIdentity(), DateTimeOffset.UtcNow)
};
}
private static string GenerateDeterministicProofId(BinaryEvidence evidence, DateTimeOffset timestamp)
{
var seed = $"{evidence.Identity.BinaryKey}:{evidence.LayerDigest}:{timestamp:O}";
var hash = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(seed));
return $"proof:{hash[..32]}";
}
private static string CreateBinaryProofSegment(BinaryEvidence evidence, DateTimeOffset timestamp, string proofId)
{
var matchesJson = string.Join(",\n ", evidence.Matches.Select(m => $$"""
{
"cve_id": "{{m.CveId}}",
"method": "{{m.Method}}",
"confidence": {{m.Confidence.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)}},
"vulnerable_purl": "{{m.VulnerablePurl}}"
}
"""));
return $$"""
{
"predicateType": "https://stellaops.dev/predicates/binary-fingerprint-evidence@v1",
"proofId": "{{proofId}}",
"createdAt": "{{timestamp:O}}",
"binaryIdentity": {
"format": "{{evidence.Identity.Format}}",
"buildId": "{{evidence.Identity.BuildId}}",
"fileSha256": "{{evidence.Identity.FileSha256}}",
"architecture": "{{evidence.Identity.Architecture}}",
"binaryKey": "{{evidence.Identity.BinaryKey}}"
},
"layerDigest": "{{evidence.LayerDigest}}",
"matches": [
{{matchesJson}}
]
}
""";
}
private static ScanInput CreateSampleScanInput()
{
return new ScanInput
{
ImageDigest = "sha256:9f92a8c39f8d4f7bb1a60f2be650b3019b9a1bb50d2da839efa9bf2a278a0071",
Distro = "debian",
Release = "bookworm",
Binaries = new[]
{
CreateSampleBinaryData()
}
};
}
private static BinaryVerdict ProcessBinaryScan(ScanInput input, DateTimeOffset timestamp)
{
var binaries = new List<BinaryEvidence>();
foreach (var binaryData in input.Binaries)
{
var identity = ExtractBinaryIdentity(binaryData, timestamp);
var matches = LookupVulnerabilities(identity, timestamp);
binaries.Add(new BinaryEvidence
{
Identity = identity,
LayerDigest = "sha256:layer1",
Matches = matches
});
}
return new BinaryVerdict
{
ScanId = GenerateScanId(input, timestamp),
ImageDigest = input.ImageDigest,
ScannedAt = timestamp,
Binaries = binaries.ToArray()
};
}
private static string GenerateScanId(ScanInput input, DateTimeOffset timestamp)
{
var seed = $"{input.ImageDigest}:{timestamp:O}";
var hash = CanonJson.Sha256Hex(Encoding.UTF8.GetBytes(seed));
return $"scan-{hash[..16]}";
}
private static string ComputeDeterministicBuildId(byte[] data)
{
using var sha1 = SHA1.Create();
var hash = sha1.ComputeHash(data);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string SerializeMatches(VulnMatch[] matches)
{
return JsonSerializer.Serialize(matches, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false
});
}
private static string SerializeFixStatus(FixStatusResult status)
{
return JsonSerializer.Serialize(status, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false
});
}
private static string SerializeVerdict(BinaryVerdict verdict)
{
return JsonSerializer.Serialize(verdict, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false
});
}
#endregion
#region DTOs
private sealed record BinaryIdentityResult
{
public required string Format { get; init; }
public required string BuildId { get; init; }
public required string FileSha256 { get; init; }
public required string Architecture { get; init; }
public required string BinaryKey { get; init; }
}
private sealed record VulnMatch
{
public required string CveId { get; init; }
public required string Method { get; init; }
public required decimal Confidence { get; init; }
public required string VulnerablePurl { get; init; }
}
private sealed record FixStatusInput
{
public required string Distro { get; init; }
public required string Release { get; init; }
public required string SourcePkg { get; init; }
public required string CveId { get; init; }
}
private sealed record FixStatusResult
{
public required string State { get; init; }
public required string FixedVersion { get; init; }
public required string Method { get; init; }
public required decimal Confidence { get; init; }
}
private sealed record BinaryEvidence
{
public required BinaryIdentityResult Identity { get; init; }
public required string LayerDigest { get; init; }
public required VulnMatch[] Matches { get; init; }
}
private sealed record ScanInput
{
public required string ImageDigest { get; init; }
public required string Distro { get; init; }
public required string Release { get; init; }
public required byte[][] Binaries { get; init; }
}
private sealed record BinaryVerdict
{
public required string ScanId { get; init; }
public required string ImageDigest { get; init; }
public required DateTimeOffset ScannedAt { get; init; }
public required BinaryEvidence[] Binaries { get; init; }
}
#endregion
}

View File

@@ -2,11 +2,13 @@ using System.Text;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public sealed class PromotionAttestationBuilderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task BuildAsync_SignsCanonicalPayloadAndAddsPredicateClaim()
{
var predicate = new PromotionPredicate(
@@ -39,7 +41,8 @@ public sealed class PromotionAttestationBuilderTests
canonicalJson);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task BuildAsync_MergesClaimsWithoutOverwritingPredicateType()
{
var predicate = new PromotionPredicate(

View File

@@ -2,11 +2,13 @@ using System.Text;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public sealed class SignersTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacSigner_SignsAndAudits()
{
var key = new InMemoryKeyProvider("k1", Convert.FromHexString("0f0e0d0c0b0a09080706050403020100"));
@@ -28,7 +30,8 @@ public sealed class SignersTests
Assert.Empty(audit.Missing);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacSigner_EnforcesRequiredClaims()
{
var key = new InMemoryKeyProvider("k-claims", Encoding.UTF8.GetBytes("secret"));
@@ -43,7 +46,8 @@ public sealed class SignersTests
Assert.Contains(audit.Missing, x => x.keyId == "k-claims" && x.claim == "sub");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RotatingKeyProvider_LogsRotationWhenNewKeyBecomesActive()
{
var now = new DateTimeOffset(2025, 11, 22, 10, 0, 0, TimeSpan.Zero);
@@ -62,7 +66,8 @@ public sealed class SignersTests
Assert.Equal("new", provider.KeyId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CosignSigner_UsesClientAndAudits()
{
var signatureBytes = Convert.FromBase64String(await File.ReadAllTextAsync(Path.Combine("Fixtures", "cosign.sig"))); // fixture is deterministic
@@ -89,7 +94,8 @@ public sealed class SignersTests
Assert.Equal(request.Payload, call.payload);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task KmsSigner_EnforcesRequiredClaims()
{
var signature = new byte[] { 0xCA, 0xFE, 0xBA, 0xBE };

View File

@@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Provenance/StellaOps.Provenance.Attestation/StellaOps.Provenance.Attestation.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures/cosign.sig" CopyToOutputDirectory="Always" />

View File

@@ -2,18 +2,22 @@ using System.Text;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public sealed class ToolEntrypointTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunAsync_ReturnsInvalidOnMissingArgs()
{
var code = await ToolEntrypoint.RunAsync(Array.Empty<string>(), TextWriter.Null, new StringWriter(), new TestTimeProvider(DateTimeOffset.UtcNow));
Assert.Equal(1, code);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RunAsync_VerifiesValidSignature()
{
var payload = Encoding.UTF8.GetBytes("payload");

View File

@@ -2,11 +2,14 @@ using System.Text;
using StellaOps.Provenance.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Provenance.Attestation.Tests;
public sealed class VerificationLibraryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacVerifier_FailsWhenKeyExpired()
{
var key = new InMemoryKeyProvider("k1", Encoding.UTF8.GetBytes("secret"), DateTimeOffset.UtcNow.AddMinutes(-1));
@@ -22,7 +25,8 @@ public sealed class VerificationLibraryTests
Assert.Contains("time", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HmacVerifier_FailsWhenClockSkewTooLarge()
{
var now = new DateTimeOffset(2025, 11, 22, 12, 0, 0, TimeSpan.Zero);
@@ -37,7 +41,8 @@ public sealed class VerificationLibraryTests
Assert.False(result.IsValid);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void MerkleRootVerifier_DetectsMismatch()
{
var leaves = new[]
@@ -54,7 +59,8 @@ public sealed class VerificationLibraryTests
Assert.Equal("merkle root mismatch", result.Reason);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ChainOfCustodyVerifier_ComputesAggregate()
{
var hops = new[]

View File

@@ -3,6 +3,7 @@ using FluentAssertions;
using StellaOps.Replay.Core;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Replay.Core.Tests;
public class PolicySimulationInputLockValidatorTests
@@ -19,7 +20,8 @@ public class PolicySimulationInputLockValidatorTests
RequiredScopes = new[] { "policy:simulate:shadow" }
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_passes_when_digests_match_and_shadow_scope_present()
{
var inputs = new PolicySimulationMaterializedInputs(
@@ -38,7 +40,8 @@ public class PolicySimulationInputLockValidatorTests
result.Reason.Should().Be("ok");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_detects_digest_drift()
{
var inputs = new PolicySimulationMaterializedInputs(
@@ -57,7 +60,8 @@ public class PolicySimulationInputLockValidatorTests
result.Reason.Should().Be("policy-bundle-drift");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_requires_shadow_mode_when_flagged()
{
var inputs = new PolicySimulationMaterializedInputs(
@@ -76,7 +80,8 @@ public class PolicySimulationInputLockValidatorTests
result.Reason.Should().Be("shadow-mode-required");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_fails_when_lock_stale()
{
var inputs = new PolicySimulationMaterializedInputs(

View File

@@ -1,10 +1,12 @@
using StellaOps.Cryptography;
using StellaOps.TestKit;
namespace StellaOps.Audit.ReplayToken.Tests;
public sealed class ReplayTokenGeneratorTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_SameInputs_ReturnsSameValue()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
@@ -37,7 +39,8 @@ public sealed class ReplayTokenGeneratorTests
Assert.Equal(token1.Canonical, token2.Canonical);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Generate_IgnoresArrayOrdering()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
@@ -63,7 +66,8 @@ public sealed class ReplayTokenGeneratorTests
Assert.Equal(tokenA.Value, tokenB.Value);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_MatchingInputs_ReturnsTrue()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
@@ -76,7 +80,8 @@ public sealed class ReplayTokenGeneratorTests
Assert.True(generator.Verify(token, request));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Verify_DifferentInputs_ReturnsFalse()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
@@ -90,7 +95,8 @@ public sealed class ReplayTokenGeneratorTests
Assert.False(generator.Verify(token, different));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReplayToken_Parse_RoundTripsCanonical()
{
var token = new ReplayToken("0123456789abcdef", DateTimeOffset.UnixEpoch);
@@ -101,7 +107,8 @@ public sealed class ReplayTokenGeneratorTests
Assert.Equal(token.Version, parsed.Version);
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("")]
[InlineData("replay")]
[InlineData("replay:v1.0:SHA-256")]

View File

@@ -18,5 +18,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Audit.ReplayToken\\StellaOps.Audit.ReplayToken.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,13 +2,15 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Time.Testing;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Evidence.Bundle.Tests;
public class EvidenceBundleTests
{
private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2024, 12, 15, 12, 0, 0, TimeSpan.Zero));
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_MinimalBundle_CreatesValid()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -24,21 +26,24 @@ public class EvidenceBundleTests
Assert.Equal(_timeProvider.GetUtcNow(), bundle.CreatedAt);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_MissingAlertId_Throws()
{
var builder = new EvidenceBundleBuilder(_timeProvider).WithArtifactId("sha256:abc");
Assert.Throws<InvalidOperationException>(() => builder.Build());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_MissingArtifactId_Throws()
{
var builder = new EvidenceBundleBuilder(_timeProvider).WithAlertId("ALERT-001");
Assert.Throws<InvalidOperationException>(() => builder.Build());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Builder_WithAllEvidence_ComputesHashSet()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -56,7 +61,8 @@ public class EvidenceBundleTests
Assert.Equal(64, bundle.Hashes.CombinedHash.Length);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeCompletenessScore_AllAvailable_Returns4()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -71,7 +77,8 @@ public class EvidenceBundleTests
Assert.Equal(4, bundle.ComputeCompletenessScore());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeCompletenessScore_NoneAvailable_Returns0()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -82,7 +89,8 @@ public class EvidenceBundleTests
Assert.Equal(0, bundle.ComputeCompletenessScore());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeCompletenessScore_PartialAvailable_ReturnsCorrect()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -97,7 +105,8 @@ public class EvidenceBundleTests
Assert.Equal(2, bundle.ComputeCompletenessScore());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateStatusSummary_ReturnsCorrectStatuses()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -116,7 +125,8 @@ public class EvidenceBundleTests
Assert.Equal(EvidenceStatus.Unavailable, summary.VexStatus);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ToSigningPredicate_CreatesValidPredicate()
{
var bundle = new EvidenceBundleBuilder(_timeProvider)
@@ -138,7 +148,8 @@ public class EvidenceBundleTests
public class EvidenceHashSetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_DeterministicOutput()
{
var hashes1 = new Dictionary<string, string> { ["a"] = "hash1", ["b"] = "hash2" };
@@ -150,7 +161,8 @@ public class EvidenceHashSetTests
Assert.Equal(set1.CombinedHash, set2.CombinedHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_DifferentInputs_DifferentHash()
{
var hashes1 = new Dictionary<string, string> { ["a"] = "hash1" };
@@ -162,7 +174,8 @@ public class EvidenceHashSetTests
Assert.NotEqual(set1.CombinedHash, set2.CombinedHash);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Empty_CreatesEmptyHashSet()
{
var empty = EvidenceHashSet.Empty();
@@ -172,7 +185,8 @@ public class EvidenceHashSetTests
Assert.Equal("SHA-256", empty.Algorithm);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_PreservesLabeledHashes()
{
var hashes = new Dictionary<string, string> { ["reachability"] = "h1", ["vex"] = "h2" };
@@ -183,7 +197,8 @@ public class EvidenceHashSetTests
Assert.Equal("h2", set.LabeledHashes["vex"]);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Compute_NullInput_Throws()
{
Assert.Throws<ArgumentNullException>(() => EvidenceHashSet.Compute(null!));
@@ -192,7 +207,8 @@ public class EvidenceHashSetTests
public class ServiceCollectionExtensionsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddEvidenceBundleServices_RegistersBuilder()
{
var services = new ServiceCollection();
@@ -203,7 +219,8 @@ public class ServiceCollectionExtensionsTests
Assert.NotNull(builder);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddEvidenceBundleServices_WithTimeProvider_UsesProvided()
{
var fakeTime = new FakeTimeProvider();
@@ -215,14 +232,16 @@ public class ServiceCollectionExtensionsTests
Assert.Same(fakeTime, timeProvider);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddEvidenceBundleServices_NullServices_Throws()
{
IServiceCollection? services = null;
Assert.Throws<ArgumentNullException>(() => services!.AddEvidenceBundleServices());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddEvidenceBundleServices_NullTimeProvider_Throws()
{
var services = new ServiceCollection();

View File

@@ -10,5 +10,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Evidence.Bundle\StellaOps.Evidence.Bundle.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Gateway.WebService.Tests;
public class GatewayHealthTests : IClassFixture<WebApplicationFactory<Program>>
@@ -12,7 +13,8 @@ public class GatewayHealthTests : IClassFixture<WebApplicationFactory<Program>>
_factory = factory;
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HealthEndpoint_ReturnsOk()
{
// Arrange

View File

@@ -26,5 +26,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gateway\StellaOps.Gateway.WebService\StellaOps.Gateway.WebService.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,7 @@ using StellaOps.Microservice;
using StellaOps.Router.Common.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
/// <summary>
@@ -33,7 +34,8 @@ public class EndpointDiscoveryServiceTests
_logger);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiscoverEndpoints_CallsDiscoveryProvider()
{
var codeEndpoints = new List<EndpointDescriptor>();
@@ -49,7 +51,8 @@ public class EndpointDiscoveryServiceTests
_discoveryProviderMock.Verify(x => x.DiscoverEndpoints(), Times.Once);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiscoverEndpoints_CallsYamlLoader()
{
var codeEndpoints = new List<EndpointDescriptor>();
@@ -65,7 +68,8 @@ public class EndpointDiscoveryServiceTests
_yamlLoaderMock.Verify(x => x.Load(), Times.Once);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiscoverEndpoints_PassesCodeEndpointsAndYamlConfigToMerger()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -95,7 +99,8 @@ public class EndpointDiscoveryServiceTests
_mergerMock.Verify(x => x.Merge(codeEndpoints, yamlConfig), Times.Once);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiscoverEndpoints_ReturnsMergedEndpoints()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -119,7 +124,8 @@ public class EndpointDiscoveryServiceTests
result.Should().BeSameAs(mergedEndpoints);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiscoverEndpoints_ContinuesWithNullYamlConfig_WhenLoaderReturnsNull()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -143,7 +149,8 @@ public class EndpointDiscoveryServiceTests
result.Should().BeSameAs(codeEndpoints);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DiscoverEndpoints_ContinuesWithNullYamlConfig_WhenLoaderThrows()
{
var codeEndpoints = new List<EndpointDescriptor>

View File

@@ -2,6 +2,7 @@ using System.Reflection;
using StellaOps.Microservice;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
// Test endpoint classes
@@ -23,7 +24,8 @@ public record TestResponse;
public class EndpointDiscoveryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void StellaEndpointAttribute_StoresMethodAndPath()
{
// Arrange & Act
@@ -34,7 +36,8 @@ public class EndpointDiscoveryTests
Assert.Equal("/api/test", attr.Path);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void StellaEndpointAttribute_NormalizesMethod()
{
// Arrange & Act
@@ -44,7 +47,8 @@ public class EndpointDiscoveryTests
Assert.Equal("GET", attr.Method);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void StellaEndpointAttribute_DefaultTimeoutIs30Seconds()
{
// Arrange & Act
@@ -54,7 +58,8 @@ public class EndpointDiscoveryTests
Assert.Equal(30, attr.TimeoutSeconds);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReflectionDiscovery_FindsEndpointsInCurrentAssembly()
{
// Arrange
@@ -77,7 +82,8 @@ public class EndpointDiscoveryTests
Assert.Contains(endpoints, e => e.Method == "POST" && e.Path == "/api/create");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReflectionDiscovery_SetsServiceNameAndVersion()
{
// Arrange
@@ -100,7 +106,8 @@ public class EndpointDiscoveryTests
Assert.Equal("2.0.0", endpoint.Version);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ReflectionDiscovery_SetsStreamingAndTimeout()
{
// Arrange

View File

@@ -6,6 +6,7 @@ using StellaOps.Microservice;
using StellaOps.Router.Common.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
/// <summary>
@@ -22,7 +23,8 @@ public class EndpointOverrideMergerTests
_merger = new EndpointOverrideMerger(_loggerMock.Object);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_WithNullYamlConfig_ReturnsCodeEndpointsUnchanged()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -35,7 +37,8 @@ public class EndpointOverrideMergerTests
result.Should().BeEquivalentTo(codeEndpoints);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_WithEmptyYamlConfig_ReturnsCodeEndpointsUnchanged()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -49,7 +52,8 @@ public class EndpointOverrideMergerTests
result.Should().BeEquivalentTo(codeEndpoints);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_OverridesTimeout_WhenYamlSpecifiesTimeout()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -75,7 +79,8 @@ public class EndpointOverrideMergerTests
result[0].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_OverridesStreaming_WhenYamlSpecifiesStreaming()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -101,7 +106,8 @@ public class EndpointOverrideMergerTests
result[0].SupportsStreaming.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_OverridesClaims_WhenYamlSpecifiesClaims()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -132,7 +138,8 @@ public class EndpointOverrideMergerTests
result[0].RequiringClaims[0].Value.Should().Be("admin");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_PreservesCodeDefaults_WhenYamlDoesNotOverride()
{
var originalTimeout = TimeSpan.FromSeconds(45);
@@ -160,7 +167,8 @@ public class EndpointOverrideMergerTests
result[0].SupportsStreaming.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_MatchesCaseInsensitively()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -186,7 +194,8 @@ public class EndpointOverrideMergerTests
result[0].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(1));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_LeavesUnmatchedEndpointsUnchanged()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -216,7 +225,8 @@ public class EndpointOverrideMergerTests
result[2].DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); // unchanged
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_LogsWarning_WhenYamlOverrideDoesNotMatchAnyEndpoint()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -248,7 +258,8 @@ public class EndpointOverrideMergerTests
Times.Once);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_AppliesMultipleOverrides()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -282,7 +293,8 @@ public class EndpointOverrideMergerTests
result[1].DefaultTimeout.Should().Be(TimeSpan.FromMinutes(2));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_PreservesOriginalEndpointProperties()
{
var codeEndpoints = new List<EndpointDescriptor>
@@ -322,7 +334,8 @@ public class EndpointOverrideMergerTests
result[0].HandlerType.Should().Be(typeof(object));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Merge_YamlOverridesCodeClaims_Completely()
{
var codeEndpoints = new List<EndpointDescriptor>

View File

@@ -3,6 +3,7 @@ using StellaOps.Microservice;
using StellaOps.Router.Common.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
public class EndpointRegistryTests
@@ -19,7 +20,8 @@ public class EndpointRegistryTests
};
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_ExactMatch_ReturnsEndpoint()
{
var registry = new EndpointRegistry();
@@ -34,7 +36,8 @@ public class EndpointRegistryTests
match.PathParameters.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_MethodMismatch_ReturnsFalse()
{
var registry = new EndpointRegistry();
@@ -46,7 +49,8 @@ public class EndpointRegistryTests
match.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_PathMismatch_ReturnsFalse()
{
var registry = new EndpointRegistry();
@@ -58,7 +62,8 @@ public class EndpointRegistryTests
match.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_WithPathParameter_ExtractsParameter()
{
var registry = new EndpointRegistry();
@@ -72,7 +77,8 @@ public class EndpointRegistryTests
match.PathParameters["id"].Should().Be("123");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_MethodCaseInsensitive_ReturnsMatch()
{
var registry = new EndpointRegistry();
@@ -84,7 +90,8 @@ public class EndpointRegistryTests
match.Should().NotBeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_PathCaseInsensitive_ReturnsMatch()
{
var registry = new EndpointRegistry();
@@ -96,7 +103,8 @@ public class EndpointRegistryTests
match.Should().NotBeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RegisterAll_MultipeEndpoints_AllRegistered()
{
var registry = new EndpointRegistry();
@@ -112,7 +120,8 @@ public class EndpointRegistryTests
registry.GetAllEndpoints().Should().HaveCount(3);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetAllEndpoints_ReturnsAllRegistered()
{
var registry = new EndpointRegistry();
@@ -128,7 +137,8 @@ public class EndpointRegistryTests
all.Should().Contain(endpoint2);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_FirstMatchWins_WhenMultiplePossible()
{
var registry = new EndpointRegistry();
@@ -145,7 +155,8 @@ public class EndpointRegistryTests
match!.Endpoint.Should().Be(endpoint1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_EmptyRegistry_ReturnsFalse()
{
var registry = new EndpointRegistry();
@@ -156,7 +167,8 @@ public class EndpointRegistryTests
match.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Constructor_CaseSensitive_RespectsSetting()
{
var registry = new EndpointRegistry(caseInsensitive: false);

View File

@@ -2,6 +2,7 @@ using FluentAssertions;
using StellaOps.Microservice;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
/// <summary>
@@ -9,7 +10,8 @@ namespace StellaOps.Microservice.Tests;
/// </summary>
public class MicroserviceYamlConfigTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void MicroserviceYamlConfig_DefaultsToEmptyEndpoints()
{
var config = new MicroserviceYamlConfig();
@@ -18,7 +20,8 @@ public class MicroserviceYamlConfigTests
config.Endpoints.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void EndpointOverrideConfig_DefaultsToEmptyStrings()
{
var config = new EndpointOverrideConfig();
@@ -30,7 +33,8 @@ public class MicroserviceYamlConfigTests
config.RequiringClaims.Should().BeNull();
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("30s", 30)]
[InlineData("60s", 60)]
[InlineData("1s", 1)]
@@ -44,7 +48,8 @@ public class MicroserviceYamlConfigTests
result.Should().Be(TimeSpan.FromSeconds(expectedSeconds));
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("5m", 5)]
[InlineData("10m", 10)]
[InlineData("1m", 1)]
@@ -58,7 +63,8 @@ public class MicroserviceYamlConfigTests
result.Should().Be(TimeSpan.FromMinutes(expectedMinutes));
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("1h", 1)]
[InlineData("2h", 2)]
[InlineData("24h", 24)]
@@ -72,7 +78,8 @@ public class MicroserviceYamlConfigTests
result.Should().Be(TimeSpan.FromHours(expectedHours));
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("00:00:30", 30)]
[InlineData("00:05:00", 300)]
[InlineData("01:00:00", 3600)]
@@ -86,7 +93,8 @@ public class MicroserviceYamlConfigTests
result.Should().Be(TimeSpan.FromSeconds(expectedSeconds));
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
@@ -99,7 +107,8 @@ public class MicroserviceYamlConfigTests
result.Should().BeNull();
}
[Theory]
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("invalid")]
[InlineData("abc")]
[InlineData("30x")]
@@ -112,7 +121,8 @@ public class MicroserviceYamlConfigTests
result.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ClaimRequirementConfig_ToClaimRequirement_ConvertsCorrectly()
{
var config = new ClaimRequirementConfig
@@ -127,7 +137,8 @@ public class MicroserviceYamlConfigTests
result.Value.Should().Be("admin");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ClaimRequirementConfig_ToClaimRequirement_HandlesNullValue()
{
var config = new ClaimRequirementConfig

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Microservice;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
/// <summary>
@@ -29,7 +30,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ReturnsNull_WhenConfigFilePathIsNull()
{
var options = new StellaMicroserviceOptions
@@ -46,7 +48,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ReturnsNull_WhenConfigFilePathIsEmpty()
{
var options = new StellaMicroserviceOptions
@@ -63,7 +66,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ReturnsNull_WhenFileDoesNotExist()
{
var options = new StellaMicroserviceOptions
@@ -80,7 +84,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ParsesValidYaml()
{
var yamlContent = """
@@ -111,7 +116,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result.Endpoints[0].SupportsStreaming.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ParsesMultipleEndpoints()
{
var yamlContent = """
@@ -143,7 +149,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result!.Endpoints.Should().HaveCount(3);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ParsesClaimRequirements()
{
var yamlContent = """
@@ -178,7 +185,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result.Endpoints[0].RequiringClaims![1].Value.Should().Be("delete");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_HandlesEmptyEndpointsList()
{
var yamlContent = """
@@ -201,7 +209,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result!.Endpoints.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_IgnoresUnknownProperties()
{
var yamlContent = """
@@ -228,7 +237,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
result!.Endpoints.Should().HaveCount(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ThrowsOnInvalidYaml()
{
var yamlContent = """
@@ -252,7 +262,8 @@ public class MicroserviceYamlLoaderTests : IDisposable
act.Should().Throw<Exception>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ResolvesRelativePath()
{
var yamlContent = """

View File

@@ -7,6 +7,8 @@ using StellaOps.Router.Common.Frames;
using StellaOps.Router.Common.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
public sealed class RequestDispatcherTests
@@ -17,7 +19,8 @@ public sealed class RequestDispatcherTests
PropertyNameCaseInsensitive = true
};
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DispatchAsync_WhenEndpointNotFound_Returns404()
{
var registry = new EndpointRegistry();
@@ -44,7 +47,8 @@ public sealed class RequestDispatcherTests
Encoding.UTF8.GetString(response.Payload.Span).Should().Be("Not Found");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DispatchAsync_WhenBodyEmpty_BindsFromPathAndQueryParameters()
{
var registry = new EndpointRegistry();
@@ -86,7 +90,8 @@ public sealed class RequestDispatcherTests
dto.Filter.Should().Be("active");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DispatchAsync_WhenBodyPresent_PathAndQueryOverrideJsonProperties()
{
var registry = new EndpointRegistry();

View File

@@ -2,11 +2,13 @@ using StellaOps.Microservice;
using StellaOps.Router.Common.Enums;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
public class StellaMicroserviceOptionsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void StellaMicroserviceOptions_CanBeCreated()
{
// Arrange & Act
@@ -24,7 +26,8 @@ public class StellaMicroserviceOptionsTests
Assert.NotEmpty(options.InstanceId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RouterEndpointConfig_CanBeCreated()
{
// Arrange & Act
@@ -41,7 +44,8 @@ public class StellaMicroserviceOptionsTests
Assert.Equal(TransportType.Tcp, config.TransportType);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsIfServiceNameEmpty()
{
// Arrange
@@ -56,7 +60,8 @@ public class StellaMicroserviceOptionsTests
Assert.Throws<InvalidOperationException>(() => options.Validate());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsIfVersionInvalid()
{
// Arrange
@@ -72,7 +77,8 @@ public class StellaMicroserviceOptionsTests
Assert.Contains("not valid semver", ex.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ThrowsIfNoRouters()
{
// Arrange
@@ -88,7 +94,8 @@ public class StellaMicroserviceOptionsTests
Assert.Contains("router endpoint is required", ex.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_AcceptsValidSemver()
{
// Arrange
@@ -104,7 +111,8 @@ public class StellaMicroserviceOptionsTests
options.Validate();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_AcceptsSemverWithPrerelease()
{
// Arrange
@@ -120,7 +128,8 @@ public class StellaMicroserviceOptionsTests
options.Validate();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DefaultHeartbeatInterval_Is10Seconds()
{
// Arrange & Act

View File

@@ -25,5 +25,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,6 +4,8 @@ using FluentAssertions;
using StellaOps.Microservice;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Microservice.Tests;
public class TypedEndpointAdapterTests
@@ -41,7 +43,8 @@ public class TypedEndpointAdapterTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Adapt_TypedWithRequest_DeserializesAndSerializes()
{
var handler = new TestTypedHandler();
@@ -69,7 +72,8 @@ public class TypedEndpointAdapterTests
result.Success.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Adapt_TypedNoRequest_SerializesResponse()
{
var handler = new TestNoRequestHandler();
@@ -93,7 +97,8 @@ public class TypedEndpointAdapterTests
result!.Message.Should().Be("No request needed");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Adapt_RawHandler_PassesThroughDirectly()
{
var handler = new TestRawHandler();
@@ -112,7 +117,8 @@ public class TypedEndpointAdapterTests
response.StatusCode.Should().Be(200);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Adapt_InvalidJson_ReturnsBadRequest()
{
var handler = new TestTypedHandler();
@@ -131,7 +137,8 @@ public class TypedEndpointAdapterTests
response.StatusCode.Should().Be(400);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Adapt_EmptyBody_ReturnsBadRequest()
{
var handler = new TestTypedHandler();
@@ -150,7 +157,8 @@ public class TypedEndpointAdapterTests
response.StatusCode.Should().Be(400);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Adapt_WithCancellation_PropagatesCancellation()
{
var handler = new CancellableHandler();

View File

@@ -1,11 +1,13 @@
using StellaOps.Router.Common.Enums;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Common.Tests;
public class FrameTypeTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void FrameType_HasExpectedValues()
{
// Verify all expected frame types exist
@@ -16,7 +18,8 @@ public class FrameTypeTests
Assert.True(Enum.IsDefined(typeof(FrameType), FrameType.Cancel));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TransportType_HasExpectedValues()
{
// Verify all expected transport types exist (no HTTP per spec)
@@ -27,7 +30,8 @@ public class FrameTypeTests
Assert.True(Enum.IsDefined(typeof(TransportType), TransportType.RabbitMq));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void InstanceHealthStatus_HasExpectedValues()
{
// Verify all expected health statuses exist

View File

@@ -21,5 +21,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,11 +6,14 @@ using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Enums;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Config.Tests;
public class RouterConfigTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RouterConfig_HasDefaultValues()
{
// Arrange & Act
@@ -23,7 +26,8 @@ public class RouterConfigTests
config.StaticInstances.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RoutingOptions_HasDefaultValues()
{
// Arrange & Act
@@ -37,7 +41,8 @@ public class RouterConfigTests
options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void StaticInstanceConfig_RequiredProperties()
{
// Arrange & Act
@@ -59,7 +64,8 @@ public class RouterConfigTests
instance.Weight.Should().Be(100);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RouterConfigOptions_HasDefaultValues()
{
// Arrange & Act
@@ -76,7 +82,8 @@ public class RouterConfigTests
public class RouterConfigProviderTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ReturnsSuccess_ForValidConfig()
{
// Arrange
@@ -92,7 +99,8 @@ public class RouterConfigProviderTests
result.Errors.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Current_ReturnsDefaultConfig_WhenNoFileSpecified()
{
// Arrange
@@ -112,7 +120,8 @@ public class RouterConfigProviderTests
public class ConfigValidationTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validation_Fails_WhenPayloadLimitsInvalid()
{
// Arrange
@@ -127,7 +136,8 @@ public class ConfigValidationTests
result.IsValid.Should().BeTrue();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ConfigValidationResult_Success_HasNoErrors()
{
// Arrange & Act
@@ -138,7 +148,8 @@ public class ConfigValidationTests
result.Errors.Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ConfigValidationResult_WithErrors_IsNotValid()
{
// Arrange & Act
@@ -155,7 +166,8 @@ public class ConfigValidationTests
public class ServiceCollectionExtensionsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddRouterConfig_RegistersServices()
{
// Arrange
@@ -172,7 +184,8 @@ public class ServiceCollectionExtensionsTests
configProvider.Should().NotBeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddRouterConfig_WithPath_SetsConfigPath()
{
// Arrange
@@ -191,7 +204,8 @@ public class ServiceCollectionExtensionsTests
configProvider!.Options.ConfigPath.Should().Be(path);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void AddRouterConfigFromYaml_SetsConfigPath()
{
// Arrange
@@ -214,7 +228,8 @@ public class ServiceCollectionExtensionsTests
public class ConfigChangedEventArgsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Constructor_SetsProperties()
{
// Arrange
@@ -243,7 +258,8 @@ public class HotReloadTests : IDisposable
_tempConfigPath = Path.Combine(_tempDir, "router.yaml");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task HotReload_UpdatesConfig_WhenFileChanges()
{
// Arrange
@@ -296,7 +312,8 @@ routing:
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ReloadAsync_LoadsNewConfig()
{
// Arrange

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Router.Config\StellaOps.Router.Config.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,11 +8,13 @@ using StellaOps.Router.Gateway.State;
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class ConnectionManagerTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task StartAsync_WhenHelloInvalid_RejectsAndClosesChannel()
{
var (manager, server, registry, routingState) = Create();
@@ -59,7 +61,8 @@ public sealed class ConnectionManagerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WhenClientDisconnects_RemovesFromRoutingState()
{
var (manager, server, registry, routingState) = Create();
@@ -102,7 +105,8 @@ public sealed class ConnectionManagerTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task WhenMultipleClientsConnect_TracksAndCleansIndependently()
{
var (manager, server, registry, routingState) = Create();

View File

@@ -6,11 +6,13 @@ using StellaOps.Router.Gateway.Configuration;
using StellaOps.Router.Gateway.Routing;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class DefaultRoutingPluginTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ChooseInstanceAsync_WhenNoCandidates_ReturnsNull()
{
var plugin = CreatePlugin(gatewayRegion: "eu1");
@@ -38,7 +40,8 @@ public sealed class DefaultRoutingPluginTests
decision.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ChooseInstanceAsync_WhenRequestedVersionDoesNotMatch_ReturnsNull()
{
var plugin = CreatePlugin(
@@ -63,7 +66,8 @@ public sealed class DefaultRoutingPluginTests
decision.Should().BeNull();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ChooseInstanceAsync_PrefersHealthyOverDegraded()
{
var plugin = CreatePlugin(
@@ -93,7 +97,8 @@ public sealed class DefaultRoutingPluginTests
decision!.Connection.ConnectionId.Should().Be("inv-healthy");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ChooseInstanceAsync_PrefersLocalRegionOverRemote()
{
var plugin = CreatePlugin(
@@ -120,7 +125,8 @@ public sealed class DefaultRoutingPluginTests
decision!.Connection.ConnectionId.Should().Be("inv-eu1");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ChooseInstanceAsync_WhenNoLocal_UsesNeighborRegion()
{
var plugin = CreatePlugin(
@@ -148,7 +154,8 @@ public sealed class DefaultRoutingPluginTests
decision!.Connection.ConnectionId.Should().Be("inv-eu2");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ChooseInstanceAsync_WhenTied_UsesRoundRobin()
{
var plugin = CreatePlugin(

View File

@@ -4,11 +4,13 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Gateway.State;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class InMemoryRoutingStateTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEndpoint_WhenExactMatch_ReturnsEndpointDescriptor()
{
var routingState = new InMemoryRoutingState();
@@ -33,7 +35,8 @@ public sealed class InMemoryRoutingStateTests
resolved.Path.Should().Be("/items");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEndpoint_WhenTemplateMatch_ReturnsEndpointDescriptor()
{
var routingState = new InMemoryRoutingState();
@@ -55,7 +58,8 @@ public sealed class InMemoryRoutingStateTests
resolved!.Path.Should().Be("/items/{sku}");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RemoveConnection_RemovesEndpointsFromIndex()
{
var routingState = new InMemoryRoutingState();
@@ -78,7 +82,8 @@ public sealed class InMemoryRoutingStateTests
routingState.GetAllConnections().Should().BeEmpty();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetConnectionsFor_FiltersByServiceAndVersion()
{
var routingState = new InMemoryRoutingState();

View File

@@ -2,11 +2,13 @@ using FluentAssertions;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class InMemoryValkeyRateLimitStoreTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task IncrementAndCheckAsync_UsesSmallestWindowAsRepresentativeWhenAllowed()
{
var store = new InMemoryValkeyRateLimitStore();
@@ -25,7 +27,8 @@ public sealed class InMemoryValkeyRateLimitStoreTests
result.RetryAfterSeconds.Should().Be(0);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task IncrementAndCheckAsync_DeniesWhenLimitExceeded()
{
var store = new InMemoryValkeyRateLimitStore();

View File

@@ -2,11 +2,13 @@ using FluentAssertions;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class InstanceRateLimiterTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryAcquire_ReportsMostConstrainedRuleWhenAllowed()
{
var limiter = new InstanceRateLimiter(
@@ -24,7 +26,8 @@ public sealed class InstanceRateLimiterTests
decision.CurrentCount.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryAcquire_DeniesWhenAnyRuleIsExceeded()
{
var limiter = new InstanceRateLimiter(

View File

@@ -2,11 +2,13 @@ using FluentAssertions;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class LimitInheritanceResolverTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEnvironmentTarget_UsesEnvironmentDefaultsWhenNoMicroserviceOverride()
{
var config = new RateLimitConfig
@@ -32,7 +34,8 @@ public sealed class LimitInheritanceResolverTests
target.Rules[0].MaxRequests.Should().Be(600);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEnvironmentTarget_UsesMicroserviceOverrideWhenPresent()
{
var config = new RateLimitConfig
@@ -65,7 +68,8 @@ public sealed class LimitInheritanceResolverTests
target.Rules[0].MaxRequests.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEnvironmentTarget_DisablesWhenNoRulesAtAnyLevel()
{
var config = new RateLimitConfig
@@ -84,7 +88,8 @@ public sealed class LimitInheritanceResolverTests
target.Enabled.Should().BeFalse();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEnvironmentTarget_UsesRouteOverrideWhenPresent()
{
var config = new RateLimitConfig
@@ -126,7 +131,8 @@ public sealed class LimitInheritanceResolverTests
target.Rules[0].MaxRequests.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveEnvironmentTarget_DoesNotTreatRouteAsOverrideWhenItHasNoRules()
{
var config = new RateLimitConfig

View File

@@ -15,11 +15,14 @@ using StellaOps.Router.Gateway.Middleware;
using StellaOps.Router.Gateway.State;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class MiddlewareErrorScenarioTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EndpointResolutionMiddleware_WhenNoEndpoint_Returns404StructuredError()
{
var context = CreateContext(method: "GET", path: "/missing");
@@ -45,7 +48,8 @@ public sealed class MiddlewareErrorScenarioTests
body.GetProperty("traceId").GetString().Should().Be("trace-1");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RoutingDecisionMiddleware_WhenNoInstances_Returns503StructuredError()
{
var context = CreateContext(method: "GET", path: "/items");
@@ -84,7 +88,8 @@ public sealed class MiddlewareErrorScenarioTests
body.GetProperty("version").GetString().Should().Be("1.0.0");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AuthorizationMiddleware_WhenMissingClaim_Returns403StructuredError()
{
var context = CreateContext(method: "GET", path: "/items");
@@ -128,7 +133,8 @@ public sealed class MiddlewareErrorScenarioTests
body.GetProperty("details").GetProperty("requiredClaimValue").GetString().Should().Be("admin");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GlobalErrorHandlerMiddleware_WhenUnhandledException_Returns500StructuredError()
{
var context = CreateContext(method: "GET", path: "/boom");

View File

@@ -3,11 +3,13 @@ using Microsoft.Extensions.Configuration;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class RateLimitConfigTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_BindsRoutesAndRules()
{
var configuration = new ConfigurationBuilder()
@@ -41,7 +43,8 @@ public sealed class RateLimitConfigTests
route.Rules[0].MaxRequests.Should().Be(50);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Load_ThrowsForInvalidRegexRoute()
{
var configuration = new ConfigurationBuilder()

View File

@@ -7,11 +7,14 @@ using StellaOps.Router.Gateway;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class RateLimitMiddlewareTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task InvokeAsync_EnforcesEnvironmentLimit_WithRetryAfterAndJsonBody()
{
var config = new RateLimitConfig

View File

@@ -2,11 +2,13 @@ using FluentAssertions;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class RateLimitRouteMatcherTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_ExactBeatsPrefixAndRegex()
{
var microservice = new MicroserviceLimitsConfig
@@ -43,7 +45,8 @@ public sealed class RateLimitRouteMatcherTests
match!.Value.Name.Should().Be("exact");
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryMatch_LongestPrefixWins()
{
var microservice = new MicroserviceLimitsConfig

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Router.Gateway.RateLimit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class RateLimitServiceTests
@@ -26,7 +27,8 @@ public sealed class RateLimitServiceTests
}
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CheckLimitAsync_DoesNotInvokeEnvironmentLimiterUntilActivationGateTriggers()
{
var config = new RateLimitConfig
@@ -58,7 +60,8 @@ public sealed class RateLimitServiceTests
store.Calls.Should().Be(1);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CheckLimitAsync_EnforcesPerRouteEnvironmentRules()
{
var config = new RateLimitConfig

View File

@@ -5,11 +5,14 @@ using StellaOps.Router.Gateway.Configuration;
using StellaOps.Router.Gateway.DependencyInjection;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class RouterNodeConfigValidationTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RouterNodeConfig_WhenRegionMissing_ThrowsOptionsValidationException()
{
var services = new ServiceCollection();
@@ -22,7 +25,8 @@ public sealed class RouterNodeConfigValidationTests
act.Should().Throw<OptionsValidationException>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RouterNodeConfig_WhenRegionProvided_GeneratesNodeIdIfMissing()
{
var services = new ServiceCollection();

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Router.Gateway\StellaOps.Router.Gateway.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,6 +4,7 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.InMemory.Tests;
public class CancelFlowTests
@@ -24,7 +25,8 @@ public class CancelFlowTests
_client = provider.GetRequiredService<InMemoryTransportClient>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SendCancelAsync_SendsCancelFrame()
{
// Arrange
@@ -57,7 +59,8 @@ public class CancelFlowTests
Assert.Equal(1, _registry.Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task OnCancelReceived_IsInvoked()
{
// Arrange

View File

@@ -6,6 +6,7 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.InMemory.Tests;
public class HelloHeartbeatFlowTests
@@ -26,7 +27,8 @@ public class HelloHeartbeatFlowTests
_client = provider.GetRequiredService<InMemoryTransportClient>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ConnectAsync_SendsHelloAndRegistersEndpoints()
{
// Arrange
@@ -61,7 +63,8 @@ public class HelloHeartbeatFlowTests
Assert.Equal(TransportType.InMemory, connections[0].TransportType);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SendHeartbeatAsync_SendsHeartbeatFrame()
{
// Arrange
@@ -92,7 +95,8 @@ public class HelloHeartbeatFlowTests
Assert.Equal(1, _registry.Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DisconnectAsync_RemovesConnection()
{
// Arrange

View File

@@ -3,11 +3,14 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.InMemory.Tests;
public class InMemoryChannelTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ToMicroservice_WritesAndReads()
{
// Arrange
@@ -28,7 +31,8 @@ public class InMemoryChannelTests
Assert.Equal("corr-1", readFrame.CorrelationId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ToGateway_WritesAndReads()
{
// Arrange
@@ -49,7 +53,8 @@ public class InMemoryChannelTests
Assert.Equal("corr-1", readFrame.CorrelationId);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Dispose_CancelsLifetimeToken()
{
// Arrange
@@ -62,7 +67,8 @@ public class InMemoryChannelTests
Assert.True(channel.LifetimeToken.IsCancellationRequested);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Dispose_CompletesChannels()
{
// Arrange
@@ -76,7 +82,8 @@ public class InMemoryChannelTests
Assert.True(channel.ToGateway.Reader.Completion.IsCompleted);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void BoundedChannel_RespectsBufferSize()
{
// Arrange & Act
@@ -86,7 +93,8 @@ public class InMemoryChannelTests
Assert.NotNull(channel);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Instance_CanBeSetAndRetrieved()
{
// Arrange

View File

@@ -1,11 +1,14 @@
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.InMemory.Tests;
public class InMemoryConnectionRegistryTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateChannel_CreatesNewChannel()
{
// Arrange
@@ -20,7 +23,8 @@ public class InMemoryConnectionRegistryTests
Assert.Equal(1, registry.Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateChannel_ThrowsIfDuplicate()
{
// Arrange
@@ -31,7 +35,8 @@ public class InMemoryConnectionRegistryTests
Assert.Throws<InvalidOperationException>(() => registry.CreateChannel("conn-1"));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetChannel_ReturnsNullForUnknown()
{
// Arrange
@@ -44,7 +49,8 @@ public class InMemoryConnectionRegistryTests
Assert.Null(channel);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetChannel_ReturnsExistingChannel()
{
// Arrange
@@ -58,7 +64,8 @@ public class InMemoryConnectionRegistryTests
Assert.Same(created, retrieved);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RemoveChannel_RemovesAndDisposes()
{
// Arrange
@@ -74,7 +81,8 @@ public class InMemoryConnectionRegistryTests
Assert.True(channel.LifetimeToken.IsCancellationRequested);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void RemoveChannel_ReturnsFalseForUnknown()
{
// Arrange
@@ -87,7 +95,8 @@ public class InMemoryConnectionRegistryTests
Assert.False(removed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Dispose_DisposesAllChannels()
{
// Arrange

View File

@@ -5,6 +5,7 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.InMemory.Tests;
public class RequestResponseFlowTests
@@ -25,7 +26,8 @@ public class RequestResponseFlowTests
_client = provider.GetRequiredService<InMemoryTransportClient>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RequestResponse_RoundTrip()
{
// Arrange
@@ -93,7 +95,8 @@ public class RequestResponseFlowTests
Assert.Equal(1, _registry.Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SendRequestAsync_TimesOut()
{
// Arrange

View File

@@ -21,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,6 +5,8 @@ using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.InMemory;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.InMemory.Tests;
public class StreamingFlowTests
@@ -25,7 +27,8 @@ public class StreamingFlowTests
_client = provider.GetRequiredService<InMemoryTransportClient>();
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SendStreamingAsync_SendsHeaderAndDataFrames()
{
// Arrange
@@ -93,7 +96,8 @@ public class StreamingFlowTests
Assert.Equal(1, _registry.Count);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task RequestStreamData_IsHandled()
{
// Arrange

View File

@@ -21,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Router.Transport.Udp/StellaOps.Router.Transport.Udp.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -8,6 +8,8 @@ using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Router.Transport.Udp.Tests;
public class UdpTransportTests
@@ -17,7 +19,8 @@ public class UdpTransportTests
private static int GetNextPort() => BasePort + Interlocked.Increment(ref _portOffset);
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UdpFrameProtocol_SerializeAndParse_RoundTrip()
{
// Arrange
@@ -38,7 +41,8 @@ public class UdpTransportTests
Assert.Equal(originalFrame.Payload.ToArray(), parsed.Payload.ToArray());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UdpFrameProtocol_ParseFrame_WithEmptyPayload()
{
// Arrange
@@ -58,7 +62,8 @@ public class UdpTransportTests
Assert.Empty(parsed.Payload.ToArray());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void UdpFrameProtocol_ParseFrame_ThrowsOnTooSmallDatagram()
{
// Arrange
@@ -68,7 +73,8 @@ public class UdpTransportTests
Assert.Throws<InvalidOperationException>(() => UdpFrameProtocol.ParseFrame(tooSmall));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PayloadTooLargeException_HasCorrectProperties()
{
// Arrange & Act
@@ -81,7 +87,8 @@ public class UdpTransportTests
Assert.Contains("8192", exception.Message);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransportServer_StartsAndStops()
{
// Arrange
@@ -108,7 +115,8 @@ public class UdpTransportTests
await server.StopAsync(CancellationToken.None);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransportClient_ConnectsAndDisconnects()
{
// Arrange
@@ -153,7 +161,8 @@ public class UdpTransportTests
await server.StopAsync(CancellationToken.None);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransport_RequestResponse_Works()
{
// Arrange
@@ -234,7 +243,8 @@ public class UdpTransportTests
await server.StopAsync(CancellationToken.None);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransport_PayloadTooLarge_ThrowsException()
{
// Arrange
@@ -300,7 +310,8 @@ public class UdpTransportTests
await server.StopAsync(CancellationToken.None);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransport_StreamingNotSupported_ThrowsNotSupportedException()
{
// Arrange
@@ -347,7 +358,8 @@ public class UdpTransportTests
CancellationToken.None));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransport_Timeout_ThrowsTimeoutException()
{
// Arrange
@@ -411,7 +423,8 @@ public class UdpTransportTests
await server.StopAsync(CancellationToken.None);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ServiceCollectionExtensions_RegistersServerCorrectly()
{
// Arrange
@@ -433,7 +446,8 @@ public class UdpTransportTests
Assert.Same(server, udpServer);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ServiceCollectionExtensions_RegistersClientCorrectly()
{
// Arrange
@@ -459,7 +473,8 @@ public class UdpTransportTests
Assert.Same(microserviceTransport, udpClient);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UdpTransport_HeartbeatSent()
{
// Arrange

View File

@@ -16,5 +16,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\VulnExplorer\\StellaOps.VulnExplorer.Api\\StellaOps.VulnExplorer.Api.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc.Testing;
using StellaOps.VulnExplorer.Api.Models;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.VulnExplorer.Api.Tests;
public class VulnApiTests : IClassFixture<WebApplicationFactory<Program>>
@@ -15,7 +16,8 @@ public class VulnApiTests : IClassFixture<WebApplicationFactory<Program>>
this.factory = factory.WithWebHostBuilder(_ => { });
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_ReturnsDeterministicOrder()
{
var client = factory.CreateClient();
@@ -29,7 +31,8 @@ public class VulnApiTests : IClassFixture<WebApplicationFactory<Program>>
Assert.Equal(new[] { "vuln-0001", "vuln-0002" }, payload!.Items.Select(v => v.Id));
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_FiltersByCve()
{
var client = factory.CreateClient();
@@ -43,7 +46,8 @@ public class VulnApiTests : IClassFixture<WebApplicationFactory<Program>>
Assert.Equal("vuln-0002", payload.Items[0].Id);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Detail_ReturnsNotFoundWhenMissing()
{
var client = factory.CreateClient();
@@ -53,7 +57,8 @@ public class VulnApiTests : IClassFixture<WebApplicationFactory<Program>>
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Detail_ReturnsRationaleAndPaths()
{
var client = factory.CreateClient();

View File

@@ -0,0 +1,302 @@
// -----------------------------------------------------------------------------
// BinaryLookupBenchmarks.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-20 - Performance benchmarks for binary lookup
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using BenchmarkDotNet.Attributes;
using StellaOps.BinaryIndex.Core.Models;
using StellaOps.BinaryIndex.Core.Services;
namespace StellaOps.Bench.BinaryLookup.Benchmarks;
/// <summary>
/// Performance benchmarks for binary vulnerability lookup operations.
/// Measures single and batch lookup performance, cache efficiency,
/// and overall throughput for scanner integration.
/// </summary>
[MemoryDiagnoser]
[SimpleJob(warmupCount: 3, iterationCount: 10)]
public class BinaryLookupBenchmarks
{
private readonly BinaryIdentity[] _testIdentities = GenerateTestIdentities(100);
private readonly byte[][] _testFingerprints = GenerateTestFingerprints(100);
[Params(1, 10, 50, 100)]
public int BatchSize { get; set; }
/// <summary>
/// Generate sample binary identities for benchmarking.
/// </summary>
private static BinaryIdentity[] GenerateTestIdentities(int count)
{
return Enumerable.Range(0, count).Select(i => new BinaryIdentity
{
Format = BinaryFormat.Elf,
BuildId = GenerateBuildId(i),
FileSha256 = GenerateSha256(i),
Architecture = "x86_64",
BinaryKey = $"libtest{i}:1.0.{i}"
}).ToArray();
}
/// <summary>
/// Generate sample fingerprints for benchmarking.
/// </summary>
private static byte[][] GenerateTestFingerprints(int count)
{
return Enumerable.Range(0, count)
.Select(i => GenerateFingerprintBytes(i))
.ToArray();
}
private static string GenerateBuildId(int seed)
{
// Generate deterministic 40-char hex build ID
var bytes = new byte[20];
for (int i = 0; i < 20; i++)
{
bytes[i] = (byte)((seed + i * 17) % 256);
}
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static string GenerateSha256(int seed)
{
var bytes = new byte[32];
for (int i = 0; i < 32; i++)
{
bytes[i] = (byte)((seed + i * 31) % 256);
}
return "sha256:" + Convert.ToHexString(bytes).ToLowerInvariant();
}
private static byte[] GenerateFingerprintBytes(int seed)
{
var bytes = new byte[64];
for (int i = 0; i < 64; i++)
{
bytes[i] = (byte)((seed + i * 13) % 256);
}
return bytes;
}
/// <summary>
/// Benchmark binary identity extraction from Build-ID.
/// Target: < 1ms per identity
/// </summary>
[Benchmark(Description = "Identity extraction from Build-ID")]
public BinaryIdentity BenchmarkIdentityExtraction()
{
return new BinaryIdentity
{
Format = BinaryFormat.Elf,
BuildId = "8d8f09a0d7e2c1b3a5f4e6d8c0b2a4e6f8d0c2b4",
FileSha256 = "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
Architecture = "x86_64",
BinaryKey = "openssl:1.1.1w-1"
};
}
/// <summary>
/// Benchmark batch identity creation for scanner integration.
/// Target: < 10ms per batch of 100
/// </summary>
[Benchmark(Description = "Batch identity creation")]
public ImmutableArray<BinaryIdentity> BenchmarkBatchIdentityCreation()
{
return _testIdentities.Take(BatchSize)
.ToImmutableArray();
}
/// <summary>
/// Benchmark fingerprint hash computation.
/// Target: < 5ms per fingerprint
/// </summary>
[Benchmark(Description = "Fingerprint hash computation")]
public string BenchmarkFingerprintHash()
{
var fingerprint = _testFingerprints[0];
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = sha256.ComputeHash(fingerprint);
return Convert.ToHexString(hash);
}
/// <summary>
/// Benchmark batch fingerprint comparison.
/// Target: < 100ms for 100 comparisons
/// </summary>
[Benchmark(Description = "Batch fingerprint comparison")]
public int BenchmarkBatchFingerprintComparison()
{
var target = _testFingerprints[0];
var matches = 0;
for (int i = 0; i < BatchSize; i++)
{
var candidate = _testFingerprints[i];
var similarity = ComputeHammingSimilarity(target, candidate);
if (similarity > 0.7)
{
matches++;
}
}
return matches;
}
/// <summary>
/// Benchmark binary key generation for lookup.
/// Target: < 0.1ms per key
/// </summary>
[Benchmark(Description = "Binary key generation")]
public string BenchmarkBinaryKeyGeneration()
{
var identity = _testIdentities[0];
return $"{identity.BinaryKey}:{identity.Architecture}:{identity.Format}";
}
/// <summary>
/// Benchmark lookup key construction with distro.
/// Target: < 0.1ms per key
/// </summary>
[Benchmark(Description = "Distro-aware lookup key")]
public string BenchmarkDistroLookupKey()
{
var identity = _testIdentities[0];
var distro = "debian";
var release = "bookworm";
return $"{distro}:{release}:{identity.BinaryKey}";
}
private static double ComputeHammingSimilarity(byte[] a, byte[] b)
{
if (a.Length != b.Length) return 0.0;
var matching = 0;
var total = a.Length * 8;
for (int i = 0; i < a.Length; i++)
{
var xor = (byte)(a[i] ^ b[i]);
matching += 8 - PopCount(xor);
}
return (double)matching / total;
}
private static int PopCount(byte b)
{
var count = 0;
while (b != 0)
{
count += b & 1;
b >>= 1;
}
return count;
}
}
/// <summary>
/// Benchmarks for cache layer performance.
/// </summary>
[MemoryDiagnoser]
[SimpleJob(warmupCount: 3, iterationCount: 10)]
public class CacheLayerBenchmarks
{
private readonly Dictionary<string, object> _mockCache = new();
private readonly string[] _testKeys;
public CacheLayerBenchmarks()
{
// Pre-populate cache with test data
_testKeys = Enumerable.Range(0, 1000)
.Select(i => $"binary:tenant1:buildid{i:D8}")
.ToArray();
foreach (var key in _testKeys.Take(800)) // 80% pre-cached
{
_mockCache[key] = new { Matches = Array.Empty<object>() };
}
}
[Params(10, 100, 500)]
public int LookupCount { get; set; }
/// <summary>
/// Benchmark cache hit performance.
/// Target: > 80% hit rate, < 0.01ms per hit
/// </summary>
[Benchmark(Description = "Cache hit lookup")]
public int BenchmarkCacheHits()
{
var hits = 0;
for (int i = 0; i < LookupCount; i++)
{
var key = _testKeys[i % 800]; // Always hit
if (_mockCache.ContainsKey(key))
{
hits++;
}
}
return hits;
}
/// <summary>
/// Benchmark cache miss handling.
/// Target: < 1ms per miss for key generation
/// </summary>
[Benchmark(Description = "Cache miss handling")]
public int BenchmarkCacheMisses()
{
var misses = 0;
for (int i = 0; i < LookupCount; i++)
{
var key = _testKeys[800 + (i % 200)]; // Always miss
if (!_mockCache.ContainsKey(key))
{
misses++;
}
}
return misses;
}
/// <summary>
/// Benchmark mixed workload (realistic scenario).
/// Target: > 80% hit rate overall
/// </summary>
[Benchmark(Description = "Mixed cache workload")]
public (int hits, int misses) BenchmarkMixedWorkload()
{
var hits = 0;
var misses = 0;
var random = new Random(42); // Deterministic seed
for (int i = 0; i < LookupCount; i++)
{
var key = _testKeys[random.Next(_testKeys.Length)];
if (_mockCache.ContainsKey(key))
{
hits++;
}
else
{
misses++;
}
}
return (hits, misses);
}
/// <summary>
/// Benchmark cache key generation.
/// </summary>
[Benchmark(Description = "Cache key generation")]
public string BenchmarkCacheKeyGeneration()
{
var tenant = "tenant1";
var buildId = "8d8f09a0d7e2c1b3a5f4e6d8c0b2a4e6f8d0c2b4";
return $"binary:{tenant}:{buildId}";
}
}

View File

@@ -0,0 +1,20 @@
// -----------------------------------------------------------------------------
// Program.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-20 - Performance benchmarks for binary lookup
// -----------------------------------------------------------------------------
using BenchmarkDotNet.Running;
namespace StellaOps.Bench.BinaryLookup;
/// <summary>
/// Entry point for binary lookup benchmark suite.
/// </summary>
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" Condition="'$(OS)' == 'Windows_NT'" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Core\StellaOps.BinaryIndex.Core.csproj" />
<ProjectReference Include="..\..\..\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Fingerprints\StellaOps.BinaryIndex.Fingerprints.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
source "https://rubygems.org"
git "https://github.com/example/git-gem.git", branch: "main" do
gem "git-gem"
end
gem "httparty", "~> 0.21.0"
path "../vendor/path-gem" do
gem "path-gem", "~> 2.1"
end

View File

@@ -0,0 +1,31 @@
GIT
remote: https://github.com/example/git-gem.git
revision: 0123456789abcdef0123456789abcdef01234567
branch: main
specs:
git-gem (0.5.0)
PATH
remote: vendor/plugins/path-gem
specs:
path-gem (2.1.3)
rake (~> 13.0)
GEM
remote: https://rubygems.org/
specs:
httparty (0.21.0)
multi_xml (~> 0.5)
multi_xml (0.6.0)
rake (13.1.0)
PLATFORMS
ruby
DEPENDENCIES
git-gem!
httparty (~> 0.21.0)
path-gem (~> 2.1)!
BUNDLED WITH
2.5.10

View File

@@ -0,0 +1,7 @@
require "git-gem"
require "path-gem"
require "httparty"
puts GitGem.version
puts PathGem::Runner.new.perform
puts HTTParty.get("https://example.invalid")

View File

@@ -0,0 +1,130 @@
[
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/git-gem@0.5.0",
"purl": "pkg:gem/git-gem@0.5.0",
"name": "git-gem",
"version": "0.5.0",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"capability.net": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "app/main.rb",
"runtime.files": "app/main.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "git:https://github.com/example/git-gem.git@0123456789abcdef0123456789abcdef01234567"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/httparty@0.21.0",
"purl": "pkg:gem/httparty@0.21.0",
"name": "httparty",
"version": "0.21.0",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"capability.net": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "app/main.rb",
"runtime.files": "app/main.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/multi_xml@0.6.0",
"purl": "pkg:gem/multi_xml@0.6.0",
"name": "multi_xml",
"version": "0.6.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.net": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/path-gem@2.1.3",
"purl": "pkg:gem/path-gem@2.1.3",
"name": "path-gem",
"version": "2.1.3",
"type": "gem",
"usedByEntrypoint": true,
"metadata": {
"artifact": "vendor/cache/path-gem-2.1.3.gem",
"capability.net": "true",
"declaredOnly": "false",
"groups": "default",
"lockfile": "Gemfile.lock",
"runtime.entrypoints": "app/main.rb",
"runtime.files": "app/main.rb",
"runtime.reasons": "require-static",
"runtime.used": "true",
"source": "vendor-cache"
},
"evidence": [
{
"kind": "file",
"source": "path-gem-2.1.3.gem",
"locator": "vendor/cache/path-gem-2.1.3.gem"
}
]
},
{
"analyzerId": "ruby",
"componentKey": "purl::pkg:gem/rake@13.1.0",
"purl": "pkg:gem/rake@13.1.0",
"name": "rake",
"version": "13.1.0",
"type": "gem",
"usedByEntrypoint": false,
"metadata": {
"capability.net": "true",
"declaredOnly": "true",
"groups": "default",
"lockfile": "Gemfile.lock",
"source": "https://rubygems.org/"
},
"evidence": [
{
"kind": "file",
"source": "Gemfile.lock",
"locator": "Gemfile.lock"
}
]
}
]

Some files were not shown because too many files have changed in this diff Show More