using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Concelier.Persistence.Postgres.Models; using StellaOps.Concelier.Persistence.Postgres; using StellaOps.Concelier.Persistence.Postgres.Repositories; using Xunit; using StellaOps.TestKit; namespace StellaOps.Concelier.Persistence.Tests; /// /// Integration tests for . /// [Collection(ConcelierPostgresCollection.Name)] public sealed class SourceStateRepositoryTests : IAsyncLifetime { private readonly ConcelierPostgresFixture _fixture; private readonly ConcelierDataSource _dataSource; private readonly SourceRepository _sourceRepository; private readonly SourceStateRepository _repository; public SourceStateRepositoryTests(ConcelierPostgresFixture fixture) { _fixture = fixture; var options = fixture.CreateOptions(); _dataSource = new ConcelierDataSource(Options.Create(options), NullLogger.Instance); _sourceRepository = new SourceRepository(_dataSource, NullLogger.Instance); _repository = new SourceStateRepository(_dataSource, NullLogger.Instance); } public ValueTask InitializeAsync() => new(_fixture.TruncateAllTablesAsync()); public async ValueTask DisposeAsync() => await _dataSource.DisposeAsync(); [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpsertAsync_ShouldCreateNewState() { // Arrange var source = await CreateTestSourceAsync(); var state = new SourceStateEntity { Id = Guid.NewGuid(), SourceId = source.Id, LastSyncAt = DateTimeOffset.UtcNow, LastSuccessAt = DateTimeOffset.UtcNow, Cursor = """{"lastModified": "2025-01-01T00:00:00Z"}""", ErrorCount = 0, SyncCount = 1 }; // Act var result = await _repository.UpsertAsync(state); // Assert result.Should().NotBeNull(); result.SourceId.Should().Be(source.Id); result.Cursor.Should().Contain("lastModified"); result.SyncCount.Should().Be(1); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetBySourceIdAsync_ShouldReturnState_WhenExists() { // Arrange var source = await CreateTestSourceAsync(); var state = new SourceStateEntity { Id = Guid.NewGuid(), SourceId = source.Id, LastSyncAt = DateTimeOffset.UtcNow }; await _repository.UpsertAsync(state); // Act var result = await _repository.GetBySourceIdAsync(source.Id); // Assert result.Should().NotBeNull(); result!.SourceId.Should().Be(source.Id); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetBySourceIdAsync_ShouldReturnNull_WhenNotExists() { // Act var result = await _repository.GetBySourceIdAsync(Guid.NewGuid()); // Assert result.Should().BeNull(); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpsertAsync_ShouldUpdateExistingState() { // Arrange var source = await CreateTestSourceAsync(); var state = new SourceStateEntity { Id = Guid.NewGuid(), SourceId = source.Id, LastSyncAt = DateTimeOffset.UtcNow.AddHours(-1), ErrorCount = 0, SyncCount = 1 }; await _repository.UpsertAsync(state); // Create updated version (same source_id triggers update) var updatedState = new SourceStateEntity { Id = Guid.NewGuid(), // Different ID but same source_id SourceId = source.Id, LastSyncAt = DateTimeOffset.UtcNow, LastSuccessAt = DateTimeOffset.UtcNow, Cursor = """{"page": 10}""", ErrorCount = 0, SyncCount = 2 }; // Act var result = await _repository.UpsertAsync(updatedState); // Assert result.Should().NotBeNull(); result.LastSuccessAt.Should().NotBeNull(); result.Cursor.Should().Contain("page"); result.SyncCount.Should().Be(2); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpsertAsync_ShouldTrackErrorCount() { // Arrange var source = await CreateTestSourceAsync(); var state = new SourceStateEntity { Id = Guid.NewGuid(), SourceId = source.Id, LastSyncAt = DateTimeOffset.UtcNow, ErrorCount = 3, LastError = "Connection failed" }; // Act var result = await _repository.UpsertAsync(state); // Assert result.Should().NotBeNull(); result.ErrorCount.Should().Be(3); result.LastError.Should().Be("Connection failed"); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpsertAsync_ShouldTrackSyncMetrics() { // Arrange var source = await CreateTestSourceAsync(); var syncTime = DateTimeOffset.UtcNow; var state = new SourceStateEntity { Id = Guid.NewGuid(), SourceId = source.Id, LastSyncAt = syncTime, LastSuccessAt = syncTime, SyncCount = 100, ErrorCount = 2 }; // Act var result = await _repository.UpsertAsync(state); // Assert result.Should().NotBeNull(); result.SyncCount.Should().Be(100); result.LastSyncAt.Should().BeCloseTo(syncTime, TimeSpan.FromSeconds(1)); result.LastSuccessAt.Should().BeCloseTo(syncTime, TimeSpan.FromSeconds(1)); } private async Task CreateTestSourceAsync() { var id = Guid.NewGuid(); var key = $"source-{id:N}"[..20]; var source = new SourceEntity { Id = id, Key = key, Name = $"Test Source {key}", SourceType = "nvd", Priority = 100, Enabled = true }; return await _sourceRepository.UpsertAsync(source); } }