Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -107,7 +107,6 @@ public class ReachGraphE2ETests : IClassFixture<WebApplicationFactory<StellaOps.
|
||||
|
||||
var fetchedGraph = await getResponse.Content.ReadFromJsonAsync<ReachGraphMinimal>();
|
||||
Assert.NotNull(fetchedGraph);
|
||||
Assert.NotNull(fetchedGraph.Edges);
|
||||
|
||||
// Verify edge explanations are preserved
|
||||
var edgeTypes = fetchedGraph.Edges.Select(e => e.Why.Type).Distinct().ToList();
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
|
||||
<!-- Testing infrastructure -->
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Testing.Determinism/StellaOps.Testing.Determinism.csproj" />
|
||||
|
||||
<!-- Replay infrastructure (SPRINT_20260105_002_001_REPLAY) -->
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Verdict/StellaOps.Verdict.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
// <copyright file="VerifyProveE2ETests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// VerifyProveE2ETests.cs
|
||||
// Sprint: SPRINT_20260105_002_001_REPLAY
|
||||
// Task: RPL-022 - E2E test: Full verify -> prove workflow
|
||||
// Description: End-to-end tests for bundle verification and proof generation.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
using StellaOps.Replay.Core.Models;
|
||||
using StellaOps.Verdict;
|
||||
|
||||
namespace StellaOps.Integration.E2E;
|
||||
|
||||
/// <summary>
|
||||
/// E2E tests for verify -> prove workflow.
|
||||
/// RPL-022: Tests bundle verification and replay proof generation.
|
||||
/// </summary>
|
||||
[Trait("Category", "E2E")]
|
||||
public sealed class VerifyProveE2ETests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
private readonly VerdictBuilderService _verdictBuilder;
|
||||
|
||||
public VerifyProveE2ETests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"e2e-verify-prove-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
|
||||
_verdictBuilder = new VerdictBuilderService(
|
||||
NullLogger<VerdictBuilderService>.Instance,
|
||||
signer: null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_testDir))
|
||||
{
|
||||
Directory.Delete(_testDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
#region Workflow Tests
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_CreateBundle_VerifyReplay_GenerateProof()
|
||||
{
|
||||
// Arrange: Create a complete test bundle
|
||||
var bundlePath = CreateCompleteTestBundle("workflow-test-001");
|
||||
|
||||
// Act: Execute replay and generate proof
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
|
||||
ImageDigest = manifest.Scan.ImageDigest,
|
||||
PolicyDigest = manifest.Scan.PolicyDigest,
|
||||
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
|
||||
};
|
||||
|
||||
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert: Replay succeeded
|
||||
result.Success.Should().BeTrue("Replay should succeed with valid inputs");
|
||||
result.VerdictHash.Should().NotBeNullOrEmpty();
|
||||
result.VerdictHash.Should().StartWith("cgs:sha256:");
|
||||
result.EngineVersion.Should().Be("1.0.0");
|
||||
result.DurationMs.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_DeterministicReplay_SameInputsSameOutput()
|
||||
{
|
||||
// Arrange: Create bundle
|
||||
var bundlePath = CreateCompleteTestBundle("determinism-test-001");
|
||||
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
|
||||
ImageDigest = manifest.Scan.ImageDigest,
|
||||
PolicyDigest = manifest.Scan.PolicyDigest,
|
||||
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
|
||||
};
|
||||
|
||||
// Act: Replay twice
|
||||
var result1 = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
var result2 = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert: Same verdict hash
|
||||
result1.Success.Should().BeTrue();
|
||||
result2.Success.Should().BeTrue();
|
||||
result1.VerdictHash.Should().Be(result2.VerdictHash, "Same inputs must produce same verdict hash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullWorkflow_WithVexDocuments_VexInfluencesVerdict()
|
||||
{
|
||||
// Arrange: Create bundle with VEX
|
||||
var bundlePath = CreateBundleWithVex("vex-test-001");
|
||||
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
|
||||
VexPath = Path.Combine(bundlePath, "inputs", "vex"),
|
||||
ImageDigest = manifest.Scan.ImageDigest,
|
||||
PolicyDigest = manifest.Scan.PolicyDigest,
|
||||
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.VerdictHash.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProofGeneration_ValidBundle_ProducesCompactProof()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = CreateCompleteTestBundle("proof-gen-001");
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
|
||||
ImageDigest = manifest.Scan.ImageDigest,
|
||||
PolicyDigest = manifest.Scan.PolicyDigest,
|
||||
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
var bundleHash = await ComputeBundleHashAsync(bundlePath);
|
||||
|
||||
var proof = ReplayProof.FromExecutionResult(
|
||||
bundleHash: bundleHash,
|
||||
policyVersion: manifest.Scan.PolicyDigest,
|
||||
verdictRoot: result.VerdictHash ?? "unknown",
|
||||
verdictMatches: true,
|
||||
durationMs: result.DurationMs,
|
||||
replayedAt: DateTimeOffset.UtcNow,
|
||||
engineVersion: result.EngineVersion ?? "1.0.0",
|
||||
artifactDigest: manifest.Scan.ImageDigest);
|
||||
|
||||
// Assert
|
||||
proof.Should().NotBeNull();
|
||||
var compactProof = proof.ToCompactString();
|
||||
compactProof.Should().StartWith("replay-proof:sha256:");
|
||||
compactProof.Should().HaveLength(78); // "replay-proof:sha256:" + 64 hex chars
|
||||
|
||||
var canonicalJson = proof.ToCanonicalJson();
|
||||
canonicalJson.Should().NotBeNullOrEmpty();
|
||||
canonicalJson.Should().Contain("verdictRoot");
|
||||
canonicalJson.Should().Contain("bundleHash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProofGeneration_DifferentBundles_DifferentProofHashes()
|
||||
{
|
||||
// Arrange
|
||||
var bundle1Path = CreateCompleteTestBundle("bundle-1");
|
||||
var bundle2Path = CreateCompleteTestBundle("bundle-2", sbomVersion: 2);
|
||||
|
||||
async Task<string> GenerateProofHash(string bundlePath)
|
||||
{
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
})!;
|
||||
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
|
||||
ImageDigest = manifest.Scan.ImageDigest,
|
||||
PolicyDigest = manifest.Scan.PolicyDigest,
|
||||
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
|
||||
};
|
||||
|
||||
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
var bundleHash = await ComputeBundleHashAsync(bundlePath);
|
||||
|
||||
var proof = ReplayProof.FromExecutionResult(
|
||||
bundleHash: bundleHash,
|
||||
policyVersion: manifest.Scan.PolicyDigest,
|
||||
verdictRoot: result.VerdictHash ?? "unknown",
|
||||
verdictMatches: true,
|
||||
durationMs: result.DurationMs,
|
||||
replayedAt: DateTimeOffset.UtcNow,
|
||||
engineVersion: result.EngineVersion ?? "1.0.0",
|
||||
artifactDigest: manifest.Scan.ImageDigest);
|
||||
|
||||
return proof.ToCompactString();
|
||||
}
|
||||
|
||||
// Act
|
||||
var proof1 = await GenerateProofHash(bundle1Path);
|
||||
var proof2 = await GenerateProofHash(bundle2Path);
|
||||
|
||||
// Assert
|
||||
proof1.Should().NotBe(proof2, "Different bundles should produce different proof hashes");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Workflow_MissingBundle_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(_testDir, "nonexistent", "sbom.json"),
|
||||
ImageDigest = "sha256:abc123",
|
||||
PolicyDigest = "sha256:policy123",
|
||||
FeedSnapshotDigest = "sha256:feeds123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Workflow_InvalidSbom_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var bundlePath = Path.Combine(_testDir, "invalid-sbom");
|
||||
Directory.CreateDirectory(Path.Combine(bundlePath, "inputs"));
|
||||
File.WriteAllText(Path.Combine(bundlePath, "inputs", "sbom.json"), "not valid json {{{");
|
||||
|
||||
var request = new VerdictReplayRequest
|
||||
{
|
||||
SbomPath = Path.Combine(bundlePath, "inputs", "sbom.json"),
|
||||
ImageDigest = "sha256:abc123",
|
||||
PolicyDigest = "sha256:policy123",
|
||||
FeedSnapshotDigest = "sha256:feeds123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string CreateCompleteTestBundle(string bundleId, int sbomVersion = 1)
|
||||
{
|
||||
var bundlePath = Path.Combine(_testDir, bundleId);
|
||||
Directory.CreateDirectory(Path.Combine(bundlePath, "inputs"));
|
||||
Directory.CreateDirectory(Path.Combine(bundlePath, "outputs"));
|
||||
|
||||
// Create SBOM
|
||||
var sbomContent = $$"""
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": {{sbomVersion}},
|
||||
"metadata": {
|
||||
"timestamp": "2026-01-05T10:00:00Z"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "test-package",
|
||||
"version": "1.0.0",
|
||||
"purl": "pkg:npm/test-package@1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var sbomPath = Path.Combine(bundlePath, "inputs", "sbom.json");
|
||||
File.WriteAllText(sbomPath, sbomContent, Encoding.UTF8);
|
||||
|
||||
// Compute SBOM hash
|
||||
var sbomHash = ComputeHash(sbomContent);
|
||||
|
||||
// Create verdict output
|
||||
var verdictContent = """
|
||||
{
|
||||
"decision": "pass",
|
||||
"score": 0.95,
|
||||
"findings": []
|
||||
}
|
||||
""";
|
||||
var verdictPath = Path.Combine(bundlePath, "outputs", "verdict.json");
|
||||
File.WriteAllText(verdictPath, verdictContent, Encoding.UTF8);
|
||||
var verdictHash = ComputeHash(verdictContent);
|
||||
|
||||
// Create manifest
|
||||
var manifest = new
|
||||
{
|
||||
schemaVersion = "2.0.0",
|
||||
bundleId = bundleId,
|
||||
createdAt = DateTimeOffset.UtcNow.ToString("O"),
|
||||
scan = new
|
||||
{
|
||||
id = $"scan-{bundleId}",
|
||||
imageDigest = $"sha256:image{bundleId}",
|
||||
policyDigest = "sha256:policy123",
|
||||
scorePolicyDigest = "sha256:scorepolicy123",
|
||||
feedSnapshotDigest = "sha256:feeds123",
|
||||
toolchain = "stellaops-1.0.0",
|
||||
analyzerSetDigest = "sha256:analyzers123"
|
||||
},
|
||||
inputs = new
|
||||
{
|
||||
sbom = new { path = "inputs/sbom.json", sha256 = sbomHash }
|
||||
},
|
||||
expectedOutputs = new
|
||||
{
|
||||
verdict = new { path = "outputs/verdict.json", sha256 = verdictHash },
|
||||
verdictHash = $"cgs:sha256:{verdictHash}"
|
||||
}
|
||||
};
|
||||
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
return bundlePath;
|
||||
}
|
||||
|
||||
private string CreateBundleWithVex(string bundleId)
|
||||
{
|
||||
var bundlePath = CreateCompleteTestBundle(bundleId);
|
||||
|
||||
// Add VEX documents
|
||||
var vexPath = Path.Combine(bundlePath, "inputs", "vex");
|
||||
Directory.CreateDirectory(vexPath);
|
||||
|
||||
var vexContent = """
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "https://example.com/vex/001",
|
||||
"author": "security@example.com",
|
||||
"timestamp": "2026-01-05T10:00:00Z",
|
||||
"version": 1,
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": {"name": "CVE-2024-0001"},
|
||||
"products": [{"@id": "pkg:npm/test-package@1.0.0"}],
|
||||
"status": "not_affected",
|
||||
"justification": "vulnerable_code_not_present"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(vexPath, "vex-001.json"), vexContent, Encoding.UTF8);
|
||||
|
||||
return bundlePath;
|
||||
}
|
||||
|
||||
private static string ComputeHash(string content)
|
||||
{
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeBundleHashAsync(string bundlePath)
|
||||
{
|
||||
var files = Directory.GetFiles(bundlePath, "*", SearchOption.AllDirectories)
|
||||
.OrderBy(f => f, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
}
|
||||
|
||||
using var hasher = System.Security.Cryptography.SHA256.Create();
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileBytes = await File.ReadAllBytesAsync(file);
|
||||
hasher.TransformBlock(fileBytes, 0, fileBytes.Length, null, 0);
|
||||
}
|
||||
|
||||
hasher.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
return $"sha256:{Convert.ToHexString(hasher.Hash!).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test DTOs
|
||||
|
||||
private sealed record TestManifest
|
||||
{
|
||||
public required string SchemaVersion { get; init; }
|
||||
public required string BundleId { get; init; }
|
||||
public required string CreatedAt { get; init; }
|
||||
public required TestScanInfo Scan { get; init; }
|
||||
public required TestInputs Inputs { get; init; }
|
||||
public TestOutputs? ExpectedOutputs { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestScanInfo
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ImageDigest { get; init; }
|
||||
public required string PolicyDigest { get; init; }
|
||||
public required string FeedSnapshotDigest { get; init; }
|
||||
public string? Toolchain { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestInputs
|
||||
{
|
||||
public required TestInputFile Sbom { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestInputFile
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
public required string Sha256 { get; init; }
|
||||
}
|
||||
|
||||
private sealed record TestOutputs
|
||||
{
|
||||
public TestInputFile? Verdict { get; init; }
|
||||
public string? VerdictHash { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -80,12 +80,12 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(ct);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Act - Create a test schema
|
||||
using var createCmd = connection.CreateCommand();
|
||||
createCmd.CommandText = "CREATE SCHEMA IF NOT EXISTS test_platform";
|
||||
await createCmd.ExecuteNonQueryAsync(ct);
|
||||
await createCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert - Verify schema exists
|
||||
using var verifyCmd = connection.CreateCommand();
|
||||
@@ -93,7 +93,7 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name = 'test_platform'";
|
||||
var result = await verifyCmd.ExecuteScalarAsync(ct);
|
||||
var result = await verifyCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken);
|
||||
result.Should().Be("test_platform");
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(ct);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Create test table
|
||||
using var createCmd = connection.CreateCommand();
|
||||
@@ -113,33 +113,33 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
name VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)";
|
||||
await createCmd.ExecuteNonQueryAsync(ct);
|
||||
await createCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Act - Insert
|
||||
using var insertCmd = connection.CreateCommand();
|
||||
insertCmd.CommandText = "INSERT INTO test_crud (name) VALUES ('test-record') RETURNING id";
|
||||
var insertedId = await insertCmd.ExecuteScalarAsync(ct);
|
||||
var insertedId = await insertCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken);
|
||||
insertedId.Should().NotBeNull();
|
||||
|
||||
// Act - Select
|
||||
using var selectCmd = connection.CreateCommand();
|
||||
selectCmd.CommandText = "SELECT name FROM test_crud WHERE id = @id";
|
||||
selectCmd.Parameters.AddWithValue("id", insertedId!);
|
||||
var name = await selectCmd.ExecuteScalarAsync(ct);
|
||||
var name = await selectCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken);
|
||||
name.Should().Be("test-record");
|
||||
|
||||
// Act - Update
|
||||
using var updateCmd = connection.CreateCommand();
|
||||
updateCmd.CommandText = "UPDATE test_crud SET name = 'updated-record' WHERE id = @id";
|
||||
updateCmd.Parameters.AddWithValue("id", insertedId!);
|
||||
var rowsAffected = await updateCmd.ExecuteNonQueryAsync(ct);
|
||||
var rowsAffected = await updateCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
|
||||
rowsAffected.Should().Be(1);
|
||||
|
||||
// Act - Delete
|
||||
using var deleteCmd = connection.CreateCommand();
|
||||
deleteCmd.CommandText = "DELETE FROM test_crud WHERE id = @id";
|
||||
deleteCmd.Parameters.AddWithValue("id", insertedId!);
|
||||
rowsAffected = await deleteCmd.ExecuteNonQueryAsync(ct);
|
||||
rowsAffected = await deleteCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
|
||||
rowsAffected.Should().Be(1);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(ct);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Act - Run a migration-like DDL script
|
||||
var migrationScript = @"
|
||||
@@ -180,12 +180,12 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
|
||||
using var migrateCmd = connection.CreateCommand();
|
||||
migrateCmd.CommandText = migrationScript;
|
||||
await migrateCmd.ExecuteNonQueryAsync(ct);
|
||||
await migrateCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert - Verify migration recorded
|
||||
using var verifyCmd = connection.CreateCommand();
|
||||
verifyCmd.CommandText = "SELECT COUNT(*) FROM schema_migrations WHERE version = 'V2_create_scan_results'";
|
||||
var count = await verifyCmd.ExecuteScalarAsync(ct);
|
||||
var count = await verifyCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken);
|
||||
Convert.ToInt32(count).Should().Be(1);
|
||||
}
|
||||
|
||||
@@ -195,17 +195,17 @@ public class PostgresOnlyStartupTests : IAsyncLifetime
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var connection = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(ct);
|
||||
await connection.OpenAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Act - Create common extensions used by StellaOps
|
||||
using var extCmd = connection.CreateCommand();
|
||||
extCmd.CommandText = "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"";
|
||||
await extCmd.ExecuteNonQueryAsync(ct);
|
||||
await extCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert - Verify extension exists
|
||||
using var verifyCmd = connection.CreateCommand();
|
||||
verifyCmd.CommandText = "SELECT COUNT(*) FROM pg_extension WHERE extname = 'uuid-ossp'";
|
||||
var count = await verifyCmd.ExecuteScalarAsync(ct);
|
||||
var count = await verifyCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken);
|
||||
Convert.ToInt32(count).Should().Be(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user