Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.AirGap.Importer.Tests;
|
||||
|
||||
public class DsseVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FailsWhenUntrustedKey()
|
||||
{
|
||||
var verifier = new DsseVerifier();
|
||||
@@ -18,10 +19,12 @@ public class DsseVerifierTests
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifiesRsaPssSignature()
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
using StellaOps.TestKit;
|
||||
var pub = rsa.ExportSubjectPublicKeyInfo();
|
||||
var payload = "hello-world";
|
||||
var payloadType = "application/vnd.stella.bundle";
|
||||
|
||||
@@ -10,7 +10,8 @@ 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 +55,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\"}";
|
||||
@@ -62,6 +64,7 @@ public sealed class ImportValidatorTests
|
||||
var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
using StellaOps.TestKit;
|
||||
var pub = rsa.ExportSubjectPublicKeyInfo();
|
||||
|
||||
var payload = "bundle-body";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -32,7 +32,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 +48,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 +64,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 +79,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,11 +94,13 @@ 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();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
metrics.RecordRekorInclusionLatency(seconds: 0.5, success: false);
|
||||
|
||||
Assert.Contains(_measurements, m =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 } };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -23,7 +23,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 +43,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");
|
||||
@@ -56,6 +58,7 @@ public sealed class SbomIngestServiceCollectionExtensionsTests : IDisposable
|
||||
services.AddSbomIngestPipeline();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var processor = provider.GetRequiredService<SbomIngestProcessor>();
|
||||
|
||||
var snapshot = LoadSnapshot();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -6,19 +6,22 @@ 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");
|
||||
var key = Convert.ToHexString(Encoding.UTF8.GetBytes("secret"));
|
||||
using var hmac = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes("secret"));
|
||||
using StellaOps.TestKit;
|
||||
var sig = Convert.ToHexString(hmac.ComputeHash(payload));
|
||||
|
||||
var tmp = Path.GetTempFileName();
|
||||
|
||||
@@ -6,7 +6,8 @@ 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 +23,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 +39,8 @@ public sealed class VerificationLibraryTests
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MerkleRootVerifier_DetectsMismatch()
|
||||
{
|
||||
var leaves = new[]
|
||||
@@ -54,7 +57,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[]
|
||||
@@ -64,6 +68,7 @@ public sealed class VerificationLibraryTests
|
||||
};
|
||||
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
using StellaOps.TestKit;
|
||||
var aggregate = sha.ComputeHash(Array.Empty<byte>().Concat(hops[0]).ToArray());
|
||||
aggregate = sha.ComputeHash(aggregate.Concat(hops[1]).ToArray());
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -17,7 +17,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 +45,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 +88,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();
|
||||
@@ -103,6 +106,7 @@ public sealed class RequestDispatcherTests
|
||||
services.AddTransient<GetItemHandler>();
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var dispatcher = new RequestDispatcher(
|
||||
registry,
|
||||
provider,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,8 @@ public class TypedEndpointAdapterTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Adapt_TypedWithRequest_DeserializesAndSerializes()
|
||||
{
|
||||
var handler = new TestTypedHandler();
|
||||
@@ -69,7 +70,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 +95,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 +115,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 +135,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 +155,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();
|
||||
@@ -187,6 +193,7 @@ public class TypedEndpointAdapterTests
|
||||
|
||||
response.Body.Position = 0;
|
||||
using var reader = new StreamReader(response.Body);
|
||||
using StellaOps.TestKit;
|
||||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace StellaOps.Router.Config.Tests;
|
||||
|
||||
public class RouterConfigTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RouterConfig_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -23,7 +24,8 @@ public class RouterConfigTests
|
||||
config.StaticInstances.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RoutingOptions_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -37,7 +39,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 +62,8 @@ public class RouterConfigTests
|
||||
instance.Weight.Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RouterConfigOptions_HasDefaultValues()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -76,7 +80,8 @@ public class RouterConfigTests
|
||||
|
||||
public class RouterConfigProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ReturnsSuccess_ForValidConfig()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +97,8 @@ public class RouterConfigProviderTests
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Current_ReturnsDefaultConfig_WhenNoFileSpecified()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +118,8 @@ public class RouterConfigProviderTests
|
||||
|
||||
public class ConfigValidationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validation_Fails_WhenPayloadLimitsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -127,7 +134,8 @@ public class ConfigValidationTests
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConfigValidationResult_Success_HasNoErrors()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -138,7 +146,8 @@ public class ConfigValidationTests
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ConfigValidationResult_WithErrors_IsNotValid()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -155,7 +164,8 @@ public class ConfigValidationTests
|
||||
|
||||
public class ServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddRouterConfig_RegistersServices()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +182,8 @@ public class ServiceCollectionExtensionsTests
|
||||
configProvider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddRouterConfig_WithPath_SetsConfigPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -191,7 +202,8 @@ public class ServiceCollectionExtensionsTests
|
||||
configProvider!.Options.ConfigPath.Should().Be(path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddRouterConfigFromYaml_SetsConfigPath()
|
||||
{
|
||||
// Arrange
|
||||
@@ -214,7 +226,8 @@ public class ServiceCollectionExtensionsTests
|
||||
|
||||
public class ConfigChangedEventArgsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
// Arrange
|
||||
@@ -243,7 +256,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 +310,8 @@ routing:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReloadAsync_LoadsNewConfig()
|
||||
{
|
||||
// Arrange
|
||||
@@ -314,6 +329,7 @@ routing:
|
||||
var logger = NullLogger<RouterConfigProvider>.Instance;
|
||||
using var provider = new RouterConfigProvider(options, logger);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
provider.Current.Routing.LocalRegion.Should().Be("eu1");
|
||||
|
||||
// Act - update file and manually reload
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,8 @@ 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 +46,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 +86,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 +131,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");
|
||||
@@ -173,6 +177,7 @@ public sealed class MiddlewareErrorScenarioTests
|
||||
{
|
||||
context.Response.Body.Position = 0;
|
||||
using var doc = JsonDocument.Parse(context.Response.Body);
|
||||
using StellaOps.TestKit;
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,7 +11,8 @@ 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
|
||||
@@ -86,6 +87,7 @@ public sealed class RateLimitMiddlewareTests
|
||||
var body = await new StreamReader(context.Response.Body, Encoding.UTF8).ReadToEndAsync();
|
||||
using var json = JsonDocument.Parse(body);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
json.RootElement.GetProperty("error").GetString().Should().Be("rate_limit_exceeded");
|
||||
json.RootElement.GetProperty("scope").GetString().Should().Be("environment");
|
||||
json.RootElement.GetProperty("limit").GetInt64().Should().Be(1);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,8 @@ 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 +23,8 @@ public sealed class RouterNodeConfigValidationTests
|
||||
act.Should().Throw<OptionsValidationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RouterNodeConfig_WhenRegionProvided_GeneratesNodeIdIfMissing()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -31,6 +33,7 @@ public sealed class RouterNodeConfigValidationTests
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var config = provider.GetRequiredService<IOptions<RouterNodeConfig>>().Value;
|
||||
|
||||
config.Region.Should().Be("test");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Router.Transport.InMemory.Tests;
|
||||
|
||||
public class InMemoryChannelTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ToMicroservice_WritesAndReads()
|
||||
{
|
||||
// Arrange
|
||||
@@ -28,7 +29,8 @@ public class InMemoryChannelTests
|
||||
Assert.Equal("corr-1", readFrame.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ToGateway_WritesAndReads()
|
||||
{
|
||||
// Arrange
|
||||
@@ -49,7 +51,8 @@ public class InMemoryChannelTests
|
||||
Assert.Equal("corr-1", readFrame.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CancelsLifetimeToken()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +65,8 @@ public class InMemoryChannelTests
|
||||
Assert.True(channel.LifetimeToken.IsCancellationRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_CompletesChannels()
|
||||
{
|
||||
// Arrange
|
||||
@@ -76,7 +80,8 @@ public class InMemoryChannelTests
|
||||
Assert.True(channel.ToGateway.Reader.Completion.IsCompleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BoundedChannel_RespectsBufferSize()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -86,11 +91,13 @@ public class InMemoryChannelTests
|
||||
Assert.NotNull(channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Instance_CanBeSetAndRetrieved()
|
||||
{
|
||||
// Arrange
|
||||
using var channel = new InMemoryChannel("test-1");
|
||||
using StellaOps.TestKit;
|
||||
var instance = new InstanceDescriptor
|
||||
{
|
||||
InstanceId = "inst-1",
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace StellaOps.Router.Transport.InMemory.Tests;
|
||||
|
||||
public class InMemoryConnectionRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateChannel_CreatesNewChannel()
|
||||
{
|
||||
// Arrange
|
||||
@@ -20,7 +21,8 @@ public class InMemoryConnectionRegistryTests
|
||||
Assert.Equal(1, registry.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreateChannel_ThrowsIfDuplicate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -31,7 +33,8 @@ public class InMemoryConnectionRegistryTests
|
||||
Assert.Throws<InvalidOperationException>(() => registry.CreateChannel("conn-1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetChannel_ReturnsNullForUnknown()
|
||||
{
|
||||
// Arrange
|
||||
@@ -44,7 +47,8 @@ public class InMemoryConnectionRegistryTests
|
||||
Assert.Null(channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetChannel_ReturnsExistingChannel()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +62,8 @@ public class InMemoryConnectionRegistryTests
|
||||
Assert.Same(created, retrieved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RemoveChannel_RemovesAndDisposes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,12 +79,14 @@ public class InMemoryConnectionRegistryTests
|
||||
Assert.True(channel.LifetimeToken.IsCancellationRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RemoveChannel_ReturnsFalseForUnknown()
|
||||
{
|
||||
// Arrange
|
||||
using var registry = new InMemoryConnectionRegistry();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
var removed = registry.RemoveChannel("unknown");
|
||||
|
||||
@@ -87,7 +94,8 @@ public class InMemoryConnectionRegistryTests
|
||||
Assert.False(removed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Dispose_DisposesAllChannels()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,8 @@ public class StreamingFlowTests
|
||||
_client = provider.GetRequiredService<InMemoryTransportClient>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SendStreamingAsync_SendsHeaderAndDataFrames()
|
||||
{
|
||||
// Arrange
|
||||
@@ -69,6 +70,7 @@ public class StreamingFlowTests
|
||||
var testData = Encoding.UTF8.GetBytes("Test streaming data");
|
||||
using var requestBody = new MemoryStream(testData);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
Func<Stream, Task> readResponse = _ => Task.CompletedTask;
|
||||
|
||||
// Act - this will send header + data frames
|
||||
@@ -93,7 +95,8 @@ public class StreamingFlowTests
|
||||
Assert.Equal(1, _registry.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RequestStreamData_IsHandled()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -17,7 +17,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 +39,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 +60,8 @@ public class UdpTransportTests
|
||||
Assert.Empty(parsed.Payload.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UdpFrameProtocol_ParseFrame_ThrowsOnTooSmallDatagram()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +71,8 @@ public class UdpTransportTests
|
||||
Assert.Throws<InvalidOperationException>(() => UdpFrameProtocol.ParseFrame(tooSmall));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PayloadTooLargeException_HasCorrectProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -81,7 +85,8 @@ public class UdpTransportTests
|
||||
Assert.Contains("8192", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransportServer_StartsAndStops()
|
||||
{
|
||||
// Arrange
|
||||
@@ -108,7 +113,8 @@ public class UdpTransportTests
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransportClient_ConnectsAndDisconnects()
|
||||
{
|
||||
// Arrange
|
||||
@@ -153,7 +159,8 @@ public class UdpTransportTests
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransport_RequestResponse_Works()
|
||||
{
|
||||
// Arrange
|
||||
@@ -234,7 +241,8 @@ public class UdpTransportTests
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransport_PayloadTooLarge_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -300,7 +308,8 @@ public class UdpTransportTests
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransport_StreamingNotSupported_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -347,7 +356,8 @@ public class UdpTransportTests
|
||||
CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransport_Timeout_ThrowsTimeoutException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -411,7 +421,8 @@ public class UdpTransportTests
|
||||
await server.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceCollectionExtensions_RegistersServerCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -433,7 +444,8 @@ public class UdpTransportTests
|
||||
Assert.Same(server, udpServer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceCollectionExtensions_RegistersClientCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -459,7 +471,8 @@ public class UdpTransportTests
|
||||
Assert.Same(microserviceTransport, udpClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UdpTransport_HeartbeatSent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -480,6 +493,7 @@ public class UdpTransportTests
|
||||
});
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var server = provider.GetRequiredService<UdpTransportServer>();
|
||||
var client = provider.GetRequiredService<UdpTransportClient>();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
20
src/__Tests/__Benchmarks/binary-lookup/Program.cs
Normal file
20
src/__Tests/__Benchmarks/binary-lookup/Program.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -10,7 +10,8 @@ public class CorpusFixtureTests
|
||||
private static readonly string RepoRoot = ReachbenchFixtureTests.LocateRepoRoot();
|
||||
private static readonly string CorpusRoot = Path.Combine(RepoRoot, "tests", "reachability", "corpus");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestExistsAndIsDeterministic()
|
||||
{
|
||||
var manifestPath = Path.Combine(CorpusRoot, "manifest.json");
|
||||
@@ -21,7 +22,8 @@ public class CorpusFixtureTests
|
||||
doc.RootElement.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CorpusEntriesMatchManifestHashes()
|
||||
{
|
||||
var manifestPath = Path.Combine(CorpusRoot, "manifest.json");
|
||||
@@ -53,7 +55,8 @@ public class CorpusFixtureTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GroundTruthFilesContainRequiredFields()
|
||||
{
|
||||
var manifestPath = Path.Combine(CorpusRoot, "manifest.json");
|
||||
@@ -69,6 +72,7 @@ public class CorpusFixtureTests
|
||||
File.Exists(truthPath).Should().BeTrue($"{id} missing ground-truth.json");
|
||||
|
||||
using var truthDoc = JsonDocument.Parse(File.ReadAllBytes(truthPath));
|
||||
using StellaOps.TestKit;
|
||||
truthDoc.RootElement.GetProperty("schema_version").GetString().Should().Be(expectedSchemaVersion, $"{id} ground-truth schema_version mismatch");
|
||||
truthDoc.RootElement.GetProperty("case_id").GetString().Should().Be(id, $"{id} ground-truth case_id must match manifest id");
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ public sealed class FixtureCoverageTests
|
||||
private static readonly string CorpusRoot = Path.Combine(ReachabilityRoot, "corpus");
|
||||
private static readonly string SamplesPublicRoot = Path.Combine(ReachabilityRoot, "samples-public");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CorpusAndPublicSamplesCoverExpectedLanguageBuckets()
|
||||
{
|
||||
var corpusLanguages = ReadManifestLanguages(Path.Combine(CorpusRoot, "manifest.json"));
|
||||
@@ -21,7 +22,8 @@ public sealed class FixtureCoverageTests
|
||||
samplesLanguages.Should().Contain(new[] { "csharp", "js", "php" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CorpusManifestIsSorted()
|
||||
{
|
||||
var keys = ReadManifestKeys(Path.Combine(CorpusRoot, "manifest.json"));
|
||||
@@ -48,6 +50,7 @@ public sealed class FixtureCoverageTests
|
||||
File.Exists(manifestPath).Should().BeTrue($"{manifestPath} should exist");
|
||||
|
||||
using var doc = JsonDocument.Parse(File.ReadAllBytes(manifestPath));
|
||||
using StellaOps.TestKit;
|
||||
return doc.RootElement.EnumerateArray()
|
||||
.Select(entry => $"{entry.GetProperty("language").GetString()}/{entry.GetProperty("id").GetString()}")
|
||||
.ToArray();
|
||||
|
||||
@@ -7,6 +7,7 @@ using StellaOps.Reachability.FixtureTests.PatchOracle;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Reachability.FixtureTests;
|
||||
|
||||
/// <summary>
|
||||
@@ -21,14 +22,16 @@ public class PatchOracleHarnessTests
|
||||
|
||||
#region Oracle Loading Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Loader_IndexExists()
|
||||
{
|
||||
var loader = new PatchOracleLoader(PatchOracleRoot);
|
||||
loader.IndexExists().Should().BeTrue("patch-oracle INDEX.json should exist");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Loader_IndexLoadsSuccessfully()
|
||||
{
|
||||
var loader = new PatchOracleLoader(PatchOracleRoot);
|
||||
@@ -40,7 +43,8 @@ public class PatchOracleHarnessTests
|
||||
index.Oracles.Should().NotBeEmpty("should have at least one oracle defined");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Loader_AllOraclesLoadSuccessfully()
|
||||
{
|
||||
var loader = new PatchOracleLoader(PatchOracleRoot);
|
||||
@@ -56,7 +60,8 @@ public class PatchOracleHarnessTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Loader_LoadOracleById()
|
||||
{
|
||||
var loader = new PatchOracleLoader(PatchOracleRoot);
|
||||
@@ -72,7 +77,8 @@ public class PatchOracleHarnessTests
|
||||
|
||||
#region Comparer Tests - Pass Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_PassesWhenAllExpectedElementsPresent()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -112,7 +118,8 @@ public class PatchOracleHarnessTests
|
||||
result.Violations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_PassesWithWildcardPatterns()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -146,7 +153,8 @@ public class PatchOracleHarnessTests
|
||||
|
||||
#region Comparer Tests - Fail Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_FailsWhenExpectedFunctionMissing()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -177,7 +185,8 @@ public class PatchOracleHarnessTests
|
||||
result.Summary.MissingFunctions.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_FailsWhenExpectedEdgeMissing()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -211,7 +220,8 @@ public class PatchOracleHarnessTests
|
||||
result.Summary.MissingEdges.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_FailsWhenExpectedRootMissing()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -241,7 +251,8 @@ public class PatchOracleHarnessTests
|
||||
result.Summary.MissingRoots.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_FailsWhenForbiddenFunctionPresent()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -274,7 +285,8 @@ public class PatchOracleHarnessTests
|
||||
result.Summary.ForbiddenFunctionsPresent.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_FailsWhenForbiddenEdgePresent()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -311,7 +323,8 @@ public class PatchOracleHarnessTests
|
||||
|
||||
#region Confidence Threshold Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_RespectsMinConfidenceThreshold()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -343,7 +356,8 @@ public class PatchOracleHarnessTests
|
||||
result.Summary.MissingEdges.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_EdgeSpecificConfidenceOverridesDefault()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -378,7 +392,8 @@ public class PatchOracleHarnessTests
|
||||
|
||||
#region Strict Mode Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Comparer_StrictModeRejectsUnexpectedNodes()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -416,7 +431,8 @@ public class PatchOracleHarnessTests
|
||||
|
||||
#region Report Generation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Result_GeneratesReadableReport()
|
||||
{
|
||||
var oracle = new PatchOracleDefinition
|
||||
@@ -469,7 +485,8 @@ public class PatchOracleHarnessTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(AllOracleData))]
|
||||
public void AllOracles_HaveValidStructure(string oracleId, string caseRef, string variant)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Reachability.Lifters;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Reachability.FixtureTests;
|
||||
|
||||
public sealed class ReachabilityLifterTests : IDisposable
|
||||
@@ -35,7 +36,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NodeLifter_ExtractsPackageInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +76,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Edges.Should().Contain(e => e.EdgeType == "import");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NodeLifter_ExtractsEntrypoints()
|
||||
{
|
||||
// Arrange
|
||||
@@ -110,7 +113,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Edges.Should().Contain(e => e.EdgeType == "spawn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NodeLifter_ExtractsImportsFromSource()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,7 +151,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Edges.Count(e => e.EdgeType == "import").Should().BeGreaterOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DotNetLifter_ExtractsProjectInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +194,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Edges.Count(e => e.EdgeType == "import").Should().BeGreaterOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DotNetLifter_ExtractsProjectReferences()
|
||||
{
|
||||
// Arrange
|
||||
@@ -237,7 +243,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Edges.Should().Contain(e => e.EdgeType == "import");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LifterRegistry_CombinesMultipleLanguages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -279,7 +286,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Nodes.Should().Contain(n => n.Lang == "dotnet");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LifterRegistry_SelectsSpecificLanguages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -305,7 +313,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
graph.Nodes.Should().OnlyContain(n => n.Lang == "node");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LifterRegistry_LiftAndWrite_CreatesOutputFiles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -339,7 +348,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
result.Nodes.RecordCount.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphBuilder_AddsRichNodes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -373,7 +383,8 @@ public sealed class ReachabilityLifterTests : IDisposable
|
||||
node.Source!.Evidence.Should().Contain("src/main.ts:42");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphBuilder_AddsRichEdges()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.Replay.Core;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Reachability.FixtureTests;
|
||||
|
||||
public sealed class ReachabilityReplayWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AttachEvidence_AppendsGraphsAndTracesDeterministically()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
@@ -48,7 +50,8 @@ public sealed class ReachabilityReplayWriterTests
|
||||
manifest.Reachability.RuntimeTraces[1].CasUri.Should().Be("cas://trace/2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AttachEvidence_DoesNotCreateSectionWhenEmpty()
|
||||
{
|
||||
var manifest = new ReplayManifest();
|
||||
|
||||
@@ -22,7 +22,8 @@ public class ReachbenchEvaluationHarnessTests
|
||||
.Select(path => new object[] { Path.GetFileName(path)! });
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(CaseIds))]
|
||||
public void GroundTruthStatusesMatchVariantIntent(string caseId)
|
||||
{
|
||||
@@ -45,7 +46,8 @@ public class ReachbenchEvaluationHarnessTests
|
||||
.Be("not_affected", $"{caseId} unreachable variant should be marked not_affected for evaluation harness");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(CaseIds))]
|
||||
public void TruthGraphsAlignWithExpectedReachability(string caseId)
|
||||
{
|
||||
@@ -62,6 +64,7 @@ public class ReachbenchEvaluationHarnessTests
|
||||
File.Exists(truthPath).Should().BeTrue();
|
||||
|
||||
using var truthDoc = JsonDocument.Parse(File.ReadAllBytes(truthPath));
|
||||
using StellaOps.TestKit;
|
||||
var paths = truthDoc.RootElement.GetProperty("paths");
|
||||
paths.ValueKind.Should().Be(JsonValueKind.Array, $"{caseId}:{variant} should list truth paths as an array");
|
||||
return paths.GetArrayLength();
|
||||
|
||||
@@ -13,7 +13,8 @@ public class ReachbenchFixtureTests
|
||||
RepoRoot, "tests", "reachability", "fixtures", "reachbench-2025-expanded");
|
||||
private static readonly string CasesRoot = Path.Combine(FixtureRoot, "cases");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void IndexListsAllCases()
|
||||
{
|
||||
Directory.Exists(FixtureRoot).Should().BeTrue("reachbench fixtures should exist under tests/reachability/fixtures");
|
||||
@@ -61,7 +62,8 @@ public class ReachbenchFixtureTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(CaseVariantData))]
|
||||
public void CaseVariantContainsExpectedArtifacts(string caseId, string variantPath)
|
||||
{
|
||||
@@ -94,7 +96,8 @@ public class ReachbenchFixtureTests
|
||||
VerifyManifestHashes(caseId, variantPath, requiredFiles);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(CaseVariantData))]
|
||||
public void CaseGroundTruthMatchesVariants(string caseId, string variantPath)
|
||||
{
|
||||
@@ -143,6 +146,7 @@ public class ReachbenchFixtureTests
|
||||
var manifestPath = Path.Combine(variantPath, "manifest.json");
|
||||
using var manifestStream = File.OpenRead(manifestPath);
|
||||
using var manifestDoc = JsonDocument.Parse(manifestStream);
|
||||
using StellaOps.TestKit;
|
||||
var files = manifestDoc.RootElement.GetProperty("files");
|
||||
|
||||
foreach (var file in requiredFiles.Where(f => f != "manifest.json"))
|
||||
|
||||
@@ -19,7 +19,8 @@ public class SamplesPublicFixtureTests
|
||||
"repro.sh"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ManifestExistsAndIsSorted()
|
||||
{
|
||||
var manifestPath = Path.Combine(SamplesPublicRoot, "manifest.json");
|
||||
@@ -27,6 +28,7 @@ public class SamplesPublicFixtureTests
|
||||
|
||||
using var stream = File.OpenRead(manifestPath);
|
||||
using var doc = JsonDocument.Parse(stream);
|
||||
using StellaOps.TestKit;
|
||||
doc.RootElement.ValueKind.Should().Be(JsonValueKind.Array);
|
||||
|
||||
var keys = doc.RootElement.EnumerateArray()
|
||||
@@ -37,7 +39,8 @@ public class SamplesPublicFixtureTests
|
||||
keys.Should().BeInAscendingOrder(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SamplesPublicEntriesMatchManifestHashes()
|
||||
{
|
||||
var manifestPath = Path.Combine(SamplesPublicRoot, "manifest.json");
|
||||
|
||||
@@ -2,11 +2,13 @@ using FluentAssertions;
|
||||
using StellaOps.Scanner.Reachability;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Reachability.FixtureTests;
|
||||
|
||||
public sealed class SymbolIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForJava_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForJava("com.example", "MyClass", "doSomething", "(Ljava/lang/String;)V");
|
||||
@@ -15,7 +17,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().HaveLength("sym:java:".Length + 43); // Base64url SHA-256 without padding = 43 chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForJava_IsDeterministic()
|
||||
{
|
||||
var id1 = SymbolId.ForJava("com.example", "MyClass", "doSomething", "(Ljava/lang/String;)V");
|
||||
@@ -24,7 +27,8 @@ public sealed class SymbolIdTests
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForJava_IsCaseInsensitive()
|
||||
{
|
||||
var id1 = SymbolId.ForJava("com.example", "MyClass", "doSomething", "()V");
|
||||
@@ -33,7 +37,8 @@ public sealed class SymbolIdTests
|
||||
id1.Should().Be(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForDotNet_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForDotNet("MyAssembly", "MyNamespace", "MyClass", "MyMethod(System.String)");
|
||||
@@ -41,7 +46,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:dotnet:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForDotNet_DifferentSignaturesProduceDifferentIds()
|
||||
{
|
||||
var id1 = SymbolId.ForDotNet("MyAssembly", "MyNamespace", "MyClass", "Method(String)");
|
||||
@@ -50,7 +56,8 @@ public sealed class SymbolIdTests
|
||||
id1.Should().NotBe(id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForNode_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForNode("express", "lib/router", "function");
|
||||
@@ -58,7 +65,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:node:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForNode_HandlesScopedPackages()
|
||||
{
|
||||
var id1 = SymbolId.ForNode("@angular/core", "src/render", "function");
|
||||
@@ -68,7 +76,8 @@ public sealed class SymbolIdTests
|
||||
id1.Should().StartWith("sym:node:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForGo_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForGo("github.com/example/repo", "pkg/http", "Server", "HandleRequest");
|
||||
@@ -76,7 +85,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:go:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForGo_FunctionWithoutReceiver()
|
||||
{
|
||||
var id = SymbolId.ForGo("github.com/example/repo", "pkg/main", "", "main");
|
||||
@@ -84,7 +94,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:go:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForRust_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForRust("my_crate", "foo::bar", "my_function", "_ZN8my_crate3foo3bar11my_functionE");
|
||||
@@ -92,7 +103,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:rust:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForSwift_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForSwift("MyModule", "MyClass", "myMethod", null);
|
||||
@@ -100,7 +112,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:swift:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForShell_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForShell("scripts/deploy.sh", "run_migration");
|
||||
@@ -108,7 +121,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:shell:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForBinary_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForBinary("7f6e5d4c3b2a1908", ".text", "_start");
|
||||
@@ -116,7 +130,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:binary:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForPython_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForPython("requests", "requests.api", "get");
|
||||
@@ -124,7 +139,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:python:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForRuby_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForRuby("rails", "ActiveRecord::Base", "#save");
|
||||
@@ -132,7 +148,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:ruby:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ForPhp_CreatesCanonicalSymbolId()
|
||||
{
|
||||
var id = SymbolId.ForPhp("laravel/framework", "Illuminate\\Http", "Request::input");
|
||||
@@ -140,7 +157,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:php:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidSymbolId_ReturnsComponents()
|
||||
{
|
||||
var id = SymbolId.ForJava("com.example", "MyClass", "method", "()V");
|
||||
@@ -152,7 +170,8 @@ public sealed class SymbolIdTests
|
||||
result.Value.Fragment.Should().HaveLength(43);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_InvalidSymbolId_ReturnsNull()
|
||||
{
|
||||
SymbolId.Parse("invalid").Should().BeNull();
|
||||
@@ -162,7 +181,8 @@ public sealed class SymbolIdTests
|
||||
SymbolId.Parse(null!).Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FromTuple_CreatesSymbolIdFromRawTuple()
|
||||
{
|
||||
var tuple = "my\0canonical\0tuple";
|
||||
@@ -171,7 +191,8 @@ public sealed class SymbolIdTests
|
||||
id.Should().StartWith("sym:custom:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllLanguagesAreDifferent()
|
||||
{
|
||||
// Same tuple data should produce different IDs for different languages
|
||||
|
||||
@@ -3,11 +3,13 @@ using FluentAssertions;
|
||||
using StellaOps.Replay.Core;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
public sealed class CanonicalJsonTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalJson_OrdersPropertiesLexicographically()
|
||||
{
|
||||
var payload = new
|
||||
@@ -22,7 +24,8 @@ public sealed class CanonicalJsonTests
|
||||
canonical.Should().Be("{\"alpha\":{\"m\":7,\"z\":9},\"list\":[{\"x\":1,\"y\":2}],\"zeta\":1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalJson_PreservesNumbersAndBooleans()
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<JsonElement>("{\"b\":true,\"a\":1.25}");
|
||||
|
||||
@@ -3,11 +3,13 @@ using FluentAssertions;
|
||||
using StellaOps.Replay.Core;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
public sealed class DeterministicHashTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sha256Hex_ComputesLowercaseDigest()
|
||||
{
|
||||
var digest = DeterministicHash.Sha256Hex("replay-core");
|
||||
@@ -15,7 +17,8 @@ public sealed class DeterministicHashTests
|
||||
digest.Should().Be("a914f5ac6a57aab0189bb55bcb0ef6bcdbd86f77198c8669eab5ae38a325e41d");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MerkleRootHex_IsDeterministic()
|
||||
{
|
||||
var leaves = new[] { "alpha", "beta", "gamma" }
|
||||
|
||||
@@ -4,11 +4,13 @@ using FluentAssertions;
|
||||
using StellaOps.Replay.Core;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
public sealed class DsseEnvelopeTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildUnsigned_ProducesCanonicalPayload()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
public sealed class ReplayBundleWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteTarZstAsync_IsDeterministicAndSorted()
|
||||
{
|
||||
var entries = new[]
|
||||
@@ -47,6 +48,7 @@ public sealed class ReplayBundleWriterTests
|
||||
{
|
||||
names.Add(entry.Name);
|
||||
using var ms = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
entry.DataStream!.CopyTo(ms);
|
||||
var text = System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||
text.Should().Be(entry.Name.StartsWith("a") ? "alpha" : "beta");
|
||||
@@ -55,7 +57,8 @@ public sealed class ReplayBundleWriterTests
|
||||
names.Should().BeEquivalentTo(new[] { "a.txt", "b.txt" }, opts => opts.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildCasUri_UsesPrefixAndShard()
|
||||
{
|
||||
ReplayBundleWriter.BuildCasUri("abcdef", null).Should().Be("cas://replay/ab/abcdef.tar.zst");
|
||||
|
||||
@@ -3,11 +3,13 @@ using FluentAssertions;
|
||||
using StellaOps.Replay.Core;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Replay.Core.Tests;
|
||||
|
||||
public sealed class ReplayManifestExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddsReachabilityEvidence()
|
||||
{
|
||||
var manifest = new ReplayManifest
|
||||
|
||||
@@ -6,6 +6,7 @@ using StellaOps.Scanner.ReachabilityDrift;
|
||||
using StellaOps.Scanner.ReachabilityDrift.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.ScannerSignals.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,7 +24,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
|
||||
#region Drift Detection Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WhenPathBecomesReachable_ReportsNewlyReachableSink()
|
||||
{
|
||||
// Arrange: unreachable -> reachable (guard removed)
|
||||
@@ -56,7 +58,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
sink.Path.Sink.NodeId.Should().Be("jndi-lookup-sink");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WhenPathBecomesUnreachable_ReportsNewlyUnreachableSink()
|
||||
{
|
||||
// Arrange: reachable -> unreachable (guard added)
|
||||
@@ -85,7 +88,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
sink.Cause.Kind.Should().Be(DriftCauseKind.GuardAdded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WhenNoChange_ReportsNoDrift()
|
||||
{
|
||||
// Arrange: same graph, no changes
|
||||
@@ -111,7 +115,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_IsDeterministic_SameInputsProduceSameOutputs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -139,7 +144,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_ResultDigest_IsStableAcrossRuns()
|
||||
{
|
||||
// Arrange
|
||||
@@ -165,7 +171,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
|
||||
#region CodeChangeFact Extraction Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CodeChangeFactExtractor_DetectsAddedEdge()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +192,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
c.Details.Value.GetRawText().Contains("edge_added", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CodeChangeFactExtractor_DetectsRemovedEdge()
|
||||
{
|
||||
// Arrange
|
||||
@@ -209,7 +217,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
|
||||
#region Multi-Sink Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WithMultipleSinks_ReportsAllDriftedSinks()
|
||||
{
|
||||
// Arrange: Multiple sinks become reachable
|
||||
@@ -234,7 +243,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
sinkIds.Should().Contain("file-write-sink");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_OrderingSinks_IsStableAndDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -263,7 +273,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
|
||||
#region Path Compression Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WithFullPath_IncludesIntermediateNodes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -288,7 +299,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
sink.Path.FullPath!.Value.Length.Should().BeGreaterThan(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WithoutFullPath_OmitsIntermediateNodes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -316,7 +328,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WithLanguageMismatch_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -330,7 +343,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
act.Should().Throw<ArgumentException>().WithMessage("*Language mismatch*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WithNullBaseGraph_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -342,7 +356,8 @@ public sealed class ReachabilityDriftIntegrationTests
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DetectDrift_WithNullHeadGraph_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -27,7 +27,8 @@ public sealed class ScannerToSignalsReachabilityTests
|
||||
private static readonly string RepoRoot = LocateRepoRoot();
|
||||
private static readonly string FixtureRoot = Path.Combine(RepoRoot, "tests", "reachability", "fixtures", "reachbench-2025-expanded", "cases");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ScannerBuilderFeedsSignalsScoringPipeline()
|
||||
{
|
||||
var caseId = "java-log4j-CVE-2021-44228-log4shell";
|
||||
@@ -327,6 +328,7 @@ public sealed class ScannerToSignalsReachabilityTests
|
||||
if (request.ManifestContent is not null)
|
||||
{
|
||||
await using var manifestBuffer = new MemoryStream();
|
||||
using StellaOps.TestKit;
|
||||
await request.ManifestContent.CopyToAsync(manifestBuffer, cancellationToken).ConfigureAwait(false);
|
||||
manifests[computedHash] = manifestBuffer.ToArray();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,7 +14,8 @@ public class CallgraphSchemaMigratorTests
|
||||
{
|
||||
#region EnsureV1 - Schema Version Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_SetsSchemaToV1_WhenNotSet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -29,7 +31,8 @@ public class CallgraphSchemaMigratorTests
|
||||
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesV1Schema_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -45,7 +48,8 @@ public class CallgraphSchemaMigratorTests
|
||||
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_UpdatesLegacySchema_ToV1()
|
||||
{
|
||||
// Arrange
|
||||
@@ -65,7 +69,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Language Parsing Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("dotnet", CallgraphLanguage.DotNet)]
|
||||
[InlineData(".net", CallgraphLanguage.DotNet)]
|
||||
[InlineData("csharp", CallgraphLanguage.DotNet)]
|
||||
@@ -102,7 +107,8 @@ public class CallgraphSchemaMigratorTests
|
||||
result.LanguageType.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesLanguageType_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +129,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Node Visibility Inference Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersPublicVisibility_ForStandardNames()
|
||||
{
|
||||
// Arrange
|
||||
@@ -143,7 +150,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Public);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersPrivateVisibility_ForUnderscorePrefixed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +171,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Private);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersPrivateVisibility_ForAngleBracketNames()
|
||||
{
|
||||
// Arrange
|
||||
@@ -183,7 +192,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Private);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersInternalVisibility_ForInternalNamespace()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +213,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Visibility.Should().Be(SymbolVisibility.Internal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesVisibility_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -227,7 +238,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Symbol Key Building Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_BuildsSymbolKey_FromNamespaceAndName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -247,7 +259,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.SymbolKey.Should().Be("MyApp.Services.ProcessOrder");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_BuildsSymbolKey_FromNameOnly_WhenNoNamespace()
|
||||
{
|
||||
// Arrange
|
||||
@@ -267,7 +280,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.SymbolKey.Should().Be("GlobalMethod");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesSymbolKey_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -291,7 +305,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Entrypoint Candidate Detection Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Main")]
|
||||
[InlineData("main")]
|
||||
[InlineData("MAIN")]
|
||||
@@ -314,7 +329,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("OrdersController")]
|
||||
[InlineData("UserController")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForControllerNames(string name)
|
||||
@@ -336,7 +352,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("RequestHandler")]
|
||||
[InlineData("EventHandler")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForHandlerNames(string name)
|
||||
@@ -358,7 +375,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.IsEntrypointCandidate.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(".cctor")]
|
||||
[InlineData("ModuleInitializer")]
|
||||
public void EnsureV1_DetectsEntrypointCandidate_ForModuleInitializers(string name)
|
||||
@@ -384,7 +402,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Edge Reason Inference Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("call", EdgeReason.DirectCall)]
|
||||
[InlineData("direct", EdgeReason.DirectCall)]
|
||||
[InlineData("virtual", EdgeReason.VirtualCall)]
|
||||
@@ -422,7 +441,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Reason.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersRuntimeMinted_ForRuntimeKind()
|
||||
{
|
||||
// Arrange
|
||||
@@ -442,7 +462,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Reason.Should().Be(EdgeReason.RuntimeMinted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersDynamicImport_ForHeuristicKind()
|
||||
{
|
||||
// Arrange
|
||||
@@ -462,7 +483,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Reason.Should().Be(EdgeReason.DynamicImport);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesEdgeReason_WhenAlreadySet()
|
||||
{
|
||||
// Arrange
|
||||
@@ -486,7 +508,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Entrypoint Inference Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersEntrypoints_FromEntrypointCandidateNodes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -509,7 +532,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.NodeId.Should().Be("main");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersEntrypoints_FromExplicitRoots()
|
||||
{
|
||||
// Arrange
|
||||
@@ -535,7 +559,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.NodeId.Should().Be("init");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_PreservesEntrypoints_WhenAlreadyPresent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -567,7 +592,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Ordering Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_SortsNodes_ByIdAlphabetically()
|
||||
{
|
||||
// Arrange
|
||||
@@ -588,7 +614,8 @@ public class CallgraphSchemaMigratorTests
|
||||
result.Nodes.Select(n => n.Id).Should().BeInAscendingOrder();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_SortsEdges_BySourceThenTargetThenTypeThenOffset()
|
||||
{
|
||||
// Arrange
|
||||
@@ -613,7 +640,8 @@ public class CallgraphSchemaMigratorTests
|
||||
sortedEdges[0].Type.Should().Be("call");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_SortsEntrypoints_ByPhaseThenOrder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -641,14 +669,16 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region EnsureV1 - Null Handling Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_ThrowsArgumentNullException_ForNullDocument()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => CallgraphSchemaMigrator.EnsureV1(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_HandlesEmptyNodes_Gracefully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -664,7 +694,8 @@ public class CallgraphSchemaMigratorTests
|
||||
result.Nodes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_HandlesEmptyEdges_Gracefully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -684,7 +715,8 @@ public class CallgraphSchemaMigratorTests
|
||||
|
||||
#region Framework Inference Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersAspNetCoreFramework_ForDotNetController()
|
||||
{
|
||||
// Arrange
|
||||
@@ -706,7 +738,8 @@ public class CallgraphSchemaMigratorTests
|
||||
.Which.Framework.Should().Be(EntrypointFramework.AspNetCore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureV1_InfersSpringFramework_ForJavaController()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -9,6 +9,7 @@ using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Signals.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -45,7 +46,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_DeserializesWithoutError(string fixtureName)
|
||||
{
|
||||
@@ -57,7 +59,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document!.Id.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_NodesHaveRequiredFields(string fixtureName)
|
||||
{
|
||||
@@ -71,7 +74,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_EdgesReferenceValidNodes(string fixtureName)
|
||||
{
|
||||
@@ -87,7 +91,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_EntrypointsReferenceValidNodes(string fixtureName)
|
||||
{
|
||||
@@ -102,7 +107,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DotNetFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
@@ -111,7 +117,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.DotNet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "java-spring-boot.json"));
|
||||
@@ -120,7 +127,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.Java);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NodeFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "node-express-api.json"));
|
||||
@@ -129,7 +137,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.Node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoFixture_HasCorrectLanguageEnum()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "go-gin-api.json"));
|
||||
@@ -138,7 +147,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.LanguageType.Should().Be(CallgraphLanguage.Go);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_ContainsAllEdgeReasons()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
@@ -153,7 +163,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_ContainsAllEdgeKinds()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
@@ -168,7 +179,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllVisibilityFixture_ContainsAllVisibilityLevels()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-visibility-levels.json"));
|
||||
@@ -183,7 +195,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LegacyFixture_HasNoSchemaField()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "legacy-no-schema.json"));
|
||||
@@ -193,7 +206,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.Schema.Should().Be(CallgraphSchemaVersions.V1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void LegacyFixture_MigratesToV1Schema()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "legacy-no-schema.json"));
|
||||
@@ -208,7 +222,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
migrated.Edges.Should().AllSatisfy(e => Enum.IsDefined(e.Reason).Should().BeTrue());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("dotnet-aspnetcore-minimal")]
|
||||
[InlineData("java-spring-boot")]
|
||||
[InlineData("node-express-api")]
|
||||
@@ -227,7 +242,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
migrated2.Entrypoints.Should().HaveCount(migrated1.Entrypoints.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EdgeReason_SerializesAsCamelCaseString()
|
||||
{
|
||||
var edge = new CallgraphEdge
|
||||
@@ -243,7 +259,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
json.Should().Contain("\"reason\": \"directCall\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SymbolVisibility_SerializesAsCamelCaseString()
|
||||
{
|
||||
var node = new CallgraphNode
|
||||
@@ -259,7 +276,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
json.Should().Contain("\"visibility\": \"public\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EntrypointKind_SerializesAsCamelCaseString()
|
||||
{
|
||||
var entrypoint = new CallgraphEntrypoint
|
||||
@@ -275,7 +293,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
json.Should().Contain("\"framework\": \"aspNetCore\"");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_NodesSortedById(string fixtureName)
|
||||
{
|
||||
@@ -288,7 +307,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
nodeIds.Should().Equal(sortedIds, $"Nodes in {fixtureName} should be sorted by Id for determinism");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(GoldenFixtures))]
|
||||
public void GoldenFixture_EntrypointsSortedByOrder(string fixtureName)
|
||||
{
|
||||
@@ -301,7 +321,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
orders.Should().Equal(sortedOrders, $"Entrypoints in {fixtureName} should be sorted by Order for determinism");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DotNetFixture_HasCorrectAspNetCoreEntrypoints()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
@@ -311,7 +332,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Http && e.Route == "/weatherforecast");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void JavaFixture_HasCorrectSpringEntrypoints()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "java-spring-boot.json"));
|
||||
@@ -321,7 +343,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Http && e.Route == "/owners/{ownerId}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoFixture_HasModuleInitEntrypoint()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "go-gin-api.json"));
|
||||
@@ -330,7 +353,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.ModuleInit && e.Phase == EntrypointPhase.ModuleInit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_ReflectionEdgeIsUnresolved()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
@@ -341,7 +365,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
reflectionEdge.Weight.Should().BeLessThan(1.0, "Reflection edges should have lower confidence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AllEdgeReasonsFixture_DiBindingHasProvenance()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
|
||||
@@ -351,7 +376,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
diEdge.Provenance.Should().NotBeNullOrEmpty("DI binding edges should include provenance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Artifacts_HaveRequiredFields()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
@@ -366,7 +392,8 @@ public sealed class CallgraphSchemaV1DeterminismTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Metadata_HasRequiredToolInfo()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
|
||||
|
||||
@@ -41,7 +41,8 @@ public sealed class ReachabilityScoringTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(CaseVariants))]
|
||||
public async Task RecomputedFactsMatchTruthFixtures(string caseId, string variant)
|
||||
{
|
||||
@@ -142,6 +143,7 @@ public sealed class ReachabilityScoringTests
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(line);
|
||||
using StellaOps.TestKit;
|
||||
if (doc.RootElement.TryGetProperty("sid", out var sidProp))
|
||||
{
|
||||
runtimeHits.Add(sidProp.GetString()!);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user