Search/AdvisoryAI and DAL conversion to EF finishes up. Preparation for microservices consolidation.

This commit is contained in:
master
2026-02-25 18:19:22 +02:00
parent 4db038123b
commit 63c70a6d37
447 changed files with 52257 additions and 2636 deletions

View File

@@ -0,0 +1,78 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using StellaOps.Scanner.Storage.EfCore.CompiledModels;
using StellaOps.Scanner.Storage.EfCore.Models;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.Storage.Tests;
/// <summary>
/// Guard tests ensuring the EF Core compiled model is real (not a stub)
/// and contains all expected entity type registrations.
/// </summary>
public sealed class CompiledModelGuardTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_Instance_IsNotNull()
{
ScannerDbContextModel.Instance.Should().NotBeNull(
"compiled model must be generated via 'dotnet ef dbcontext optimize', not a stub");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_HasExpectedEntityTypeCount()
{
var entityTypes = ScannerDbContextModel.Instance.GetEntityTypes().ToList();
entityTypes.Should().HaveCount(24,
"scanner compiled model must contain exactly 24 entity types (regenerate with 'dotnet ef dbcontext optimize' if count differs)");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(typeof(ScanManifestEntity))]
[InlineData(typeof(ScanMetricsEntity))]
[InlineData(typeof(ArtifactBomEntity))]
[InlineData(typeof(ReachabilityResultEntity))]
[InlineData(typeof(RiskStateSnapshotEntity))]
[InlineData(typeof(MaterialRiskChangeEntity))]
[InlineData(typeof(ProofBundleEntity))]
[InlineData(typeof(IdempotencyKeyEntity))]
[InlineData(typeof(CallGraphSnapshotEntity))]
[InlineData(typeof(BinaryIdentityEntity))]
[InlineData(typeof(BinaryPackageMapEntity))]
[InlineData(typeof(BinaryVulnAssertionEntity))]
[InlineData(typeof(SecretDetectionSettingsEntity))]
[InlineData(typeof(CodeChangeEntity))]
[InlineData(typeof(EpssRawEntity))]
[InlineData(typeof(EpssSignalEntity))]
[InlineData(typeof(EpssSignalConfigEntity))]
[InlineData(typeof(VexCandidateEntity))]
[InlineData(typeof(FuncProofEntity))]
[InlineData(typeof(FuncNodeEntity))]
[InlineData(typeof(FuncTraceEntity))]
[InlineData(typeof(ReachabilityDriftResultEntity))]
[InlineData(typeof(DriftedSinkEntity))]
[InlineData(typeof(FacetSealEntity))]
public void CompiledModel_ContainsEntityType(Type entityType)
{
var found = ScannerDbContextModel.Instance.FindEntityType(entityType);
found.Should().NotBeNull(
$"compiled model must contain entity type '{entityType.Name}' — regenerate if missing");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_EntityTypes_HaveTableNames()
{
var entityTypes = ScannerDbContextModel.Instance.GetEntityTypes();
foreach (var entityType in entityTypes)
{
var tableName = entityType.GetTableName();
tableName.Should().NotBeNullOrWhiteSpace(
$"entity type '{entityType.ClrType.Name}' must have a table name configured");
}
}
}

View File

@@ -87,7 +87,7 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
WHERE model_date = @ModelDate
ORDER BY cve_id
""",
new { ModelDate = day2 })).ToList();
new { ModelDate = day2.ToDateTime(TimeOnly.MinValue) })).ToList();
Assert.Equal(3, changes.Count);

View File

@@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>

View File

@@ -0,0 +1,230 @@
// -----------------------------------------------------------------------------
// TimeProviderIntegrationTests.cs
// Verifies the NOW() -> TimeProvider migration: repositories that previously
// relied on SQL NOW() for timestamp columns now use _timeProvider.GetUtcNow(),
// allowing tests to inject a fixed clock and assert that stored timestamps
// match the injected time rather than wall-clock time.
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Scanner.Storage.Entities;
using StellaOps.Scanner.Storage.Postgres;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.Storage.Tests;
[Collection("scanner-postgres")]
public sealed class TimeProviderIntegrationTests : IAsyncLifetime
{
private readonly ScannerPostgresFixture _fixture;
private ScannerDataSource _dataSource = null!;
private NpgsqlDataSource _npgsqlDataSource = null!;
/// <summary>
/// A distinctive past time that could never be confused with wall-clock UTC.
/// If a timestamp column equals this value, it was set by TimeProvider, not SQL NOW().
/// </summary>
private static readonly DateTimeOffset FixedTime =
new(2020, 6, 15, 12, 0, 0, TimeSpan.Zero);
public TimeProviderIntegrationTests(ScannerPostgresFixture fixture)
{
_fixture = fixture;
}
public async ValueTask InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
var options = new ScannerStorageOptions
{
Postgres = new PostgresOptions
{
ConnectionString = _fixture.ConnectionString,
SchemaName = _fixture.SchemaName
}
};
_dataSource = new ScannerDataSource(
Options.Create(options),
NullLoggerFactory.Instance.CreateLogger<ScannerDataSource>());
_npgsqlDataSource = NpgsqlDataSource.Create(_fixture.ConnectionString);
}
public async ValueTask DisposeAsync()
{
await _npgsqlDataSource.DisposeAsync();
}
// -------------------------------------------------------------------------
// Test 1: SecretDetectionSettings — Update uses TimeProvider for updated_at
// -------------------------------------------------------------------------
[Fact]
[Trait("Category", TestCategories.Integration)]
public async Task SecretDetectionSettings_Update_SetsUpdatedAtFromTimeProvider()
{
// Arrange — create with system clock so the row exists in the DB
var systemRepo = new PostgresSecretDetectionSettingsRepository(_dataSource);
var tenantId = Guid.NewGuid();
var settings = new SecretDetectionSettingsRow
{
TenantId = tenantId,
Enabled = true,
RevelationPolicy = "{}",
EnabledRuleCategories = ["generic"],
DisabledRuleIds = [],
AlertSettings = "{}",
MaxFileSizeBytes = 1_048_576,
ExcludedFileExtensions = [],
ExcludedPaths = [],
ScanBinaryFiles = false,
RequireSignedRuleBundles = false,
UpdatedBy = "test-agent"
};
var created = await systemRepo.CreateAsync(settings);
// The DB-generated timestamps should be recent (wall-clock), not our fixed time.
created.CreatedAt.Should().BeAfter(DateTimeOffset.UtcNow.AddMinutes(-5),
"CreateAsync timestamps come from the DB and should be recent wall-clock time");
// Act — update using a repository wired to the fixed time provider
var fixedTimeProvider = new FixedTimeProvider(FixedTime);
var fixedRepo = new PostgresSecretDetectionSettingsRepository(_dataSource, fixedTimeProvider);
created.Enabled = false;
var updated = await fixedRepo.UpdateAsync(created, expectedVersion: created.Version);
updated.Should().BeTrue("the update should succeed with the correct version");
// Assert — read back and verify updated_at matches the fixed time
var readBack = await systemRepo.GetByTenantAsync(tenantId);
readBack.Should().NotBeNull();
readBack!.UpdatedAt.Should().BeCloseTo(FixedTime, TimeSpan.FromSeconds(1),
"updated_at must come from the injected TimeProvider, not SQL NOW()");
readBack.Enabled.Should().BeFalse("the updated value should be persisted");
}
// -------------------------------------------------------------------------
// Test 2: FuncProofRepository — Store uses TimeProvider for created_at_utc
// -------------------------------------------------------------------------
[Fact]
[Trait("Category", TestCategories.Integration)]
public async Task FuncProof_Store_SetsCreatedAtUtcFromTimeProvider()
{
// Arrange
var fixedTimeProvider = new FixedTimeProvider(FixedTime);
var repo = new PostgresFuncProofRepository(_npgsqlDataSource, fixedTimeProvider);
var scanId = Guid.NewGuid();
var proofId = $"blake3:{Guid.NewGuid():N}";
var document = new FuncProofDocumentRow
{
ScanId = scanId,
ProofId = proofId,
BuildId = $"gnu:{Guid.NewGuid():N}",
BuildIdType = "gnu-build-id",
FileSha256 = $"sha256:{Guid.NewGuid():N}",
BinaryFormat = "elf",
Architecture = "x86_64",
IsStripped = false,
FunctionCount = 42,
TraceCount = 7,
ProofContent = """{"version":"1.0","functions":[]}""",
CompressedContent = null,
DsseEnvelopeId = null,
OciArtifactDigest = null,
RekorEntryId = null,
GeneratorVersion = "1.0.0-test",
GeneratedAtUtc = FixedTime.AddHours(-1)
};
// Act
var id = await repo.StoreAsync(document);
// Assert — read back and verify created_at_utc matches fixed time
var readBack = await repo.GetByIdAsync(id);
readBack.Should().NotBeNull();
readBack!.CreatedAtUtc.Should().BeCloseTo(FixedTime, TimeSpan.FromSeconds(1),
"created_at_utc must come from the injected TimeProvider, not SQL NOW()");
}
// -------------------------------------------------------------------------
// Test 3: FuncProofRepository — Upsert conflict path sets updated_at_utc
// -------------------------------------------------------------------------
[Fact]
[Trait("Category", TestCategories.Integration)]
public async Task FuncProof_StoreConflict_SetsUpdatedAtUtcFromTimeProvider()
{
// Arrange — first insert with system clock
var systemRepo = new PostgresFuncProofRepository(_npgsqlDataSource);
var scanId = Guid.NewGuid();
var proofId = $"blake3:{Guid.NewGuid():N}";
var document = new FuncProofDocumentRow
{
ScanId = scanId,
ProofId = proofId,
BuildId = $"gnu:{Guid.NewGuid():N}",
BuildIdType = "gnu-build-id",
FileSha256 = $"sha256:{Guid.NewGuid():N}",
BinaryFormat = "elf",
Architecture = "x86_64",
IsStripped = false,
FunctionCount = 10,
TraceCount = 3,
ProofContent = """{"version":"1.0","functions":[]}""",
CompressedContent = null,
DsseEnvelopeId = null,
OciArtifactDigest = null,
RekorEntryId = null,
GeneratorVersion = "1.0.0-test",
GeneratedAtUtc = DateTimeOffset.UtcNow
};
var originalId = await systemRepo.StoreAsync(document);
// Verify no updated_at_utc on first insert
var original = await systemRepo.GetByIdAsync(originalId);
original.Should().NotBeNull();
original!.UpdatedAtUtc.Should().BeNull("first insert should not set updated_at_utc");
// Act — store again with the same proof_id using fixed time (triggers ON CONFLICT)
var fixedTimeProvider = new FixedTimeProvider(FixedTime);
var fixedRepo = new PostgresFuncProofRepository(_npgsqlDataSource, fixedTimeProvider);
var conflictId = await fixedRepo.StoreAsync(document);
// Assert — the returned id should be the same as the original
conflictId.Should().Be(originalId);
var readBack = await systemRepo.GetByIdAsync(originalId);
readBack.Should().NotBeNull();
readBack!.UpdatedAtUtc.Should().NotBeNull("ON CONFLICT path should set updated_at_utc");
readBack.UpdatedAtUtc!.Value.Should().BeCloseTo(FixedTime, TimeSpan.FromSeconds(1),
"updated_at_utc on conflict must come from the injected TimeProvider, not SQL NOW()");
}
// -------------------------------------------------------------------------
// Helper: FixedTimeProvider
// -------------------------------------------------------------------------
internal sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _fixedTime;
public FixedTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
public override DateTimeOffset GetUtcNow() => _fixedTime;
}
}

View File

@@ -0,0 +1,66 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using StellaOps.Scanner.Triage.CompiledModels;
using StellaOps.Scanner.Triage.Entities;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.Triage.Tests;
/// <summary>
/// Guard tests ensuring the EF Core compiled model is real (not a stub)
/// and contains all expected entity type registrations.
/// </summary>
public sealed class CompiledModelGuardTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_Instance_IsNotNull()
{
TriageDbContextModel.Instance.Should().NotBeNull(
"compiled model must be generated via 'dotnet ef dbcontext optimize', not a stub");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_HasExpectedEntityTypeCount()
{
var entityTypes = TriageDbContextModel.Instance.GetEntityTypes().ToList();
entityTypes.Should().HaveCount(11,
"triage compiled model must contain exactly 11 entity types (regenerate with 'dotnet ef dbcontext optimize' if count differs)");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData(typeof(TriageFinding))]
[InlineData(typeof(TriageEffectiveVex))]
[InlineData(typeof(TriageReachabilityResult))]
[InlineData(typeof(TriageRiskResult))]
[InlineData(typeof(TriageDecision))]
[InlineData(typeof(TriageEvidenceArtifact))]
[InlineData(typeof(TriageSnapshot))]
[InlineData(typeof(TriageScan))]
[InlineData(typeof(TriagePolicyDecision))]
[InlineData(typeof(TriageAttestation))]
[InlineData(typeof(TriageCaseCurrent))]
public void CompiledModel_ContainsEntityType(Type entityType)
{
var found = TriageDbContextModel.Instance.FindEntityType(entityType);
found.Should().NotBeNull(
$"compiled model must contain entity type '{entityType.Name}' — regenerate if missing");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CompiledModel_EntityTypes_HaveTableOrViewNames()
{
var entityTypes = TriageDbContextModel.Instance.GetEntityTypes();
foreach (var entityType in entityTypes)
{
var tableName = entityType.GetTableName();
var viewName = entityType.GetViewName();
(tableName ?? viewName).Should().NotBeNullOrWhiteSpace(
$"entity type '{entityType.ClrType.Name}' must have a table or view name configured");
}
}
}