Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -9,7 +9,6 @@ using StellaOps.Signals.Persistence.Postgres.Repositories;
|
||||
using StellaOps.Signals.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Signals.Persistence.Tests;
|
||||
|
||||
@@ -48,12 +47,12 @@ public sealed class CallGraphProjectionIntegrationTests : IAsyncLifetime
|
||||
NullLogger<CallGraphSyncService>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE signals.scans CASCADE;");
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
@@ -188,3 +187,6 @@ public sealed class CallGraphProjectionIntegrationTests : IAsyncLifetime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ public sealed class CallGraphSyncServiceTests : IAsyncLifetime
|
||||
_queryRepository = new PostgresCallGraphQueryRepository(_dataSource, NullLogger<PostgresCallGraphQueryRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.ExecuteSqlAsync("TRUNCATE TABLE signals.scans CASCADE;");
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
@@ -134,3 +134,6 @@ public sealed class CallGraphSyncServiceTests : IAsyncLifetime
|
||||
stats2.EdgeCount.Should().Be(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ public sealed class PostgresCallgraphRepositoryTests : IAsyncLifetime
|
||||
_repository = new PostgresCallgraphRepository(dataSource, NullLogger<PostgresCallgraphRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -154,3 +154,6 @@ public sealed class PostgresCallgraphRepositoryTests : IAsyncLifetime
|
||||
result.Id.Should().HaveLength(32); // GUID without hyphens
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// <copyright file="ScmEventMapperTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Signals.Scm.Models;
|
||||
using StellaOps.Signals.Scm.Webhooks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests.Scm;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SCM event mappers.
|
||||
/// @sprint SPRINT_20251229_013_SIGNALS_scm_ci_connectors
|
||||
/// </summary>
|
||||
public sealed class ScmEventMapperTests
|
||||
{
|
||||
#region GitHub Event Mapper Tests
|
||||
|
||||
[Fact]
|
||||
public void GitHubMapper_PushEvent_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GitHubEventMapper();
|
||||
var payload = CreateGitHubPushPayload();
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("push", "delivery-123", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmProvider.GitHub, result.Provider);
|
||||
Assert.Equal(ScmEventType.Push, result.EventType);
|
||||
Assert.Equal("delivery-123", result.EventId);
|
||||
Assert.Equal("refs/heads/main", result.Ref);
|
||||
Assert.NotNull(result.Repository);
|
||||
Assert.Equal("owner/repo", result.Repository.FullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitHubMapper_PullRequestMergedEvent_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GitHubEventMapper();
|
||||
var payload = CreateGitHubPrMergedPayload();
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("pull_request", "delivery-456", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmProvider.GitHub, result.Provider);
|
||||
Assert.Equal(ScmEventType.PullRequestMerged, result.EventType);
|
||||
Assert.NotNull(result.PullRequest);
|
||||
Assert.Equal(42, result.PullRequest.Number);
|
||||
Assert.Equal("closed", result.PullRequest.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitHubMapper_ReleaseEvent_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GitHubEventMapper();
|
||||
var payload = CreateGitHubReleasePayload();
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("release", "delivery-789", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmProvider.GitHub, result.Provider);
|
||||
Assert.Equal(ScmEventType.ReleasePublished, result.EventType);
|
||||
Assert.NotNull(result.Release);
|
||||
Assert.Equal("v1.0.0", result.Release.TagName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitHubMapper_UnknownEvent_ReturnsUnknownType()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GitHubEventMapper();
|
||||
var payload = JsonSerializer.SerializeToElement(new { });
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("unknown_event", "delivery-000", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmEventType.Unknown, result.EventType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GitLab Event Mapper Tests
|
||||
|
||||
[Fact]
|
||||
public void GitLabMapper_PushEvent_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GitLabEventMapper();
|
||||
var payload = CreateGitLabPushPayload();
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("Push Hook", "delivery-123", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmProvider.GitLab, result.Provider);
|
||||
Assert.Equal(ScmEventType.Push, result.EventType);
|
||||
Assert.Equal("refs/heads/main", result.Ref);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitLabMapper_MergeRequestEvent_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GitLabEventMapper();
|
||||
var payload = CreateGitLabMrMergedPayload();
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("Merge Request Hook", "delivery-456", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmProvider.GitLab, result.Provider);
|
||||
Assert.Equal(ScmEventType.PullRequestMerged, result.EventType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gitea Event Mapper Tests
|
||||
|
||||
[Fact]
|
||||
public void GiteaMapper_PushEvent_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mapper = new GiteaEventMapper();
|
||||
var payload = CreateGiteaPushPayload();
|
||||
|
||||
// Act
|
||||
var result = mapper.Map("push", "delivery-123", payload);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(ScmProvider.Gitea, result.Provider);
|
||||
Assert.Equal(ScmEventType.Push, result.EventType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static JsonElement CreateGitHubPushPayload()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
@ref = "refs/heads/main",
|
||||
after = "abc123def456",
|
||||
repository = new
|
||||
{
|
||||
id = 12345,
|
||||
full_name = "owner/repo",
|
||||
clone_url = "https://github.com/owner/repo.git"
|
||||
},
|
||||
sender = new
|
||||
{
|
||||
login = "testuser",
|
||||
id = 1
|
||||
}
|
||||
};
|
||||
return JsonSerializer.SerializeToElement(payload);
|
||||
}
|
||||
|
||||
private static JsonElement CreateGitHubPrMergedPayload()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
action = "closed",
|
||||
pull_request = new
|
||||
{
|
||||
number = 42,
|
||||
merged = true,
|
||||
title = "Test PR",
|
||||
head = new { sha = "abc123" },
|
||||
@base = new { @ref = "main" }
|
||||
},
|
||||
repository = new
|
||||
{
|
||||
id = 12345,
|
||||
full_name = "owner/repo"
|
||||
}
|
||||
};
|
||||
return JsonSerializer.SerializeToElement(payload);
|
||||
}
|
||||
|
||||
private static JsonElement CreateGitHubReleasePayload()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
action = "published",
|
||||
release = new
|
||||
{
|
||||
tag_name = "v1.0.0",
|
||||
name = "Release 1.0.0",
|
||||
draft = false,
|
||||
prerelease = false
|
||||
},
|
||||
repository = new
|
||||
{
|
||||
id = 12345,
|
||||
full_name = "owner/repo"
|
||||
}
|
||||
};
|
||||
return JsonSerializer.SerializeToElement(payload);
|
||||
}
|
||||
|
||||
private static JsonElement CreateGitLabPushPayload()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
@ref = "refs/heads/main",
|
||||
after = "abc123def456",
|
||||
project = new
|
||||
{
|
||||
id = 12345,
|
||||
path_with_namespace = "group/project",
|
||||
git_http_url = "https://gitlab.com/group/project.git"
|
||||
},
|
||||
user_name = "testuser"
|
||||
};
|
||||
return JsonSerializer.SerializeToElement(payload);
|
||||
}
|
||||
|
||||
private static JsonElement CreateGitLabMrMergedPayload()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
object_kind = "merge_request",
|
||||
object_attributes = new
|
||||
{
|
||||
iid = 42,
|
||||
state = "merged",
|
||||
action = "merge",
|
||||
title = "Test MR"
|
||||
},
|
||||
project = new
|
||||
{
|
||||
id = 12345,
|
||||
path_with_namespace = "group/project"
|
||||
}
|
||||
};
|
||||
return JsonSerializer.SerializeToElement(payload);
|
||||
}
|
||||
|
||||
private static JsonElement CreateGiteaPushPayload()
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
@ref = "refs/heads/main",
|
||||
after = "abc123def456",
|
||||
repository = new
|
||||
{
|
||||
id = 12345,
|
||||
full_name = "owner/repo",
|
||||
clone_url = "https://gitea.example.com/owner/repo.git"
|
||||
},
|
||||
sender = new
|
||||
{
|
||||
login = "testuser"
|
||||
}
|
||||
};
|
||||
return JsonSerializer.SerializeToElement(payload);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// <copyright file="ScmWebhookValidatorTests.cs" company="Stella Operations">
|
||||
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Signals.Scm.Webhooks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.Tests.Scm;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SCM webhook signature validators.
|
||||
/// @sprint SPRINT_20251229_013_SIGNALS_scm_ci_connectors
|
||||
/// </summary>
|
||||
public sealed class ScmWebhookValidatorTests
|
||||
{
|
||||
private const string TestSecret = "test-webhook-secret-12345";
|
||||
private const string TestPayload = "{\"action\":\"push\",\"ref\":\"refs/heads/main\"}";
|
||||
|
||||
#region GitHub Validator Tests
|
||||
|
||||
[Fact]
|
||||
public void GitHubValidator_ValidSignature_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitHubWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
var signature = ComputeGitHubSignature(payload, TestSecret);
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, signature, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitHubValidator_InvalidSignature_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitHubWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
var wrongSignature = "sha256=0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, wrongSignature, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitHubValidator_MissingPrefix_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitHubWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
var signatureWithoutPrefix = ComputeGitHubSignature(payload, TestSecret)[7..]; // Remove "sha256="
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, signatureWithoutPrefix, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void GitHubValidator_NullOrEmptySignature_ReturnsFalse(string? signature)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitHubWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, signature, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void GitHubValidator_NullOrEmptySecret_ReturnsFalse(string? secret)
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitHubWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
var signature = "sha256=abc123";
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, signature, secret!);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GitLab Validator Tests
|
||||
|
||||
[Fact]
|
||||
public void GitLabValidator_ValidToken_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitLabWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, TestSecret, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitLabValidator_InvalidToken_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitLabWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, "wrong-token", TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GitLabValidator_CaseSensitive_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GitLabWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, TestSecret.ToUpperInvariant(), TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gitea Validator Tests
|
||||
|
||||
[Fact]
|
||||
public void GiteaValidator_ValidSignature_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GiteaWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
var signature = ComputeGiteaSignature(payload, TestSecret);
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, signature, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GiteaValidator_InvalidSignature_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var validator = new GiteaWebhookValidator();
|
||||
var payload = Encoding.UTF8.GetBytes(TestPayload);
|
||||
var wrongSignature = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
||||
// Act
|
||||
var result = validator.IsValid(payload, wrongSignature, TestSecret);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
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 static string ComputeGiteaSignature(byte[] payload, string secret)
|
||||
{
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
var hash = HMACSHA256.HashData(secretBytes, payload);
|
||||
return Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -12,9 +12,9 @@
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<!-- FsCheck for property-based testing (EvidenceWeightedScore) -->
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<!-- Verify for snapshot testing (EvidenceWeightedScore) -->
|
||||
<PackageReference Include="Verify.Xunit" />
|
||||
<PackageReference Include="Verify.XunitV3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" >
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -29,4 +29,6 @@
|
||||
<ProjectReference Include="../../StellaOps.Signals/StellaOps.Signals.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user