subController_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. package sub
  2. import (
  3. "bytes"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. "time"
  8. )
  9. // newTestSUBController builds a controller with just the bits loadSubTemplate
  10. // needs, so the template tests don't require a database.
  11. func newTestSUBController() *SUBController {
  12. return &SUBController{subTemplateCache: map[string]*cachedSubTemplate{}}
  13. }
  14. func writeFile(t *testing.T, path, content string) {
  15. t.Helper()
  16. if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
  17. t.Fatalf("write %s: %v", path, err)
  18. }
  19. }
  20. func renderTemplate(t *testing.T, a *SUBController, dir string, data map[string]any) string {
  21. t.Helper()
  22. tmpl, err := a.loadSubTemplate(dir)
  23. if err != nil {
  24. t.Fatalf("loadSubTemplate: unexpected error: %v", err)
  25. }
  26. if tmpl == nil {
  27. t.Fatal("loadSubTemplate: expected a template, got nil")
  28. }
  29. var buf bytes.Buffer
  30. if err := tmpl.Execute(&buf, data); err != nil {
  31. t.Fatalf("execute: %v", err)
  32. }
  33. return buf.String()
  34. }
  35. func TestLoadSubTemplate_RendersIndex(t *testing.T) {
  36. dir := t.TempDir()
  37. writeFile(t, filepath.Join(dir, "index.html"), `<h1>{{ .sId }}</h1>`)
  38. got := renderTemplate(t, newTestSUBController(), dir, map[string]any{"sId": "abc-123"})
  39. if want := `<h1>abc-123</h1>`; got != want {
  40. t.Fatalf("rendered = %q, want %q", got, want)
  41. }
  42. }
  43. func TestLoadSubTemplate_PrefersSubHTML(t *testing.T) {
  44. dir := t.TempDir()
  45. writeFile(t, filepath.Join(dir, "index.html"), `from-index`)
  46. writeFile(t, filepath.Join(dir, "sub.html"), `from-sub`)
  47. got := renderTemplate(t, newTestSUBController(), dir, nil)
  48. if got != "from-sub" {
  49. t.Fatalf("rendered = %q, want %q (sub.html should take precedence)", got, "from-sub")
  50. }
  51. }
  52. func TestLoadSubTemplate_FallbackCases(t *testing.T) {
  53. a := newTestSUBController()
  54. t.Run("missing dir", func(t *testing.T) {
  55. tmpl, err := a.loadSubTemplate(filepath.Join(t.TempDir(), "does-not-exist"))
  56. if tmpl != nil || err != nil {
  57. t.Fatalf("got (%v, %v), want (nil, nil)", tmpl, err)
  58. }
  59. })
  60. t.Run("path is a file not a dir", func(t *testing.T) {
  61. file := filepath.Join(t.TempDir(), "index.html")
  62. writeFile(t, file, `whatever`)
  63. tmpl, err := a.loadSubTemplate(file)
  64. if tmpl != nil || err != nil {
  65. t.Fatalf("got (%v, %v), want (nil, nil)", tmpl, err)
  66. }
  67. })
  68. t.Run("dir without template file", func(t *testing.T) {
  69. tmpl, err := a.loadSubTemplate(t.TempDir())
  70. if tmpl != nil || err != nil {
  71. t.Fatalf("got (%v, %v), want (nil, nil)", tmpl, err)
  72. }
  73. })
  74. }
  75. func TestLoadSubTemplate_MalformedTemplate(t *testing.T) {
  76. dir := t.TempDir()
  77. // Unterminated action — html/template fails to parse this.
  78. writeFile(t, filepath.Join(dir, "index.html"), `<h1>{{ .sId </h1>`)
  79. tmpl, err := newTestSUBController().loadSubTemplate(dir)
  80. if err == nil {
  81. t.Fatal("expected a parse error for a malformed template, got nil")
  82. }
  83. if tmpl != nil {
  84. t.Fatalf("expected nil template on parse error, got %v", tmpl)
  85. }
  86. }
  87. func TestLoadSubTemplate_CacheHitAndInvalidation(t *testing.T) {
  88. a := newTestSUBController()
  89. dir := t.TempDir()
  90. path := filepath.Join(dir, "index.html")
  91. // v1 with a fixed mtime.
  92. writeFile(t, path, `v1`)
  93. t1 := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
  94. if err := os.Chtimes(path, t1, t1); err != nil {
  95. t.Fatalf("chtimes: %v", err)
  96. }
  97. first, err := a.loadSubTemplate(dir)
  98. if err != nil || first == nil {
  99. t.Fatalf("first load: (%v, %v)", first, err)
  100. }
  101. // Same mtime → cache hit returns the identical parsed template.
  102. second, err := a.loadSubTemplate(dir)
  103. if err != nil {
  104. t.Fatalf("second load: %v", err)
  105. }
  106. if second != first {
  107. t.Fatal("expected cache hit to return the same *template.Template pointer")
  108. }
  109. // New content + newer mtime → cache invalidated, fresh content served.
  110. writeFile(t, path, `v2`)
  111. t2 := t1.Add(time.Hour)
  112. if err := os.Chtimes(path, t2, t2); err != nil {
  113. t.Fatalf("chtimes: %v", err)
  114. }
  115. third, err := a.loadSubTemplate(dir)
  116. if err != nil || third == nil {
  117. t.Fatalf("third load: (%v, %v)", third, err)
  118. }
  119. if third == first {
  120. t.Fatal("expected cache invalidation to re-parse the template after mtime change")
  121. }
  122. var buf bytes.Buffer
  123. if err := third.Execute(&buf, nil); err != nil {
  124. t.Fatalf("execute: %v", err)
  125. }
  126. if buf.String() != "v2" {
  127. t.Fatalf("rendered = %q, want %q after edit", buf.String(), "v2")
  128. }
  129. }