Add integration tests for migration categories and execution
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled

- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations.
- Added tests for edge cases, including null, empty, and whitespace migration names.
- Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers.
- Included tests for migration execution, schema creation, and handling of pending release migrations.
- Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
This commit is contained in:
master
2025-12-04 19:10:54 +02:00
parent 600f3a7a3c
commit 75f6942769
301 changed files with 32810 additions and 1128 deletions

View File

@@ -48,43 +48,72 @@ namespace StellaOps.Cli.Commands;
internal static class CommandHandlers
{
private const string KmsPassphraseEnvironmentVariable = "STELLAOPS_KMS_PASSPHRASE";
private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
{
// Simple SHA256 check using sidecar .sha256 file if present; fail closed on mismatch.
var shaPath = path + ".sha256";
if (!File.Exists(shaPath))
{
logger.LogError("Checksum file missing for bundle {Bundle}. Expected sidecar {Sidecar}.", path, shaPath);
Environment.ExitCode = 21;
throw new InvalidOperationException("Checksum file missing");
}
var expected = (await File.ReadAllTextAsync(shaPath, cancellationToken).ConfigureAwait(false)).Trim();
using var stream = File.OpenRead(path);
var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
var actual = Convert.ToHexString(hash).ToLowerInvariant();
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
{
logger.LogError("Checksum mismatch for {Bundle}. Expected {Expected} but found {Actual}", path, expected, actual);
Environment.ExitCode = 22;
throw new InvalidOperationException("Checksum verification failed");
}
logger.LogInformation("Checksum verified for {Bundle}", path);
}
private static readonly JsonSerializerOptions KmsJsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
public static async Task HandleScannerDownloadAsync(
IServiceProvider services,
string channel,
string? output,
bool overwrite,
bool install,
/// <summary>
/// Standard JSON serializer options for CLI output.
/// </summary>
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// JSON serializer options for output (alias for JsonOptions).
/// </summary>
private static readonly JsonSerializerOptions JsonOutputOptions = JsonOptions;
/// <summary>
/// Sets the verbosity level for logging.
/// </summary>
private static void SetVerbosity(IServiceProvider services, bool verbose)
{
// Configure logging level based on verbose flag
var loggerFactory = services.GetService<ILoggerFactory>();
if (loggerFactory is not null && verbose)
{
// Enable debug logging when verbose is true
var logger = loggerFactory.CreateLogger("StellaOps.Cli.Commands.CommandHandlers");
logger.LogDebug("Verbose logging enabled");
}
}
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
{
// Simple SHA256 check using sidecar .sha256 file if present; fail closed on mismatch.
var shaPath = path + ".sha256";
if (!File.Exists(shaPath))
{
logger.LogError("Checksum file missing for bundle {Bundle}. Expected sidecar {Sidecar}.", path, shaPath);
Environment.ExitCode = 21;
throw new InvalidOperationException("Checksum file missing");
}
var expected = (await File.ReadAllTextAsync(shaPath, cancellationToken).ConfigureAwait(false)).Trim();
using var stream = File.OpenRead(path);
var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
var actual = Convert.ToHexString(hash).ToLowerInvariant();
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
{
logger.LogError("Checksum mismatch for {Bundle}. Expected {Expected} but found {Actual}", path, expected, actual);
Environment.ExitCode = 22;
throw new InvalidOperationException("Checksum verification failed");
}
logger.LogInformation("Checksum verified for {Bundle}", path);
}
public static async Task HandleScannerDownloadAsync(
IServiceProvider services,
string channel,
string? output,
bool overwrite,
bool install,
bool verbose,
CancellationToken cancellationToken)
{
@@ -114,29 +143,29 @@ internal static class CommandHandlers
CliMetrics.RecordScannerDownload(channel, result.FromCache);
if (install)
{
await VerifyBundleAsync(result.Path, logger, cancellationToken).ConfigureAwait(false);
var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
CliMetrics.RecordScannerInstall(channel);
}
if (install)
{
await VerifyBundleAsync(result.Path, logger, cancellationToken).ConfigureAwait(false);
var installer = scope.ServiceProvider.GetRequiredService<IScannerInstaller>();
await installer.InstallAsync(result.Path, verbose, cancellationToken).ConfigureAwait(false);
CliMetrics.RecordScannerInstall(channel);
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to download scanner bundle.");
if (Environment.ExitCode == 0)
{
Environment.ExitCode = 1;
}
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to download scanner bundle.");
if (Environment.ExitCode == 0)
{
Environment.ExitCode = 1;
}
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
public static async Task HandleTaskRunnerSimulateAsync(
@@ -264,15 +293,15 @@ internal static class CommandHandlers
{
var console = AnsiConsole.Console;
console.MarkupLine($"[bold]Scan[/]: {result.ScanId}");
console.MarkupLine($"Image: {result.ImageDigest}");
console.MarkupLine($"Generated: {result.GeneratedAt:O}");
console.MarkupLine($"Outcome: {result.Graph.Outcome}");
if (result.BestPlan is not null)
{
console.MarkupLine($"Best Terminal: {result.BestPlan.TerminalPath} (conf {result.BestPlan.Confidence:F1}, user {result.BestPlan.User}, cwd {result.BestPlan.WorkingDirectory})");
}
console.MarkupLine($"[bold]Scan[/]: {result.ScanId}");
console.MarkupLine($"Image: {result.ImageDigest}");
console.MarkupLine($"Generated: {result.GeneratedAt:O}");
console.MarkupLine($"Outcome: {result.Graph.Outcome}");
if (result.BestPlan is not null)
{
console.MarkupLine($"Best Terminal: {result.BestPlan.TerminalPath} (conf {result.BestPlan.Confidence:F1}, user {result.BestPlan.User}, cwd {result.BestPlan.WorkingDirectory})");
}
var planTable = new Table()
.AddColumn("Terminal")
@@ -284,15 +313,15 @@ internal static class CommandHandlers
foreach (var plan in result.Graph.Plans.OrderByDescending(p => p.Confidence))
{
var confidence = plan.Confidence.ToString("F1", CultureInfo.InvariantCulture);
planTable.AddRow(
plan.TerminalPath,
plan.Runtime ?? "-",
plan.Type.ToString(),
confidence,
plan.User,
plan.WorkingDirectory);
}
var confidence = plan.Confidence.ToString("F1", CultureInfo.InvariantCulture);
planTable.AddRow(
plan.TerminalPath,
plan.Runtime ?? "-",
plan.Type.ToString(),
confidence,
plan.User,
plan.WorkingDirectory);
}
if (planTable.Rows.Count > 0)
{
@@ -6860,7 +6889,7 @@ internal static class CommandHandlers
}
AnsiConsole.Write(violationTable);
}
}
private static int DetermineVerifyExitCode(AocVerifyResponse response)
{
@@ -10895,13 +10924,10 @@ stella policy test {policyName}.stella
Code = diag.Code,
Message = diag.Message,
Severity = diag.Severity.ToString().ToLowerInvariant(),
Line = diag.Line,
Column = diag.Column,
Span = diag.Span,
Suggestion = diag.Suggestion
Path = diag.Path
};
if (diag.Severity == PolicyDsl.DiagnosticSeverity.Error)
if (diag.Severity == PolicyIssueSeverity.Error)
{
errors.Add(diagnostic);
}
@@ -10939,7 +10965,7 @@ stella policy test {policyName}.stella
InputPath = fullPath,
IrPath = irPath,
Digest = digest,
SyntaxVersion = compileResult.Document?.SyntaxVersion,
SyntaxVersion = compileResult.Document?.Syntax,
PolicyName = compileResult.Document?.Name,
RuleCount = compileResult.Document?.Rules.Length ?? 0,
ProfileCount = compileResult.Document?.Profiles.Length ?? 0,
@@ -10985,24 +11011,14 @@ stella policy test {policyName}.stella
foreach (var err in errors)
{
var location = err.Line.HasValue ? $":{err.Line}" : "";
if (err.Column.HasValue) location += $":{err.Column}";
AnsiConsole.MarkupLine($"[red]error[{Markup.Escape(err.Code)}]{location}: {Markup.Escape(err.Message)}[/]");
if (!string.IsNullOrWhiteSpace(err.Suggestion))
{
AnsiConsole.MarkupLine($" [cyan]suggestion: {Markup.Escape(err.Suggestion)}[/]");
}
var location = !string.IsNullOrWhiteSpace(err.Path) ? $" at {err.Path}" : "";
AnsiConsole.MarkupLine($"[red]error[{Markup.Escape(err.Code)}]{Markup.Escape(location)}: {Markup.Escape(err.Message)}[/]");
}
foreach (var warn in warnings)
{
var location = warn.Line.HasValue ? $":{warn.Line}" : "";
if (warn.Column.HasValue) location += $":{warn.Column}";
AnsiConsole.MarkupLine($"[yellow]warning[{Markup.Escape(warn.Code)}]{location}: {Markup.Escape(warn.Message)}[/]");
if (!string.IsNullOrWhiteSpace(warn.Suggestion))
{
AnsiConsole.MarkupLine($" [cyan]suggestion: {Markup.Escape(warn.Suggestion)}[/]");
}
var location = !string.IsNullOrWhiteSpace(warn.Path) ? $" at {warn.Path}" : "";
AnsiConsole.MarkupLine($"[yellow]warning[{Markup.Escape(warn.Code)}]{Markup.Escape(location)}: {Markup.Escape(warn.Message)}[/]");
}
}
@@ -13248,18 +13264,6 @@ stella policy test {policyName}.stella
}
}
private static string GetVexStatusMarkup(string status)
{
return status?.ToLowerInvariant() switch
{
"affected" => "[red]affected[/]",
"not_affected" => "[green]not_affected[/]",
"fixed" => "[blue]fixed[/]",
"under_investigation" => "[yellow]under_investigation[/]",
_ => Markup.Escape(status ?? "(unknown)")
};
}
#endregion
#region Vulnerability Explorer (CLI-VULN-29-001)
@@ -14543,13 +14547,13 @@ stella policy test {policyName}.stella
var fixText = obs.Fix?.Available == true ? "[green]available[/]" : "[grey]none[/]";
table.AddRow(
Markup.Escape(obs.ObservationId),
Markup.Escape(sourceVendor),
Markup.Escape(aliasesText),
Markup.Escape(severityText),
new Markup(Markup.Escape(obs.ObservationId)),
new Markup(Markup.Escape(sourceVendor)),
new Markup(Markup.Escape(aliasesText)),
new Markup(Markup.Escape(severityText)),
new Markup(kevText),
new Markup(fixText),
obs.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture));
new Markup(Markup.Escape(obs.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture))));
}
AnsiConsole.Write(table);
@@ -15386,12 +15390,12 @@ stella policy test {policyName}.stella
var size = FormatSize(snapshot.SizeBytes);
table.AddRow(
Markup.Escape(snapshot.SnapshotId.Length > 20 ? snapshot.SnapshotId[..17] + "..." : snapshot.SnapshotId),
Markup.Escape(snapshot.CaseId),
new Markup(Markup.Escape(snapshot.SnapshotId.Length > 20 ? snapshot.SnapshotId[..17] + "..." : snapshot.SnapshotId)),
new Markup(Markup.Escape(snapshot.CaseId)),
new Markup(statusMarkup),
artifactCount,
size,
snapshot.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture));
new Markup(Markup.Escape(artifactCount)),
new Markup(Markup.Escape(size)),
new Markup(Markup.Escape(snapshot.CreatedAt.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture))));
}
AnsiConsole.Write(table);