save progress
This commit is contained in:
@@ -3,8 +3,10 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
@@ -34,18 +36,6 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
|
||||
trigger: GraphBuildJobTrigger.SbomVersion,
|
||||
createdAt: DateTimeOffset.UtcNow);
|
||||
|
||||
private static GraphOverlayJob OverlayJob(string tenant, string id, GraphJobStatus status = GraphJobStatus.Pending)
|
||||
=> new(
|
||||
id: id,
|
||||
tenantId: tenant,
|
||||
graphSnapshotId: "snap-1",
|
||||
status: status,
|
||||
createdAt: DateTimeOffset.UtcNow,
|
||||
attempts: 0,
|
||||
targetGraphId: "graph-1",
|
||||
correlationId: null,
|
||||
metadata: null);
|
||||
|
||||
[Fact]
|
||||
public async Task InsertAndGetBuildJob()
|
||||
{
|
||||
@@ -117,7 +107,7 @@ public sealed class GraphJobRepositoryTests : IAsyncLifetime
|
||||
private SchedulerDataSource CreateDataSource()
|
||||
{
|
||||
var options = _fixture.Fixture.CreateOptions();
|
||||
options.SchemaName = _fixture.SchemaName;
|
||||
return new SchedulerDataSource(Options.Create(options));
|
||||
options.SchemaName = SchedulerDataSource.DefaultSchemaName;
|
||||
return new SchedulerDataSource(Options.Create(options), NullLogger<SchedulerDataSource>.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Scheduler.Storage.Postgres;
|
||||
using Xunit;
|
||||
@@ -15,6 +16,44 @@ public sealed class SchedulerPostgresFixture : PostgresIntegrationFixture, IColl
|
||||
=> typeof(SchedulerDataSource).Assembly;
|
||||
|
||||
protected override string GetModuleName() => "Scheduler";
|
||||
|
||||
public new async Task TruncateAllTablesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Base fixture truncates the randomly-generated test schema (e.g. schema_migrations table lives there).
|
||||
await Fixture.TruncateAllTablesAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Scheduler migrations create the canonical `scheduler.*` schema explicitly, so we must truncate it as well
|
||||
// to ensure test isolation between methods.
|
||||
await using var connection = new NpgsqlConnection(ConnectionString);
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
const string listTablesSql = """
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'scheduler'
|
||||
AND table_type = 'BASE TABLE';
|
||||
""";
|
||||
|
||||
var tables = new List<string>();
|
||||
await using (var command = new NpgsqlCommand(listTablesSql, connection))
|
||||
await using (var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
tables.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
|
||||
if (tables.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var qualified = tables.Select(static t => $"scheduler.\"{t}\"");
|
||||
var truncateSql = $"TRUNCATE TABLE {string.Join(", ", qualified)} RESTART IDENTITY CASCADE;";
|
||||
await using var truncateCommand = new NpgsqlCommand(truncateSql, connection);
|
||||
await truncateCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -12,17 +12,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Update="xunit" Version="2.9.2" />
|
||||
<PackageReference Update="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Update="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Models;
|
||||
using StellaOps.Scheduler.Storage.Postgres.Repositories;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Tests;
|
||||
|
||||
public sealed class FailureSignatureEndpointTests : IClassFixture<SchedulerWebApplicationFactory>
|
||||
{
|
||||
private readonly SchedulerWebApplicationFactory _factory;
|
||||
|
||||
public FailureSignatureEndpointTests(SchedulerWebApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BestMatch_WhenMissing_ReturnsNoContent()
|
||||
{
|
||||
var repository = new StubFailureSignatureRepository(match: null);
|
||||
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<IFailureSignatureRepository>();
|
||||
services.AddSingleton<IFailureSignatureRepository>(repository);
|
||||
}));
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-failure-signatures");
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "scheduler.runs.read");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/scheduler/failure-signatures/best-match?scopeType=repo&scopeId=acme/repo&toolchainHash=tch_123");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
Assert.NotNull(repository.LastCall);
|
||||
Assert.Equal(FailureSignatureScopeType.Repo, repository.LastCall!.Value.ScopeType);
|
||||
Assert.Equal("acme/repo", repository.LastCall!.Value.ScopeId);
|
||||
Assert.Equal("tch_123", repository.LastCall!.Value.ToolchainHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BestMatch_WhenPresent_ReturnsPayload()
|
||||
{
|
||||
var signatureId = Guid.Parse("e22132b0-2aa7-4cde-94a9-0b335d321c61");
|
||||
var firstSeen = new DateTimeOffset(2025, 12, 18, 10, 0, 0, TimeSpan.Zero);
|
||||
var lastSeen = firstSeen.AddMinutes(5);
|
||||
|
||||
var signature = new FailureSignatureEntity
|
||||
{
|
||||
SignatureId = signatureId,
|
||||
TenantId = "tenant-failure-signatures",
|
||||
ScopeType = FailureSignatureScopeType.Repo,
|
||||
ScopeId = "acme/repo",
|
||||
ToolchainHash = "tch_123",
|
||||
ErrorCode = "E123",
|
||||
ErrorCategory = ErrorCategory.Network,
|
||||
OccurrenceCount = 7,
|
||||
FirstSeenAt = firstSeen,
|
||||
LastSeenAt = lastSeen,
|
||||
PredictedOutcome = PredictedOutcome.Fail,
|
||||
ConfidenceScore = 0.85m
|
||||
};
|
||||
|
||||
var repository = new StubFailureSignatureRepository(signature);
|
||||
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<IFailureSignatureRepository>();
|
||||
services.AddSingleton<IFailureSignatureRepository>(repository);
|
||||
}));
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-failure-signatures");
|
||||
client.DefaultRequestHeaders.Add("X-Scopes", "scheduler.runs.read");
|
||||
|
||||
var response = await client.GetAsync("/api/v1/scheduler/failure-signatures/best-match?scopeType=repo&scopeId=acme/repo&toolchainHash=tch_123");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal(signatureId, payload.GetProperty("signatureId").GetGuid());
|
||||
Assert.Equal("repo", payload.GetProperty("scopeType").GetString());
|
||||
Assert.Equal("acme/repo", payload.GetProperty("scopeId").GetString());
|
||||
Assert.Equal("tch_123", payload.GetProperty("toolchainHash").GetString());
|
||||
Assert.Equal("E123", payload.GetProperty("errorCode").GetString());
|
||||
Assert.Equal("network", payload.GetProperty("errorCategory").GetString());
|
||||
Assert.Equal("fail", payload.GetProperty("predictedOutcome").GetString());
|
||||
Assert.Equal(7, payload.GetProperty("occurrenceCount").GetInt32());
|
||||
Assert.Equal(0.85m, payload.GetProperty("confidenceScore").GetDecimal());
|
||||
Assert.Equal(firstSeen, payload.GetProperty("firstSeenAt").GetDateTimeOffset());
|
||||
Assert.Equal(lastSeen, payload.GetProperty("lastSeenAt").GetDateTimeOffset());
|
||||
}
|
||||
|
||||
private sealed class StubFailureSignatureRepository : IFailureSignatureRepository
|
||||
{
|
||||
private readonly FailureSignatureEntity? _match;
|
||||
|
||||
public StubFailureSignatureRepository(FailureSignatureEntity? match)
|
||||
{
|
||||
_match = match;
|
||||
}
|
||||
|
||||
public (string TenantId, FailureSignatureScopeType ScopeType, string ScopeId, string? ToolchainHash)? LastCall { get; private set; }
|
||||
|
||||
public Task<FailureSignatureEntity> CreateAsync(FailureSignatureEntity signature, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<FailureSignatureEntity?> GetByIdAsync(string tenantId, Guid signatureId, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<FailureSignatureEntity?> GetByKeyAsync(string tenantId, FailureSignatureScopeType scopeType, string scopeId, string toolchainHash, string? errorCode, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<FailureSignatureEntity>> GetByScopeAsync(string tenantId, FailureSignatureScopeType scopeType, string scopeId, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<FailureSignatureEntity>> GetUnresolvedAsync(string tenantId, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<IReadOnlyList<FailureSignatureEntity>> GetByPredictedOutcomeAsync(string tenantId, PredictedOutcome outcome, decimal minConfidence = 0.5m, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<FailureSignatureEntity> UpsertOccurrenceAsync(string tenantId, FailureSignatureScopeType scopeType, string scopeId, string toolchainHash, string? errorCode, ErrorCategory? errorCategory, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<bool> UpdateResolutionAsync(string tenantId, Guid signatureId, ResolutionStatus status, string? notes, string? resolvedBy, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<bool> UpdatePredictionAsync(string tenantId, Guid signatureId, PredictedOutcome outcome, decimal confidence, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<bool> DeleteAsync(string tenantId, Guid signatureId, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<int> PruneResolvedAsync(string tenantId, TimeSpan olderThan, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public Task<FailureSignatureEntity?> GetBestMatchAsync(
|
||||
string tenantId,
|
||||
FailureSignatureScopeType scopeType,
|
||||
string scopeId,
|
||||
string? toolchainHash = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
LastCall = (tenantId, scopeType, scopeId, toolchainHash);
|
||||
return Task.FromResult(_match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user