204 lines
6.2 KiB
C#
204 lines
6.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Integration tests for <see cref="SourceStateRepository"/>.
|
|
/// </summary>
|
|
[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<ConcelierDataSource>.Instance);
|
|
_sourceRepository = new SourceRepository(_dataSource, NullLogger<SourceRepository>.Instance);
|
|
_repository = new SourceStateRepository(_dataSource, NullLogger<SourceStateRepository>.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<SourceEntity> 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);
|
|
}
|
|
}
|
|
|
|
|
|
|