package templates import ( "errors" "os" "path/filepath" "testing" "galaxy/mail/internal/domain/common" "github.com/stretchr/testify/require" ) func TestNewCatalogBuildsImmutableRegistry(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "fr-fr", "subject.tmpl"), "Tour {{.turn_number}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "fr-fr", "text.tmpl"), "Bonjour {{with .player}}{{.name}}{{end}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "fr-fr", "html.tmpl"), "

{{.player.name}}

") catalog, err := NewCatalog(rootDir) require.NoError(t, err) require.Equal(t, filepath.Clean(rootDir), catalog.RootDir()) locale, err := common.ParseLocale("fr-FR") require.NoError(t, err) resolved, err := catalog.Lookup(common.TemplateID("game.turn_ready"), locale) require.NoError(t, err) require.False(t, resolved.LocaleFallbackUsed()) require.Equal(t, common.Locale("fr-FR"), resolved.ResolvedLocale()) require.Equal(t, []string{"player", "player.name", "turn_number"}, resolved.RequiredVariablePaths()) subject, err := resolved.ExecuteSubject(map[string]any{ "turn_number": 54, "player": map[string]any{ "name": "Pilot", }, }) require.NoError(t, err) require.Equal(t, "Tour 54", subject) textBody, err := resolved.ExecuteText(map[string]any{ "player": map[string]any{ "name": "Pilot", }, }) require.NoError(t, err) require.Equal(t, "Bonjour Pilot", textBody) htmlBody, ok, err := resolved.ExecuteHTML(map[string]any{ "player": map[string]any{ "name": "Pilot", }, }) require.NoError(t, err) require.True(t, ok) require.Equal(t, "

Pilot

", htmlBody) } func TestCatalogLookupFallsBackToEnglish(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "subject.tmpl"), "Turn {{.turn_number}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "text.tmpl"), "Hello {{.player.name}}") catalog, err := NewCatalog(rootDir) require.NoError(t, err) locale, err := common.ParseLocale("fr-FR") require.NoError(t, err) resolved, err := catalog.Lookup(common.TemplateID("game.turn_ready"), locale) require.NoError(t, err) require.True(t, resolved.LocaleFallbackUsed()) require.Equal(t, common.Locale("en"), resolved.ResolvedLocale()) } func TestCatalogLookupRejectsMissingEnglishFallback(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "fr-FR", "subject.tmpl"), "Tour {{.turn_number}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "fr-FR", "text.tmpl"), "Bonjour {{.player.name}}") catalog, err := NewCatalog(rootDir) require.NoError(t, err) locale, err := common.ParseLocale("de-DE") require.NoError(t, err) _, err = catalog.Lookup(common.TemplateID("game.turn_ready"), locale) require.Error(t, err) require.True(t, errors.Is(err, ErrFallbackMissing)) } func TestCatalogLookupRejectsUnknownTemplateFamily(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") catalog, err := NewCatalog(rootDir) require.NoError(t, err) locale, err := common.ParseLocale("en") require.NoError(t, err) _, err = catalog.Lookup(common.TemplateID("game.turn_ready"), locale) require.Error(t, err) require.True(t, errors.Is(err, ErrTemplateNotFound)) } func TestCatalogAllowsTemplateWithoutHTML(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") catalog, err := NewCatalog(rootDir) require.NoError(t, err) locale, err := common.ParseLocale("en") require.NoError(t, err) resolved, err := catalog.Lookup(common.TemplateID("auth.login_code"), locale) require.NoError(t, err) htmlBody, ok, err := resolved.ExecuteHTML(map[string]any{"code": "123456"}) require.NoError(t, err) require.False(t, ok) require.Empty(t, htmlBody) } func TestCatalogVersionIsDeterministic(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "subject.tmpl"), "Turn {{.turn_number}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "text.tmpl"), "Hello {{.player.name}}") firstCatalog, err := NewCatalog(rootDir) require.NoError(t, err) secondCatalog, err := NewCatalog(rootDir) require.NoError(t, err) locale, err := common.ParseLocale("en") require.NoError(t, err) firstResolved, err := firstCatalog.Lookup(common.TemplateID("game.turn_ready"), locale) require.NoError(t, err) secondResolved, err := secondCatalog.Lookup(common.TemplateID("game.turn_ready"), locale) require.NoError(t, err) require.Equal(t, firstResolved.Template().Version, secondResolved.Template().Version) } func TestNewCatalogRejectsMissingDirectory(t *testing.T) { t.Parallel() _, err := NewCatalog(filepath.Join(t.TempDir(), "missing")) require.Error(t, err) require.Contains(t, err.Error(), "stat root dir") } func TestNewCatalogRejectsMissingRequiredStartupTemplate(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "subject.tmpl"), "Turn {{.turn_number}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "text.tmpl"), "Hello {{.player.name}}") _, err := NewCatalog(rootDir) require.Error(t, err) require.Contains(t, err.Error(), `required template "auth.login_code" locale "en" is missing`) } func TestNewCatalogRejectsBrokenTemplateParse(t *testing.T) { t.Parallel() rootDir := t.TempDir() writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "subject.tmpl"), "Your login code") writeTemplateFile(t, rootDir, filepath.Join("auth.login_code", "en", "text.tmpl"), "Code: {{.code}}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "subject.tmpl"), "{{if .turn_number}") writeTemplateFile(t, rootDir, filepath.Join("game.turn_ready", "en", "text.tmpl"), "Hello {{.player.name}}") _, err := NewCatalog(rootDir) require.Error(t, err) require.True(t, errors.Is(err, ErrTemplateParseFailed)) } func writeTemplateFile(t *testing.T, rootDir string, relativePath string, contents string) { t.Helper() absolutePath := filepath.Join(rootDir, relativePath) require.NoError(t, os.MkdirAll(filepath.Dir(absolutePath), 0o755)) require.NoError(t, os.WriteFile(absolutePath, []byte(contents), 0o644)) }