Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// Unit tests for RegistryWebhookService
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.SbomService.Models;
|
||||
using StellaOps.SbomService.Repositories;
|
||||
using StellaOps.SbomService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public class RegistryWebhookServiceTests
|
||||
{
|
||||
private readonly Mock<IRegistrySourceRepository> _sourceRepoMock;
|
||||
private readonly Mock<IRegistrySourceRunRepository> _runRepoMock;
|
||||
private readonly Mock<IRegistrySourceService> _sourceServiceMock;
|
||||
private readonly Mock<IClock> _clockMock;
|
||||
private readonly RegistryWebhookService _service;
|
||||
|
||||
public RegistryWebhookServiceTests()
|
||||
{
|
||||
_sourceRepoMock = new Mock<IRegistrySourceRepository>();
|
||||
_runRepoMock = new Mock<IRegistrySourceRunRepository>();
|
||||
_sourceServiceMock = new Mock<IRegistrySourceService>();
|
||||
_clockMock = new Mock<IClock>();
|
||||
_clockMock.Setup(c => c.UtcNow).Returns(DateTimeOffset.Parse("2025-12-29T12:00:00Z"));
|
||||
|
||||
_service = new RegistryWebhookService(
|
||||
_sourceRepoMock.Object,
|
||||
_runRepoMock.Object,
|
||||
_sourceServiceMock.Object,
|
||||
NullLogger<RegistryWebhookService>.Instance,
|
||||
_clockMock.Object);
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ProcessWebhookAsync_WithInvalidSourceId_ReturnsFailure()
|
||||
{
|
||||
// Arrange - invalid GUID format
|
||||
var invalidSourceId = "not-a-guid";
|
||||
|
||||
// Act
|
||||
var result = await _service.ProcessWebhookAsync(
|
||||
invalidSourceId, "harbor", "{}", null);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.Message.Should().Contain("Invalid source ID");
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ProcessWebhookAsync_WithUnknownSource_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var sourceId = Guid.NewGuid();
|
||||
_sourceRepoMock
|
||||
.Setup(r => r.GetByIdAsync(sourceId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RegistrySource?)null);
|
||||
|
||||
// Act
|
||||
var result = await _service.ProcessWebhookAsync(
|
||||
sourceId.ToString(), "harbor", "{}", null);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.Message.Should().Contain("Source not found");
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ProcessWebhookAsync_WithInactiveSource_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
var source = CreateTestSource();
|
||||
source.Status = RegistrySourceStatus.Paused;
|
||||
|
||||
_sourceRepoMock
|
||||
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(source);
|
||||
|
||||
// Act
|
||||
var result = await _service.ProcessWebhookAsync(
|
||||
source.Id.ToString(), "harbor", "{}", null);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.Message.Should().Contain("not active");
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ProcessWebhookAsync_WithValidHarborPushEvent_TriggersRun()
|
||||
{
|
||||
// Arrange
|
||||
var source = CreateTestSource();
|
||||
var harborPayload = CreateHarborPushPayload("library/nginx", "latest");
|
||||
|
||||
_sourceRepoMock
|
||||
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(source);
|
||||
|
||||
var expectedRun = CreateTestRun(source.Id);
|
||||
_sourceServiceMock
|
||||
.Setup(s => s.TriggerAsync(
|
||||
source.Id,
|
||||
"webhook",
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedRun);
|
||||
|
||||
// Act
|
||||
var result = await _service.ProcessWebhookAsync(
|
||||
source.Id.ToString(), "harbor", harborPayload, null);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Message.Should().Contain("Scan triggered");
|
||||
result.TriggeredRunId.Should().Be(expectedRun.Id.ToString());
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ValidateSignature_WithNoSecret_ReturnsTrue()
|
||||
{
|
||||
// Act
|
||||
var result = _service.ValidateSignature("{}", null, null, "harbor");
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public async Task ValidateSignature_WithSecretButNoSignature_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _service.ValidateSignature("{}", null, "secret123", "harbor");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public void ValidateSignature_WithValidHarborSignature_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var payload = "{}";
|
||||
var secret = "secret123";
|
||||
|
||||
// Calculate expected signature (HMAC-SHA256 with sha256= prefix in hex format)
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
var signature = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
// Act
|
||||
var result = _service.ValidateSignature(payload, signature, secret, "harbor");
|
||||
|
||||
// Assert
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Fact]
|
||||
public void ValidateSignature_WithInvalidSignature_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
var result = _service.ValidateSignature("{}", "invalid-signature", "secret123", "harbor");
|
||||
|
||||
// Assert
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static RegistrySource CreateTestSource() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Harbor",
|
||||
Type = RegistrySourceType.Harbor,
|
||||
RegistryUrl = "https://harbor.example.com",
|
||||
Status = RegistrySourceStatus.Active,
|
||||
TriggerMode = RegistryTriggerMode.Webhook
|
||||
};
|
||||
|
||||
private static RegistrySourceRun CreateTestRun(Guid sourceId) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SourceId = sourceId,
|
||||
Status = RegistryRunStatus.Running,
|
||||
TriggerType = "webhook",
|
||||
StartedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
private static string CreateHarborPushPayload(string repository, string tag) => JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "PUSH_ARTIFACT",
|
||||
occur_at = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
@operator = "admin",
|
||||
event_data = new
|
||||
{
|
||||
resources = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
resource_url = $"harbor.example.com/{repository}:{tag}",
|
||||
digest = "sha256:abc123def456",
|
||||
tag
|
||||
}
|
||||
},
|
||||
repository = new
|
||||
{
|
||||
name = repository,
|
||||
repo_full_name = repository,
|
||||
@namespace = repository.Contains('/') ? repository.Split('/')[0] : repository
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user