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.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("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); } }