- 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>
414 lines
13 KiB
TypeScript
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">');
|
|
});
|
|
});
|
|
});
|