Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -120,6 +120,53 @@ public sealed class GraphJobRepository : IGraphJobRepository
|
||||
public ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken)
|
||||
=> ListOverlayJobsAsync(tenantId, status: null, limit: 50, cancellationToken);
|
||||
|
||||
// Cross-tenant overloads for background services - scans all tenants
|
||||
public async ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = "SELECT payload FROM scheduler.graph_jobs WHERE type=@Type";
|
||||
if (status is not null)
|
||||
{
|
||||
sql += " AND status=@Status";
|
||||
}
|
||||
sql += " ORDER BY created_at LIMIT @Limit";
|
||||
|
||||
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
var results = await conn.QueryAsync<string>(sql, new
|
||||
{
|
||||
Type = (short)GraphJobQueryType.Build,
|
||||
Status = status is not null ? (short)status : (short?)null,
|
||||
Limit = limit
|
||||
});
|
||||
|
||||
return results
|
||||
.Select(r => CanonicalJsonSerializer.Deserialize<GraphBuildJob>(r))
|
||||
.Where(r => r is not null)!
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
public async ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = "SELECT payload FROM scheduler.graph_jobs WHERE type=@Type";
|
||||
if (status is not null)
|
||||
{
|
||||
sql += " AND status=@Status";
|
||||
}
|
||||
sql += " ORDER BY created_at LIMIT @Limit";
|
||||
|
||||
await using var conn = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
var results = await conn.QueryAsync<string>(sql, new
|
||||
{
|
||||
Type = (short)GraphJobQueryType.Overlay,
|
||||
Status = status is not null ? (short)status : (short?)null,
|
||||
Limit = limit
|
||||
});
|
||||
|
||||
return results
|
||||
.Select(r => CanonicalJsonSerializer.Deserialize<GraphOverlayJob>(r))
|
||||
.Where(r => r is not null)!
|
||||
.ToArray()!;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken)
|
||||
{
|
||||
const string sql = @"UPDATE scheduler.graph_jobs
|
||||
|
||||
@@ -19,4 +19,8 @@ public interface IGraphJobRepository
|
||||
ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken);
|
||||
ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken);
|
||||
ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken);
|
||||
|
||||
// Cross-tenant overloads for background services
|
||||
ValueTask<IReadOnlyCollection<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken);
|
||||
ValueTask<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -58,4 +58,9 @@ public interface IJobHistoryRepository
|
||||
/// Deletes old history entries.
|
||||
/// </summary>
|
||||
Task<int> DeleteOlderThanAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets recent failed jobs across all tenants for background indexing.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<JobHistoryEntity>> GetRecentFailedAsync(int limit, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -210,6 +210,31 @@ public sealed class JobHistoryRepository : RepositoryBase<SchedulerDataSource>,
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<JobHistoryEntity>> GetRecentFailedAsync(int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, job_id, tenant_id, project_id, job_type, status, attempt, payload_digest,
|
||||
result, reason, worker_id, duration_ms, created_at, completed_at, archived_at
|
||||
FROM scheduler.job_history
|
||||
WHERE status = 'failed'::scheduler.job_status OR status = 'timed_out'::scheduler.job_status
|
||||
ORDER BY completed_at DESC
|
||||
LIMIT @limit
|
||||
""";
|
||||
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "limit", limit);
|
||||
|
||||
var results = new List<JobHistoryEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
results.Add(MapJobHistory(reader));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static JobHistoryEntity MapJobHistory(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetInt64(reader.GetOrdinal("id")),
|
||||
|
||||
@@ -89,8 +89,7 @@ public sealed class PartitionMaintenanceWorker : BackgroundService
|
||||
|
||||
_logger.LogInformation("Starting partition maintenance cycle");
|
||||
|
||||
await using var conn = await _dataSource.GetConnectionAsync(ct);
|
||||
await conn.OpenAsync(ct);
|
||||
await using var conn = await _dataSource.OpenSystemConnectionAsync(ct);
|
||||
|
||||
foreach (var (schemaTable, _) in opts.ManagedTables)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Events;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -94,10 +95,13 @@ public sealed class FailureSignatureIndexer : BackgroundService
|
||||
_logger.LogDebug("Starting failure indexing batch");
|
||||
|
||||
// Get recent failed jobs that haven't been indexed
|
||||
var failedJobs = await _historyRepository.GetRecentFailedJobsAsync(
|
||||
var historyEntries = await _historyRepository.GetRecentFailedAsync(
|
||||
_options.Value.BatchSize,
|
||||
ct);
|
||||
|
||||
// Convert history entries to failed job records
|
||||
var failedJobs = historyEntries.Select(ConvertToFailedJobRecord).ToList();
|
||||
|
||||
var indexed = 0;
|
||||
foreach (var job in failedJobs)
|
||||
{
|
||||
@@ -278,6 +282,58 @@ public sealed class FailureSignatureIndexer : BackgroundService
|
||||
|
||||
return (errorCode, ErrorCategory.Unknown);
|
||||
}
|
||||
|
||||
private static FailedJobRecord ConvertToFailedJobRecord(JobHistoryEntity entity)
|
||||
{
|
||||
// Try to extract additional details from the result JSON
|
||||
string? imageDigest = null;
|
||||
string? artifactDigest = null;
|
||||
string? repository = null;
|
||||
string? errorCode = null;
|
||||
string? scannerVersion = null;
|
||||
string? runtimeVersion = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entity.Result))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(entity.Result);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("imageDigest", out var imgProp))
|
||||
imageDigest = imgProp.GetString();
|
||||
if (root.TryGetProperty("artifactDigest", out var artProp))
|
||||
artifactDigest = artProp.GetString();
|
||||
if (root.TryGetProperty("repository", out var repoProp))
|
||||
repository = repoProp.GetString();
|
||||
if (root.TryGetProperty("errorCode", out var codeProp))
|
||||
errorCode = codeProp.GetString();
|
||||
if (root.TryGetProperty("scannerVersion", out var scanVerProp))
|
||||
scannerVersion = scanVerProp.GetString();
|
||||
if (root.TryGetProperty("runtimeVersion", out var rtVerProp))
|
||||
runtimeVersion = rtVerProp.GetString();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Result is not valid JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
return new FailedJobRecord
|
||||
{
|
||||
JobId = entity.JobId,
|
||||
TenantId = entity.TenantId,
|
||||
JobType = entity.JobType,
|
||||
ImageDigest = imageDigest,
|
||||
ArtifactDigest = artifactDigest,
|
||||
Repository = repository,
|
||||
Error = entity.Reason,
|
||||
ErrorCode = errorCode,
|
||||
ScannerVersion = scannerVersion,
|
||||
RuntimeVersion = runtimeVersion,
|
||||
FailedAt = entity.CompletedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories.Services;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Observability;
|
||||
|
||||
|
||||
@@ -202,11 +202,11 @@ internal sealed class PlannerQueueDispatchService : IPlannerQueueDispatchService
|
||||
return map;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, SurfaceManifestPointer>> PrefetchManifestsAsync(
|
||||
private async Task<IReadOnlyDictionary<string, Queue.SurfaceManifestPointer>> PrefetchManifestsAsync(
|
||||
IReadOnlyList<string> digests,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var results = new Dictionary<string, SurfaceManifestPointer>(StringComparer.Ordinal);
|
||||
var results = new Dictionary<string, Queue.SurfaceManifestPointer>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var digest in digests)
|
||||
{
|
||||
@@ -224,7 +224,7 @@ internal sealed class PlannerQueueDispatchService : IPlannerQueueDispatchService
|
||||
continue;
|
||||
}
|
||||
|
||||
var pointer = new SurfaceManifestPointer(digest, manifest.Tenant);
|
||||
var pointer = new Queue.SurfaceManifestPointer(digest, manifest.Tenant);
|
||||
results[digest] = pointer;
|
||||
_metrics.RecordSurfaceManifestPrefetch(result: "hit");
|
||||
}
|
||||
@@ -239,9 +239,7 @@ internal sealed class PlannerQueueDispatchService : IPlannerQueueDispatchService
|
||||
}
|
||||
}
|
||||
|
||||
return results.Count == 0
|
||||
? (IReadOnlyDictionary<string, SurfaceManifestPointer>)EmptyReadOnlyDictionary<string, SurfaceManifestPointer>.Instance
|
||||
: results;
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ using Scheduler.Backfill;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Backfill.Tests;
|
||||
|
||||
public class BackfillMappingsTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(ScheduleMode.AnalysisOnly, "analysisonly")]
|
||||
[InlineData(ScheduleMode.ContentRefresh, "contentrefresh")]
|
||||
public void ScheduleMode_is_lower_snake(ScheduleMode mode, string expected)
|
||||
@@ -15,7 +17,8 @@ public class BackfillMappingsTests
|
||||
BackfillMappings.ToScheduleMode(mode).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(RunState.Planning, "planning")]
|
||||
[InlineData(RunState.Completed, "completed")]
|
||||
[InlineData(RunState.Cancelled, "cancelled")]
|
||||
@@ -24,7 +27,8 @@ public class BackfillMappingsTests
|
||||
BackfillMappings.ToRunState(state).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(RunTrigger.Cron, "cron")]
|
||||
[InlineData(RunTrigger.Manual, "manual")]
|
||||
public void RunTrigger_is_lower(RunTrigger trigger, string expected)
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace StellaOps.Scheduler.ImpactIndex.Tests;
|
||||
|
||||
public sealed class FixtureImpactIndexTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByPurls_UsesEmbeddedFixtures()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages);
|
||||
@@ -38,7 +39,8 @@ public sealed class FixtureImpactIndexTests
|
||||
result.SchemaVersion.Should().Be(SchedulerSchemaVersions.ImpactSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByPurls_UsageOnlyFiltersInventoryOnlyComponents()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages);
|
||||
@@ -63,7 +65,8 @@ public sealed class FixtureImpactIndexTests
|
||||
inventoryResult.Images.Single().UsedByEntrypoint.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAll_ReturnsDeterministicFixtureSet()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages);
|
||||
@@ -78,7 +81,8 @@ public sealed class FixtureImpactIndexTests
|
||||
second.Images.Should().Equal(first.Images);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByVulnerabilities_ReturnsEmptySet()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages);
|
||||
@@ -93,7 +97,8 @@ public sealed class FixtureImpactIndexTests
|
||||
result.Images.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FixtureDirectoryOption_LoadsFromFileSystem()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages);
|
||||
@@ -104,6 +109,7 @@ public sealed class FixtureImpactIndexTests
|
||||
});
|
||||
using var _ = loggerFactory;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var result = await impactIndex.ResolveAllAsync(selector, usageOnly: false);
|
||||
|
||||
result.Images.Should().HaveCount(6);
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Emit.Index;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.ImpactIndex.Tests;
|
||||
|
||||
public sealed class RoaringImpactIndexTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_RegistersComponentsAndUsage()
|
||||
{
|
||||
var (stream, digest) = CreateBomIndex(
|
||||
@@ -50,7 +52,8 @@ public sealed class RoaringImpactIndexTests
|
||||
usageOnly.Images.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IngestAsync_ReplacesExistingImageData()
|
||||
{
|
||||
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
|
||||
@@ -86,7 +89,8 @@ public sealed class RoaringImpactIndexTests
|
||||
impactSet.Images[0].UsedByEntrypoint.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByPurlsAsync_RespectsTenantNamespaceAndTagFilters()
|
||||
{
|
||||
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
|
||||
@@ -129,7 +133,8 @@ public sealed class RoaringImpactIndexTests
|
||||
result.Images[0].Tags.Should().Contain("prod-eu");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAllAsync_UsageOnlyFiltersEntrypointImages()
|
||||
{
|
||||
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
|
||||
@@ -166,7 +171,8 @@ public sealed class RoaringImpactIndexTests
|
||||
allImages.Images.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RemoveAsync_RemovesImageAndComponents()
|
||||
{
|
||||
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
|
||||
@@ -201,7 +207,8 @@ public sealed class RoaringImpactIndexTests
|
||||
impact.Images.Should().ContainSingle(img => img.ImageDigest == digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateSnapshotAsync_CompactsIdsAndRestores()
|
||||
{
|
||||
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
|
||||
|
||||
@@ -2,7 +2,8 @@ namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class AuditRecordTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AuditRecordNormalizesMetadataAndIdentifiers()
|
||||
{
|
||||
var actor = new AuditActor(actorId: "user_admin", displayName: "Cluster Admin", kind: "user");
|
||||
|
||||
@@ -2,7 +2,8 @@ namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class GraphJobStateMachineTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(GraphJobStatus.Pending, GraphJobStatus.Pending, true)]
|
||||
[InlineData(GraphJobStatus.Pending, GraphJobStatus.Queued, true)]
|
||||
[InlineData(GraphJobStatus.Pending, GraphJobStatus.Running, true)]
|
||||
@@ -17,7 +18,8 @@ public sealed class GraphJobStateMachineTests
|
||||
Assert.Equal(expected, GraphJobStateMachine.CanTransition(from, to));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureTransition_UpdatesBuildJobLifecycle()
|
||||
{
|
||||
var createdAt = new DateTimeOffset(2025, 10, 26, 12, 0, 0, TimeSpan.Zero);
|
||||
@@ -51,7 +53,8 @@ public sealed class GraphJobStateMachineTests
|
||||
Assert.Null(job.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureTransition_ToFailedRequiresError()
|
||||
{
|
||||
var job = new GraphBuildJob(
|
||||
@@ -71,7 +74,8 @@ public sealed class GraphJobStateMachineTests
|
||||
DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureTransition_ToFailedSetsError()
|
||||
{
|
||||
var job = new GraphOverlayJob(
|
||||
@@ -96,7 +100,8 @@ public sealed class GraphJobStateMachineTests
|
||||
Assert.Equal("cartographer timeout", failed.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_RequiresCompletedAtForTerminalState()
|
||||
{
|
||||
var job = new GraphOverlayJob(
|
||||
@@ -112,7 +117,8 @@ public sealed class GraphJobStateMachineTests
|
||||
Assert.Throws<InvalidOperationException>(() => GraphJobStateMachine.Validate(job));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphOverlayJob_NormalizesSubjectsAndMetadata()
|
||||
{
|
||||
var createdAt = DateTimeOffset.UtcNow;
|
||||
@@ -147,7 +153,8 @@ public sealed class GraphJobStateMachineTests
|
||||
Assert.Equal("run-123", job.Metadata["policyrunid"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphBuildJob_NormalizesDigestAndMetadata()
|
||||
{
|
||||
var job = new GraphBuildJob(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class ImpactSetTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ImpactSetSortsImagesByDigest()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
||||
@@ -47,7 +49,8 @@ public sealed class ImpactSetTests
|
||||
Assert.Contains("\"snapshotId\":\"snap-001\"", json, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ImpactImageRejectsInvalidDigest()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new ImpactImage("sha1:not-supported", "registry", "repo"));
|
||||
|
||||
@@ -2,11 +2,13 @@ using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class PolicyRunModelsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyRunInputs_NormalizesEnvironmentKeys()
|
||||
{
|
||||
var inputs = new PolicyRunInputs(
|
||||
@@ -28,7 +30,8 @@ public sealed class PolicyRunModelsTests
|
||||
Assert.Equal("global", inputs.Environment["region"].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicySimulationWebhookPayloadFactory_ComputesSucceeded()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
@@ -43,7 +46,8 @@ public sealed class PolicyRunModelsTests
|
||||
Assert.NotNull(payload.LatencySeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicySimulationWebhookPayloadFactory_ComputesFailureReason()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
@@ -90,7 +94,8 @@ public sealed class PolicyRunModelsTests
|
||||
CancelledAt: status == PolicyRunJobStatus.Cancelled ? timestamp : null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyRunStatus_ThrowsOnNegativeAttempts()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new PolicyRunStatus(
|
||||
@@ -105,7 +110,8 @@ public sealed class PolicyRunModelsTests
|
||||
attempts: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyDiffSummary_NormalizesSeverityKeys()
|
||||
{
|
||||
var summary = new PolicyDiffSummary(
|
||||
@@ -122,7 +128,8 @@ public sealed class PolicyRunModelsTests
|
||||
Assert.True(summary.BySeverity.ContainsKey("High"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyExplainTrace_LowercasesMetadataKeys()
|
||||
{
|
||||
var trace = new PolicyExplainTrace(
|
||||
|
||||
@@ -4,13 +4,15 @@ using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class RescanDeltaEventSampleTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RescanDeltaEventSampleAlignsWithContracts()
|
||||
{
|
||||
const string fileName = "scheduler.rescan.delta@1.sample.json";
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class RunStateMachineTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureTransition_FromQueuedToRunningSetsStartedAt()
|
||||
{
|
||||
var run = new Run(
|
||||
@@ -29,7 +31,8 @@ public sealed class RunStateMachineTests
|
||||
Assert.Null(updated.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureTransition_ToCompletedPopulatesFinishedAt()
|
||||
{
|
||||
var run = new Run(
|
||||
@@ -58,7 +61,8 @@ public sealed class RunStateMachineTests
|
||||
Assert.Equal(1, updated.Stats.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnsureTransition_ErrorRequiresMessage()
|
||||
{
|
||||
var run = new Run(
|
||||
@@ -78,7 +82,8 @@ public sealed class RunStateMachineTests
|
||||
Assert.Contains("requires a non-empty error message", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ThrowsWhenTerminalWithoutFinishedAt()
|
||||
{
|
||||
var run = new Run(
|
||||
@@ -93,7 +98,8 @@ public sealed class RunStateMachineTests
|
||||
Assert.Throws<InvalidOperationException>(() => RunStateMachine.Validate(run));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RunReasonExtension_NormalizesImpactWindow()
|
||||
{
|
||||
var reason = new RunReason(manualReason: "delta");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class RunValidationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RunStatsRejectsNegativeValues()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new RunStats(candidates: -1));
|
||||
@@ -18,7 +20,8 @@ public sealed class RunValidationTests
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new RunStats(newLow: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeltaSummarySortsTopFindingsBySeverityThenId()
|
||||
{
|
||||
var summary = new DeltaSummary(
|
||||
@@ -43,7 +46,8 @@ public sealed class RunValidationTests
|
||||
Assert.Equal(new[] { "CVE-2025-0001", "CVE-2025-0002" }, summary.KevHits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RunSerializationIncludesDeterministicOrdering()
|
||||
{
|
||||
var stats = new RunStats(candidates: 10, deduped: 8, queued: 8, completed: 5, deltas: 3, newCriticals: 2);
|
||||
|
||||
@@ -8,7 +8,8 @@ public sealed class SamplePayloadTests
|
||||
{
|
||||
private static readonly string SamplesRoot = LocateSamplesRoot();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ScheduleSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("schedule.json");
|
||||
@@ -21,7 +22,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RunSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("run.json");
|
||||
@@ -34,7 +36,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ImpactSetSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("impact-set.json");
|
||||
@@ -47,7 +50,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AuditSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("audit.json");
|
||||
@@ -60,7 +64,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphBuildJobSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("graph-build-job.json");
|
||||
@@ -73,7 +78,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphOverlayJobSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("graph-overlay-job.json");
|
||||
@@ -87,7 +93,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyRunRequestSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("policy-run-request.json");
|
||||
@@ -103,7 +110,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyRunStatusSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("policy-run-status.json");
|
||||
@@ -117,7 +125,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyDiffSummarySample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("policy-diff-summary.json");
|
||||
@@ -131,7 +140,8 @@ public sealed class SamplePayloadTests
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyExplainTraceSample_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var json = ReadSample("policy-explain-trace.json");
|
||||
@@ -145,7 +155,8 @@ public sealed class SamplePayloadTests
|
||||
var canonical = CanonicalJsonSerializer.Serialize(trace);
|
||||
AssertJsonEquivalent(json, canonical);
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PolicyRunJob_RoundtripsThroughCanonicalSerializer()
|
||||
{
|
||||
var metadata = ImmutableSortedDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
@@ -237,6 +248,7 @@ public sealed class SamplePayloadTests
|
||||
private static string NormalizeJson(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
using StellaOps.TestKit;
|
||||
return JsonSerializer.Serialize(document.RootElement, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class ScheduleSerializationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ScheduleSerialization_IsDeterministicRegardlessOfInputOrdering()
|
||||
{
|
||||
var selectionA = new Selector(
|
||||
@@ -77,6 +78,7 @@ public sealed class ScheduleSerializationTests
|
||||
Assert.Equal(jsonA, jsonB);
|
||||
|
||||
using var doc = JsonDocument.Parse(jsonA);
|
||||
using StellaOps.TestKit;
|
||||
var root = doc.RootElement;
|
||||
Assert.Equal(SchedulerSchemaVersions.Schedule, root.GetProperty("schemaVersion").GetString());
|
||||
Assert.Equal("analysis-only", root.GetProperty("mode").GetString());
|
||||
@@ -86,7 +88,8 @@ public sealed class ScheduleSerializationTests
|
||||
Assert.Equal(new[] { "team-a", "team-b" }, namespaces);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("not-a-timezone")]
|
||||
public void Schedule_ThrowsWhenTimezoneInvalid(string timezone)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Scheduler.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Models.Tests;
|
||||
|
||||
public sealed class SchedulerSchemaMigrationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeSchedule_DefaultsSchemaVersionWhenMissing()
|
||||
{
|
||||
var schedule = new Schedule(
|
||||
@@ -35,7 +37,8 @@ public sealed class SchedulerSchemaMigrationTests
|
||||
Assert.Empty(result.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeRun_StrictModeRemovesUnknownProperties()
|
||||
{
|
||||
var run = new Run(
|
||||
@@ -54,7 +57,8 @@ public sealed class SchedulerSchemaMigrationTests
|
||||
Assert.Contains(result.Warnings, warning => warning.Contains("extraField", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeImpactSet_ThrowsForUnsupportedVersion()
|
||||
{
|
||||
var impactSet = new ImpactSet(
|
||||
@@ -70,7 +74,8 @@ public sealed class SchedulerSchemaMigrationTests
|
||||
Assert.Contains("Unsupported scheduler schema version", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeSchedule_Legacy0_UpgradesToLatestVersion()
|
||||
{
|
||||
var legacy = new JsonObject
|
||||
@@ -116,7 +121,8 @@ public sealed class SchedulerSchemaMigrationTests
|
||||
Assert.Contains(result.Warnings, warning => warning.Contains("schedule.subscribers", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeRun_Legacy0_BackfillsMissingStats()
|
||||
{
|
||||
var legacy = new JsonObject
|
||||
@@ -146,7 +152,8 @@ public sealed class SchedulerSchemaMigrationTests
|
||||
Assert.Contains(result.Warnings, warning => warning.Contains("run.stats.newMedium", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void UpgradeImpactSet_Legacy0_ComputesTotal()
|
||||
{
|
||||
var legacy = new JsonObject
|
||||
|
||||
@@ -5,11 +5,13 @@ using FluentAssertions;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Queue.Tests;
|
||||
|
||||
public sealed class PlannerAndRunnerMessageTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PlannerMessage_CanonicalSerialization_RoundTrips()
|
||||
{
|
||||
var schedule = new Schedule(
|
||||
@@ -68,7 +70,8 @@ public sealed class PlannerAndRunnerMessageTests
|
||||
roundTrip.Should().BeEquivalentTo(message, options => options.WithStrictOrdering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RunnerSegmentMessage_RequiresAtLeastOneDigest()
|
||||
{
|
||||
var act = () => new RunnerSegmentQueueMessage(
|
||||
@@ -80,7 +83,8 @@ public sealed class PlannerAndRunnerMessageTests
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RunnerSegmentMessage_CanonicalSerialization_RoundTrips()
|
||||
{
|
||||
var message = new RunnerSegmentQueueMessage(
|
||||
|
||||
@@ -2,30 +2,24 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using DotNet.Testcontainers.Configurations;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StackExchange.Redis;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Queue.Redis;
|
||||
using Testcontainers.Redis;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scheduler.Queue.Tests;
|
||||
|
||||
public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
{
|
||||
private readonly RedisTestcontainer _redis;
|
||||
private readonly RedisContainer _redis;
|
||||
private string? _skipReason;
|
||||
|
||||
public RedisSchedulerQueueTests()
|
||||
{
|
||||
var configuration = new RedisTestcontainerConfiguration();
|
||||
|
||||
_redis = new TestcontainersBuilder<RedisTestcontainer>()
|
||||
.WithDatabase(configuration)
|
||||
.Build();
|
||||
_redis = new RedisBuilder().Build();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
@@ -50,7 +44,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
await _redis.DisposeAsync().AsTask();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PlannerQueue_EnqueueLeaseAck_RemovesMessage()
|
||||
{
|
||||
if (SkipIfUnavailable())
|
||||
@@ -86,7 +81,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
afterAck.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunnerQueue_Retry_IncrementsDeliveryAttempt()
|
||||
{
|
||||
if (SkipIfUnavailable())
|
||||
@@ -122,7 +118,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
secondLease[0].Attempt.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PlannerQueue_ClaimExpired_ReassignsLease()
|
||||
{
|
||||
if (SkipIfUnavailable())
|
||||
@@ -155,7 +152,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
await reclaimed[0].AcknowledgeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PlannerQueue_RecordsDepthMetrics()
|
||||
{
|
||||
if (SkipIfUnavailable())
|
||||
@@ -190,7 +188,8 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
plannerDepth.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RunnerQueue_DropWhenDeadLetterDisabled()
|
||||
{
|
||||
if (SkipIfUnavailable())
|
||||
@@ -209,6 +208,7 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
TimeProvider.System,
|
||||
async config => (IConnectionMultiplexer)await ConnectionMultiplexer.ConnectAsync(config).ConfigureAwait(false));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var message = TestData.CreateRunnerMessage();
|
||||
await queue.EnqueueAsync(message);
|
||||
|
||||
@@ -234,7 +234,7 @@ public sealed class RedisSchedulerQueueTests : IAsyncLifetime
|
||||
RetryMaxBackoff = TimeSpan.FromMilliseconds(50),
|
||||
Redis = new SchedulerRedisQueueOptions
|
||||
{
|
||||
ConnectionString = _redis.ConnectionString,
|
||||
ConnectionString = _redis.GetConnectionString(),
|
||||
Database = 0,
|
||||
InitializationTimeout = TimeSpan.FromSeconds(10),
|
||||
Planner = new RedisSchedulerStreamOptions
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace StellaOps.Scheduler.Queue.Tests;
|
||||
|
||||
public sealed class SchedulerQueueServiceCollectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddSchedulerQueues_RegistersNatsTransport()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -32,6 +33,7 @@ public sealed class SchedulerQueueServiceCollectionExtensionsTests
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var plannerQueue = provider.GetRequiredService<ISchedulerPlannerQueue>();
|
||||
var runnerQueue = provider.GetRequiredService<ISchedulerRunnerQueue>();
|
||||
|
||||
@@ -39,7 +41,8 @@ public sealed class SchedulerQueueServiceCollectionExtensionsTests
|
||||
runnerQueue.Should().BeOfType<NatsSchedulerRunnerQueue>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SchedulerQueueHealthCheck_ReturnsHealthy_WhenTransportsReachable()
|
||||
{
|
||||
var healthCheck = new SchedulerQueueHealthCheck(
|
||||
@@ -57,7 +60,8 @@ public sealed class SchedulerQueueServiceCollectionExtensionsTests
|
||||
result.Status.Should().Be(HealthStatus.Healthy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SchedulerQueueHealthCheck_ReturnsUnhealthy_WhenRunnerPingFails()
|
||||
{
|
||||
var healthCheck = new SchedulerQueueHealthCheck(
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="DotNet.Testcontainers" Version="1.7.0-beta.2269" />
|
||||
<PackageReference Include="Testcontainers" Version="4.4.0" />
|
||||
<PackageReference Include="Testcontainers.Redis" Version="4.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(SchedulerPostgresCollection.Name)]
|
||||
@@ -26,7 +27,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryAcquire_SucceedsOnFirstAttempt()
|
||||
{
|
||||
// Arrange
|
||||
@@ -39,7 +41,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
acquired.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryAcquire_FailsWhenAlreadyHeld()
|
||||
{
|
||||
// Arrange
|
||||
@@ -53,7 +56,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
secondAcquire.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Release_AllowsReacquisition()
|
||||
{
|
||||
// Arrange
|
||||
@@ -68,7 +72,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
reacquired.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Extend_ExtendsLockDuration()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +87,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
extended.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Extend_FailsForDifferentHolder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -96,7 +102,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
extended.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Get_ReturnsLockInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +118,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
lockInfo!.HolderId.Should().Be("worker-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ListByTenant_ReturnsTenantsLocks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -127,7 +135,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
locks.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryAcquire_IsExclusiveAcrossConcurrentCallers()
|
||||
{
|
||||
// Arrange
|
||||
@@ -156,7 +165,8 @@ public sealed class DistributedLockRepositoryTests : IAsyncLifetime
|
||||
persisted!.HolderId.Should().Be(winningHolder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryAcquire_AllowsReacquireAfterExpiration()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Scheduler.Storage.Postgres;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(SchedulerPostgresCollection.Name)]
|
||||
@@ -36,7 +37,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
|
||||
trigger: GraphBuildJobTrigger.SbomVersion,
|
||||
createdAt: DateTimeOffset.UtcNow);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InsertAndGetBuildJob()
|
||||
{
|
||||
var dataSource = CreateDataSource();
|
||||
@@ -52,7 +54,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
|
||||
fetched.Status.Should().Be(GraphJobStatus.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryReplaceSucceedsWithExpectedStatus()
|
||||
{
|
||||
var dataSource = CreateDataSource();
|
||||
@@ -70,7 +73,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
|
||||
fetched!.Status.Should().Be(GraphJobStatus.Running);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TryReplaceFailsOnUnexpectedStatus()
|
||||
{
|
||||
var dataSource = CreateDataSource();
|
||||
@@ -85,7 +89,8 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
|
||||
updated.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ListBuildJobsHonorsStatusAndLimit()
|
||||
{
|
||||
var dataSource = CreateDataSource();
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Scheduler.Storage.Postgres.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(SchedulerPostgresCollection.Name)]
|
||||
@@ -27,7 +28,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAndGet_RoundTripsTrigger()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +59,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
fetched.CronExpression.Should().Be("0 0 * * *");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByName_ReturnsCorrectTrigger()
|
||||
{
|
||||
// Arrange
|
||||
@@ -72,7 +75,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
fetched!.Id.Should().Be(trigger.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllTriggersForTenant()
|
||||
{
|
||||
// Arrange
|
||||
@@ -89,7 +93,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
triggers.Select(t => t.Name).Should().Contain(["trigger1", "trigger2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetDueTriggers_ReturnsTriggersReadyToFire()
|
||||
{
|
||||
// Arrange - One due trigger, one future trigger
|
||||
@@ -128,7 +133,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
dueTriggers[0].Name.Should().Be("due");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecordFire_UpdatesTriggerState()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +154,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
fetched.FireCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetEnabled_TogglesEnableState()
|
||||
{
|
||||
// Arrange
|
||||
@@ -170,7 +177,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
enabled!.Enabled.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Delete_RemovesTrigger()
|
||||
{
|
||||
// Arrange
|
||||
@@ -185,7 +193,8 @@ public sealed class TriggerRepositoryTests : IAsyncLifetime
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetDueTriggers_IsDeterministicForEqualNextFire()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Scheduler.Storage.Postgres.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Storage.Postgres.Tests;
|
||||
|
||||
[Collection(SchedulerPostgresCollection.Name)]
|
||||
@@ -26,7 +27,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAndGet_RoundTripsWorker()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +52,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
|
||||
fetched.JobTypes.Should().BeEquivalentTo(["scan", "sbom"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Heartbeat_UpdatesLastHeartbeat()
|
||||
{
|
||||
// Arrange
|
||||
@@ -67,7 +70,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
|
||||
fetched.CurrentJobs.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ListByStatus_ReturnsWorkersWithStatus()
|
||||
{
|
||||
// Arrange
|
||||
@@ -91,7 +95,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
|
||||
activeWorkers[0].Id.Should().Be(activeWorker.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SetStatus_ChangesWorkerStatus()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +111,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
|
||||
fetched!.Status.Should().Be(WorkerStatus.Draining);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Delete_RemovesWorker()
|
||||
{
|
||||
// Arrange
|
||||
@@ -121,7 +127,8 @@ public sealed class WorkerRepositoryTests : IAsyncLifetime
|
||||
fetched.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task List_ReturnsAllWorkers()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class CartographerWebhookClientTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NotifyAsync_PostsPayload_WhenEnabled()
|
||||
{
|
||||
var handler = new RecordingHandler();
|
||||
@@ -68,13 +69,15 @@ public sealed class CartographerWebhookClientTests
|
||||
Assert.Equal("tenant-alpha", json.GetProperty("tenantId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NotifyAsync_Skips_WhenDisabled()
|
||||
{
|
||||
var handler = new RecordingHandler();
|
||||
var httpClient = new HttpClient(handler);
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerCartographerOptions());
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.AddDebug());
|
||||
using StellaOps.TestKit;
|
||||
var client = new CartographerWebhookClient(httpClient, new OptionsMonitorStub<SchedulerCartographerOptions>(options), loggerFactory.CreateLogger<CartographerWebhookClient>());
|
||||
|
||||
var job = new GraphOverlayJob(
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConselierWebhook_AcceptsValidSignature()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -53,7 +54,8 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ConselierWebhook_RejectsInvalidSignature()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -74,7 +76,8 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExcitorWebhook_HonoursRateLimit()
|
||||
{
|
||||
using var restrictedFactory = _factory.WithWebHostBuilder(builder =>
|
||||
@@ -122,6 +125,7 @@ public sealed class EventWebhookEndpointTests : IClassFixture<WebApplicationFact
|
||||
private static string ComputeSignature(string secret, string payload)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
using StellaOps.TestKit;
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
return "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebAp
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BestMatch_WhenMissing_ReturnsNoContent()
|
||||
{
|
||||
var repository = new StubFailureSignatureRepository(match: null);
|
||||
@@ -41,7 +42,8 @@ public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebAp
|
||||
Assert.Equal("tch_123", repository.LastCall!.Value.ToolchainHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BestMatch_WhenPresent_ReturnsPayload()
|
||||
{
|
||||
var signatureId = Guid.Parse("e22132b0-2aa7-4cde-94a9-0b335d321c61");
|
||||
@@ -74,6 +76,7 @@ public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebAp
|
||||
}));
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
using StellaOps.TestKit;
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-failure-signatures");
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "scheduler.runs.read");
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateGraphBuildJob_RequiresGraphWriteScope()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -27,7 +28,8 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
|
||||
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateGraphBuildJob_AndList()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -58,7 +60,8 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
|
||||
Assert.Equal("pending", first.GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompleteOverlayJob_UpdatesStatusAndMetrics()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -98,6 +101,7 @@ public sealed class GraphJobEndpointTests : IClassFixture<SchedulerWebApplicatio
|
||||
metricsResponse.EnsureSuccessStatusCode();
|
||||
var metricsJson = await metricsResponse.Content.ReadAsStringAsync();
|
||||
using var metricsDoc = JsonDocument.Parse(metricsJson);
|
||||
using StellaOps.TestKit;
|
||||
var metricsRoot = metricsDoc.RootElement;
|
||||
Assert.Equal("tenant-bravo", metricsRoot.GetProperty("tenantId").GetString());
|
||||
Assert.True(metricsRoot.GetProperty("completed").GetInt32() >= 1);
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class GraphJobEventPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_LogsEvent_WhenDriverUnsupported()
|
||||
{
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerEventsOptions
|
||||
@@ -63,12 +64,14 @@ public sealed class GraphJobEventPublisherTests
|
||||
Assert.Contains("\"resultUri\":\"oras://result\"", eventPayload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishAsync_Suppressed_WhenDisabled()
|
||||
{
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerEventsOptions());
|
||||
var loggerProvider = new ListLoggerProvider();
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(loggerProvider));
|
||||
using StellaOps.TestKit;
|
||||
var publisher = new GraphJobEventPublisher(new OptionsMonitorStub<SchedulerEventsOptions>(options), new ThrowingRedisConnectionFactory(), loggerFactory.CreateLogger<GraphJobEventPublisher>());
|
||||
|
||||
var overlayJob = new GraphOverlayJob(
|
||||
|
||||
@@ -6,13 +6,15 @@ using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.WebService.GraphJobs;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class GraphJobServiceTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTime = new(2025, 11, 4, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompleteBuildJob_PersistsMetadataAndPublishesOnce()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
@@ -50,7 +52,8 @@ public sealed class GraphJobServiceTests
|
||||
Assert.Equal("oras://cartographer/bundle", resultUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompleteBuildJob_IsIdempotentWhenAlreadyCompleted()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
@@ -84,7 +87,8 @@ public sealed class GraphJobServiceTests
|
||||
Assert.Single(webhook.Notifications);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CompleteBuildJob_UpdatesResultUriWithoutReemittingEvent()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
@@ -131,7 +135,8 @@ public sealed class GraphJobServiceTests
|
||||
Assert.Equal("oras://cartographer/bundle-v2", resultUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBuildJob_NormalizesSbomDigest()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
@@ -151,7 +156,8 @@ public sealed class GraphJobServiceTests
|
||||
Assert.Equal("sha256:" + new string('a', 64), created.SbomDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBuildJob_RejectsDigestWithoutPrefix()
|
||||
{
|
||||
var store = new TrackingGraphJobStore();
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.Scheduler.ImpactIndex;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class ImpactIndexFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FixtureDirectoryExists()
|
||||
{
|
||||
var fixtureDirectory = GetFixtureDirectory();
|
||||
@@ -23,7 +25,8 @@ public sealed class ImpactIndexFixtureTests
|
||||
Assert.Contains(sampleFile, files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FixtureImpactIndexLoadsSampleImage()
|
||||
{
|
||||
var fixtureDirectory = GetFixtureDirectory();
|
||||
|
||||
@@ -11,7 +11,8 @@ public sealed class PolicyRunEndpointTests : IClassFixture<WebApplicationFactory
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateListGetPolicyRun()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -58,10 +59,12 @@ public sealed class PolicyRunEndpointTests : IClassFixture<WebApplicationFactory
|
||||
Assert.Equal(runId, retrieved.GetProperty("run").GetProperty("runId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MissingScopeReturnsForbidden()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
using StellaOps.TestKit;
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-policy");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/scheduler/policy/runs");
|
||||
|
||||
@@ -19,7 +19,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateListGetSimulation()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -56,7 +57,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.Equal(runId, simulation.GetProperty("simulation").GetProperty("runId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MetricsEndpointWithoutProviderReturns501()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -67,7 +69,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MetricsEndpointReturnsSummary()
|
||||
{
|
||||
var stub = new StubPolicySimulationMetricsProvider
|
||||
@@ -111,7 +114,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.Equal(2.0, payload.GetProperty("policy_simulation_latency").GetProperty("mean_seconds").GetDouble());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateSimulationRequiresScopeHeader()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -126,7 +130,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateSimulationRequiresPolicySimulateScope()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -142,7 +147,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CancelSimulationMarksStatus()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -167,7 +173,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.True(cancelled.GetProperty("simulation").GetProperty("cancellationRequested").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RetrySimulationCreatesNewRun()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -197,7 +204,8 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
Assert.Equal(runId, retryOf.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StreamSimulationEmitsCoreEvents()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -227,6 +235,7 @@ public sealed class PolicySimulationEndpointTests : IClassFixture<WebApplication
|
||||
var seenHeartbeat = false;
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
using StellaOps.TestKit;
|
||||
while (!cts.Token.IsCancellationRequested && !(seenRetry && seenInitial && seenQueueLag && seenHeartbeat))
|
||||
{
|
||||
var readTask = reader.ReadLineAsync();
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class PolicySimulationMetricsProviderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CaptureAsync_ComputesQueueDepthAndLatency()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
@@ -61,7 +62,8 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
Assert.Equal(29.8, response.Latency.P99.Value, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CaptureAsync_UpdatesSnapshotAndEmitsTenantTaggedGauge()
|
||||
{
|
||||
var repository = new StubPolicyRunJobRepository();
|
||||
@@ -133,7 +135,8 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
listener.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordLatency_EmitsHistogramMeasurement()
|
||||
{
|
||||
var repository = new StubPolicyRunJobRepository();
|
||||
@@ -149,6 +152,7 @@ public sealed class PolicySimulationMetricsProviderTests
|
||||
instrument.Name == "policy_simulation_latency_seconds")
|
||||
{
|
||||
meterListener.EnableMeasurementEvents(instrument);
|
||||
using StellaOps.TestKit;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,7 +23,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateListCancelRun()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -76,7 +77,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
Assert.Equal("cancelled", runDetail.GetProperty("run").GetProperty("state").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PreviewImpactForSchedule()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -113,7 +115,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
Assert.True(preview.GetProperty("sample").GetArrayLength() <= 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RetryRunCreatesNewRun()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -148,7 +151,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
Assert.Contains("retry-of:", retryRun.GetProperty("reason").GetProperty("manualReason").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetRunDeltasReturnsMetadata()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -207,7 +211,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
Assert.Equal(1, deltasJson.GetProperty("deltas").GetArrayLength());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueueLagSummaryReturnsDepth()
|
||||
{
|
||||
SchedulerQueueMetrics.RecordDepth("redis", "scheduler-runner", 7);
|
||||
@@ -231,7 +236,8 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StreamRunEmitsInitialEvent()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -261,6 +267,7 @@ public sealed class RunEndpointTests : IClassFixture<WebApplicationFactory<Progr
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
using StellaOps.TestKit;
|
||||
var seenRetry = false;
|
||||
var seenInitial = false;
|
||||
var seenQueueLag = false;
|
||||
|
||||
@@ -9,7 +9,8 @@ public sealed class ScheduleEndpointTests : IClassFixture<WebApplicationFactory<
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateListAndRetrieveSchedule()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
@@ -51,10 +52,12 @@ public sealed class ScheduleEndpointTests : IClassFixture<WebApplicationFactory<
|
||||
Assert.Equal("Nightly", scheduleJson.GetProperty("schedule").GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PauseAndResumeSchedule()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
using StellaOps.TestKit;
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-controls");
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "scheduler.schedules.write scheduler.schedules.read");
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Scheduler.WebService.Hosting;
|
||||
using StellaOps.Scheduler.WebService.Options;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public class SchedulerPluginHostFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_usesDefaults_whenOptionsEmpty()
|
||||
{
|
||||
var options = new SchedulerOptions.PluginOptions();
|
||||
@@ -36,7 +38,8 @@ public class SchedulerPluginHostFactoryTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_respectsConfiguredValues()
|
||||
{
|
||||
var options = new SchedulerOptions.PluginOptions
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class GraphBuildExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
@@ -43,7 +44,8 @@ public sealed class GraphBuildExecutionServiceTests
|
||||
Assert.Empty(completion.Notifications);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CompletesJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
@@ -85,7 +87,8 @@ public sealed class GraphBuildExecutionServiceTests
|
||||
Assert.True(repository.ReplaceCalls >= 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Fails_AfterMaxAttempts()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
@@ -116,7 +119,8 @@ public sealed class GraphBuildExecutionServiceTests
|
||||
Assert.Equal("network", completion.Notifications[0].Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository
|
||||
@@ -126,6 +130,7 @@ public sealed class GraphBuildExecutionServiceTests
|
||||
var cartographer = new StubCartographerBuildClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
using StellaOps.TestKit;
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class GraphOverlayExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
@@ -42,7 +43,8 @@ public sealed class GraphOverlayExecutionServiceTests
|
||||
Assert.Equal(0, cartographer.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CompletesJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
@@ -79,7 +81,8 @@ public sealed class GraphOverlayExecutionServiceTests
|
||||
Assert.Equal("graph_snap_2", notification.GraphSnapshotId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Fails_AfterRetries()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository();
|
||||
@@ -109,7 +112,8 @@ public sealed class GraphOverlayExecutionServiceTests
|
||||
Assert.Equal("overlay failed", completion.Notifications[0].Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
|
||||
{
|
||||
var repository = new RecordingGraphJobRepository
|
||||
@@ -119,6 +123,7 @@ public sealed class GraphOverlayExecutionServiceTests
|
||||
var cartographer = new StubCartographerOverlayClient();
|
||||
var completion = new RecordingCompletionClient();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
using StellaOps.TestKit;
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
|
||||
{
|
||||
Graph = new SchedulerWorkerOptions.GraphOptions
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Worker.Execution;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class HttpScannerReportClientTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenReportReturnsFindings_ProducesDeltaSummary()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler(request =>
|
||||
@@ -75,7 +77,8 @@ public sealed class HttpScannerReportClientTests
|
||||
Assert.NotNull(result.Dsse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenReportFails_RetriesAndThrows()
|
||||
{
|
||||
var callCount = 0;
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class ImpactShardPlannerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PlanShards_ReturnsSingleShardWhenParallelismNotSpecified()
|
||||
{
|
||||
var impactSet = CreateImpactSet(count: 3);
|
||||
@@ -19,7 +21,8 @@ public sealed class ImpactShardPlannerTests
|
||||
Assert.Equal(3, shards[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PlanShards_RespectsMaxJobsLimit()
|
||||
{
|
||||
var impactSet = CreateImpactSet(count: 5);
|
||||
@@ -31,7 +34,8 @@ public sealed class ImpactShardPlannerTests
|
||||
Assert.True(shards.All(shard => shard.Count <= 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PlanShards_DistributesImagesEvenly()
|
||||
{
|
||||
var impactSet = CreateImpactSet(count: 10);
|
||||
|
||||
@@ -5,11 +5,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scheduler.ImpactIndex;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class ImpactTargetingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByPurlsAsync_DeduplicatesKeysAndInvokesIndex()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
||||
@@ -38,7 +40,8 @@ public sealed class ImpactTargetingServiceTests
|
||||
Assert.Equal(new[] { "pkg:npm/a", "pkg:npm/b" }, capturedKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByVulnerabilitiesAsync_ReturnsEmptyWhenNoIds()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
||||
@@ -52,7 +55,8 @@ public sealed class ImpactTargetingServiceTests
|
||||
Assert.Null(index.LastVulnerabilityIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAllAsync_DelegatesToIndex()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
||||
@@ -74,7 +78,8 @@ public sealed class ImpactTargetingServiceTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByPurlsAsync_DeduplicatesImpactImagesByDigest()
|
||||
{
|
||||
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
|
||||
@@ -130,7 +135,8 @@ public sealed class ImpactTargetingServiceTests
|
||||
Assert.Equal("api", image.Labels["component"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveByPurlsAsync_FiltersImagesBySelectorConstraints()
|
||||
{
|
||||
var selector = new Selector(
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PlannerBackgroundServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RespectsTenantFairnessCap()
|
||||
{
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T12:00:00Z"));
|
||||
@@ -71,7 +72,8 @@ public sealed class PlannerBackgroundServiceTests
|
||||
Assert.Equal(new[] { "run-a1", "run-b1" }, processedIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_PrioritizesManualAndEventTriggers()
|
||||
{
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-27T18:00:00Z"));
|
||||
@@ -93,6 +95,7 @@ public sealed class PlannerBackgroundServiceTests
|
||||
var targetingService = new StubImpactTargetingService(timeProvider);
|
||||
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
using StellaOps.TestKit;
|
||||
var executionService = new PlannerExecutionService(
|
||||
scheduleRepository,
|
||||
repository,
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PlannerExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProcessAsync_WithImpactedImages_QueuesPlannerMessage()
|
||||
{
|
||||
var schedule = CreateSchedule();
|
||||
@@ -52,7 +53,8 @@ public sealed class PlannerExecutionServiceTests
|
||||
Assert.Equal(impactSet.Images.Length, result.UpdatedRun.Stats.Queued);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProcessAsync_WithNoImpactedImages_CompletesWithoutWork()
|
||||
{
|
||||
var schedule = CreateSchedule();
|
||||
@@ -68,7 +70,8 @@ public sealed class PlannerExecutionServiceTests
|
||||
Assert.Empty(plannerQueue.Messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProcessAsync_WhenScheduleMissing_MarksRunAsFailed()
|
||||
{
|
||||
var run = CreateRun(scheduleId: "missing");
|
||||
@@ -81,6 +84,7 @@ public sealed class PlannerExecutionServiceTests
|
||||
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var service = new PlannerExecutionService(
|
||||
scheduleRepository,
|
||||
runRepository,
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Scheduler.Queue;
|
||||
using StellaOps.Scheduler.Worker.Planning;
|
||||
using StellaOps.Scheduler.Worker.Options;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PlannerQueueDispatchServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DispatchAsync_EnqueuesRunnerSegmentsDeterministically()
|
||||
{
|
||||
var run = CreateRun();
|
||||
@@ -54,7 +56,8 @@ public sealed class PlannerQueueDispatchServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DispatchAsync_NoImages_ReturnsNoWork()
|
||||
{
|
||||
var run = CreateRun();
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PolicyRunDispatchBackgroundServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_DoesNotLease_WhenPolicyDispatchDisabled()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
@@ -23,6 +24,7 @@ public sealed class PolicyRunDispatchBackgroundServiceTests
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
await service.StartAsync(cts.Token);
|
||||
await service.StopAsync(CancellationToken.None);
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_CancelsJob_WhenCancellationRequested()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
@@ -70,7 +71,8 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
Assert.Equal("cancelled", webhook.Payloads[0].Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SubmitsJob_OnSuccess()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
@@ -105,7 +107,8 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
Assert.Empty(webhook.Payloads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RetriesJob_OnFailure()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
@@ -139,7 +142,8 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
Assert.Empty(webhook.Payloads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_MarksJobFailed_WhenAttemptsExceeded()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
@@ -174,7 +178,8 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
Assert.Equal("failed", webhook.Payloads[0].Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_NoWork_CompletesJob()
|
||||
{
|
||||
var repository = new RecordingPolicyRunJobRepository();
|
||||
@@ -182,6 +187,7 @@ public sealed class PolicyRunExecutionServiceTests
|
||||
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
|
||||
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
using StellaOps.TestKit;
|
||||
var targeting = new StubPolicyRunTargetingService
|
||||
{
|
||||
OnEnsureTargets = job => PolicyRunTargetingResult.NoWork(job, "empty")
|
||||
|
||||
@@ -11,11 +11,13 @@ using StellaOps.Scheduler.Worker.Options;
|
||||
using StellaOps.Scheduler.Worker.Policy;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PolicyRunTargetingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_ReturnsUnchanged_ForNonIncrementalJob()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -27,7 +29,8 @@ public sealed class PolicyRunTargetingServiceTests
|
||||
Assert.Equal(job, result.Job);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_ReturnsUnchanged_WhenSbomSetAlreadyPresent()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -39,7 +42,8 @@ public sealed class PolicyRunTargetingServiceTests
|
||||
Assert.Equal(PolicyRunTargetingStatus.Unchanged, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_ReturnsNoWork_WhenNoCandidatesResolved()
|
||||
{
|
||||
var impact = new StubImpactTargetingService();
|
||||
@@ -53,7 +57,8 @@ public sealed class PolicyRunTargetingServiceTests
|
||||
Assert.Equal("no_matches", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_TargetsDirectSboms()
|
||||
{
|
||||
var service = CreateService();
|
||||
@@ -66,7 +71,8 @@ public sealed class PolicyRunTargetingServiceTests
|
||||
Assert.Equal(new[] { "sbom:S-1", "sbom:S-2" }, result.Job.Inputs.SbomSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_TargetsUsingImpactIndex()
|
||||
{
|
||||
var impact = new StubImpactTargetingService
|
||||
@@ -100,7 +106,8 @@ public sealed class PolicyRunTargetingServiceTests
|
||||
Assert.Equal(new[] { "sbom:S-42" }, result.Job.Inputs.SbomSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_FallsBack_WhenLimitExceeded()
|
||||
{
|
||||
var service = CreateService(configure: options => options.MaxSboms = 1);
|
||||
@@ -112,7 +119,8 @@ public sealed class PolicyRunTargetingServiceTests
|
||||
Assert.Equal(PolicyRunTargetingStatus.Unchanged, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnsureTargetsAsync_FallbacksToDigest_WhenLabelMissing()
|
||||
{
|
||||
var impact = new StubImpactTargetingService
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class PolicySimulationWebhookClientTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NotifyAsync_Disabled_DoesNotInvokeEndpoint()
|
||||
{
|
||||
var handler = new RecordingHandler();
|
||||
@@ -29,11 +30,13 @@ public sealed class PolicySimulationWebhookClientTests
|
||||
Assert.False(handler.WasInvoked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task NotifyAsync_SendsPayload_WhenEnabled()
|
||||
{
|
||||
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted));
|
||||
using var httpClient = new HttpClient(handler);
|
||||
using StellaOps.TestKit;
|
||||
var options = CreateOptions(o =>
|
||||
{
|
||||
o.Policy.Webhook.Enabled = true;
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class RunnerExecutionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_UpdatesRunStatsAndDeltas()
|
||||
{
|
||||
var run = CreateRun();
|
||||
@@ -102,7 +103,8 @@ public sealed class RunnerExecutionServiceTests
|
||||
Assert.Single(eventPublisher.RescanDeltaPayloads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenRunMissing_ReturnsRunMissing()
|
||||
{
|
||||
var repository = new InMemoryRunRepository();
|
||||
@@ -110,6 +112,7 @@ public sealed class RunnerExecutionServiceTests
|
||||
var eventPublisher = new RecordingSchedulerEventPublisher();
|
||||
using var metrics = new SchedulerWorkerMetrics();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var service = new RunnerExecutionService(
|
||||
repository,
|
||||
new RecordingRunSummaryService(),
|
||||
|
||||
@@ -12,11 +12,13 @@ using StellaOps.Scheduler.Worker.Events;
|
||||
using StellaOps.Scheduler.Worker.Execution;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Scheduler.Worker.Tests;
|
||||
|
||||
public sealed class SchedulerEventPublisherTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishReportReadyAsync_EnqueuesNotifyEvent()
|
||||
{
|
||||
var queue = new RecordingNotifyEventQueue();
|
||||
@@ -50,7 +52,8 @@ public sealed class SchedulerEventPublisherTests
|
||||
Assert.Equal(1, deltaNode["newCritical"]!.GetValue<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task PublishRescanDeltaAsync_EnqueuesDeltaEvent()
|
||||
{
|
||||
var queue = new RecordingNotifyEventQueue();
|
||||
|
||||
Reference in New Issue
Block a user