8ce647552a
TemplateService.render(source, vars, opts) → { content, provenance }. Stored
key → resolve+render; inline → render without a DB row (key/version null);
explicit version pins. Provenance = {templateKey, version, locale, sha256(content)}.
10 tests (svc + repo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
2.9 KiB
TypeScript
56 lines
2.9 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import { resetDb } from '../test-utils/reset-db';
|
|
import { TemplateRepository } from './template.repository';
|
|
import { TemplateService } from './template.service';
|
|
import { TemplateNotFoundError } from './template.repository';
|
|
import type { PrismaService } from '../prisma/prisma.service';
|
|
|
|
const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
|
|
const prisma = new PrismaClient({ datasources: { db: { url } } });
|
|
const svc = new TemplateService(new TemplateRepository(prisma as unknown as PrismaService));
|
|
|
|
beforeAll(async () => { await prisma.$connect(); });
|
|
afterAll(async () => { await prisma.$disconnect(); });
|
|
beforeEach(async () => { await resetDb(prisma); });
|
|
|
|
describe('TemplateService.render', () => {
|
|
it('resolves a stored template and renders it, with provenance', async () => {
|
|
await prisma.iiosMessageTemplate.create({
|
|
data: { key: 'welcome', channel: 'EMAIL', locale: 'en', version: 2, subject: 'Hi {{firstName}}', bodyHtml: '<p>{{firstName}}</p>', variables: ['firstName'] },
|
|
});
|
|
const { content, provenance } = await svc.render({ key: 'welcome' }, { firstName: 'Dana' }, { channel: 'EMAIL' });
|
|
expect(content.subject).toBe('Hi Dana');
|
|
expect(content.html).toBe('<p>Dana</p>');
|
|
expect(provenance).toMatchObject({ templateKey: 'welcome', templateVersion: 2, templateLocale: 'en' });
|
|
expect(provenance.renderedHash).toMatch(/^[a-f0-9]{64}$/);
|
|
});
|
|
|
|
it('renders an inline source without a DB row (key/version null)', async () => {
|
|
const { content, provenance } = await svc.render(
|
|
{ inline: { subject: 'Ad-hoc {{x}}', html: '<b>{{x}}</b>', variables: ['x'] } },
|
|
{ x: 'Y' },
|
|
{ channel: 'EMAIL' },
|
|
);
|
|
expect(content.subject).toBe('Ad-hoc Y');
|
|
expect(content.html).toBe('<b>Y</b>');
|
|
expect(provenance.templateKey).toBeNull();
|
|
expect(provenance.templateVersion).toBeNull();
|
|
expect(provenance.renderedHash).toMatch(/^[a-f0-9]{64}$/);
|
|
});
|
|
|
|
it('pins to an explicit version when asked', async () => {
|
|
await prisma.iiosMessageTemplate.create({ data: { key: 'k', channel: 'EMAIL', locale: 'en', version: 1, bodyText: 'v1' } });
|
|
await prisma.iiosMessageTemplate.create({ data: { key: 'k', channel: 'EMAIL', locale: 'en', version: 2, bodyText: 'v2' } });
|
|
const latest = await svc.render({ key: 'k' }, {}, { channel: 'EMAIL' });
|
|
const pinned = await svc.render({ key: 'k', version: 1 }, {}, { channel: 'EMAIL' });
|
|
expect(latest.content.text).toBe('v2');
|
|
expect(pinned.content.text).toBe('v1');
|
|
expect(pinned.provenance.templateVersion).toBe(1);
|
|
});
|
|
|
|
it('throws for an unknown stored key', async () => {
|
|
await expect(svc.render({ key: 'missing' }, {}, { channel: 'EMAIL' })).rejects.toBeInstanceOf(TemplateNotFoundError);
|
|
});
|
|
});
|