using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using MicrosoftOptions = Microsoft.Extensions.Options; using StellaOps.SbomService.Models; using StellaOps.SbomService.Storage.Postgres.Repositories; using Xunit; namespace StellaOps.SbomService.Storage.Postgres.Tests; [Collection(SbomServicePostgresCollection.Name)] public sealed class PostgresEntrypointRepositoryTests : IAsyncLifetime { private readonly SbomServicePostgresFixture _fixture; private readonly PostgresEntrypointRepository _repository; private readonly string _tenantId = "tenant-" + Guid.NewGuid().ToString("N")[..8]; public PostgresEntrypointRepositoryTests(SbomServicePostgresFixture fixture) { _fixture = fixture; var options = fixture.Fixture.CreateOptions(); options.SchemaName = fixture.SchemaName; var dataSource = new SbomServiceDataSource(MicrosoftOptions.Options.Create(options), NullLogger.Instance); _repository = new PostgresEntrypointRepository(dataSource, NullLogger.Instance); } public async Task InitializeAsync() { await _fixture.TruncateAllTablesAsync(); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task UpsertAndList_RoundTripsEntrypoint() { // Arrange var entrypoint = new Entrypoint( Artifact: "ghcr.io/test/api", Service: "web", Path: "/api", Scope: "runtime", RuntimeFlag: true); // Act await _repository.UpsertAsync(_tenantId, entrypoint, CancellationToken.None); var fetched = await _repository.ListAsync(_tenantId, CancellationToken.None); // Assert fetched.Should().HaveCount(1); fetched[0].Artifact.Should().Be("ghcr.io/test/api"); fetched[0].Service.Should().Be("web"); fetched[0].Path.Should().Be("/api"); fetched[0].RuntimeFlag.Should().BeTrue(); } [Fact] public async Task UpsertAsync_UpdatesExistingEntrypoint() { // Arrange var entrypoint1 = new Entrypoint("ghcr.io/test/api", "web", "/old", "runtime", false); var entrypoint2 = new Entrypoint("ghcr.io/test/api", "web", "/new", "build", true); // Act await _repository.UpsertAsync(_tenantId, entrypoint1, CancellationToken.None); await _repository.UpsertAsync(_tenantId, entrypoint2, CancellationToken.None); var fetched = await _repository.ListAsync(_tenantId, CancellationToken.None); // Assert fetched.Should().HaveCount(1); fetched[0].Path.Should().Be("/new"); fetched[0].Scope.Should().Be("build"); fetched[0].RuntimeFlag.Should().BeTrue(); } [Fact] public async Task ListAsync_ReturnsOrderedByArtifactServicePath() { // Arrange await _repository.UpsertAsync(_tenantId, new Entrypoint("z-api", "web", "/z", "runtime", true), CancellationToken.None); await _repository.UpsertAsync(_tenantId, new Entrypoint("a-api", "web", "/a", "runtime", true), CancellationToken.None); await _repository.UpsertAsync(_tenantId, new Entrypoint("a-api", "worker", "/b", "runtime", true), CancellationToken.None); // Act var fetched = await _repository.ListAsync(_tenantId, CancellationToken.None); // Assert fetched.Should().HaveCount(3); fetched[0].Artifact.Should().Be("a-api"); fetched[0].Service.Should().Be("web"); fetched[1].Artifact.Should().Be("a-api"); fetched[1].Service.Should().Be("worker"); fetched[2].Artifact.Should().Be("z-api"); } [Fact] public async Task ListAsync_ReturnsEmptyForUnknownTenant() { // Act var fetched = await _repository.ListAsync("unknown-tenant", CancellationToken.None); // Assert fetched.Should().BeEmpty(); } }