Files
git.stella-ops.org/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Tests/Dispatch/SimpleTemplateRendererTests.cs

272 lines
8.6 KiB
C#

using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Notify.Models;
using StellaOps.Notifier.Worker.Dispatch;
using Xunit;
namespace StellaOps.Notifier.Tests.Dispatch;
public sealed class SimpleTemplateRendererTests
{
private readonly SimpleTemplateRenderer _renderer;
public SimpleTemplateRendererTests()
{
_renderer = new SimpleTemplateRenderer(NullLogger<SimpleTemplateRenderer>.Instance);
}
[Fact]
public async Task RenderAsync_SimpleVariableSubstitution_ReplacesVariables()
{
var template = NotifyTemplate.Create(
templateId: "tpl-1",
tenantId: "tenant-a",
channelType: NotifyChannelType.Slack,
key: "test-template",
locale: "en",
body: "Hello {{actor}}, event {{kind}} occurred.");
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "policy.violation",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: new JsonObject(),
actor: "admin@example.com",
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Contains("Hello admin@example.com", result.Body);
Assert.Contains("event policy.violation occurred", result.Body);
Assert.NotEmpty(result.BodyHash);
}
[Fact]
public async Task RenderAsync_PayloadVariables_FlattenedAndAvailable()
{
var template = NotifyTemplate.Create(
templateId: "tpl-2",
tenantId: "tenant-a",
channelType: NotifyChannelType.Webhook,
key: "payload-test",
locale: "en",
body: "Image: {{image}}, Severity: {{severity}}");
var payload = new JsonObject
{
["image"] = "registry.local/api:v1.0",
["severity"] = "critical"
};
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "scan.complete",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: payload,
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Contains("Image: registry.local/api:v1.0", result.Body);
Assert.Contains("Severity: critical", result.Body);
}
[Fact]
public async Task RenderAsync_NestedPayloadVariables_SupportsDotNotation()
{
var template = NotifyTemplate.Create(
templateId: "tpl-3",
tenantId: "tenant-a",
channelType: NotifyChannelType.Slack,
key: "nested-test",
locale: "en",
body: "Package: {{package.name}} v{{package.version}}");
var payload = new JsonObject
{
["package"] = new JsonObject
{
["name"] = "lodash",
["version"] = "4.17.21"
}
};
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "vulnerability.found",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: payload,
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Contains("Package: lodash v4.17.21", result.Body);
}
[Fact]
public async Task RenderAsync_SensitiveKeys_AreRedacted()
{
var template = NotifyTemplate.Create(
templateId: "tpl-4",
tenantId: "tenant-a",
channelType: NotifyChannelType.Webhook,
key: "redact-test",
locale: "en",
body: "Token: {{apikey}}, User: {{username}}");
var payload = new JsonObject
{
["apikey"] = "secret-token-12345",
["username"] = "testuser"
};
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "auth.event",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: payload,
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Contains("[REDACTED]", result.Body);
Assert.Contains("User: testuser", result.Body);
Assert.DoesNotContain("secret-token-12345", result.Body);
}
[Fact]
public async Task RenderAsync_MissingVariables_ReplacedWithEmptyString()
{
var template = NotifyTemplate.Create(
templateId: "tpl-5",
tenantId: "tenant-a",
channelType: NotifyChannelType.Slack,
key: "missing-test",
locale: "en",
body: "Value: {{nonexistent}}-end");
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "test.event",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: new JsonObject(),
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Equal("Value: -end", result.Body);
}
[Fact]
public async Task RenderAsync_EachBlock_IteratesOverArray()
{
var template = NotifyTemplate.Create(
templateId: "tpl-6",
tenantId: "tenant-a",
channelType: NotifyChannelType.Slack,
key: "each-test",
locale: "en",
body: "Items:{{#each items}} {{this}}{{/each}}");
var payload = new JsonObject
{
["items"] = new JsonArray("alpha", "beta", "gamma")
};
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "list.event",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: payload,
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Contains("alpha", result.Body);
Assert.Contains("beta", result.Body);
Assert.Contains("gamma", result.Body);
}
[Fact]
public async Task RenderAsync_SubjectFromMetadata_RendersSubject()
{
var template = NotifyTemplate.Create(
templateId: "tpl-7",
tenantId: "tenant-a",
channelType: NotifyChannelType.Webhook,
key: "subject-test",
locale: "en",
body: "Body content",
metadata: new[] { new KeyValuePair<string, string>("subject", "Alert: {{kind}}") });
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "critical.alert",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: new JsonObject(),
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Equal("Alert: critical.alert", result.Subject);
}
[Fact]
public async Task RenderAsync_BodyHash_IsConsistent()
{
var template = NotifyTemplate.Create(
templateId: "tpl-8",
tenantId: "tenant-a",
channelType: NotifyChannelType.Slack,
key: "hash-test",
locale: "en",
body: "Static content");
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "test.event",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: new JsonObject(),
version: "1");
var result1 = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
var result2 = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Equal(result1.BodyHash, result2.BodyHash);
Assert.Equal(64, result1.BodyHash.Length); // SHA256 hex
}
[Fact]
public async Task RenderAsync_Format_PreservedFromTemplate()
{
var template = NotifyTemplate.Create(
templateId: "tpl-9",
tenantId: "tenant-a",
channelType: NotifyChannelType.Slack,
key: "format-test",
locale: "en",
body: "Content",
format: NotifyDeliveryFormat.Markdown);
var notifyEvent = NotifyEvent.Create(
eventId: Guid.NewGuid(),
kind: "test.event",
tenant: "tenant-a",
ts: DateTimeOffset.UtcNow,
payload: new JsonObject(),
version: "1");
var result = await _renderer.RenderAsync(template, notifyEvent, CancellationToken.None);
Assert.Equal(NotifyDeliveryFormat.Markdown, result.Format);
}
}