Search/AdvisoryAI and DAL conversion to EF finishes up. Preparation for microservices consolidation.
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user