Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -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
}