save checkpoint: save features

This commit is contained in:
master
2026-02-12 10:27:23 +02:00
parent dca86e1248
commit 5bca406787
8837 changed files with 1796879 additions and 5294 deletions

View File

@@ -0,0 +1,200 @@
using System;
using System.Text;
using System.Text.Json;
using StellaOps.Platform.Analytics.Models;
using StellaOps.Platform.Analytics.Options;
using StellaOps.Platform.Analytics.Services;
using StellaOps.Scanner.Surface.FS;
using Xunit;
namespace StellaOps.Platform.Analytics.Tests;
public sealed class ScannerPlatformEventsBehaviorTests
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
[Fact]
public void IsSupportedScannerEventKind_RecognizesReportReadyAndScanCompleted()
{
Assert.True(AnalyticsIngestionService.IsSupportedScannerEventKind(OrchestratorEventKinds.ScannerReportReady));
Assert.True(AnalyticsIngestionService.IsSupportedScannerEventKind(OrchestratorEventKinds.ScannerScanCompleted));
Assert.False(AnalyticsIngestionService.IsSupportedScannerEventKind("scanner.unknown"));
}
[Fact]
public void TryExtractDssePayload_DecodesPayload()
{
var payloadJson = JsonSerializer.Serialize(CreateReportReadyPayload());
var dsseElement = ToElement(new
{
payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson)),
payloadType = "application/vnd.in-toto+json"
});
Assert.True(AnalyticsIngestionService.TryExtractDssePayload(
dsseElement,
out var payloadBytes,
out var payloadType));
Assert.Equal("application/vnd.in-toto+json", payloadType);
Assert.Equal(payloadJson, Encoding.UTF8.GetString(payloadBytes));
}
[Fact]
public void TryDeserializeScannerPayload_ReportReadyDsseEnvelope_ParsesReportReadyPayload()
{
var expected = CreateReportReadyPayload();
var payloadJson = JsonSerializer.Serialize(expected);
var dsseElement = ToElement(new
{
payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson)),
payloadType = "application/vnd.in-toto+json"
});
var result = AnalyticsIngestionService.TryDeserializeScannerPayload(
dsseElement,
OrchestratorEventKinds.ScannerReportReady,
SerializerOptions,
out var parsed);
Assert.True(result);
Assert.Equal(expected.ReportId, parsed.ReportId);
Assert.Equal(expected.ImageDigest, parsed.ImageDigest);
Assert.NotNull(parsed.Report.Surface);
}
[Fact]
public void TryDeserializeScannerPayload_ScanCompletedPayload_MapsToReportReadyPayload()
{
var source = CreateReportReadyPayload();
var completedElement = ToElement(new ScanCompletedEventPayload
{
ScanId = "scan-completed-1",
ReportId = source.ReportId,
ImageDigest = source.ImageDigest,
GeneratedAt = source.GeneratedAt,
Report = source.Report
});
var result = AnalyticsIngestionService.TryDeserializeScannerPayload(
completedElement,
OrchestratorEventKinds.ScannerScanCompleted,
SerializerOptions,
out var parsed);
Assert.True(result);
Assert.Equal("scan-completed-1", parsed.ScanId);
Assert.Equal(source.ReportId, parsed.ReportId);
Assert.Equal(source.ImageDigest, parsed.ImageDigest);
Assert.NotNull(parsed.Report.Surface);
}
[Fact]
public void ResolveScannerSubscriptionPosition_UsesCheckpointWhenPresent()
{
var position = AnalyticsIngestionService.ResolveScannerSubscriptionPosition(
startFromBeginning: false,
resumeFromCheckpoint: true,
checkpointEntryId: "1739244123456-0");
Assert.Equal("1739244123456-0", position.Value);
}
[Fact]
public void ResolveScannerSubscriptionPosition_StartFromBeginningOverridesCheckpoint()
{
var position = AnalyticsIngestionService.ResolveScannerSubscriptionPosition(
startFromBeginning: true,
resumeFromCheckpoint: true,
checkpointEntryId: "1739244123456-0");
Assert.Equal("0", position.Value);
}
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData("$", null)]
[InlineData("0", null)]
[InlineData("1739244123456-0", "1739244123456-0")]
public void NormalizeScannerCheckpointEntryId_NormalizesExpectedValues(string? input, string? expected)
{
Assert.Equal(expected, AnalyticsIngestionService.NormalizeScannerCheckpointEntryId(input));
}
[Fact]
public void ResolveScannerCheckpointPath_UsesConfiguredRelativePathWithCasRoot()
{
var streamOptions = new AnalyticsStreamOptions
{
ResumeFromCheckpoint = true,
ScannerCheckpointFilePath = "checkpoints/scanner.position"
};
var casOptions = new AnalyticsCasOptions
{
RootPath = "/var/lib/stellaops/cas"
};
var path = AnalyticsIngestionService.ResolveScannerCheckpointPath(streamOptions, casOptions);
Assert.Equal(
System.IO.Path.Combine("/var/lib/stellaops/cas", "checkpoints/scanner.position"),
path);
}
[Fact]
public void ResolveScannerCheckpointPath_UsesDefaultPathWhenOnlyCasRootConfigured()
{
var streamOptions = new AnalyticsStreamOptions
{
ResumeFromCheckpoint = true
};
var casOptions = new AnalyticsCasOptions
{
RootPath = "/var/lib/stellaops/cas"
};
var path = AnalyticsIngestionService.ResolveScannerCheckpointPath(streamOptions, casOptions);
Assert.Equal(
System.IO.Path.Combine("/var/lib/stellaops/cas", ".state", "platform-scanner-stream.checkpoint"),
path);
}
private static ReportReadyEventPayload CreateReportReadyPayload()
{
return new ReportReadyEventPayload
{
ReportId = "report-001",
ScanId = "scan-001",
ImageDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
GeneratedAt = new DateTimeOffset(2026, 2, 11, 12, 0, 0, TimeSpan.Zero),
Report = new ReportDocumentPayload
{
ReportId = "report-001",
ImageDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
GeneratedAt = new DateTimeOffset(2026, 2, 11, 12, 0, 0, TimeSpan.Zero),
Surface = new SurfacePointersPayload
{
Tenant = "tenant-a",
GeneratedAt = new DateTimeOffset(2026, 2, 11, 12, 0, 0, TimeSpan.Zero),
ManifestDigest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
Manifest = new SurfaceManifestDocument
{
Tenant = "tenant-a",
GeneratedAt = new DateTimeOffset(2026, 2, 11, 12, 0, 0, TimeSpan.Zero)
}
}
}
};
}
private static JsonElement ToElement<T>(T value)
{
using var document = JsonDocument.Parse(JsonSerializer.Serialize(value));
return document.RootElement.Clone();
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.Linq;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using StellaOps.Platform.WebService.Contracts;
using Xunit;
@@ -32,4 +34,75 @@ public sealed class QuotaEndpointsTests : IClassFixture<PlatformWebApplicationFa
items.Select(item => item.QuotaId).ToArray());
Assert.Equal(77000m, items[0].Remaining);
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task QuotaAlerts_CreateAndList_AreTenantScoped()
{
var tenantId = "tenant-quotas-alerts-a";
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-quotas-alerts");
var createRequest = new PlatformQuotaAlertRequest(
QuotaId: "gateway.requests",
Threshold: 85m,
Condition: "gt",
Severity: "high");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/quotas/alerts",
createRequest,
TestContext.Current.CancellationToken);
createResponse.EnsureSuccessStatusCode();
var createdAlert = await createResponse.Content.ReadFromJsonAsync<PlatformQuotaAlert>(TestContext.Current.CancellationToken);
Assert.NotNull(createdAlert);
Assert.Equal("gateway.requests", createdAlert!.QuotaId);
Assert.Equal("high", createdAlert.Severity);
Assert.Equal("actor-quotas-alerts", createdAlert.CreatedBy);
var listForSameTenant = await client.GetFromJsonAsync<PlatformListResponse<PlatformQuotaAlert>>(
"/api/v1/platform/quotas/alerts",
TestContext.Current.CancellationToken);
Assert.NotNull(listForSameTenant);
Assert.Contains(
listForSameTenant!.Items,
alert => string.Equals(alert.AlertId, createdAlert.AlertId, StringComparison.Ordinal));
client.DefaultRequestHeaders.Remove("X-StellaOps-Tenant");
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-quotas-alerts-b");
var listForOtherTenant = await client.GetFromJsonAsync<PlatformListResponse<PlatformQuotaAlert>>(
"/api/v1/platform/quotas/alerts",
TestContext.Current.CancellationToken);
Assert.NotNull(listForOtherTenant);
Assert.DoesNotContain(
listForOtherTenant!.Items,
alert => string.Equals(alert.AlertId, createdAlert.AlertId, StringComparison.Ordinal));
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task QuotaAlerts_RejectMissingQuotaId()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-quotas-alerts-validation");
var invalidRequest = new PlatformQuotaAlertRequest(
QuotaId: " ",
Threshold: 90m,
Condition: "gt",
Severity: "critical");
var response = await client.PostAsJsonAsync(
"/api/v1/platform/quotas/alerts",
invalidRequest,
TestContext.Current.CancellationToken);
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
Assert.NotNull(body);
Assert.Equal("quotaId is required.", body!["error"]?.GetValue<string>());
}
}

View File

@@ -38,4 +38,41 @@ public sealed class SearchEndpointsTests : IClassFixture<PlatformWebApplicationF
},
items);
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task Search_AppliesSourceFilterAndPagination()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-search-filtered");
var response = await client.GetFromJsonAsync<PlatformListResponse<PlatformSearchItem>>(
"/api/v1/platform/search?sources=scanner,findings&limit=1&offset=1",
TestContext.Current.CancellationToken);
Assert.NotNull(response);
Assert.Equal(2, response!.Count);
Assert.Equal(1, response.Limit);
Assert.Equal(1, response.Offset);
var singleItem = Assert.Single(response.Items);
Assert.Equal("finding-cve-2025-1001", singleItem.EntityId);
Assert.Equal("findings", singleItem.Source);
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task Search_AliasEndpointHonorsQueryFilter()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-search-alias");
var response = await client.GetFromJsonAsync<PlatformListResponse<PlatformSearchItem>>(
"/api/v1/search?q=tenant",
TestContext.Current.CancellationToken);
Assert.NotNull(response);
var item = Assert.Single(response!.Items);
Assert.Equal("tenant-acme", item.EntityId);
Assert.Equal("authority", item.Source);
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class SetupEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public SetupEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SetupWorkflow_CreateResumeExecuteSkipFinalizeAndDefinitions_Passes()
{
var tenantId = $"tenant-setup-{Guid.NewGuid():N}";
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "setup-tester");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/setup/sessions",
new CreateSetupSessionRequest(),
TestContext.Current.CancellationToken);
createResponse.EnsureSuccessStatusCode();
var created = await createResponse.Content.ReadFromJsonAsync<SetupSessionResponse>(
TestContext.Current.CancellationToken);
Assert.NotNull(created);
Assert.Equal(SetupSessionStatus.InProgress, created!.Session.Status);
var sessionId = created.Session.SessionId;
var resumeResponse = await client.PostAsync(
"/api/v1/setup/sessions/resume",
content: null,
TestContext.Current.CancellationToken);
resumeResponse.EnsureSuccessStatusCode();
var resumed = await resumeResponse.Content.ReadFromJsonAsync<SetupSessionResponse>(
TestContext.Current.CancellationToken);
Assert.NotNull(resumed);
Assert.Equal(sessionId, resumed!.Session.SessionId);
var definitions = await client.GetFromJsonAsync<SetupStepDefinitionsResponse>(
"/api/v1/setup/definitions/steps",
TestContext.Current.CancellationToken);
Assert.NotNull(definitions);
Assert.NotEmpty(definitions!.Steps);
Assert.True(
definitions.Steps.Select(step => step.OrderIndex)
.SequenceEqual(definitions.Steps.Select(step => step.OrderIndex).OrderBy(i => i)));
var requiredFlow = new[]
{
SetupStepId.Database,
SetupStepId.Valkey,
SetupStepId.Migrations,
SetupStepId.Admin,
SetupStepId.Crypto
};
foreach (var step in requiredFlow)
{
var executeResponse = await client.PostAsJsonAsync(
"/api/v1/setup/steps/execute",
new ExecuteSetupStepRequest(step),
TestContext.Current.CancellationToken);
executeResponse.EnsureSuccessStatusCode();
var executed = await executeResponse.Content.ReadFromJsonAsync<ExecuteSetupStepResponse>(
TestContext.Current.CancellationToken);
Assert.NotNull(executed);
Assert.True(executed!.Success);
Assert.Equal(SetupStepStatus.Passed, executed.StepState.Status);
}
var skipResponse = await client.PostAsJsonAsync(
"/api/v1/setup/steps/skip",
new SkipSetupStepRequest(SetupStepId.Llm, "llm provider deferred"),
TestContext.Current.CancellationToken);
skipResponse.EnsureSuccessStatusCode();
var skipped = await skipResponse.Content.ReadFromJsonAsync<SetupSessionResponse>(
TestContext.Current.CancellationToken);
Assert.NotNull(skipped);
var llmStep = skipped!.Session.Steps.First(step => step.StepId == SetupStepId.Llm);
Assert.Equal(SetupStepStatus.Skipped, llmStep.Status);
var finalizeResponse = await client.PostAsJsonAsync(
"/api/v1/setup/sessions/finalize",
new FinalizeSetupSessionRequest(),
TestContext.Current.CancellationToken);
finalizeResponse.EnsureSuccessStatusCode();
var finalized = await finalizeResponse.Content.ReadFromJsonAsync<FinalizeSetupSessionResponse>(
TestContext.Current.CancellationToken);
Assert.NotNull(finalized);
Assert.Equal(SetupSessionStatus.CompletedPartial, finalized!.FinalStatus);
Assert.Contains(finalized.SkippedSteps, step => step.StepId == SetupStepId.Llm);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SkipRequiredStep_ReturnsProblemDetails()
{
var tenantId = $"tenant-setup-required-{Guid.NewGuid():N}";
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "setup-tester");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/setup/sessions",
new CreateSetupSessionRequest(),
TestContext.Current.CancellationToken);
createResponse.EnsureSuccessStatusCode();
var skipResponse = await client.PostAsJsonAsync(
"/api/v1/setup/steps/skip",
new SkipSetupStepRequest(SetupStepId.Database, "should fail"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, skipResponse.StatusCode);
var problem = await skipResponse.Content.ReadFromJsonAsync<ProblemDetails>(
TestContext.Current.CancellationToken);
Assert.NotNull(problem);
Assert.Equal("Invalid Operation", problem!.Title);
}
}