audit, advisories and doctors/setup work
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// <copyright file="ScmWebhookServiceTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Scm.Models;
|
||||
using StellaOps.Signals.Scm.Services;
|
||||
using StellaOps.Signals.Scm.Webhooks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests.Scm;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SCM webhook processing.
|
||||
/// </summary>
|
||||
public sealed class ScmWebhookServiceTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedTimestamp =
|
||||
DateTimeOffset.Parse("2025-01-01T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_RejectsWhenSecretMissingAndUnsignedNotAllowed()
|
||||
{
|
||||
var options = new SignalsOptions();
|
||||
options.Scm.AllowUnsignedWebhooks = false;
|
||||
|
||||
var triggerService = new TestScmTriggerService();
|
||||
var service = CreateService(options, triggerService);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("{}");
|
||||
|
||||
var result = await service.ProcessAsync(
|
||||
ScmProvider.GitHub,
|
||||
eventType: "push",
|
||||
deliveryId: "delivery-1",
|
||||
signature: null,
|
||||
payload: payload,
|
||||
integrationId: null,
|
||||
tenantId: null);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal(401, result.StatusCode);
|
||||
Assert.Equal(0, triggerService.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_AllowsUnsignedWhenConfigured()
|
||||
{
|
||||
var options = new SignalsOptions();
|
||||
options.Scm.AllowUnsignedWebhooks = true;
|
||||
|
||||
var triggerService = new TestScmTriggerService();
|
||||
var service = CreateService(options, triggerService);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("{}");
|
||||
|
||||
var result = await service.ProcessAsync(
|
||||
ScmProvider.GitHub,
|
||||
eventType: "push",
|
||||
deliveryId: "delivery-2",
|
||||
signature: null,
|
||||
payload: payload,
|
||||
integrationId: "integration-1",
|
||||
tenantId: "tenant-1");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(202, result.StatusCode);
|
||||
Assert.Equal(1, triggerService.Calls);
|
||||
Assert.Equal("integration-1", result.Event?.IntegrationId);
|
||||
Assert.Equal("tenant-1", result.Event?.TenantId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ValidSignature_AcceptsAndDispatches()
|
||||
{
|
||||
var options = new SignalsOptions();
|
||||
options.Scm.DefaultSecret = "test-secret";
|
||||
|
||||
var triggerService = new TestScmTriggerService();
|
||||
var service = CreateService(options, triggerService);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("{}");
|
||||
var signature = ComputeGitHubSignature(payload, options.Scm.DefaultSecret);
|
||||
|
||||
var result = await service.ProcessAsync(
|
||||
ScmProvider.GitHub,
|
||||
eventType: "push",
|
||||
deliveryId: "delivery-3",
|
||||
signature: signature,
|
||||
payload: payload,
|
||||
integrationId: null,
|
||||
tenantId: null);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(202, result.StatusCode);
|
||||
Assert.Equal(1, triggerService.Calls);
|
||||
Assert.Equal("delivery-3", result.Event?.EventId);
|
||||
}
|
||||
|
||||
private static ScmWebhookService CreateService(SignalsOptions options, TestScmTriggerService triggerService)
|
||||
{
|
||||
return new ScmWebhookService(
|
||||
NullLogger<ScmWebhookService>.Instance,
|
||||
Options.Create(options),
|
||||
triggerService,
|
||||
new IWebhookSignatureValidator[] { new GitHubWebhookValidator() },
|
||||
new IScmEventMapper[] { new TestScmEventMapper(ScmProvider.GitHub) });
|
||||
}
|
||||
|
||||
private static string ComputeGitHubSignature(byte[] payload, string secret)
|
||||
{
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
var hash = HMACSHA256.HashData(secretBytes, payload);
|
||||
return $"sha256={Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
private sealed class TestScmEventMapper : IScmEventMapper
|
||||
{
|
||||
public TestScmEventMapper(ScmProvider provider)
|
||||
{
|
||||
Provider = provider;
|
||||
}
|
||||
|
||||
public ScmProvider Provider { get; }
|
||||
|
||||
public NormalizedScmEvent? Map(string eventType, string deliveryId, JsonElement payload)
|
||||
{
|
||||
return new NormalizedScmEvent
|
||||
{
|
||||
EventId = deliveryId,
|
||||
Provider = Provider,
|
||||
EventType = ScmEventType.Push,
|
||||
Timestamp = FixedTimestamp,
|
||||
Repository = new ScmRepository
|
||||
{
|
||||
FullName = "stellaops/signals"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestScmTriggerService : IScmTriggerService
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
|
||||
public Task<ScmTriggerResult> ProcessEventAsync(NormalizedScmEvent scmEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Calls++;
|
||||
return Task.FromResult(new ScmTriggerResult
|
||||
{
|
||||
TriggersDispatched = true,
|
||||
ScanTriggersCount = 1,
|
||||
SbomTriggersCount = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user