Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
430
src/__Tests/e2e/Integrations/Helpers/WebhookTestHelper.cs
Normal file
430
src/__Tests/e2e/Integrations/Helpers/WebhookTestHelper.cs
Normal file
@@ -0,0 +1,430 @@
|
||||
// =============================================================================
|
||||
// WebhookTestHelper.cs
|
||||
// Sprint: SPRINT_20251229_019 - Integration E2E Validation
|
||||
// Description: Utility class for webhook testing operations
|
||||
// =============================================================================
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Integration.E2E.Integrations.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods for webhook testing, including signature generation
|
||||
/// and payload manipulation.
|
||||
/// </summary>
|
||||
public static class WebhookTestHelper
|
||||
{
|
||||
#region Signature Generation
|
||||
|
||||
/// <summary>
|
||||
/// Generates an HMAC-SHA256 signature for a webhook payload.
|
||||
/// </summary>
|
||||
/// <param name="payload">The webhook payload.</param>
|
||||
/// <param name="secret">The webhook secret.</param>
|
||||
/// <param name="prefix">Optional prefix for the signature (e.g., "sha256=").</param>
|
||||
/// <returns>The generated signature.</returns>
|
||||
public static string GenerateHmacSha256Signature(string payload, string secret, string prefix = "sha256=")
|
||||
{
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||
|
||||
using var hmac = new HMACSHA256(secretBytes);
|
||||
var hash = hmac.ComputeHash(payloadBytes);
|
||||
|
||||
return prefix + Convert.ToHexStringLower(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a GitHub-style webhook signature.
|
||||
/// </summary>
|
||||
public static string GenerateGitHubSignature(string payload, string secret)
|
||||
{
|
||||
return GenerateHmacSha256Signature(payload, secret, "sha256=");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a GitLab-style webhook token.
|
||||
/// GitLab uses X-Gitlab-Token header with the secret directly.
|
||||
/// </summary>
|
||||
public static string GenerateGitLabToken(string secret)
|
||||
{
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Gitea-style webhook signature.
|
||||
/// </summary>
|
||||
public static string GenerateGiteaSignature(string payload, string secret)
|
||||
{
|
||||
return GenerateHmacSha256Signature(payload, secret, "sha256=");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Harbor-style webhook signature.
|
||||
/// </summary>
|
||||
public static string GenerateHarborSignature(string payload, string secret)
|
||||
{
|
||||
return GenerateHmacSha256Signature(payload, secret, "sha256=");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Docker Hub-style webhook signature.
|
||||
/// </summary>
|
||||
public static string GenerateDockerHubSignature(string payload, string secret)
|
||||
{
|
||||
return GenerateHmacSha256Signature(payload, secret, "sha256=");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Payload Manipulation
|
||||
|
||||
/// <summary>
|
||||
/// Modifies a JSON payload by updating a specific field.
|
||||
/// </summary>
|
||||
/// <param name="payload">The original JSON payload.</param>
|
||||
/// <param name="jsonPath">Dot-separated path to the field (e.g., "repository.name").</param>
|
||||
/// <param name="newValue">The new value to set.</param>
|
||||
/// <returns>The modified payload.</returns>
|
||||
public static string ModifyPayloadField(string payload, string jsonPath, object newValue)
|
||||
{
|
||||
var doc = JsonDocument.Parse(payload);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(payload, JsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to parse payload");
|
||||
|
||||
SetNestedValue(dict, jsonPath.Split('.'), newValue);
|
||||
|
||||
return JsonSerializer.Serialize(dict, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Corrupts a payload by modifying its hash/digest field.
|
||||
/// </summary>
|
||||
public static string CorruptPayloadDigest(string payload)
|
||||
{
|
||||
// Try common digest field names
|
||||
var digestFields = new[] { "digest", "image-digest", "sha", "hash" };
|
||||
|
||||
foreach (var field in digestFields)
|
||||
{
|
||||
if (payload.Contains($"\"{field}\""))
|
||||
{
|
||||
// Replace any sha256 digest with a corrupted one
|
||||
return System.Text.RegularExpressions.Regex.Replace(
|
||||
payload,
|
||||
@"sha256:[a-f0-9]{64}",
|
||||
"sha256:0000000000000000000000000000000000000000000000000000000000000000");
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal valid webhook payload for testing.
|
||||
/// </summary>
|
||||
public static string CreateMinimalPayload(string provider, string eventType = "push")
|
||||
{
|
||||
return provider.ToLowerInvariant() switch
|
||||
{
|
||||
"harbor" => CreateMinimalHarborPayload(),
|
||||
"dockerhub" => CreateMinimalDockerHubPayload(),
|
||||
"acr" => CreateMinimalAcrPayload(),
|
||||
"ecr" => CreateMinimalEcrPayload(),
|
||||
"gcr" => CreateMinimalGcrPayload(),
|
||||
"ghcr" => CreateMinimalGhcrPayload(),
|
||||
"github" => CreateMinimalGitHubPushPayload(),
|
||||
"gitlab" => CreateMinimalGitLabPushPayload(),
|
||||
"gitea" => CreateMinimalGiteaPushPayload(),
|
||||
_ => throw new ArgumentException($"Unknown provider: {provider}")
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Minimal Payload Generators
|
||||
|
||||
private static string CreateMinimalHarborPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "PUSH_ARTIFACT",
|
||||
occur_at = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
@operator = "test",
|
||||
event_data = new
|
||||
{
|
||||
resources = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
digest = "sha256:test123",
|
||||
tag = "latest",
|
||||
resource_url = "harbor.example.com/library/test:latest"
|
||||
}
|
||||
},
|
||||
repository = new
|
||||
{
|
||||
name = "test",
|
||||
repo_full_name = "library/test"
|
||||
}
|
||||
}
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalDockerHubPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
push_data = new
|
||||
{
|
||||
tag = "latest",
|
||||
pushed_at = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
},
|
||||
repository = new
|
||||
{
|
||||
repo_name = "stellaops/test",
|
||||
name = "test",
|
||||
@namespace = "stellaops"
|
||||
}
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalAcrPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
id = Guid.NewGuid().ToString(),
|
||||
action = "push",
|
||||
target = new
|
||||
{
|
||||
repository = "stellaops/test",
|
||||
tag = "latest",
|
||||
digest = "sha256:test123"
|
||||
}
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalEcrPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new Dictionary<string, object>
|
||||
{
|
||||
["version"] = "0",
|
||||
["id"] = Guid.NewGuid().ToString(),
|
||||
["detail-type"] = "ECR Image Action",
|
||||
["source"] = "aws.ecr",
|
||||
["detail"] = new Dictionary<string, object>
|
||||
{
|
||||
["action-type"] = "PUSH",
|
||||
["repository-name"] = "stellaops/test",
|
||||
["image-tag"] = "latest"
|
||||
}
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalGcrPayload()
|
||||
{
|
||||
var innerData = new { action = "INSERT", tag = "latest", digest = "sha256:test123" };
|
||||
var base64Data = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(innerData)));
|
||||
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
message = new
|
||||
{
|
||||
data = base64Data,
|
||||
messageId = "gcr-test-123"
|
||||
}
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalGhcrPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
action = "published",
|
||||
package = new
|
||||
{
|
||||
name = "test-package",
|
||||
package_type = "container",
|
||||
package_version = new
|
||||
{
|
||||
version = "v1.0.0"
|
||||
}
|
||||
},
|
||||
repository = new
|
||||
{
|
||||
full_name = "stellaops/test"
|
||||
}
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalGitHubPushPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
@ref = "refs/heads/main",
|
||||
after = "abc123",
|
||||
repository = new
|
||||
{
|
||||
full_name = "stellaops/test",
|
||||
name = "test",
|
||||
default_branch = "main"
|
||||
},
|
||||
pusher = new { name = "test-user" },
|
||||
sender = new { login = "test-user", type = "User" },
|
||||
commits = Array.Empty<object>()
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalGitLabPushPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
object_kind = "push",
|
||||
@ref = "refs/heads/main",
|
||||
after = "abc123",
|
||||
project = new
|
||||
{
|
||||
path_with_namespace = "stellaops/test",
|
||||
name = "test",
|
||||
default_branch = "main"
|
||||
},
|
||||
user_name = "test-user",
|
||||
commits = Array.Empty<object>()
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
private static string CreateMinimalGiteaPushPayload()
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
@ref = "refs/heads/main",
|
||||
after = "abc123",
|
||||
repository = new
|
||||
{
|
||||
full_name = "stellaops/test",
|
||||
name = "test",
|
||||
default_branch = "main"
|
||||
},
|
||||
pusher = new { login = "test-user" },
|
||||
sender = new { login = "test-user" },
|
||||
commits = Array.Empty<object>()
|
||||
}, JsonOptions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a webhook payload contains required fields.
|
||||
/// </summary>
|
||||
public static bool ValidateRequiredFields(string payload, params string[] fields)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(payload);
|
||||
var root = doc.RootElement;
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (!HasNestedProperty(root, field.Split('.')))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a field value from a JSON payload.
|
||||
/// </summary>
|
||||
public static string? ExtractField(string payload, string jsonPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(payload);
|
||||
var element = doc.RootElement;
|
||||
|
||||
foreach (var part in jsonPath.Split('.'))
|
||||
{
|
||||
if (!element.TryGetProperty(part, out element))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return element.ValueKind == JsonValueKind.String
|
||||
? element.GetString()
|
||||
: element.GetRawText();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
private static void SetNestedValue(Dictionary<string, object> dict, string[] path, object value)
|
||||
{
|
||||
var current = dict;
|
||||
for (var i = 0; i < path.Length - 1; i++)
|
||||
{
|
||||
if (!current.TryGetValue(path[i], out var next))
|
||||
{
|
||||
next = new Dictionary<string, object>();
|
||||
current[path[i]] = next;
|
||||
}
|
||||
|
||||
if (next is JsonElement jsonElement)
|
||||
{
|
||||
var nested = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonElement.GetRawText());
|
||||
if (nested != null)
|
||||
{
|
||||
current[path[i]] = nested;
|
||||
current = nested;
|
||||
}
|
||||
}
|
||||
else if (next is Dictionary<string, object> nestedDict)
|
||||
{
|
||||
current = nestedDict;
|
||||
}
|
||||
}
|
||||
|
||||
current[path[^1]] = value;
|
||||
}
|
||||
|
||||
private static bool HasNestedProperty(JsonElement element, string[] path)
|
||||
{
|
||||
var current = element;
|
||||
foreach (var part in path)
|
||||
{
|
||||
if (current.ValueKind != JsonValueKind.Object ||
|
||||
!current.TryGetProperty(part, out current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user