417 lines
14 KiB
C#
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
|
|
}
|