feat(alerting): MustacheRenderer with literal fallback on missing vars
Sentinel-substitution approach: unresolved {{x.y.z}} tokens are replaced
with a unique NUL-delimited sentinel before Mustache compilation, rendered
as opaque text, then post-replaced with the original {{x.y.z}} literal.
Malformed templates (unclosed {{) are caught and return the raw template.
Never throws. 9 unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package com.cameleer.server.app.alerting.notify;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MustacheRendererTest {
|
||||
|
||||
private MustacheRenderer renderer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
renderer = new MustacheRenderer();
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleVariable_rendersValue() {
|
||||
var ctx = Map.<String, Object>of("name", "production");
|
||||
assertThat(renderer.render("Env: {{name}}", ctx)).isEqualTo("Env: production");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedPath_rendersValue() {
|
||||
var ctx = Map.<String, Object>of(
|
||||
"alert", Map.of("state", "FIRING"));
|
||||
assertThat(renderer.render("State: {{alert.state}}", ctx)).isEqualTo("State: FIRING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingVariable_rendersLiteralMustache() {
|
||||
var ctx = Map.<String, Object>of("known", "yes");
|
||||
String result = renderer.render("{{known}} and {{missing.path}}", ctx);
|
||||
assertThat(result).isEqualTo("yes and {{missing.path}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingVariable_exactLiteralNoPadding() {
|
||||
// The rendered literal must be exactly {{x}} — no surrounding whitespace or delimiter residue.
|
||||
String result = renderer.render("{{unknown}}", Map.of());
|
||||
assertThat(result).isEqualTo("{{unknown}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedTemplate_returnsRawTemplate() {
|
||||
String broken = "Hello {{unclosed";
|
||||
String result = renderer.render(broken, Map.of());
|
||||
assertThat(result).isEqualTo(broken);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullTemplate_returnsEmptyString() {
|
||||
assertThat(renderer.render(null, Map.of())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyTemplate_returnsEmptyString() {
|
||||
assertThat(renderer.render("", Map.of())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedResolvedAndUnresolved_rendersCorrectly() {
|
||||
var ctx = Map.<String, Object>of(
|
||||
"env", Map.of("slug", "prod"),
|
||||
"alert", Map.of("id", "abc-123"));
|
||||
String tmpl = "{{env.slug}} / {{alert.id}} / {{alert.resolvedAt}}";
|
||||
String result = renderer.render(tmpl, ctx);
|
||||
assertThat(result).isEqualTo("prod / abc-123 / {{alert.resolvedAt}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainText_noTokens_returnsAsIs() {
|
||||
assertThat(renderer.render("No tokens here.", Map.of())).isEqualTo("No tokens here.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user