functions_test.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package template // import "miniflux.app/v2/internal/template"
  4. import (
  5. "strings"
  6. "testing"
  7. "time"
  8. "miniflux.app/v2/internal/locale"
  9. "miniflux.app/v2/internal/model"
  10. )
  11. func TestDict(t *testing.T) {
  12. d, err := dict("k1", "v1", "k2", "v2")
  13. if err != nil {
  14. t.Fatalf(`The dict should be valid: %v`, err)
  15. }
  16. if value, found := d["k1"]; found {
  17. if value != "v1" {
  18. t.Fatalf(`Unexpected value for k1: got %q`, value)
  19. }
  20. }
  21. if value, found := d["k2"]; found {
  22. if value != "v2" {
  23. t.Fatalf(`Unexpected value for k2: got %q`, value)
  24. }
  25. }
  26. }
  27. func TestDictWithInvalidNumberOfArguments(t *testing.T) {
  28. _, err := dict("k1")
  29. if err == nil {
  30. t.Fatal(`An error should be returned if the number of arguments are not even`)
  31. }
  32. }
  33. func TestDictWithInvalidMap(t *testing.T) {
  34. _, err := dict(1, 2)
  35. if err == nil {
  36. t.Fatal(`An error should be returned if the dict keys are not string`)
  37. }
  38. }
  39. func TestTruncate(t *testing.T) {
  40. scenarios := []struct {
  41. name string
  42. input string
  43. max int
  44. expected string
  45. }{
  46. {
  47. name: "short ascii",
  48. input: "Short text",
  49. max: 25,
  50. expected: "Short text",
  51. },
  52. {
  53. name: "short unicode",
  54. input: "Короткий текст",
  55. max: 25,
  56. expected: "Короткий текст",
  57. },
  58. {
  59. name: "exact ascii length",
  60. input: "Short text",
  61. max: len("Short text"),
  62. expected: "Short text",
  63. },
  64. {
  65. name: "long ascii",
  66. input: "This is a really pretty long English text",
  67. max: 25,
  68. expected: "This is a really pretty l…",
  69. },
  70. {
  71. name: "long unicode",
  72. input: "Это реально очень длинный русский текст",
  73. max: 25,
  74. expected: "Это реально очень длинный…",
  75. },
  76. }
  77. for _, scenario := range scenarios {
  78. t.Run(scenario.name, func(t *testing.T) {
  79. result := truncate(scenario.input, scenario.max)
  80. if result != scenario.expected {
  81. t.Fatalf(`Unexpected output, got %q instead of %q`, result, scenario.expected)
  82. }
  83. })
  84. }
  85. }
  86. func TestTruncateInvalidMax(t *testing.T) {
  87. scenarios := []struct {
  88. name string
  89. max int
  90. }{
  91. {name: "zero", max: 0},
  92. {name: "negative", max: -1},
  93. }
  94. for _, scenario := range scenarios {
  95. t.Run(scenario.name, func(t *testing.T) {
  96. defer func() {
  97. if recover() == nil {
  98. t.Fatal("Expected panic for non-positive max")
  99. }
  100. }()
  101. _ = truncate("Short text", scenario.max)
  102. })
  103. }
  104. }
  105. func TestIsEmail(t *testing.T) {
  106. if !isEmail("user@domain.tld") {
  107. t.Fatal(`This email is valid and should returns true`)
  108. }
  109. if isEmail("invalid") {
  110. t.Fatal(`This email is not valid and should returns false`)
  111. }
  112. }
  113. func TestDuration(t *testing.T) {
  114. now := time.Now()
  115. var dt = []struct {
  116. in time.Time
  117. out string
  118. }{
  119. {time.Time{}, ""},
  120. {now.Add(time.Hour), "1h0m0s"},
  121. {now.Add(time.Minute), "1m0s"},
  122. {now.Add(time.Minute * 40), "40m0s"},
  123. {now.Add(time.Millisecond * 40), "0s"},
  124. {now.Add(time.Millisecond * 80), "0s"},
  125. {now.Add(time.Millisecond * 400), "0s"},
  126. {now.Add(time.Millisecond * 800), "1s"},
  127. {now.Add(time.Millisecond * 4321), "4s"},
  128. {now.Add(time.Millisecond * 8765), "9s"},
  129. {now.Add(time.Microsecond * 12345678), "12s"},
  130. {now.Add(time.Microsecond * 87654321), "1m28s"},
  131. }
  132. for i, tt := range dt {
  133. if out := durationImpl(tt.in, now); out != tt.out {
  134. t.Errorf(`%d. content mismatch for "%v": expected=%q got=%q`, i, tt.in, tt.out, out)
  135. }
  136. }
  137. }
  138. func TestElapsedTime(t *testing.T) {
  139. printer := locale.NewPrinter("en_US")
  140. var dt = []struct {
  141. in time.Time
  142. out string
  143. }{
  144. {time.Time{}, printer.Print("time_elapsed.not_yet")},
  145. {time.Now().Add(time.Hour), printer.Print("time_elapsed.not_yet")},
  146. {time.Now(), printer.Print("time_elapsed.now")},
  147. {time.Now().Add(-time.Minute), printer.Plural("time_elapsed.minutes", 1, 1)},
  148. {time.Now().Add(-time.Minute * 40), printer.Plural("time_elapsed.minutes", 40, 40)},
  149. {time.Now().Add(-time.Hour), printer.Plural("time_elapsed.hours", 1, 1)},
  150. {time.Now().Add(-time.Hour * 3), printer.Plural("time_elapsed.hours", 3, 3)},
  151. {time.Now().Add(-time.Hour * 32), printer.Print("time_elapsed.yesterday")},
  152. {time.Now().Add(-time.Hour * 24 * 3), printer.Plural("time_elapsed.days", 3, 3)},
  153. {time.Now().Add(-time.Hour * 24 * 14), printer.Plural("time_elapsed.days", 14, 14)},
  154. {time.Now().Add(-time.Hour * 24 * 15), printer.Plural("time_elapsed.days", 15, 15)},
  155. {time.Now().Add(-time.Hour * 24 * 21), printer.Plural("time_elapsed.weeks", 3, 3)},
  156. {time.Now().Add(-time.Hour * 24 * 32), printer.Plural("time_elapsed.months", 1, 1)},
  157. {time.Now().Add(-time.Hour * 24 * 60), printer.Plural("time_elapsed.months", 2, 2)},
  158. {time.Now().Add(-time.Hour * 24 * 366), printer.Plural("time_elapsed.years", 1, 1)},
  159. {time.Now().Add(-time.Hour * 24 * 365 * 3), printer.Plural("time_elapsed.years", 3, 3)},
  160. }
  161. for i, tt := range dt {
  162. if out := elapsedTime(printer, "Local", tt.in); out != tt.out {
  163. t.Errorf(`%d. content mismatch for "%v": expected=%q got=%q`, i, tt.in, tt.out, out)
  164. }
  165. }
  166. }
  167. func TestFormatFileSize(t *testing.T) {
  168. scenarios := []struct {
  169. input int64
  170. expected string
  171. }{
  172. {0, "0 B"},
  173. {1, "1 B"},
  174. {500, "500 B"},
  175. {1024, "1.0 KiB"},
  176. {43520, "42.5 KiB"},
  177. {5000 * 1024 * 1024, "4.9 GiB"},
  178. }
  179. for _, scenario := range scenarios {
  180. result := formatFileSize(scenario.input)
  181. if result != scenario.expected {
  182. t.Errorf(`Unexpected result, got %q instead of %q for %d`, result, scenario.expected, scenario.input)
  183. }
  184. }
  185. }
  186. func TestQueryString(t *testing.T) {
  187. params, err := dict("q", "ai", "unread", true, "offset", 20)
  188. if err != nil {
  189. t.Fatalf(`The dict should be valid: %v`, err)
  190. }
  191. got := (&funcMap{}).Map()["queryString"].(func(map[string]any) string)(params)
  192. if got == "" {
  193. t.Fatalf("Expected a query string, got an empty string")
  194. }
  195. if !strings.HasPrefix(got, "?") {
  196. t.Fatalf(`Expected query string to start with "?", got %q`, got)
  197. }
  198. if !strings.Contains(got, "q=ai") {
  199. t.Fatalf(`Expected query string to contain q=ai, got %q`, got)
  200. }
  201. if !strings.Contains(got, "unread=1") {
  202. t.Fatalf(`Expected query string to contain unread=1, got %q`, got)
  203. }
  204. if !strings.Contains(got, "offset=20") {
  205. t.Fatalf(`Expected query string to contain offset=20, got %q`, got)
  206. }
  207. empty, err := dict("q", "", "unread", false, "offset", 0)
  208. if err != nil {
  209. t.Fatalf(`The dict should be valid: %v`, err)
  210. }
  211. got = (&funcMap{}).Map()["queryString"].(func(map[string]any) string)(empty)
  212. if got != "" {
  213. t.Fatalf(`Expected empty query string, got %q`, got)
  214. }
  215. }
  216. func TestCSPExternalFont(t *testing.T) {
  217. want := []string{
  218. `default-src 'none';`,
  219. `img-src * data:;`,
  220. `media-src *;`,
  221. `frame-src *;`,
  222. `style-src 'nonce-1234';`,
  223. `script-src 'nonce-1234'`,
  224. `'strict-dynamic';`,
  225. `font-src test.com;`,
  226. `require-trusted-types-for 'script';`,
  227. `trusted-types html url;`,
  228. `manifest-src 'self';`,
  229. }
  230. got := csp(&model.User{ExternalFontHosts: "test.com"}, "1234")
  231. for _, value := range want {
  232. if !strings.Contains(got, value) {
  233. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  234. }
  235. }
  236. }
  237. func TestCSPNoUser(t *testing.T) {
  238. want := []string{
  239. `default-src 'none';`,
  240. `img-src * data:;`,
  241. `media-src *;`,
  242. `frame-src *;`,
  243. `style-src 'nonce-1234';`,
  244. `script-src 'nonce-1234'`,
  245. `'strict-dynamic';`,
  246. `require-trusted-types-for 'script';`,
  247. `trusted-types html url;`,
  248. `manifest-src 'self';`,
  249. }
  250. got := csp(nil, "1234")
  251. for _, value := range want {
  252. if !strings.Contains(got, value) {
  253. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  254. }
  255. }
  256. }
  257. func TestCSPCustomJSExternalFont(t *testing.T) {
  258. want := []string{
  259. `default-src 'none';`,
  260. `img-src * data:;`,
  261. `media-src *;`,
  262. `frame-src *;`,
  263. `style-src 'nonce-1234';`,
  264. `script-src 'nonce-1234'`,
  265. `'strict-dynamic';`,
  266. `require-trusted-types-for 'script';`,
  267. `trusted-types html url;`,
  268. `manifest-src 'self';`,
  269. }
  270. got := csp(&model.User{ExternalFontHosts: "test.com", CustomJS: "alert(1)"}, "1234")
  271. for _, value := range want {
  272. if !strings.Contains(got, value) {
  273. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  274. }
  275. }
  276. }
  277. func TestCSPExternalFontStylesheet(t *testing.T) {
  278. want := []string{
  279. `default-src 'none';`,
  280. `img-src * data:;`,
  281. `media-src *;`,
  282. `frame-src *;`,
  283. `style-src 'nonce-1234' test.com;`,
  284. `script-src 'nonce-1234'`,
  285. `'strict-dynamic';`,
  286. `require-trusted-types-for 'script';`,
  287. `trusted-types html url;`,
  288. `manifest-src 'self';`,
  289. }
  290. got := csp(&model.User{ExternalFontHosts: "test.com", Stylesheet: "a {color: red;}"}, "1234")
  291. for _, value := range want {
  292. if !strings.Contains(got, value) {
  293. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  294. }
  295. }
  296. }