diff --git a/CHANGELOG.md b/CHANGELOG.md index afa6232..bba5dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Initial version of the project - CAP Java plugin for integrating SAP Alert Notification Service (ANS) with CAP Java applications -- `@notification` annotation on CDS events to define notification types with title, subtitle, and grouped title templates +- `@notification` annotation on CDS events to define notification types and standalone notification templates - Support for email templates via `template.email.subject` and `template.email.html` fields - Mustache syntax (`{{variableName}}`) for dynamic content substitution in templates - i18n support via `{i18n>KEY}` placeholders in all template fields with automatic language detection per recipient diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java index 818f782..e867ab5 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java @@ -5,9 +5,7 @@ import cds.gen.notificationtypeproviderservice.DeliveryChannels; import cds.gen.notificationtypeproviderservice.NotificationTypes; -import cds.gen.notificationtypeproviderservice.Templates; import com.sap.cds.Struct; -import com.sap.cds.notifications.helpers.I18nHelper; import com.sap.cds.reflect.CdsEvent; import com.sap.cds.reflect.CdsModel; import com.sap.cds.services.runtime.CdsRuntime; @@ -21,11 +19,9 @@ public class NotificationTypeAssembler { private static final Logger logger = LoggerFactory.getLogger(NotificationTypeAssembler.class); private final CdsRuntime runtime; - private final I18nHelper i18nHelper; public NotificationTypeAssembler(CdsRuntime runtime) { this.runtime = runtime; - this.i18nHelper = new I18nHelper(runtime); } /** Build all notification types from CDS model event annotations. */ @@ -48,22 +44,6 @@ private Optional extractNotificationTypeFromEvent(CdsEvent ev nt.setNotificationTypeKey(key); nt.setNotificationTypeVersion("1"); - Set locales = i18nHelper.getAvailableLocales(); - logger.debug("Creating templates for {} discovered i18n locales", locales.size()); - - List templates = new ArrayList<>(); - - // Create template for each discovered locale using EdmxI18nProvider - for (Locale locale : locales) { - String lang = locale.toLanguageTag(); - Map i18nTexts = i18nHelper.getI18nTexts(locale); - Templates template = createTemplate(event, lang, i18nTexts); - templates.add(template); - logger.debug("Created template for language: {} with {} i18n texts", lang, i18nTexts.size()); - } - - nt.setTemplates(templates); - // Extract delivery channels List deliveryChannels = extractDeliveryChannels(event); if (!deliveryChannels.isEmpty()) { @@ -74,83 +54,6 @@ private Optional extractNotificationTypeFromEvent(CdsEvent ev return Optional.of(nt); } - private Templates createTemplate(CdsEvent event, String lang, Map i18nTexts) { - Templates template = Struct.create(Templates.class); - template.setLanguage(lang); - - // Validate and set required fields - String publicTitle = - i18nHelper.resolveAnnotationValue(event, "notification.template.publicTitle", i18nTexts); - if (publicTitle == null || publicTitle.trim().isEmpty()) { - throw new IllegalStateException( - String.format( - "Missing required annotation: @notification.template.publicTitle for event '%s'. ANS" - + " requires publicTitle to be set. Please add 'publicTitle:" - + " '{i18n>TEMPLATE_PUBLIC}'' to your @notification.template annotation and" - + " define 'TEMPLATE_PUBLIC' in your i18n properties files.", - event.getName())); - } - template.setTemplatePublic(publicTitle); - - String sensitiveTitle = i18nHelper.resolveAnnotationValue(event, "notification.template.title", i18nTexts); - if (sensitiveTitle == null || sensitiveTitle.trim().isEmpty()) { - throw new IllegalStateException( - String.format( - "Missing required annotation: @notification.template.title for event '%s'. ANS" - + " requires title (sensitive template) to be set. Please add 'title:" - + " '{i18n>TEMPLATE_SENSITIVE}'' to your @notification.template annotation and" - + " define 'TEMPLATE_SENSITIVE' in your i18n properties files.", - event.getName())); - } - template.setTemplateSensitive(sensitiveTitle); - - String groupedTitle = - i18nHelper.resolveAnnotationValue(event, "notification.template.groupedTitle", i18nTexts); - if (groupedTitle == null || groupedTitle.trim().isEmpty()) { - throw new IllegalStateException( - String.format( - "Missing required annotation: @notification.template.groupedTitle for event '%s'. ANS" - + " requires groupedTitle to be set. Please add 'groupedTitle:" - + " '{i18n>TEMPLATE_GROUPED}'' to your @notification annotation and define" - + " 'TEMPLATE_GROUPED' in your i18n properties files.", - event.getName())); - } - template.setTemplateGrouped(groupedTitle); - - template.setDescription(i18nHelper.resolveAnnotationValue(event, "description", i18nTexts)); - - String subtitle = i18nHelper.resolveAnnotationValue(event, "notification.template.subtitle", i18nTexts); - if (subtitle == null || subtitle.trim().isEmpty()) { - throw new IllegalStateException( - String.format( - "Missing required annotation: @notification.template.subtitle for event '%s'. ANS" - + " requires subtitle to be set. Please add 'subtitle: '{i18n>SUBTITLE}'' to your" - + " @notification.template annotation and define 'SUBTITLE' in your i18n" - + " properties files.", - event.getName())); - } - template.setSubtitle(subtitle); - - template.setEmailSubject( - i18nHelper.resolveAnnotationValue(event, "notification.template.email.subject", i18nTexts)); - - // Resolve email HTML - if it's a file path, load the file content - String emailHtmlValue = - i18nHelper.resolveAnnotationValue(event, "notification.template.email.html", i18nTexts); - if (emailHtmlValue != null && emailHtmlValue.endsWith(".html")) { - String htmlContent = i18nHelper.loadHtmlFromClasspath(emailHtmlValue, i18nTexts); - template.setEmailHtml(htmlContent); - logger.debug("Loaded HTML template from file: {}", emailHtmlValue); - } else { - template.setEmailHtml(emailHtmlValue); - } - - template.setEmailText(i18nHelper.resolveAnnotationValue(event, "notificationType.emailText", i18nTexts)); - template.setTemplateLanguage("MUSTACHE"); - - return template; - } - @SuppressWarnings("unchecked") private List extractDeliveryChannels(CdsEvent event) { List deliveryChannels = new ArrayList<>(); diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java index ce6babf..cf1e961 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java @@ -7,12 +7,10 @@ import cds.gen.notificationtypeproviderservice.NotificationTypeProviderService; import cds.gen.notificationtypeproviderservice.NotificationTypes; -import cds.gen.notificationtypeproviderservice.Templates; import com.sap.cds.notifications.handlers.NotificationTypeAutoProvisionerHandler; import com.sap.cds.services.runtime.CdsRuntime; import customer.sample_app.handlers.mock.NotificationTypeProviderServiceMockHandler; import java.util.*; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,8 +22,7 @@ * Tests for notification type auto-provisioning (SELECT → INSERT/UPDATE flow). * *

These tests specifically verify that the provisioner correctly handles the SELECT-all → UPDATE - * (existing) / INSERT (new) flow and that each notification type gets its own unique ID and retains - * its own data after updates. + * (existing) / INSERT (new) flow and that each notification type gets its own unique ID. */ @SpringBootTest @ActiveProfiles("test") @@ -46,10 +43,6 @@ public class NotificationTypeProvisioningTest { "ServerIncident", "DeploymentNotification"); - /** - * Creates a fresh provisioner handler (same as production code does). The handler is not a Spring - * bean — it's created via new() in NotificationServiceConfiguration. - */ private NotificationTypeAutoProvisionerHandler createProvisioner() { return new NotificationTypeAutoProvisionerHandler(cdsRuntime, notificationTypeProviderService); } @@ -71,7 +64,6 @@ void testEachNotificationTypeHasUniqueId() { allTypes.size(), "All expected notification types should be provisioned"); - // Collect all IDs Map keyToId = new HashMap<>(); for (NotificationTypes nt : allTypes) { String key = nt.getNotificationTypeKey(); @@ -84,27 +76,25 @@ void testEachNotificationTypeHasUniqueId() { LOG.debug("Type: {} → ID: {}", key, id); } - // Verify all IDs are unique Set uniqueIds = new HashSet<>(keyToId.values()); assertEquals( EXPECTED_KEYS.size(), uniqueIds.size(), - "Each notification type must have a UNIQUE NotificationTypeId. " + "Found IDs: " + keyToId); + "Each notification type must have a UNIQUE NotificationTypeId. Found IDs: " + keyToId); LOG.debug("All {} notification types have unique IDs", uniqueIds.size()); } // ────────────────────────────────────────────────────────────── - // Test 2: Re-provisioning updates each type with correct data + // Test 2: Re-provisioning keeps IDs stable (UPDATE, not INSERT) // ────────────────────────────────────────────────────────────── @Test void testReProvisioningUpdatesEachTypeCorrectly() { LOG.debug("=========================================="); - LOG.debug("Test: Re-provisioning should update each type with its own correct data"); + LOG.debug("Test: Re-provisioning should update each type without changing IDs"); LOG.debug("=========================================="); - // Record state BEFORE re-provisioning Map idsBefore = new HashMap<>(); for (NotificationTypes nt : NotificationTypeProviderServiceMockHandler.getAllNotificationTypes()) { @@ -113,11 +103,8 @@ void testReProvisioningUpdatesEachTypeCorrectly() { assertEquals( EXPECTED_KEYS.size(), idsBefore.size(), "All types should exist before re-provisioning"); - // Trigger re-provisioning (simulates app restart) - LOG.debug("Triggering re-provisioning..."); createProvisioner().onApplicationPrepared(); - // Verify IDs remain the same (UPDATE, not new INSERT) Map idsAfter = new HashMap<>(); for (NotificationTypes nt : NotificationTypeProviderServiceMockHandler.getAllNotificationTypes()) { @@ -129,15 +116,7 @@ void testReProvisioningUpdatesEachTypeCorrectly() { idsAfter, "NotificationTypeIds should remain the same after re-provisioning (UPDATE, not INSERT)"); - // Verify each type's data is correct (not overwritten by another type) - assertTypeDataIsCorrect( - "CertificateExpiration", "Certificate Expiry", "Certificate: {{certificateName}}"); - assertTypeDataIsCorrect( - "SystemMaintenance", - "Maintenance Notice", - "System maintenance scheduled for {{systemName}}"); - - LOG.debug("Re-provisioning verified — all types retain correct data"); + LOG.debug("Re-provisioning verified — all types retain their IDs"); } // ────────────────────────────────────────────────────────────── @@ -150,16 +129,13 @@ void testReProvisioningUpdatesAllTypes() { LOG.debug("Test: Re-provisioning should trigger UPDATE for each existing type"); LOG.debug("=========================================="); - // Record update counts before Map countsBefore = new HashMap<>(); for (String key : EXPECTED_KEYS) { countsBefore.put(key, NotificationTypeProviderServiceMockHandler.getUpdateCount(key)); } - // Trigger re-provisioning createProvisioner().onApplicationPrepared(); - // Each type should have been updated exactly once more for (String key : EXPECTED_KEYS) { int before = countsBefore.get(key); int after = NotificationTypeProviderServiceMockHandler.getUpdateCount(key); @@ -169,8 +145,7 @@ void testReProvisioningUpdatesAllTypes() { after, "NotificationType '" + key - + "' should have been updated exactly once. " - + "Before: " + + "' should have been updated exactly once. Before: " + before + ", After: " + after); @@ -180,110 +155,4 @@ void testReProvisioningUpdatesAllTypes() { LOG.debug("All {} types were updated during re-provisioning", EXPECTED_KEYS.size()); } - - // ────────────────────────────────────────────────────────────── - // Test 4: No data cross-contamination between types - // ────────────────────────────────────────────────────────────── - - @Test - void testNoDataCrossContaminationBetweenTypes() { - LOG.debug("=========================================="); - LOG.debug("Test: Each type's English template must contain its own specific content"); - LOG.debug("=========================================="); - - Map typeToExpectedSensitive = - Map.of( - "CertificateExpiration", "certificateName", - "SystemMaintenance", "systemName"); - - for (Map.Entry entry : typeToExpectedSensitive.entrySet()) { - String typeKey = entry.getKey(); - String expectedVariable = entry.getValue(); - - NotificationTypes nt = - NotificationTypeProviderServiceMockHandler.getNotificationTypeByKeyVersion(typeKey, "1"); - assertNotNull(nt, "Type '" + typeKey + "' should exist"); - - // Find the English template - Templates enTemplate = - nt.getTemplates().stream() - .filter(t -> "en".equals(t.getLanguage())) - .findFirst() - .orElseThrow(() -> new AssertionError("No English template for " + typeKey)); - - String sensitive = enTemplate.getTemplateSensitive(); - assertNotNull(sensitive, "TemplateSensitive should not be null for " + typeKey); - - assertTrue( - sensitive.contains("{{" + expectedVariable + "}}"), - "Type '" - + typeKey - + "' TemplateSensitive should contain '{{" - + expectedVariable - + "}}' " - + "but was: '" - + sensitive - + "'. This indicates data cross-contamination from another type."); - - // Also verify publicTitle is unique per type - String publicTitle = enTemplate.getTemplatePublic(); - LOG.debug("[{}] publicTitle='{}', sensitive='{}'", typeKey, publicTitle, sensitive); - } - - // Verify all publicTitles are distinct - Set publicTitles = - EXPECTED_KEYS.stream() - .map( - key -> - NotificationTypeProviderServiceMockHandler.getNotificationTypeByKeyVersion( - key, "1")) - .map( - nt -> - nt.getTemplates().stream() - .filter(t -> "en".equals(t.getLanguage())) - .findFirst() - .map(Templates::getTemplatePublic) - .orElse(null)) - .collect(Collectors.toSet()); - - assertEquals( - EXPECTED_KEYS.size(), - publicTitles.size(), - "Each notification type must have a unique publicTitle. Found: " + publicTitles); - - LOG.debug("No cross-contamination detected — all types have unique, correct data"); - } - - // ────────────────────────────────────────────────────────────── - // Helper - // ────────────────────────────────────────────────────────────── - - private void assertTypeDataIsCorrect( - String typeKey, String expectedPublicTitle, String expectedSensitive) { - NotificationTypes nt = - NotificationTypeProviderServiceMockHandler.getNotificationTypeByKeyVersion(typeKey, "1"); - assertNotNull(nt, "Type '" + typeKey + "' should exist after re-provisioning"); - - Templates enTemplate = - nt.getTemplates().stream() - .filter(t -> "en".equals(t.getLanguage())) - .findFirst() - .orElseThrow(() -> new AssertionError("No English template for " + typeKey)); - - assertEquals( - expectedPublicTitle, - enTemplate.getTemplatePublic(), - "PublicTitle mismatch for '" - + typeKey - + "' — data may have been overwritten by another type"); - assertEquals( - expectedSensitive, - enTemplate.getTemplateSensitive(), - "SensitiveTitle mismatch for '" - + typeKey - + "' — data may have been overwritten by another type"); - - LOG.debug( - "[{}] ✓ publicTitle='{}', sensitive='{}'", typeKey, expectedPublicTitle, expectedSensitive); - } } diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java deleted file mode 100644 index 59c5934..0000000 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. - */ -package customer.sample_app.integration; - -import static org.junit.jupiter.api.Assertions.*; - -import cds.gen.notificationtypeproviderservice.NotificationTypes; -import cds.gen.notificationtypeproviderservice.Templates; -import customer.sample_app.handlers.mock.NotificationTypeProviderServiceMockHandler; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -/** - * Tests for notification type template resolution. Verifies i18n placeholders (build-time) and - * Mustache variables (runtime) in templates. - * - *

i18n variables ({i18n>KEY}) are resolved by NotificationTypeBuilder at startup. Mustache - * variables ({{variable}}) are kept as-is for ANS to resolve at runtime. - */ -@SpringBootTest -@ActiveProfiles("test") -public class NotificationTypeTemplateTest { - - private static final Logger LOG = LoggerFactory.getLogger(NotificationTypeTemplateTest.class); - - /** Pattern to detect unresolved i18n placeholders like {i18n>KEY} */ - private static final Pattern UNRESOLVED_I18N = Pattern.compile("\\{i18n>[^}]+\\}"); - - // ────────────────────────────────────────────────────────────── - // Helper methods - // ────────────────────────────────────────────────────────────── - - private NotificationTypes getNotificationType(String key) { - NotificationTypes nt = - NotificationTypeProviderServiceMockHandler.getNotificationTypeByKeyVersion(key, "1"); - assertNotNull(nt, "Notification type '" + key + "' should be auto-provisioned at startup"); - return nt; - } - - private Templates getTemplateForLanguage(NotificationTypes nt, String lang) { - List templates = nt.getTemplates(); - assertNotNull(templates, "Templates should not be null for " + nt.getNotificationTypeKey()); - - return templates.stream() - .filter(t -> lang.equals(t.getLanguage())) - .findFirst() - .orElseThrow( - () -> - new AssertionError( - "No template found for language '" - + lang - + "' in " - + nt.getNotificationTypeKey())); - } - - private void assertNoUnresolvedI18n(String value, String fieldName, String lang) { - if (value != null) { - assertFalse( - UNRESOLVED_I18N.matcher(value).find(), - "Unresolved {i18n>...} placeholder in " + fieldName + " [" + lang + "]: " + value); - } - } - - // ────────────────────────────────────────────────────────────── - // Test 1: i18n placeholders resolved for all languages - // ────────────────────────────────────────────────────────────── - - @Test - void testI18nPlaceholdersResolvedForAllLanguages() { - LOG.debug("=========================================="); - LOG.debug("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); - LOG.debug("=========================================="); - - NotificationTypes nt = getNotificationType("CertificateExpiration"); - - // Verify templates exist for all 4 languages - Map expectedSensitiveTitles = - Map.of( - "en", "Certificate: {{certificateName}}", - "de", "Zertifikat: {{certificateName}}", - "tr", "Sertifika: {{certificateName}}", - "es", "Certificado: {{certificateName}}"); - - for (Map.Entry entry : expectedSensitiveTitles.entrySet()) { - String lang = entry.getKey(); - String expectedTitle = entry.getValue(); - - Templates template = getTemplateForLanguage(nt, lang); - - assertEquals( - expectedTitle, - template.getTemplateSensitive(), - "TemplateSensitive should be resolved for language: " + lang); - - LOG.debug("[{}] TemplateSensitive: {}", lang, template.getTemplateSensitive()); - } - - // Also verify subtitle and other fields for specific languages - Templates enTemplate = getTemplateForLanguage(nt, "en"); - assertEquals("Certificate Expiration", enTemplate.getSubtitle()); - assertEquals("Certificate Expiry", enTemplate.getTemplatePublic()); - assertEquals("Certificates expiring", enTemplate.getTemplateGrouped()); - assertEquals("Certificate Expiration Alert", enTemplate.getEmailSubject()); - - Templates deTemplate = getTemplateForLanguage(nt, "de"); - assertEquals("Zertifikatablauf", deTemplate.getSubtitle()); - assertEquals("Zertifikatablauf", deTemplate.getTemplatePublic()); - assertEquals("Ablaufende Zertifikate", deTemplate.getTemplateGrouped()); - assertEquals("Zertifikatablauf-Warnung", deTemplate.getEmailSubject()); - - Templates trTemplate = getTemplateForLanguage(nt, "tr"); - assertEquals("Sertifika Sona Ermesi", trTemplate.getSubtitle()); - assertEquals("Sertifika Sona Ermesi", trTemplate.getTemplatePublic()); - assertEquals("Süresi Dolan Sertifikalar", trTemplate.getTemplateGrouped()); - assertEquals("Sertifika Sona Erme Uyarısı", trTemplate.getEmailSubject()); - - Templates esTemplate = getTemplateForLanguage(nt, "es"); - assertEquals("Expiración de Certificado", esTemplate.getSubtitle()); - assertEquals("Expiración de Certificado", esTemplate.getTemplatePublic()); - assertEquals("Certificados por expirar", esTemplate.getTemplateGrouped()); - assertEquals("Alerta de Expiración de Certificado", esTemplate.getEmailSubject()); - - LOG.debug("All 4 languages verified successfully"); - } - - // ────────────────────────────────────────────────────────────── - // Test 2: No unresolved {i18n>...} placeholders in any template - // ────────────────────────────────────────────────────────────── - - @Test - void testNoUnresolvedI18nPlaceholdersInTemplates() { - LOG.debug("=========================================="); - LOG.debug("Test: No unresolved {i18n>...} placeholders in any template"); - LOG.debug("=========================================="); - - List allTypes = - NotificationTypeProviderServiceMockHandler.getAllNotificationTypes(); - assertFalse(allTypes.isEmpty(), "At least one notification type should exist"); - - int totalTemplatesChecked = 0; - - for (NotificationTypes nt : allTypes) { - List templates = nt.getTemplates(); - if (templates == null) continue; - - for (Templates t : templates) { - String lang = t.getLanguage(); - String typeKey = nt.getNotificationTypeKey(); - String prefix = typeKey + "/" + lang; - - assertNoUnresolvedI18n(t.getTemplatePublic(), prefix + "/templatePublic", lang); - assertNoUnresolvedI18n(t.getTemplateSensitive(), prefix + "/templateSensitive", lang); - assertNoUnresolvedI18n(t.getTemplateGrouped(), prefix + "/templateGrouped", lang); - assertNoUnresolvedI18n(t.getSubtitle(), prefix + "/subtitle", lang); - assertNoUnresolvedI18n(t.getDescription(), prefix + "/description", lang); - assertNoUnresolvedI18n(t.getEmailSubject(), prefix + "/emailSubject", lang); - assertNoUnresolvedI18n(t.getEmailHtml(), prefix + "/emailHtml", lang); - assertNoUnresolvedI18n(t.getEmailText(), prefix + "/emailText", lang); - - totalTemplatesChecked++; - } - } - - LOG.debug( - "Checked {} templates across {} notification types — no unresolved i18n placeholders", - totalTemplatesChecked, - allTypes.size()); - } - - // ────────────────────────────────────────────────────────────── - // Test 3: Email HTML contains Mustache variables (runtime) - // ────────────────────────────────────────────────────────────── - - @Test - void testEmailHtmlContainsMustacheVariables() { - LOG.debug("=========================================="); - LOG.debug("Test: Email HTML should contain Mustache variables for ANS runtime"); - LOG.debug("=========================================="); - - NotificationTypes nt = getNotificationType("CertificateExpiration"); - Templates enTemplate = getTemplateForLanguage(nt, "en"); - - String emailHtml = enTemplate.getEmailHtml(); - assertNotNull(emailHtml, "Email HTML should not be null for CertificateExpiration"); - - // These Mustache variables must be present — ANS resolves them at runtime - List expectedVariables = - List.of( - "{{certificateName}}", - "{{expirationDate}}", - "{{renewLink}}", - "{{name}}", - "{{year}}", - "{{companyName}}"); - - for (String variable : expectedVariables) { - assertTrue( - emailHtml.contains(variable), "Email HTML should contain Mustache variable: " + variable); - LOG.debug("Found Mustache variable: {}", variable); - } - - LOG.debug("All {} Mustache variables present in email HTML", expectedVariables.size()); - } - - // ────────────────────────────────────────────────────────────── - // Test 4: Email HTML i18n resolved per language - // ────────────────────────────────────────────────────────────── - - @Test - void testEmailHtmlI18nResolvedPerLanguage() { - LOG.debug("=========================================="); - LOG.debug("Test: Email HTML i18n values should differ per language"); - LOG.debug("=========================================="); - - NotificationTypes nt = getNotificationType("CertificateExpiration"); - - // EMAIL_BUTTON value per language - Map expectedButtonTexts = - Map.of( - "en", "Renew Now", - "de", "Jetzt erneuern", - "tr", "Şimdi Yenile", - "es", "Renovar Ahora"); - - // EMAIL_GREETING value per language (contains Mustache variable {{name}}) - Map expectedGreetings = - Map.of( - "en", "Dear {{name}}", - "de", "Sehr geehrte(r) {{name}}", - "tr", "Sayın {{name}}", - "es", "Estimado(a) {{name}}"); - - for (String lang : List.of("en", "de", "tr", "es")) { - Templates template = getTemplateForLanguage(nt, lang); - String emailHtml = template.getEmailHtml(); - assertNotNull(emailHtml, "Email HTML should not be null for lang: " + lang); - - // Verify button text - String expectedButton = expectedButtonTexts.get(lang); - assertTrue( - emailHtml.contains(expectedButton), - "[" + lang + "] Email HTML should contain button text: '" + expectedButton + "'"); - - // Verify greeting text - String expectedGreeting = expectedGreetings.get(lang); - assertTrue( - emailHtml.contains(expectedGreeting), - "[" + lang + "] Email HTML should contain greeting: '" + expectedGreeting + "'"); - - LOG.debug("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); - } - - LOG.debug("Email HTML i18n verified for all 4 languages"); - } -}