This commit is contained in:
StellaOps Bot
2025-12-14 23:20:14 +02:00
parent 3411e825cd
commit b058dbe031
356 changed files with 68310 additions and 1108 deletions

View File

@@ -0,0 +1,327 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Npgsql;
using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Storage.Postgres.Repositories;
using Testcontainers.PostgreSql;
using Xunit;
namespace StellaOps.Unknowns.Storage.Postgres.Tests;
public sealed class PostgresUnknownRepositoryTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:16")
.Build();
private NpgsqlDataSource _dataSource = null!;
private PostgresUnknownRepository _repository = null!;
private const string TestTenantId = "test-tenant";
public async Task InitializeAsync()
{
await _postgres.StartAsync();
var connectionString = _postgres.GetConnectionString();
_dataSource = NpgsqlDataSource.Create(connectionString);
// Run schema migrations
await RunMigrationsAsync();
_repository = new PostgresUnknownRepository(
_dataSource,
NullLogger<PostgresUnknownRepository>.Instance);
}
public async Task DisposeAsync()
{
await _dataSource.DisposeAsync();
await _postgres.DisposeAsync();
}
private async Task RunMigrationsAsync()
{
await using var connection = await _dataSource.OpenConnectionAsync();
// Create schema and types
const string schema = """
CREATE SCHEMA IF NOT EXISTS unknowns;
CREATE SCHEMA IF NOT EXISTS unknowns_app;
CREATE OR REPLACE FUNCTION unknowns_app.require_current_tenant()
RETURNS TEXT
LANGUAGE plpgsql STABLE SECURITY DEFINER
AS $$
DECLARE
v_tenant TEXT;
BEGIN
v_tenant := current_setting('app.tenant_id', true);
IF v_tenant IS NULL OR v_tenant = '' THEN
RAISE EXCEPTION 'app.tenant_id session variable not set';
END IF;
RETURN v_tenant;
END;
$$;
DO $$ BEGIN
CREATE TYPE unknowns.subject_type AS ENUM (
'package', 'ecosystem', 'version', 'sbom_edge', 'file', 'runtime'
);
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
CREATE TYPE unknowns.unknown_kind AS ENUM (
'missing_sbom', 'ambiguous_package', 'missing_feed', 'unresolved_edge',
'no_version_info', 'unknown_ecosystem', 'partial_match',
'version_range_unbounded', 'unsupported_format', 'transitive_gap'
);
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
CREATE TYPE unknowns.unknown_severity AS ENUM (
'critical', 'high', 'medium', 'low', 'info'
);
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
CREATE TYPE unknowns.resolution_type AS ENUM (
'feed_updated', 'sbom_provided', 'manual_mapping',
'superseded', 'false_positive', 'wont_fix'
);
EXCEPTION WHEN duplicate_object THEN null;
END $$;
CREATE TABLE IF NOT EXISTS unknowns.unknown (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id TEXT NOT NULL,
subject_hash CHAR(64) NOT NULL,
subject_type unknowns.subject_type NOT NULL,
subject_ref TEXT NOT NULL,
kind unknowns.unknown_kind NOT NULL,
severity unknowns.unknown_severity,
context JSONB NOT NULL DEFAULT '{}',
source_scan_id UUID,
source_graph_id UUID,
source_sbom_digest TEXT,
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_to TIMESTAMPTZ,
sys_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sys_to TIMESTAMPTZ,
resolved_at TIMESTAMPTZ,
resolution_type unknowns.resolution_type,
resolution_ref TEXT,
resolution_notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by TEXT NOT NULL DEFAULT 'system',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_unknown_one_open_per_subject
ON unknowns.unknown (tenant_id, subject_hash, kind)
WHERE valid_to IS NULL AND sys_to IS NULL;
""";
await using var command = new NpgsqlCommand(schema, connection);
await command.ExecuteNonQueryAsync();
}
[Fact]
public async Task CreateAsync_ShouldCreateUnknown()
{
// Arrange
var subjectRef = "pkg:npm/lodash@4.17.21";
var kind = UnknownKind.MissingFeed;
var severity = UnknownSeverity.Medium;
// Act
var result = await _repository.CreateAsync(
TestTenantId,
UnknownSubjectType.Package,
subjectRef,
kind,
severity,
"""{"ecosystem": "npm"}""",
null,
null,
null,
"test-user",
CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.TenantId.Should().Be(TestTenantId);
result.SubjectRef.Should().Be(subjectRef);
result.Kind.Should().Be(kind);
result.Severity.Should().Be(severity);
result.IsOpen.Should().BeTrue();
result.IsResolved.Should().BeFalse();
result.IsCurrent.Should().BeTrue();
}
[Fact]
public async Task GetByIdAsync_ShouldReturnUnknown_WhenExists()
{
// Arrange
var created = await _repository.CreateAsync(
TestTenantId,
UnknownSubjectType.Package,
"pkg:npm/axios@0.21.0",
UnknownKind.AmbiguousPackage,
UnknownSeverity.Low,
null,
null,
null,
null,
"test-user",
CancellationToken.None);
// Act
var result = await _repository.GetByIdAsync(TestTenantId, created.Id, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(created.Id);
result.SubjectRef.Should().Be("pkg:npm/axios@0.21.0");
}
[Fact]
public async Task GetByIdAsync_ShouldReturnNull_WhenNotExists()
{
// Act
var result = await _repository.GetByIdAsync(TestTenantId, Guid.NewGuid(), CancellationToken.None);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetOpenUnknownsAsync_ShouldReturnOnlyOpenUnknowns()
{
// Arrange
await _repository.CreateAsync(
TestTenantId,
UnknownSubjectType.Package,
"pkg:npm/open1@1.0.0",
UnknownKind.MissingFeed,
UnknownSeverity.High,
null, null, null, null, "test-user", CancellationToken.None);
var toResolve = await _repository.CreateAsync(
TestTenantId,
UnknownSubjectType.Package,
"pkg:npm/resolved@1.0.0",
UnknownKind.MissingFeed,
UnknownSeverity.Medium,
null, null, null, null, "test-user", CancellationToken.None);
await _repository.ResolveAsync(
TestTenantId,
toResolve.Id,
ResolutionType.FeedUpdated,
null,
"Feed now covers this package",
"test-user",
CancellationToken.None);
// Act
var results = await _repository.GetOpenUnknownsAsync(TestTenantId, cancellationToken: CancellationToken.None);
// Assert
results.Should().HaveCount(1);
results[0].SubjectRef.Should().Be("pkg:npm/open1@1.0.0");
}
[Fact]
public async Task ResolveAsync_ShouldMarkAsResolved()
{
// Arrange
var created = await _repository.CreateAsync(
TestTenantId,
UnknownSubjectType.Package,
"pkg:npm/resolvable@2.0.0",
UnknownKind.MissingSbom,
UnknownSeverity.High,
null, null, null, null, "test-user", CancellationToken.None);
// Act
var resolved = await _repository.ResolveAsync(
TestTenantId,
created.Id,
ResolutionType.SbomProvided,
"sbom://digest/abc123",
"SBOM uploaded by vendor",
"test-user",
CancellationToken.None);
// Assert
resolved.IsResolved.Should().BeTrue();
resolved.ResolutionType.Should().Be(ResolutionType.SbomProvided);
resolved.ResolutionRef.Should().Be("sbom://digest/abc123");
resolved.ValidTo.Should().NotBeNull();
}
[Fact]
public async Task CountByKindAsync_ShouldReturnCorrectCounts()
{
// Arrange
var tenant = "count-test-tenant";
await _repository.CreateAsync(tenant, UnknownSubjectType.Package,
"pkg:1", UnknownKind.MissingFeed, null, null, null, null, null, "user", CancellationToken.None);
await _repository.CreateAsync(tenant, UnknownSubjectType.Package,
"pkg:2", UnknownKind.MissingFeed, null, null, null, null, null, "user", CancellationToken.None);
await _repository.CreateAsync(tenant, UnknownSubjectType.Package,
"pkg:3", UnknownKind.AmbiguousPackage, null, null, null, null, null, "user", CancellationToken.None);
// Act
var counts = await _repository.CountByKindAsync(tenant, CancellationToken.None);
// Assert
counts.Should().ContainKey(UnknownKind.MissingFeed);
counts[UnknownKind.MissingFeed].Should().Be(2);
counts.Should().ContainKey(UnknownKind.AmbiguousPackage);
counts[UnknownKind.AmbiguousPackage].Should().Be(1);
}
[Fact]
public async Task AsOfAsync_ShouldReturnHistoricalState()
{
// Arrange
var tenant = "temporal-test-tenant";
var beforeCreate = DateTimeOffset.UtcNow.AddSeconds(-1);
var created = await _repository.CreateAsync(tenant, UnknownSubjectType.Package,
"pkg:temporal@1.0.0", UnknownKind.NoVersionInfo, null, null, null, null, null, "user", CancellationToken.None);
var afterCreate = DateTimeOffset.UtcNow.AddSeconds(1);
// Act
var beforeResults = await _repository.AsOfAsync(tenant, beforeCreate, cancellationToken: CancellationToken.None);
var afterResults = await _repository.AsOfAsync(tenant, afterCreate, cancellationToken: CancellationToken.None);
// Assert
beforeResults.Should().BeEmpty();
afterResults.Should().HaveCount(1);
afterResults[0].Id.Should().Be(created.Id);
}
[Fact]
public async Task SupersedeAsync_ShouldSetSysTo()
{
// Arrange
var tenant = "supersede-test-tenant";
var created = await _repository.CreateAsync(tenant, UnknownSubjectType.Package,
"pkg:supersede@1.0.0", UnknownKind.PartialMatch, null, null, null, null, null, "user", CancellationToken.None);
// Act
await _repository.SupersedeAsync(tenant, created.Id, "new-unknown-id", CancellationToken.None);
// Assert
var result = await _repository.GetByIdAsync(tenant, created.Id, CancellationToken.None);
// After supersede, sys_to is set, so GetById (which filters sys_to IS NULL) returns null
result.Should().BeNull();
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Unknowns.Storage.Postgres.Tests</RootNamespace>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.4.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="FluentAssertions" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\__Libraries\StellaOps.Unknowns.Storage.Postgres\StellaOps.Unknowns.Storage.Postgres.csproj" />
</ItemGroup>
</Project>