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

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

View File

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

View File

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

View File

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

View File

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

View File

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