functions_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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 TestTruncateWithShortTexts(t *testing.T) {
  40. scenarios := []string{"Short text", "Короткий текст"}
  41. for _, input := range scenarios {
  42. result := truncate(input, 25)
  43. if result != input {
  44. t.Fatalf(`Unexpected output, got %q instead of %q`, result, input)
  45. }
  46. result = truncate(input, len(input))
  47. if result != input {
  48. t.Fatalf(`Unexpected output, got %q instead of %q`, result, input)
  49. }
  50. }
  51. }
  52. func TestTruncateWithLongTexts(t *testing.T) {
  53. scenarios := map[string]string{
  54. "This is a really pretty long English text": "This is a really pretty l…",
  55. "Это реально очень длинный русский текст": "Это реально очень длинный…",
  56. }
  57. for input, expected := range scenarios {
  58. result := truncate(input, 25)
  59. if result != expected {
  60. t.Fatalf(`Unexpected output, got %q instead of %q`, result, expected)
  61. }
  62. }
  63. }
  64. func TestIsEmail(t *testing.T) {
  65. if !isEmail("user@domain.tld") {
  66. t.Fatal(`This email is valid and should returns true`)
  67. }
  68. if isEmail("invalid") {
  69. t.Fatal(`This email is not valid and should returns false`)
  70. }
  71. }
  72. func TestDuration(t *testing.T) {
  73. now := time.Now()
  74. var dt = []struct {
  75. in time.Time
  76. out string
  77. }{
  78. {time.Time{}, ""},
  79. {now.Add(time.Hour), "1h0m0s"},
  80. {now.Add(time.Minute), "1m0s"},
  81. {now.Add(time.Minute * 40), "40m0s"},
  82. {now.Add(time.Millisecond * 40), "0s"},
  83. {now.Add(time.Millisecond * 80), "0s"},
  84. {now.Add(time.Millisecond * 400), "0s"},
  85. {now.Add(time.Millisecond * 800), "1s"},
  86. {now.Add(time.Millisecond * 4321), "4s"},
  87. {now.Add(time.Millisecond * 8765), "9s"},
  88. {now.Add(time.Microsecond * 12345678), "12s"},
  89. {now.Add(time.Microsecond * 87654321), "1m28s"},
  90. }
  91. for i, tt := range dt {
  92. if out := durationImpl(tt.in, now); out != tt.out {
  93. t.Errorf(`%d. content mismatch for "%v": expected=%q got=%q`, i, tt.in, tt.out, out)
  94. }
  95. }
  96. }
  97. func TestElapsedTime(t *testing.T) {
  98. printer := locale.NewPrinter("en_US")
  99. var dt = []struct {
  100. in time.Time
  101. out string
  102. }{
  103. {time.Time{}, printer.Print("time_elapsed.not_yet")},
  104. {time.Now().Add(time.Hour), printer.Print("time_elapsed.not_yet")},
  105. {time.Now(), printer.Print("time_elapsed.now")},
  106. {time.Now().Add(-time.Minute), printer.Plural("time_elapsed.minutes", 1, 1)},
  107. {time.Now().Add(-time.Minute * 40), printer.Plural("time_elapsed.minutes", 40, 40)},
  108. {time.Now().Add(-time.Hour), printer.Plural("time_elapsed.hours", 1, 1)},
  109. {time.Now().Add(-time.Hour * 3), printer.Plural("time_elapsed.hours", 3, 3)},
  110. {time.Now().Add(-time.Hour * 32), printer.Print("time_elapsed.yesterday")},
  111. {time.Now().Add(-time.Hour * 24 * 3), printer.Plural("time_elapsed.days", 3, 3)},
  112. {time.Now().Add(-time.Hour * 24 * 14), printer.Plural("time_elapsed.days", 14, 14)},
  113. {time.Now().Add(-time.Hour * 24 * 15), printer.Plural("time_elapsed.days", 15, 15)},
  114. {time.Now().Add(-time.Hour * 24 * 21), printer.Plural("time_elapsed.weeks", 3, 3)},
  115. {time.Now().Add(-time.Hour * 24 * 32), printer.Plural("time_elapsed.months", 1, 1)},
  116. {time.Now().Add(-time.Hour * 24 * 60), printer.Plural("time_elapsed.months", 2, 2)},
  117. {time.Now().Add(-time.Hour * 24 * 366), printer.Plural("time_elapsed.years", 1, 1)},
  118. {time.Now().Add(-time.Hour * 24 * 365 * 3), printer.Plural("time_elapsed.years", 3, 3)},
  119. }
  120. for i, tt := range dt {
  121. if out := elapsedTime(printer, "Local", tt.in); out != tt.out {
  122. t.Errorf(`%d. content mismatch for "%v": expected=%q got=%q`, i, tt.in, tt.out, out)
  123. }
  124. }
  125. }
  126. func TestFormatFileSize(t *testing.T) {
  127. scenarios := []struct {
  128. input int64
  129. expected string
  130. }{
  131. {0, "0 B"},
  132. {1, "1 B"},
  133. {500, "500 B"},
  134. {1024, "1.0 KiB"},
  135. {43520, "42.5 KiB"},
  136. {5000 * 1024 * 1024, "4.9 GiB"},
  137. }
  138. for _, scenario := range scenarios {
  139. result := formatFileSize(scenario.input)
  140. if result != scenario.expected {
  141. t.Errorf(`Unexpected result, got %q instead of %q for %d`, result, scenario.expected, scenario.input)
  142. }
  143. }
  144. }
  145. func TestQueryString(t *testing.T) {
  146. params, err := dict("q", "ai", "unread", true, "offset", 20)
  147. if err != nil {
  148. t.Fatalf(`The dict should be valid: %v`, err)
  149. }
  150. got := (&funcMap{}).Map()["queryString"].(func(map[string]any) string)(params)
  151. if got == "" {
  152. t.Fatalf("Expected a query string, got an empty string")
  153. }
  154. if !strings.HasPrefix(got, "?") {
  155. t.Fatalf(`Expected query string to start with "?", got %q`, got)
  156. }
  157. if !strings.Contains(got, "q=ai") {
  158. t.Fatalf(`Expected query string to contain q=ai, got %q`, got)
  159. }
  160. if !strings.Contains(got, "unread=1") {
  161. t.Fatalf(`Expected query string to contain unread=1, got %q`, got)
  162. }
  163. if !strings.Contains(got, "offset=20") {
  164. t.Fatalf(`Expected query string to contain offset=20, got %q`, got)
  165. }
  166. empty, err := dict("q", "", "unread", false, "offset", 0)
  167. if err != nil {
  168. t.Fatalf(`The dict should be valid: %v`, err)
  169. }
  170. got = (&funcMap{}).Map()["queryString"].(func(map[string]any) string)(empty)
  171. if got != "" {
  172. t.Fatalf(`Expected empty query string, got %q`, got)
  173. }
  174. }
  175. func TestCSPExternalFont(t *testing.T) {
  176. want := []string{
  177. `default-src 'none';`,
  178. `img-src * data:;`,
  179. `media-src *;`,
  180. `frame-src *;`,
  181. `style-src 'nonce-1234';`,
  182. `script-src 'nonce-1234'`,
  183. `'strict-dynamic';`,
  184. `font-src test.com;`,
  185. `require-trusted-types-for 'script';`,
  186. `trusted-types html url;`,
  187. `manifest-src 'self';`,
  188. }
  189. got := csp(&model.User{ExternalFontHosts: "test.com"}, "1234")
  190. for _, value := range want {
  191. if !strings.Contains(got, value) {
  192. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  193. }
  194. }
  195. }
  196. func TestCSPNoUser(t *testing.T) {
  197. want := []string{
  198. `default-src 'none';`,
  199. `img-src * data:;`,
  200. `media-src *;`,
  201. `frame-src *;`,
  202. `style-src 'nonce-1234';`,
  203. `script-src 'nonce-1234'`,
  204. `'strict-dynamic';`,
  205. `require-trusted-types-for 'script';`,
  206. `trusted-types html url;`,
  207. `manifest-src 'self';`,
  208. }
  209. got := csp(nil, "1234")
  210. for _, value := range want {
  211. if !strings.Contains(got, value) {
  212. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  213. }
  214. }
  215. }
  216. func TestCSPCustomJSExternalFont(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. `require-trusted-types-for 'script';`,
  226. `trusted-types html url;`,
  227. `manifest-src 'self';`,
  228. }
  229. got := csp(&model.User{ExternalFontHosts: "test.com", CustomJS: "alert(1)"}, "1234")
  230. for _, value := range want {
  231. if !strings.Contains(got, value) {
  232. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  233. }
  234. }
  235. }
  236. func TestCSPExternalFontStylesheet(t *testing.T) {
  237. want := []string{
  238. `default-src 'none';`,
  239. `img-src * data:;`,
  240. `media-src *;`,
  241. `frame-src *;`,
  242. `style-src 'nonce-1234' test.com;`,
  243. `script-src 'nonce-1234'`,
  244. `'strict-dynamic';`,
  245. `require-trusted-types-for 'script';`,
  246. `trusted-types html url;`,
  247. `manifest-src 'self';`,
  248. }
  249. got := csp(&model.User{ExternalFontHosts: "test.com", Stylesheet: "a {color: red;}"}, "1234")
  250. for _, value := range want {
  251. if !strings.Contains(got, value) {
  252. t.Errorf(`Unexpected result, didn't find %q in %q`, value, got)
  253. }
  254. }
  255. }