Files
ShieldAI/packages/shared-notifications/test/template.service.test.ts
Michael Freno 7aed2d8b2b FRE-4520: Add unit tests for notification template system
- 25 tests covering template resolution, localization fallback, variable substitution, caching, custom template registration, and edge cases
- Update package.json to use vitest for test execution
- All 25 tests passing

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-01 10:08:48 -04:00

414 lines
13 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TemplateService } from '../src/services/template.service';
import type { TemplateDefinition } from '../src/types/template.types';
describe('TemplateService', () => {
let templateService: TemplateService;
beforeEach(() => {
templateService = TemplateService.getInstance();
templateService.clearCache();
});
describe('resolveTemplate', () => {
it('resolves an English email template by ID', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'en',
variables: { name: 'John', profile_url: 'https://shieldai.app/profile' },
});
expect(resolved).not.toBeNull();
expect(resolved!.id).toBe('welcome_email');
expect(resolved!.channel).toBe('email');
expect(resolved!.locale).toBe('en');
expect(resolved!.subject).toContain('John');
expect(resolved!.body).toContain('John');
expect(resolved!.body).toContain('https://shieldai.app/profile');
});
it('resolves a Spanish email template when locale is es', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'es',
variables: { name: 'Juan', profile_url: 'https://shieldai.app/profile' },
});
expect(resolved).not.toBeNull();
expect(resolved!.locale).toBe('es');
expect(resolved!.subject).toContain('Juan');
expect(resolved!.body).toContain('Bienvenido');
});
it('falls back to English when locale is not found', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'fr',
variables: { name: 'Pierre', profile_url: 'https://shieldai.app/profile' },
});
expect(resolved).not.toBeNull();
expect(resolved!.locale).toBe('en');
});
it('falls back to language code match', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'es-MX',
variables: { name: 'Maria', profile_url: 'https://shieldai.app/profile' },
});
expect(resolved).not.toBeNull();
expect(resolved!.locale).toBe('es');
});
it('returns null for unknown template ID', () => {
const resolved = templateService.resolveTemplate({
templateId: 'nonexistent_template',
locale: 'en',
});
expect(resolved).toBeNull();
});
it('substitutes all variables in the template', () => {
const resolved = templateService.resolveTemplate({
templateId: 'alert_notification',
locale: 'en',
variables: {
alert_type: 'Dark Web Exposure',
alert_details: 'Email found in breach',
alert_time: '2026-05-01',
alert_url: 'https://shieldai.app/alerts/123',
},
});
expect(resolved).not.toBeNull();
expect(resolved!.subject).toBe('ShieldAI Alert: Dark Web Exposure');
expect(resolved!.body).toContain('Dark Web Exposure');
expect(resolved!.body).toContain('Email found in breach');
expect(resolved!.body).toContain('2026-05-01');
expect(resolved!.body).toContain('https://shieldai.app/alerts/123');
});
it('uses default values for optional variables', () => {
const resolved = templateService.resolveTemplate({
templateId: 'alert_notification',
locale: 'en',
variables: {
alert_type: 'Test Alert',
alert_details: 'Test details',
},
});
expect(resolved).not.toBeNull();
expect(resolved!.body).toContain('Just now');
});
it('keeps unsubstituted variables as-is', () => {
const resolved = templateService.resolveTemplate({
templateId: 'password_reset',
locale: 'en',
variables: {
name: 'John',
reset_url: 'https://shieldai.app/reset/abc',
},
});
expect(resolved).not.toBeNull();
expect(resolved!.body).toContain('24');
});
it('resolves SMS templates', () => {
const resolved = templateService.resolveTemplate({
templateId: 'verification_sms',
locale: 'en',
variables: { code: '123456' },
});
expect(resolved).not.toBeNull();
expect(resolved!.channel).toBe('sms');
expect(resolved!.body).toContain('123456');
expect(resolved!.body).toContain('10');
});
it('resolves push notification templates', () => {
const resolved = templateService.resolveTemplate({
templateId: 'alert_push',
locale: 'en',
variables: {
alert_type: 'Voice Clone',
alert_details: 'Suspicious call detected',
},
});
expect(resolved).not.toBeNull();
expect(resolved!.channel).toBe('push');
expect(resolved!.subject).toContain('Voice Clone');
expect(resolved!.body).toBe('Suspicious call detected');
});
});
describe('registerTemplate', () => {
it('registers a custom template', () => {
const customTemplate: TemplateDefinition = {
id: 'custom_welcome',
name: 'Custom Welcome',
channel: 'email',
locale: 'en',
category: 'onboarding',
subject: 'Welcome, {{name}}!',
body: 'Custom welcome message for {{name}}.',
variables: [
{ name: 'name', type: 'string', required: true },
],
};
templateService.registerTemplate(customTemplate);
const resolved = templateService.resolveTemplate({
templateId: 'custom_welcome',
locale: 'en',
variables: { name: 'Alice' },
});
expect(resolved).not.toBeNull();
expect(resolved!.subject).toBe('Welcome, Alice!');
expect(resolved!.body).toBe('Custom welcome message for Alice.');
});
it('registers multiple templates at once', () => {
const templates: TemplateDefinition[] = [
{
id: 'bulk_template',
name: 'Bulk Template',
channel: 'email',
locale: 'en',
category: 'test',
subject: 'Bulk test',
body: 'Bulk body {{var1}} {{var2}}',
variables: [
{ name: 'var1', type: 'string', required: true },
{ name: 'var2', type: 'string', required: true },
],
},
{
id: 'bulk_template',
name: 'Bulk Template ES',
channel: 'email',
locale: 'es',
category: 'test',
subject: 'Prueba masiva',
body: 'Cuerpo masivo {{var1}} {{var2}}',
variables: [
{ name: 'var1', type: 'string', required: true },
{ name: 'var2', type: 'string', required: true },
],
},
];
templateService.registerTemplates(templates);
const enResolved = templateService.resolveTemplate({
templateId: 'bulk_template',
locale: 'en',
variables: { var1: 'A', var2: 'B' },
});
expect(enResolved!.locale).toBe('en');
expect(enResolved!.body).toBe('Bulk body A B');
const esResolved = templateService.resolveTemplate({
templateId: 'bulk_template',
locale: 'es',
variables: { var1: 'A', var2: 'B' },
});
expect(esResolved!.locale).toBe('es');
expect(esResolved!.body).toBe('Cuerpo masivo A B');
});
it('invalidates cache after registration', () => {
const template: TemplateDefinition = {
id: 'cache_test',
name: 'Cache Test',
channel: 'email',
locale: 'en',
category: 'test',
subject: 'Original',
body: 'Original body',
variables: [],
};
templateService.registerTemplate(template);
templateService.resolveTemplate({
templateId: 'cache_test',
locale: 'en',
});
const statsBefore = templateService.getCacheStats();
expect(statsBefore.size).toBeGreaterThan(0);
templateService.registerTemplate({
...template,
subject: 'Updated',
body: 'Updated body',
});
const resolved = templateService.resolveTemplate({
templateId: 'cache_test',
locale: 'en',
});
expect(resolved!.subject).toBe('Updated');
});
});
describe('caching', () => {
it('caches resolved templates', () => {
templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'en',
variables: { name: 'Test', profile_url: 'http://test.com' },
});
const stats = templateService.getCacheStats();
expect(stats.size).toBeGreaterThan(0);
});
it('clears cache on clearCache call', () => {
templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'en',
variables: { name: 'Test', profile_url: 'http://test.com' },
});
templateService.clearCache();
const stats = templateService.getCacheStats();
expect(stats.size).toBe(0);
});
});
describe('getAvailableLocales', () => {
it('returns available locales for a template', () => {
const locales = templateService.getAvailableLocales('welcome_email');
expect(locales).toContain('en');
expect(locales).toContain('es');
});
it('returns empty array for unknown template', () => {
const locales = templateService.getAvailableLocales('nonexistent');
expect(locales).toEqual([]);
});
});
describe('getTemplateIds', () => {
it('returns all registered template IDs', () => {
const ids = templateService.getTemplateIds();
expect(ids).toContain('welcome_email');
expect(ids).toContain('alert_notification');
expect(ids).toContain('password_reset');
expect(ids).toContain('scan_complete');
expect(ids).toContain('alert_sms');
expect(ids).toContain('verification_sms');
expect(ids).toContain('alert_push');
});
});
describe('getTemplateInfo', () => {
it('returns template metadata', () => {
const info = templateService.getTemplateInfo('welcome_email');
expect(info).not.toBeNull();
expect(info!.id).toBe('welcome_email');
expect(info!.channel).toBe('email');
expect(info!.category).toBe('onboarding');
expect(info!.locales).toContain('en');
expect(info!.locales).toContain('es');
expect(info!.variables).toContain('name');
expect(info!.variables).toContain('profile_url');
});
it('returns null for unknown template', () => {
const info = templateService.getTemplateInfo('nonexistent');
expect(info).toBeNull();
});
});
describe('locale normalization', () => {
it('normalizes locale with underscore separator', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'es_ES',
variables: { name: 'Test', profile_url: 'http://test.com' },
});
expect(resolved).not.toBeNull();
expect(resolved!.locale).toBe('es');
});
it('normalizes mixed case locale', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'EN-US',
variables: { name: 'Test', profile_url: 'http://test.com' },
});
expect(resolved).not.toBeNull();
expect(resolved!.locale).toBe('en');
});
});
describe('variable substitution edge cases', () => {
it('handles numeric variable values', () => {
const resolved = templateService.resolveTemplate({
templateId: 'password_reset',
locale: 'en',
variables: {
name: 'John',
reset_url: 'https://shieldai.app/reset',
expiry_hours: 48,
},
});
expect(resolved).not.toBeNull();
expect(resolved!.body).toContain('48');
});
it('handles boolean variable values', () => {
templateService.registerTemplate({
id: 'bool_test',
name: 'Bool Test',
channel: 'email',
locale: 'en',
category: 'test',
subject: 'Test',
body: 'Value is {{flag}}',
variables: [
{ name: 'flag', type: 'boolean', required: true },
],
});
const resolved = templateService.resolveTemplate({
templateId: 'bool_test',
locale: 'en',
variables: { flag: true },
});
expect(resolved!.body).toBe('Value is true');
});
it('preserves HTML in htmlBody', () => {
const resolved = templateService.resolveTemplate({
templateId: 'welcome_email',
locale: 'en',
variables: { name: 'John', profile_url: 'https://shieldai.app/profile' },
});
expect(resolved!.htmlBody).toContain('<h1>');
expect(resolved!.htmlBody).toContain('<a href="https://shieldai.app/profile">');
});
});
});