Files
git.stella-ops.org/src/SbomService/StellaOps.SbomService.Tests/RegistrySourceServiceTests.cs
2026-01-27 08:23:42 +02:00

417 lines
14 KiB
C#

// Copyright (c) StellaOps. Licensed under BUSL-1.1.
// Unit tests for RegistrySourceService
using System.Globalization;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Repositories;
using StellaOps.SbomService.Services;
using Xunit;
namespace StellaOps.SbomService.Tests;
public class RegistrySourceServiceTests
{
private readonly Mock<IRegistrySourceRepository> _sourceRepoMock;
private readonly Mock<IRegistrySourceRunRepository> _runRepoMock;
private readonly RegistrySourceService _service;
public RegistrySourceServiceTests()
{
_sourceRepoMock = new Mock<IRegistrySourceRepository>();
_runRepoMock = new Mock<IRegistrySourceRunRepository>();
var timeProvider = new FixedTimeProvider(FixedNow);
var guidProvider = CreateGuidProvider();
var httpOptions = Options.Create(new RegistryHttpOptions
{
AllowedHosts = new List<string> { "harbor.example.com", "registry.example.com", "test-registry.example.com" }
});
var queryOptions = Options.Create(new RegistrySourceQueryOptions());
_service = new RegistrySourceService(
_sourceRepoMock.Object,
_runRepoMock.Object,
NullLogger<RegistrySourceService>.Instance,
timeProvider,
guidProvider,
httpOptions,
queryOptions);
}
[Trait("Category", "Unit")]
[Fact]
public async Task CreateAsync_WithValidRequest_CreatesRegistrySource()
{
// Arrange
var request = new CreateRegistrySourceRequest(
Name: "Test Registry",
Description: "Test description",
Type: RegistrySourceType.Harbor,
RegistryUrl: "https://harbor.example.com",
AuthRefUri: "authref://vault/harbor#credentials",
IntegrationId: null,
RepoFilters: ["myorg/*"],
TagFilters: null,
TriggerMode: RegistryTriggerMode.Webhook,
ScheduleCron: null,
WebhookSecretRefUri: "authref://vault/harbor#webhook-secret",
Tags: ["production"]);
_sourceRepoMock
.Setup(r => r.CreateAsync(It.IsAny<RegistrySource>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySource, CancellationToken>((s, _) => Task.FromResult(s));
// Act
var result = await _service.CreateAsync(request, "user@example.com", "tenant-1");
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Test Registry");
result.RegistryUrl.Should().Be("https://harbor.example.com");
result.Type.Should().Be(RegistrySourceType.Harbor);
result.Status.Should().Be(RegistrySourceStatus.Pending);
result.TriggerMode.Should().Be(RegistryTriggerMode.Webhook);
result.RepoFilters.Should().Contain("myorg/*");
result.CreatedBy.Should().Be("user@example.com");
result.TenantId.Should().Be("tenant-1");
}
[Trait("Category", "Unit")]
[Fact]
public async Task CreateAsync_TrimsTrailingSlashFromUrl()
{
// Arrange
var request = new CreateRegistrySourceRequest(
Name: "Test",
Description: null,
Type: RegistrySourceType.OciGeneric,
RegistryUrl: "https://registry.example.com/",
AuthRefUri: null,
IntegrationId: null,
RepoFilters: null,
TagFilters: null,
TriggerMode: RegistryTriggerMode.Manual,
ScheduleCron: null,
WebhookSecretRefUri: null,
Tags: null);
_sourceRepoMock
.Setup(r => r.CreateAsync(It.IsAny<RegistrySource>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySource, CancellationToken>((s, _) => Task.FromResult(s));
// Act
var result = await _service.CreateAsync(request, null, "tenant-1");
// Assert
result.RegistryUrl.Should().Be("https://registry.example.com");
}
[Trait("Category", "Unit")]
[Fact]
public async Task GetByIdAsync_WithExistingId_ReturnsSource()
{
// Arrange
var source = CreateTestSource();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
// Act
var result = await _service.GetByIdAsync(source.Id, "tenant-1");
// Assert
result.Should().NotBeNull();
result!.Id.Should().Be(source.Id);
}
[Trait("Category", "Unit")]
[Fact]
public async Task GetByIdAsync_WithNonExistingId_ReturnsNull()
{
// Arrange
var id = Guid.NewGuid();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
.ReturnsAsync((RegistrySource?)null);
// Act
var result = await _service.GetByIdAsync(id, "tenant-1");
// Assert
result.Should().BeNull();
}
[Trait("Category", "Unit")]
[Fact]
public async Task ListAsync_WithTypeFilter_ReturnsFilteredResults()
{
// Arrange
var harborSources = new[]
{
CreateTestSource(type: RegistrySourceType.Harbor),
CreateTestSource(type: RegistrySourceType.Harbor)
};
_sourceRepoMock
.Setup(r => r.GetAllAsync(It.Is<RegistrySourceQuery>(q => q.Type == RegistrySourceType.Harbor), It.IsAny<CancellationToken>()))
.ReturnsAsync(harborSources);
_sourceRepoMock
.Setup(r => r.CountAsync(It.Is<RegistrySourceQuery>(q => q.Type == RegistrySourceType.Harbor), It.IsAny<CancellationToken>()))
.ReturnsAsync(2);
var request = new ListRegistrySourcesRequest(Type: RegistrySourceType.Harbor);
// Act
var result = await _service.ListAsync(request, "tenant-1");
// Assert
result.Items.Should().HaveCount(2);
result.Items.Should().OnlyContain(s => s.Type == RegistrySourceType.Harbor);
}
[Trait("Category", "Unit")]
[Fact]
public async Task UpdateAsync_WithExistingSource_UpdatesFields()
{
// Arrange
var source = CreateTestSource();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
_sourceRepoMock
.Setup(r => r.UpdateAsync(It.IsAny<RegistrySource>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySource, CancellationToken>((s, _) => Task.FromResult(s));
var request = new UpdateRegistrySourceRequest(
Name: "Updated Name",
Description: "Updated description",
RegistryUrl: null,
AuthRefUri: null,
RepoFilters: null,
TagFilters: null,
TriggerMode: null,
ScheduleCron: null,
WebhookSecretRefUri: null,
Status: null,
Tags: null);
// Act
var result = await _service.UpdateAsync(source.Id, request, "updater@example.com", "tenant-1");
// Assert
result.Should().NotBeNull();
result!.Name.Should().Be("Updated Name");
result.Description.Should().Be("Updated description");
result.UpdatedBy.Should().Be("updater@example.com");
}
[Trait("Category", "Unit")]
[Fact]
public async Task UpdateAsync_WithNonExistingSource_ReturnsNull()
{
// Arrange
var id = Guid.NewGuid();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
.ReturnsAsync((RegistrySource?)null);
var request = new UpdateRegistrySourceRequest(
Name: "Updated", Description: null, RegistryUrl: null, AuthRefUri: null,
RepoFilters: null, TagFilters: null, TriggerMode: null, ScheduleCron: null,
WebhookSecretRefUri: null, Status: null, Tags: null);
// Act
var result = await _service.UpdateAsync(id, request, "user", "tenant-1");
// Assert
result.Should().BeNull();
}
[Trait("Category", "Unit")]
[Fact]
public async Task DeleteAsync_WithExistingSource_DeletesFromRepository()
{
// Arrange
var source = CreateTestSource();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
_sourceRepoMock
.Setup(r => r.DeleteAsync(source.Id, It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act
var result = await _service.DeleteAsync(source.Id, "deleter@example.com", "tenant-1");
// Assert
result.Should().BeTrue();
_sourceRepoMock.Verify(r => r.DeleteAsync(source.Id, It.IsAny<CancellationToken>()), Times.Once);
}
[Trait("Category", "Unit")]
[Fact]
public async Task TriggerAsync_WithActiveSource_CreatesRun()
{
// Arrange
var source = CreateTestSource();
source.Status = RegistrySourceStatus.Active;
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
_runRepoMock
.Setup(r => r.CreateAsync(It.IsAny<RegistrySourceRun>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySourceRun, CancellationToken>((run, _) => Task.FromResult(run));
_sourceRepoMock
.Setup(r => r.UpdateAsync(It.IsAny<RegistrySource>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySource, CancellationToken>((s, _) => Task.FromResult(s));
// Act
var result = await _service.TriggerAsync(source.Id, "manual", null, "user@example.com", "tenant-1");
// Assert
result.Should().NotBeNull();
result.SourceId.Should().Be(source.Id);
result.TriggerType.Should().Be("manual");
result.Status.Should().Be(RegistryRunStatus.Queued);
}
[Trait("Category", "Unit")]
[Fact]
public async Task PauseAsync_WithActiveSource_PausesSource()
{
// Arrange
var source = CreateTestSource();
source.Status = RegistrySourceStatus.Active;
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
_sourceRepoMock
.Setup(r => r.UpdateAsync(It.IsAny<RegistrySource>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySource, CancellationToken>((s, _) => Task.FromResult(s));
// Act
var result = await _service.PauseAsync(source.Id, "Maintenance", "admin@example.com", "tenant-1");
// Assert
result.Should().NotBeNull();
result!.Status.Should().Be(RegistrySourceStatus.Paused);
}
[Trait("Category", "Unit")]
[Fact]
public async Task ResumeAsync_WithPausedSource_ResumesSource()
{
// Arrange
var source = CreateTestSource();
source.Status = RegistrySourceStatus.Paused;
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
_sourceRepoMock
.Setup(r => r.UpdateAsync(It.IsAny<RegistrySource>(), It.IsAny<CancellationToken>()))
.Returns<RegistrySource, CancellationToken>((s, _) => Task.FromResult(s));
// Act
var result = await _service.ResumeAsync(source.Id, "admin@example.com", "tenant-1");
// Assert
result.Should().NotBeNull();
result!.Status.Should().Be(RegistrySourceStatus.Active);
}
[Trait("Category", "Unit")]
[Fact]
public async Task GetRunHistoryAsync_ReturnsRunsForSource()
{
// Arrange
var sourceId = Guid.NewGuid();
var source = new RegistrySource
{
Id = sourceId,
Name = "Test Registry",
Type = RegistrySourceType.Harbor,
RegistryUrl = "https://test-registry.example.com",
Status = RegistrySourceStatus.Active,
TriggerMode = RegistryTriggerMode.Manual,
TenantId = "tenant-1",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
};
var runs = new[]
{
CreateTestRun(sourceId),
CreateTestRun(sourceId),
CreateTestRun(sourceId)
};
_sourceRepoMock
.Setup(r => r.GetByIdAsync(sourceId, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
_runRepoMock
.Setup(r => r.GetBySourceIdAsync(sourceId, 50, It.IsAny<CancellationToken>()))
.ReturnsAsync(runs);
// Act
var result = await _service.GetRunHistoryAsync(sourceId, 50, "tenant-1");
// Assert
result.Should().HaveCount(3);
result.Should().OnlyContain(r => r.SourceId == sourceId);
}
#region Helper Methods
private static RegistrySource CreateTestSource(RegistrySourceType type = RegistrySourceType.Harbor) => new()
{
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
Name = "Test Registry",
Type = type,
RegistryUrl = "https://test-registry.example.com",
Status = RegistrySourceStatus.Pending,
TriggerMode = RegistryTriggerMode.Manual,
CreatedAt = FixedNow,
UpdatedAt = FixedNow,
TenantId = "tenant-1"
};
private static RegistrySourceRun CreateTestRun(Guid sourceId) => new()
{
Id = Guid.Parse("22222222-2222-2222-2222-222222222222"),
SourceId = sourceId,
Status = RegistryRunStatus.Completed,
TriggerType = "manual",
StartedAt = FixedNow.AddMinutes(-5),
CompletedAt = FixedNow
};
private static QueueGuidProvider CreateGuidProvider()
{
return new QueueGuidProvider(new[]
{
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc")
});
}
private static DateTimeOffset FixedNow =>
DateTimeOffset.Parse("2025-12-29T12:00:00Z", CultureInfo.InvariantCulture);
#endregion
}