Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -14,11 +14,14 @@ using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Policy.Analyzers.Tests;
|
||||
|
||||
public sealed class HttpClientUsageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReportsDiagnostic_ForNewHttpClient()
|
||||
{
|
||||
const string source = """
|
||||
@@ -39,7 +42,8 @@ public sealed class HttpClientUsageAnalyzerTests
|
||||
Assert.Contains(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DoesNotReportDiagnostic_InsidePolicyAssembly()
|
||||
{
|
||||
const string source = """
|
||||
@@ -57,7 +61,8 @@ public sealed class HttpClientUsageAnalyzerTests
|
||||
Assert.DoesNotContain(diagnostics, d => d.Id == HttpClientUsageAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFix_RewritesToFactoryCall()
|
||||
{
|
||||
const string source = """
|
||||
|
||||
@@ -22,6 +22,8 @@ using Microsoft.CodeAnalysis.Text;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Policy.Analyzers.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +35,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
{
|
||||
#region AIRGAP-5100-005: Expected Diagnostics & No False Positives
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("var client = new HttpClient();", true, "Direct construction should trigger diagnostic")]
|
||||
[InlineData("var client = new System.Net.Http.HttpClient();", true, "Fully qualified construction should trigger diagnostic")]
|
||||
[InlineData("HttpClient client = new();", true, "Target-typed new should trigger diagnostic")]
|
||||
@@ -60,7 +63,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
hasDiagnostic.Should().Be(shouldTrigger, reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NoDiagnostic_ForHttpClientParameter()
|
||||
{
|
||||
const string source = """
|
||||
@@ -83,7 +87,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Using HttpClient as parameter should not trigger diagnostic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NoDiagnostic_ForHttpClientField()
|
||||
{
|
||||
const string source = """
|
||||
@@ -107,7 +112,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Declaring HttpClient field should not trigger diagnostic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NoDiagnostic_ForFactoryMethodReturn()
|
||||
{
|
||||
const string source = """
|
||||
@@ -138,7 +144,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Using factory method should not trigger diagnostic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NoDiagnostic_InTestAssembly()
|
||||
{
|
||||
const string source = """
|
||||
@@ -160,7 +167,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Test assemblies should be exempt from diagnostic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NoDiagnostic_InPolicyAssembly()
|
||||
{
|
||||
const string source = """
|
||||
@@ -179,7 +187,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Policy assembly itself should be exempt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Diagnostic_HasCorrectSeverity()
|
||||
{
|
||||
const string source = """
|
||||
@@ -203,7 +212,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Diagnostic should be a warning, not an error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Diagnostic_HasCorrectLocation()
|
||||
{
|
||||
const string source = """
|
||||
@@ -228,7 +238,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
lineSpan.StartLinePosition.Line.Should().Be(8, "Diagnostic should point to line 9 (0-indexed: 8)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MultipleHttpClientUsages_ReportMultipleDiagnostics()
|
||||
{
|
||||
const string source = """
|
||||
@@ -265,7 +276,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
|
||||
#region AIRGAP-5100-006: Golden Generated Code Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFix_GeneratesExpectedFactoryCall()
|
||||
{
|
||||
const string source = """
|
||||
@@ -301,7 +313,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Code fix should match golden output exactly");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFix_PreservesTrivia()
|
||||
{
|
||||
const string source = """
|
||||
@@ -326,7 +339,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Leading comment should be preserved");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFix_DeterministicOutput()
|
||||
{
|
||||
const string source = """
|
||||
@@ -352,7 +366,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
result2.Should().Be(result3, "Code fix should be deterministic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFix_ContainsRequiredPlaceholders()
|
||||
{
|
||||
const string source = """
|
||||
@@ -383,7 +398,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
fixedCode.Should().Contain("REPLACE_INTENT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFix_UsesFullyQualifiedNames()
|
||||
{
|
||||
const string source = """
|
||||
@@ -408,7 +424,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
fixedCode.Should().Contain("global::System.Uri");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FixAllProvider_IsWellKnownBatchFixer()
|
||||
{
|
||||
var provider = new HttpClientUsageCodeFixProvider();
|
||||
@@ -418,7 +435,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
"Should use batch fixer for efficient multi-fix application");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Analyzer_SupportedDiagnostics_ContainsExpectedId()
|
||||
{
|
||||
var analyzer = new HttpClientUsageAnalyzer();
|
||||
@@ -428,7 +446,8 @@ public sealed class PolicyAnalyzerRoslynTests
|
||||
supportedDiagnostics[0].Id.Should().Be("AIRGAP001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CodeFixProvider_FixableDiagnosticIds_MatchesAnalyzer()
|
||||
{
|
||||
var analyzer = new HttpClientUsageAnalyzer();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.AirGap.Policy.Analyzers\StellaOps.AirGap.Policy.Analyzers.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,11 +8,14 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.AirGap.Policy;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Policy.Tests;
|
||||
|
||||
public sealed class EgressPolicyTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Evaluate_UnsealedEnvironment_AllowsRequest()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
@@ -29,7 +32,8 @@ public sealed class EgressPolicyTests
|
||||
Assert.Null(decision.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureAllowed_SealedEnvironmentWithMatchingRule_Allows()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
@@ -44,7 +48,8 @@ public sealed class EgressPolicyTests
|
||||
policy.EnsureAllowed(request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureAllowed_SealedEnvironmentWithoutRule_ThrowsWithGuidance()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
@@ -67,7 +72,8 @@ public sealed class EgressPolicyTests
|
||||
Assert.Equal(options.SupportContact, exception.SupportContact);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureAllowed_SealedEnvironment_AllowsLoopbackWhenConfigured()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
@@ -82,7 +88,8 @@ public sealed class EgressPolicyTests
|
||||
policy.EnsureAllowed(request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureAllowed_SealedEnvironment_AllowsPrivateNetworkWhenConfigured()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
@@ -97,7 +104,8 @@ public sealed class EgressPolicyTests
|
||||
policy.EnsureAllowed(request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureAllowed_SealedEnvironment_BlocksPrivateNetworkWhenNotConfigured()
|
||||
{
|
||||
var options = new EgressPolicyOptions
|
||||
@@ -113,7 +121,8 @@ public sealed class EgressPolicyTests
|
||||
Assert.Contains("10.10.0.5", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("https://api.example.com", true)]
|
||||
[InlineData("https://sub.api.example.com", true)]
|
||||
[InlineData("https://example.com", false)]
|
||||
@@ -132,7 +141,8 @@ public sealed class EgressPolicyTests
|
||||
Assert.Equal(expectedAllowed, decision.IsAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceCollection_AddAirGapEgressPolicy_RegistersService()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -149,7 +159,8 @@ public sealed class EgressPolicyTests
|
||||
policy.EnsureAllowed(new EgressRequest("PolicyEngine", new Uri("https://mirror.internal"), "mirror-sync"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ServiceCollection_AddAirGapEgressPolicy_BindsFromConfiguration()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -182,7 +193,8 @@ public sealed class EgressPolicyTests
|
||||
Assert.Contains("mirror.internal", blocked.Remediation, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EgressHttpClientFactory_Create_EnforcesPolicyBeforeReturningClient()
|
||||
{
|
||||
var recordingPolicy = new RecordingPolicy();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.AirGap.Policy\StellaOps.AirGap.Policy.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,7 @@ using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +56,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region AIRGAP-5100-007: Migration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Migration_SchemaContainsRequiredTables()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +79,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Migration_AirGapStateHasRequiredColumns()
|
||||
{
|
||||
// Arrange
|
||||
@@ -94,7 +97,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Migration_IsIdempotent()
|
||||
{
|
||||
// Act - Running migrations again should not fail
|
||||
@@ -107,7 +111,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
await act.Should().NotThrowAsync("Running migrations multiple times should be idempotent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Migration_HasTenantIndex()
|
||||
{
|
||||
// Act
|
||||
@@ -122,7 +127,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region AIRGAP-5100-008: Idempotency Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Idempotency_SetStateTwice_NoException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -137,7 +143,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
await act.Should().NotThrowAsync("Setting state twice should be idempotent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Idempotency_SetStateTwice_SingleRecord()
|
||||
{
|
||||
// Arrange
|
||||
@@ -154,7 +161,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
fetched.PolicyHash.Should().Be("sha256:policy-v2", "Second set should update, not duplicate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Idempotency_ConcurrentSets_NoDataCorruption()
|
||||
{
|
||||
// Arrange
|
||||
@@ -181,7 +189,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
fetched.PolicyHash.Should().StartWith("sha256:policy-");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Idempotency_SameBundleIdTwice_NoException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +212,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region AIRGAP-5100-009: Query Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryDeterminism_SameInput_SameOutput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -221,7 +231,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
result2.Should().BeEquivalentTo(result3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryDeterminism_ContentBudgets_ReturnInConsistentOrder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -252,7 +263,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryDeterminism_TimeAnchor_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -277,7 +289,8 @@ public sealed class AirGapStorageIntegrationTests : IAsyncLifetime
|
||||
fetched1.TimeAnchor.Source.Should().Be("tsa.example.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryDeterminism_MultipleTenants_IsolatedResults()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -8,6 +8,7 @@ using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(AirGapPostgresCollection.Name)]
|
||||
@@ -42,7 +43,8 @@ public sealed class PostgresAirGapStateStoreTests : IAsyncLifetime
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAsync_ReturnsDefaultStateForNewTenant()
|
||||
{
|
||||
// Act
|
||||
@@ -55,7 +57,8 @@ public sealed class PostgresAirGapStateStoreTests : IAsyncLifetime
|
||||
state.PolicyHash.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAndGet_RoundTripsState()
|
||||
{
|
||||
// Arrange
|
||||
@@ -100,7 +103,8 @@ public sealed class PostgresAirGapStateStoreTests : IAsyncLifetime
|
||||
fetched.ContentBudgets["advisories"].WarningSeconds.Should().Be(7200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAsync_UpdatesExistingState()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +140,8 @@ public sealed class PostgresAirGapStateStoreTests : IAsyncLifetime
|
||||
fetched.StalenessBudget.WarningSeconds.Should().Be(600);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetAsync_PersistsContentBudgets()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<ProjectReference Include="..\StellaOps.AirGap.Storage.Postgres\StellaOps.AirGap.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.AirGap.Controller\StellaOps.AirGap.Controller.csproj" />
|
||||
<ProjectReference Include="..\..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Text;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Bundle.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -22,7 +23,8 @@ public sealed class AirGapCliToolTests
|
||||
{
|
||||
#region AIRGAP-5100-013: Exit Code Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_SuccessfulExport_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -32,7 +34,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedExitCode.Should().Be(0, "Successful operations should return exit code 0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_UserError_ReturnsOne()
|
||||
{
|
||||
// Arrange
|
||||
@@ -43,7 +46,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedExitCode.Should().Be(1, "User errors should return exit code 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_SystemError_ReturnsTwo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -54,7 +58,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedExitCode.Should().Be(2, "System errors should return exit code 2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_MissingRequiredArgument_ReturnsOne()
|
||||
{
|
||||
// Arrange - Missing required argument scenario
|
||||
@@ -66,7 +71,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedExitCode.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_InvalidFeedPath_ReturnsOne()
|
||||
{
|
||||
// Arrange - Invalid feed path scenario
|
||||
@@ -84,7 +90,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedExitCode.Should().Be(1, "Invalid feed path should return exit code 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_HelpFlag_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -96,7 +103,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedExitCode.Should().Be(0, "--help should return exit code 0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExitCode_VersionFlag_ReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +120,8 @@ public sealed class AirGapCliToolTests
|
||||
|
||||
#region AIRGAP-5100-014: Golden Output Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoldenOutput_ExportCommand_IncludesManifestSummary()
|
||||
{
|
||||
// Arrange - Expected output structure for export command
|
||||
@@ -135,7 +144,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedOutputLines.Should().Contain(l => l.Contains("Digest:"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoldenOutput_ExportCommand_IncludesBundleDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -145,7 +155,8 @@ public sealed class AirGapCliToolTests
|
||||
digestPattern.Should().Contain("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoldenOutput_ImportCommand_IncludesImportSummary()
|
||||
{
|
||||
// Arrange - Expected output structure for import command
|
||||
@@ -165,7 +176,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedOutputLines.Should().Contain(l => l.Contains("imported successfully"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoldenOutput_ListCommand_IncludesBundleTable()
|
||||
{
|
||||
// Arrange - Expected output structure for list command
|
||||
@@ -177,7 +189,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedHeaders.Should().Contain("Version");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoldenOutput_ValidateCommand_IncludesValidationResult()
|
||||
{
|
||||
// Arrange - Expected output structure for validate command
|
||||
@@ -195,7 +208,8 @@ public sealed class AirGapCliToolTests
|
||||
expectedOutputLines.Should().Contain(l => l.Contains("Validation:"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GoldenOutput_ErrorMessage_IncludesContext()
|
||||
{
|
||||
// Arrange - Error message format
|
||||
@@ -210,7 +224,8 @@ public sealed class AirGapCliToolTests
|
||||
|
||||
#region AIRGAP-5100-015: CLI Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CliDeterminism_SameInputs_SameOutputDigest()
|
||||
{
|
||||
// Arrange - Simulate CLI determinism
|
||||
@@ -225,7 +240,8 @@ public sealed class AirGapCliToolTests
|
||||
digest1.Should().Be(digest2, "Same inputs should produce same digest");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CliDeterminism_OutputBundleName_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -241,7 +257,8 @@ public sealed class AirGapCliToolTests
|
||||
filename1.Should().Be(filename2, "Same parameters should produce same filename");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CliDeterminism_ManifestJson_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -256,7 +273,8 @@ public sealed class AirGapCliToolTests
|
||||
json1.Should().Be(json2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CliDeterminism_FeedOrdering_IsDeterministic()
|
||||
{
|
||||
// Arrange - Feeds in different order
|
||||
@@ -272,7 +290,8 @@ public sealed class AirGapCliToolTests
|
||||
"Canonical ordering should be deterministic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CliDeterminism_DigestComputation_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
@@ -290,7 +309,8 @@ public sealed class AirGapCliToolTests
|
||||
digest3.Should().Be(expectedDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CliDeterminism_TimestampFormat_IsDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -14,6 +14,7 @@ using StellaOps.AirGap.Bundle.Serialization;
|
||||
using StellaOps.AirGap.Bundle.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Bundle.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -48,7 +49,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
|
||||
#region AIRGAP-5100-016: Online → Offline Bundle Transfer Integration
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Integration_OnlineExport_OfflineImport_DataIntegrity()
|
||||
{
|
||||
// Arrange - Create source data in "online" environment
|
||||
@@ -102,7 +104,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
importedFeedContent.Should().Contain("CVE-2024-0001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Integration_BundleTransfer_PreservesAllComponents()
|
||||
{
|
||||
// Arrange - Create multi-component bundle
|
||||
@@ -143,7 +146,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
File.Exists(Path.Combine(offlinePath, "certs/root.pem")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Integration_CorruptedBundle_ImportFails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +189,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
|
||||
#region AIRGAP-5100-017: Policy Export/Import/Evaluation Integration
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Integration_PolicyExport_PolicyImport_IdenticalVerdict()
|
||||
{
|
||||
// Arrange - Create a policy in online environment
|
||||
@@ -242,7 +247,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
importedDigest.Should().Be(originalDigest, "Policy digest should match");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Integration_MultiplePolices_MaintainOrder()
|
||||
{
|
||||
// Arrange - Create multiple policies
|
||||
@@ -289,7 +295,8 @@ public sealed class AirGapIntegrationTests : IDisposable
|
||||
File.Exists(Path.Combine(offlinePath, "policies/policy3.rego")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Integration_PolicyWithCrypto_BothTransferred()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -35,7 +35,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
|
||||
#region Same Inputs → Same Hash Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_SameInputs_SameComponentDigests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +56,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
manifest1.Feeds[0].Digest.Should().Be(manifest2.Feeds[0].Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_SameManifestContent_SameBundleDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -70,7 +72,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
digest1.Should().Be(digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_MultipleBuilds_SameDigests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +96,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
digests.Distinct().Should().HaveCount(1, "All builds should produce the same digest");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Determinism_Sha256_StableAcrossCalls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +119,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
|
||||
#region Roundtrip Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Roundtrip_ExportImportReexport_IdenticalBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,6 +152,7 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
|
||||
// Re-export using the imported file
|
||||
var reimportFeedFile = CreateSourceFile("reimport/feed.json", importedContent);
|
||||
using StellaOps.TestKit;
|
||||
var request2 = new BundleBuildRequest(
|
||||
"roundtrip-test",
|
||||
"1.0.0",
|
||||
@@ -165,7 +171,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
manifest1.Feeds[0].Digest.Should().Be(manifest2.Feeds[0].Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Roundtrip_ManifestSerialize_Deserialize_Identical()
|
||||
{
|
||||
// Arrange
|
||||
@@ -179,7 +186,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
restored.Should().BeEquivalentTo(original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Roundtrip_ManifestSerialize_Reserialize_SameJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +206,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
|
||||
#region Content Independence Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_SameContent_DifferentSourcePath_SameDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -219,7 +228,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
manifest1.Feeds[0].Digest.Should().Be(manifest2.Feeds[0].Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_DifferentContent_DifferentDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -243,7 +253,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
|
||||
#region Multiple Component Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_MultipleFeeds_EachHasCorrectDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -278,7 +289,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
manifest.Feeds[2].Digest.Should().Be(ComputeSha256(content3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_OrderIndependence_SameManifestDigest()
|
||||
{
|
||||
// Note: This test verifies that the bundle digest is computed deterministically
|
||||
@@ -300,7 +312,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
|
||||
#region Binary Content Determinism
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_BinaryContent_SameDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -340,7 +353,8 @@ public sealed class BundleDeterminismTests : IAsyncLifetime
|
||||
manifest1.Feeds[0].Digest.Should().Be(manifest2.Feeds[0].Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_LargeContent_SameDigest()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -44,7 +44,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
|
||||
#region AIRGAP-5100-001: Bundle Export Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_CreatesValidBundleStructure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,7 +64,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
manifest.Feeds.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_SetsCorrectManifestFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -83,7 +85,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
manifest.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_ComputesCorrectFileDigests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -107,7 +110,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
feedDigest.Should().Be(expectedDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_ComputesCorrectBundleDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +128,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
manifest.BundleDigest.Should().HaveLength(64);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_TracksCorrectFileSizes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -146,7 +151,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
|
||||
#region AIRGAP-5100-002: Bundle Import Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_LoadsManifestCorrectly()
|
||||
{
|
||||
// Arrange - First export a bundle
|
||||
@@ -170,7 +176,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
loaded.Version.Should().Be("1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_VerifiesFileIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +205,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
loaded.Feeds[0].Digest.Should().Be(actualDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_FailsOnCorruptedFile()
|
||||
{
|
||||
// Arrange
|
||||
@@ -230,7 +238,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
|
||||
#region AIRGAP-5100-003: Determinism Tests (Same Inputs → Same Hash)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_SameInputs_ProduceSameBundleDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -271,7 +280,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
"Same content should produce same file digest");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Determinism_DifferentInputs_ProduceDifferentDigests()
|
||||
{
|
||||
// Arrange
|
||||
@@ -294,7 +304,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
"Different content should produce different digests");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Determinism_ManifestSerialization_IsStable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -314,7 +325,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
|
||||
#region AIRGAP-5100-004: Roundtrip Determinism (Export → Import → Re-export)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Roundtrip_ExportImportReexport_ProducesIdenticalFileDigests()
|
||||
{
|
||||
// Arrange - Initial export
|
||||
@@ -337,6 +349,7 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
|
||||
// Re-export using the imported bundle's files
|
||||
var reexportFeedFile = Path.Combine(bundlePath1, "feeds", "nvd.json");
|
||||
using StellaOps.TestKit;
|
||||
var reexportRequest = new BundleBuildRequest(
|
||||
imported.Name,
|
||||
imported.Version,
|
||||
@@ -360,7 +373,8 @@ public sealed class BundleExportImportTests : IDisposable
|
||||
digest1.Should().Be(digest2, "Roundtrip should produce identical file digests");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Roundtrip_ManifestSerialization_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -7,6 +7,7 @@ using StellaOps.AirGap.Bundle.Serialization;
|
||||
using StellaOps.AirGap.Bundle.Services;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Bundle.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -35,7 +36,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region L0 Export Structure Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_EmptyBundle_CreatesValidManifest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -65,7 +67,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
manifest.TotalSizeBytes.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_WithFeed_CopiesFileAndComputesDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +114,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
File.Exists(Path.Combine(outputPath, "feeds/nvd.json")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_WithPolicy_CopiesFileAndComputesDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -153,7 +157,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
File.Exists(Path.Combine(outputPath, "policies/default.rego")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_WithCryptoMaterial_CopiesFileAndComputesDigest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -195,7 +200,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
File.Exists(Path.Combine(outputPath, "certs/root.pem")).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_MultipleComponents_CalculatesTotalSize()
|
||||
{
|
||||
// Arrange
|
||||
@@ -234,7 +240,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region Digest Computation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_DigestComputation_MatchesSha256()
|
||||
{
|
||||
// Arrange
|
||||
@@ -263,7 +270,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
manifest.Feeds[0].Digest.Should().BeEquivalentTo(expectedDigest, options => options.IgnoringCase());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_BundleDigest_ComputedFromManifest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -294,7 +302,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region Directory Structure Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_CreatesNestedDirectories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -338,7 +347,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region Feed Format Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(FeedFormat.StellaOpsNative)]
|
||||
[InlineData(FeedFormat.TrivyDb)]
|
||||
[InlineData(FeedFormat.GrypeDb)]
|
||||
@@ -372,7 +382,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region Policy Type Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(PolicyType.OpaRego)]
|
||||
[InlineData(PolicyType.LatticeRules)]
|
||||
[InlineData(PolicyType.UnknownBudgets)]
|
||||
@@ -406,7 +417,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region Crypto Component Type Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(CryptoComponentType.TrustRoot)]
|
||||
[InlineData(CryptoComponentType.IntermediateCa)]
|
||||
[InlineData(CryptoComponentType.TimestampRoot)]
|
||||
@@ -441,7 +453,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
|
||||
#region Expiration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_WithExpiration_PreservesExpiryDate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -464,7 +477,8 @@ public sealed class BundleExportTests : IAsyncLifetime
|
||||
manifest.ExpiresAt.Should().BeCloseTo(expiresAt, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Export_CryptoWithExpiration_PreservesComponentExpiry()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -9,6 +9,8 @@ using StellaOps.AirGap.Bundle.Services;
|
||||
using StellaOps.AirGap.Bundle.Validation;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Bundle.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -37,7 +39,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
|
||||
#region Manifest Parsing Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Import_ManifestDeserialization_PreservesAllFields()
|
||||
{
|
||||
// Arrange
|
||||
@@ -51,7 +54,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
imported.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Import_ManifestDeserialization_HandlesEmptyCollections()
|
||||
{
|
||||
// Arrange
|
||||
@@ -67,7 +71,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
imported.CryptoMaterials.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Import_ManifestDeserialization_PreservesFeedComponents()
|
||||
{
|
||||
// Arrange
|
||||
@@ -85,7 +90,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
imported.Feeds[1].Format.Should().Be(FeedFormat.OsvJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Import_ManifestDeserialization_PreservesPolicyComponents()
|
||||
{
|
||||
// Arrange
|
||||
@@ -101,7 +107,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
imported.Policies[1].Type.Should().Be(PolicyType.LatticeRules);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Import_ManifestDeserialization_PreservesCryptoComponents()
|
||||
{
|
||||
// Arrange
|
||||
@@ -121,7 +128,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
|
||||
#region Validation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Validation_FailsWhenFilesMissing()
|
||||
{
|
||||
// Arrange
|
||||
@@ -141,7 +149,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
result.Errors.Should().Contain(e => e.Message.Contains("digest mismatch") || e.Message.Contains("FILE_NOT_FOUND"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Validation_FailsWhenDigestMismatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -158,7 +167,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
result.Errors.Should().Contain(e => e.Message.Contains("digest mismatch"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Validation_SucceedsWhenAllDigestsMatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -175,7 +185,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Validation_WarnsWhenExpired()
|
||||
{
|
||||
// Arrange
|
||||
@@ -195,7 +206,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
result.Warnings.Should().Contain(w => w.Message.Contains("expired"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Validation_WarnsWhenFeedsOld()
|
||||
{
|
||||
// Arrange
|
||||
@@ -224,7 +236,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
|
||||
#region Bundle Loader Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Loader_RegistersAllFeeds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -252,7 +265,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
feedRegistry.Received(manifest.Feeds.Length).Register(Arg.Any<FeedComponent>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Loader_RegistersAllPolicies()
|
||||
{
|
||||
// Arrange
|
||||
@@ -279,7 +293,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
policyRegistry.Received(manifest.Policies.Length).Register(Arg.Any<PolicyComponent>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Loader_ThrowsOnValidationFailure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -306,7 +321,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
.WithMessage("*validation failed*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_Loader_ThrowsOnMissingManifest()
|
||||
{
|
||||
// Arrange
|
||||
@@ -330,7 +346,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
|
||||
#region Digest Verification Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_DigestVerification_MatchesExpected()
|
||||
{
|
||||
// Arrange
|
||||
@@ -346,7 +363,8 @@ public sealed class BundleImportTests : IAsyncLifetime
|
||||
actualDigest.Should().BeEquivalentTo(expectedDigest, options => options.IgnoringCase());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Import_DigestVerification_FailsOnTamperedFile()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.AirGap.Bundle.Services;
|
||||
using StellaOps.AirGap.Bundle.Validation;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Bundle.Tests;
|
||||
|
||||
public class BundleManifestTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serializer_RoundTrip_PreservesFields()
|
||||
{
|
||||
var manifest = CreateManifest();
|
||||
@@ -19,7 +21,8 @@ public class BundleManifestTests
|
||||
deserialized.Should().BeEquivalentTo(manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Validator_FlagsMissingFeedFile()
|
||||
{
|
||||
var manifest = CreateManifest();
|
||||
@@ -30,7 +33,8 @@ public class BundleManifestTests
|
||||
result.Errors.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Builder_CopiesComponentsAndComputesDigest()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.AirGap.Bundle\StellaOps.AirGap.Bundle.csproj" />
|
||||
<ProjectReference Include="../../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,8 @@ using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.AirGap.Controller.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -26,7 +28,8 @@ public sealed class AirGapControllerContractTests
|
||||
{
|
||||
#region AIRGAP-5100-010: Contract Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contract_ExportEndpoint_ExpectedRequestStructure()
|
||||
{
|
||||
// Arrange - Define expected request structure
|
||||
@@ -56,7 +59,8 @@ public sealed class AirGapControllerContractTests
|
||||
feeds.GetArrayLength().Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contract_ExportEndpoint_ExpectedResponseStructure()
|
||||
{
|
||||
// Arrange - Define expected response structure
|
||||
@@ -87,7 +91,8 @@ public sealed class AirGapControllerContractTests
|
||||
parsed.RootElement.TryGetProperty("manifest", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contract_ImportEndpoint_ExpectedRequestStructure()
|
||||
{
|
||||
// Arrange - Import request (typically multipart form or bundle URL)
|
||||
@@ -107,7 +112,8 @@ public sealed class AirGapControllerContractTests
|
||||
parsed.RootElement.TryGetProperty("bundleDigest", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contract_ImportEndpoint_ExpectedResponseStructure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +137,8 @@ public sealed class AirGapControllerContractTests
|
||||
parsed.RootElement.TryGetProperty("feedsImported", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contract_ListBundlesEndpoint_ExpectedResponseStructure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -164,7 +171,8 @@ public sealed class AirGapControllerContractTests
|
||||
parsed.RootElement.TryGetProperty("total", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Contract_StateEndpoint_ExpectedResponseStructure()
|
||||
{
|
||||
// Arrange - AirGap state response
|
||||
@@ -197,7 +205,8 @@ public sealed class AirGapControllerContractTests
|
||||
|
||||
#region AIRGAP-5100-011: Auth Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Auth_RequiredScopes_ForExport()
|
||||
{
|
||||
// Arrange - Expected scopes for export operation
|
||||
@@ -207,7 +216,8 @@ public sealed class AirGapControllerContractTests
|
||||
requiredScopes.Should().Contain("airgap:export");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Auth_RequiredScopes_ForImport()
|
||||
{
|
||||
// Arrange - Expected scopes for import operation
|
||||
@@ -217,7 +227,8 @@ public sealed class AirGapControllerContractTests
|
||||
requiredScopes.Should().Contain("airgap:import");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Auth_RequiredScopes_ForList()
|
||||
{
|
||||
// Arrange - Expected scopes for list operation
|
||||
@@ -227,7 +238,8 @@ public sealed class AirGapControllerContractTests
|
||||
requiredScopes.Should().Contain("airgap:read");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Auth_DenyByDefault_NoTokenReturnsUnauthorized()
|
||||
{
|
||||
// Arrange - Request without token
|
||||
@@ -237,7 +249,8 @@ public sealed class AirGapControllerContractTests
|
||||
expectedStatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Auth_TenantIsolation_CannotAccessOtherTenantBundles()
|
||||
{
|
||||
// Arrange - Claims for tenant A
|
||||
@@ -256,7 +269,8 @@ public sealed class AirGapControllerContractTests
|
||||
// Requests for tenant-B bundles should be rejected
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Auth_TokenExpiry_ExpiredTokenReturnsForbidden()
|
||||
{
|
||||
// Arrange - Expired token scenario
|
||||
@@ -272,7 +286,8 @@ public sealed class AirGapControllerContractTests
|
||||
|
||||
#region AIRGAP-5100-012: OTel Trace Assertions
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OTel_ExportOperation_IncludesBundleIdTag()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +304,8 @@ public sealed class AirGapControllerContractTests
|
||||
expectedTags.Should().Contain("operation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OTel_ImportOperation_IncludesOperationTag()
|
||||
{
|
||||
// Arrange
|
||||
@@ -305,7 +321,8 @@ public sealed class AirGapControllerContractTests
|
||||
expectedTags["operation"].Should().Be("airgap.import");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OTel_Metrics_TracksExportCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -317,7 +334,8 @@ public sealed class AirGapControllerContractTests
|
||||
metricName.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OTel_Metrics_TracksImportCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -329,7 +347,8 @@ public sealed class AirGapControllerContractTests
|
||||
expectedDimensions.Should().Contain("status");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OTel_ActivitySource_HasCorrectName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -339,7 +358,8 @@ public sealed class AirGapControllerContractTests
|
||||
expectedSourceName.Should().StartWith("StellaOps.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void OTel_Spans_PropagateTraceContext()
|
||||
{
|
||||
// Arrange - Create a trace context
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\..\\StellaOps.AirGap.Importer\\StellaOps.AirGap.Importer.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user