230 lines
7.0 KiB
C#
230 lines
7.0 KiB
C#
// 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
|
|
}
|