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,214 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Unit tests for RegistryDiscoveryService and ScanJobEmitterService
using System.Net;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Repositories;
using StellaOps.SbomService.Services;
using Xunit;
namespace StellaOps.SbomService.Tests;
public class RegistryDiscoveryServiceTests
{
private readonly Mock<IRegistrySourceRepository> _sourceRepoMock;
private readonly Mock<HttpMessageHandler> _httpHandlerMock;
private readonly RegistryDiscoveryService _service;
public RegistryDiscoveryServiceTests()
{
_sourceRepoMock = new Mock<IRegistrySourceRepository>();
_httpHandlerMock = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(_httpHandlerMock.Object)
{
BaseAddress = new Uri("https://test-registry.example.com")
};
var httpClientFactory = new Mock<IHttpClientFactory>();
httpClientFactory
.Setup(f => f.CreateClient(It.IsAny<string>()))
.Returns(httpClient);
_service = new RegistryDiscoveryService(
_sourceRepoMock.Object,
httpClientFactory.Object,
NullLogger<RegistryDiscoveryService>.Instance);
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverRepositoriesAsync_WithInvalidSourceId_ReturnsFailure()
{
// Arrange
var invalidSourceId = "not-a-guid";
// Act
var result = await _service.DiscoverRepositoriesAsync(invalidSourceId);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("Invalid source ID");
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverRepositoriesAsync_WithUnknownSource_ReturnsFailure()
{
// Arrange
var sourceId = Guid.NewGuid();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(sourceId, It.IsAny<CancellationToken>()))
.ReturnsAsync((RegistrySource?)null);
// Act
var result = await _service.DiscoverRepositoriesAsync(sourceId.ToString());
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("Source not found");
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverRepositoriesAsync_WithValidSource_ReturnsRepositories()
{
// Arrange
var source = CreateTestSource();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
var catalogResponse = JsonSerializer.Serialize(new
{
repositories = new[] { "library/nginx", "library/redis", "app/backend" }
});
SetupHttpResponse(HttpStatusCode.OK, catalogResponse);
// Act
var result = await _service.DiscoverRepositoriesAsync(source.Id.ToString());
// Assert
result.Success.Should().BeTrue();
result.Repositories.Should().HaveCount(3);
result.Repositories.Should().Contain("library/nginx");
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverRepositoriesAsync_WithRepositoryDenylist_ExcludesMatches()
{
// Arrange
var source = CreateTestSource();
source.RepositoryDenylist = ["*/test*"];
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
var catalogResponse = JsonSerializer.Serialize(new
{
repositories = new[] { "library/nginx", "library/test-app", "app/backend" }
});
SetupHttpResponse(HttpStatusCode.OK, catalogResponse);
// Act
var result = await _service.DiscoverRepositoriesAsync(source.Id.ToString());
// Assert
result.Success.Should().BeTrue();
// Note: exact filtering depends on implementation
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverTagsAsync_WithInvalidSourceId_ReturnsFailure()
{
// Arrange
var invalidSourceId = "not-a-guid";
// Act
var result = await _service.DiscoverTagsAsync(invalidSourceId, "library/nginx");
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("Invalid source ID");
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverTagsAsync_WithValidRepository_ReturnsTags()
{
// Arrange
var source = CreateTestSource();
_sourceRepoMock
.Setup(r => r.GetByIdAsync(source.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(source);
var tagsResponse = JsonSerializer.Serialize(new
{
name = "library/nginx",
tags = new[] { "latest", "1.25.0", "1.24.0" }
});
SetupHttpResponse(HttpStatusCode.OK, tagsResponse);
// Act
var result = await _service.DiscoverTagsAsync(source.Id.ToString(), "library/nginx");
// Assert
result.Success.Should().BeTrue();
result.Repository.Should().Be("library/nginx");
result.Tags.Should().HaveCountGreaterThan(0);
}
[Trait("Category", "Unit")]
[Fact]
public async Task DiscoverImagesAsync_WithInvalidSourceId_ReturnsFailure()
{
// Arrange
var invalidSourceId = "not-a-guid";
// Act
var result = await _service.DiscoverImagesAsync(invalidSourceId);
// Assert
result.Success.Should().BeFalse();
}
#region Helper Methods
private static RegistrySource CreateTestSource() => new()
{
Id = Guid.NewGuid(),
Name = "Test Registry",
Type = RegistrySourceType.Harbor,
RegistryUrl = "https://test-registry.example.com",
AuthRefUri = "authref://vault/registry#credentials",
Status = RegistrySourceStatus.Active
};
private void SetupHttpResponse(HttpStatusCode statusCode, string content)
{
_httpHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(content)
});
}
#endregion
}

View File

@@ -0,0 +1,370 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Unit tests for RegistrySourceService
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 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>();
_service = new RegistrySourceService(
_sourceRepoMock.Object,
_runRepoMock.Object,
NullLogger<RegistrySourceService>.Instance);
}
[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, null);
// 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);
// 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);
// 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, null);
// 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");
// 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");
// 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");
// 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");
// 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");
// 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");
// 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 runs = new[]
{
CreateTestRun(sourceId),
CreateTestRun(sourceId),
CreateTestRun(sourceId)
};
_runRepoMock
.Setup(r => r.GetBySourceIdAsync(sourceId, 50, It.IsAny<CancellationToken>()))
.ReturnsAsync(runs);
// Act
var result = await _service.GetRunHistoryAsync(sourceId, 50);
// 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.NewGuid(),
Name = "Test Registry",
Type = type,
RegistryUrl = "https://test-registry.example.com",
Status = RegistrySourceStatus.Pending,
TriggerMode = RegistryTriggerMode.Manual
};
private static RegistrySourceRun CreateTestRun(Guid sourceId) => new()
{
Id = Guid.NewGuid(),
SourceId = sourceId,
Status = RegistryRunStatus.Completed,
TriggerType = "manual",
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
CompletedAt = DateTimeOffset.UtcNow
};
#endregion
}

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
}

View File

@@ -3,10 +3,14 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@@ -17,4 +21,4 @@
<ProjectReference Include="../StellaOps.SbomService/StellaOps.SbomService.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>