Commit b74215186e
Verified · cmc ci/build: success ci/lint: success ci/test: success
Layout: unified · split
app/cli.go +1 −1
| @@ -26,7 +26,7 @@ Copyright lost+skunk and zerolabs, X11. https://github.com/krazywarez/skunky-art | |||
| 26 | for n, x := range a { | 26 | for n, x := range a { |
| 27 | switch x { | 27 | switch x { |
| 28 | case "-c", "--config": | 28 | case "-c", "--config": |
| 29 | if len(a) >= 2 { | 29 | if n+1 < len(a) { |
| 30 | CFG.cfg = a[n+1] | 30 | CFG.cfg = a[n+1] |
| 31 | } else { | 31 | } else { |
| 32 | exit("Not enought arguments", 1) | 32 | exit("Not enought arguments", 1) |
app/cli_test.go added +48
| @@ -0,0 +1,48 @@ | |||
| 1 | package app | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "os" | ||
| 5 | "testing" | ||
| 6 | ) | ||
| 7 | |||
| 8 | // captureExit swaps the fatal exit for one that records its message. | ||
| 9 | func captureExit(t *testing.T) *[]string { | ||
| 10 | t.Helper() | ||
| 11 | orig := exit | ||
| 12 | var msgs []string | ||
| 13 | exit = func(msg string, _ int) { msgs = append(msgs, msg) } | ||
| 14 | t.Cleanup(func() { exit = orig }) | ||
| 15 | return &msgs | ||
| 16 | } | ||
| 17 | |||
| 18 | // TestConfigFlagNeedsAValue is the regression test for the bounds check: -c as | ||
| 19 | // the last argument, with other arguments before it, used to index past the | ||
| 20 | // end and panic instead of reporting the missing value. | ||
| 21 | func TestConfigFlagNeedsAValue(t *testing.T) { | ||
| 22 | args, cfg := os.Args, CFG.cfg | ||
| 23 | defer func() { os.Args, CFG.cfg = args, cfg }() | ||
| 24 | msgs := captureExit(t) | ||
| 25 | |||
| 26 | os.Args = []string{"skunkyart", "-x", "-c"} | ||
| 27 | ExecuteCommandLineArguments() | ||
| 28 | |||
| 29 | if len(*msgs) != 1 { | ||
| 30 | t.Fatalf("exit called %d times, want 1 usage error", len(*msgs)) | ||
| 31 | } | ||
| 32 | if CFG.cfg != cfg { | ||
| 33 | t.Errorf("config path changed to %q with no value given", CFG.cfg) | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 37 | func TestConfigFlagTakesTheNextArgument(t *testing.T) { | ||
| 38 | args, cfg := os.Args, CFG.cfg | ||
| 39 | defer func() { os.Args, CFG.cfg = args, cfg }() | ||
| 40 | msgs := captureExit(t) | ||
| 41 | |||
| 42 | os.Args = []string{"skunkyart", "-c", "other.json"} | ||
| 43 | ExecuteCommandLineArguments() | ||
| 44 | |||
| 45 | if len(*msgs) != 0 || CFG.cfg != "other.json" { | ||
| 46 | t.Errorf("exit calls %v, config path %q; want none and other.json", *msgs, CFG.cfg) | ||
| 47 | } | ||
| 48 | } | ||
app/escape_test.go +13
| @@ -20,6 +20,7 @@ func loadTemplates() { | |||
| 20 | static.StaticPath = "../static" | 20 | static.StaticPath = "../static" |
| 21 | static.CopyTemplatesToMemory() | 21 | static.CopyTemplatesToMemory() |
| 22 | LoadLanguages() | 22 | LoadLanguages() |
| 23 | ParseTemplates() | ||
| 23 | }) | 24 | }) |
| 24 | } | 25 | } |
| 25 | 26 | ||
| @@ -149,3 +150,15 @@ func TestErrorPageShowsOneEscapedLine(t *testing.T) { | |||
| 149 | t.Errorf("upstream error not escaped:\n%s", body) | 150 | t.Errorf("upstream error not escaped:\n%s", body) |
| 150 | } | 151 | } |
| 151 | } | 152 | } |
| 153 | |||
| 154 | // TestExecuteTemplateUsesTheRequestLanguage pins that the per-language parsed | ||
| 155 | // sets answer with the right catalogue. | ||
| 156 | func TestExecuteTemplateUsesTheRequestLanguage(t *testing.T) { | ||
| 157 | loadTemplates() | ||
| 158 | rec := httptest.NewRecorder() | ||
| 159 | s := skunkyart{Writer: rec, Host: "http://localhost", BasePath: "/", Lang: "es"} | ||
| 160 | s.ExecuteTemplate("about.htm", "html", &s) | ||
| 161 | if !strings.Contains(rec.Body.String(), "Ajustes de la instancia") { | ||
| 162 | t.Errorf("Spanish request rendered without the Spanish catalogue:\n%s", rec.Body.String()) | ||
| 163 | } | ||
| 164 | } | ||
app/util.go +41 −15
| @@ -27,10 +27,13 @@ func wr(w io.Writer, s string) { | |||
| 27 | _, _ = io.WriteString(w, s) | 27 | _, _ = io.WriteString(w, s) |
| 28 | } | 28 | } |
| 29 | 29 | ||
| 30 | func exit(msg string, code int) { | 30 | // exit is a variable so a test can observe a fatal path without ending the |
| 31 | // test binary. | ||
| 32 | var exit = func(msg string, code int) { | ||
| 31 | println(msg) | 33 | println(msg) |
| 32 | os.Exit(code) | 34 | os.Exit(code) |
| 33 | } | 35 | } |
| 36 | |||
| 34 | func try(e error) { | 37 | func try(e error) { |
| 35 | if e != nil { | 38 | if e != nil { |
| 36 | println(e.Error()) | 39 | println(e.Error()) |
| @@ -156,23 +159,46 @@ type skunkyart struct { | |||
| 156 | } | 159 | } |
| 157 | } | 160 | } |
| 158 | 161 | ||
| 159 | // ExecuteTemplate renders the named template from dir with data, responding 500 | 162 | // pageTemplates is every page template parsed once per language, by |
| 160 | // if the template cannot be parsed. | 163 | // ParseTemplates. One set per language because T is bound at parse time, so |
| 161 | func (s skunkyart) ExecuteTemplate(file, dir string, data any) { | 164 | // templates ask for a key and never have to know which catalogue answered. |
| 162 | var buf strings.Builder | 165 | var pageTemplates = map[string]*template.Template{} |
| 163 | tmp := template.New(file) | 166 | |
| 164 | // T is bound to this request's language, so templates ask for a key and | 167 | // ParseTemplates parses static/html once for each loaded language. Call it at |
| 165 | // never have to know which catalogue answered. | 168 | // startup after LoadLanguages; a template that does not parse exits the |
| 166 | tmp = tmp.Funcs(template.FuncMap{ | 169 | // process, since it would otherwise be a 500 on every request for that page. |
| 167 | "T": func(key string) string { return T(s.Lang, key) }, | 170 | func ParseTemplates() { |
| 168 | }) | 171 | langs := Languages() |
| 169 | tmp, err := tmp.ParseFS(static.Templates, dir+"/*") | 172 | if len(langs) == 0 { |
| 170 | if err != nil { | 173 | langs = []string{DefaultLang} |
| 174 | } | ||
| 175 | for _, lang := range langs { | ||
| 176 | tmp := template.New("").Funcs(template.FuncMap{ | ||
| 177 | "T": func(key string) string { return T(lang, key) }, | ||
| 178 | }) | ||
| 179 | tmp, err := tmp.ParseFS(static.Templates, "html/*") | ||
| 180 | if err != nil { | ||
| 181 | exit("templates: "+err.Error(), 1) | ||
| 182 | return | ||
| 183 | } | ||
| 184 | pageTemplates[lang] = tmp | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | // ExecuteTemplate renders the named page template with data in the request's | ||
| 189 | // language, responding 500 if the templates were never parsed. | ||
| 190 | func (s skunkyart) ExecuteTemplate(file, _ string, data any) { | ||
| 191 | tmp := pageTemplates[s.Lang] | ||
| 192 | if tmp == nil { | ||
| 193 | tmp = pageTemplates[DefaultLang] | ||
| 194 | } | ||
| 195 | if tmp == nil { | ||
| 171 | s.Writer.WriteHeader(500) | 196 | s.Writer.WriteHeader(500) |
| 172 | wr(s.Writer, err.Error()) | 197 | wr(s.Writer, "templates not parsed") |
| 173 | return | 198 | return |
| 174 | } | 199 | } |
| 175 | try(tmp.Execute(&buf, &data)) | 200 | var buf strings.Builder |
| 201 | try(tmp.ExecuteTemplate(&buf, file, &data)) | ||
| 176 | wr(s.Writer, buf.String()) | 202 | wr(s.Writer, buf.String()) |
| 177 | } | 203 | } |
| 178 | 204 | ||
main.go +1
| @@ -28,6 +28,7 @@ func main() { | |||
| 28 | // After the copy, not before: the catalogues are assets, and ExecuteConfig | 28 | // After the copy, not before: the catalogues are assets, and ExecuteConfig |
| 29 | // runs while static/ is still unread. | 29 | // runs while static/ is still unread. |
| 30 | app.LoadLanguages() | 30 | app.LoadLanguages() |
| 31 | app.ParseTemplates() | ||
| 31 | 32 | ||
| 32 | // Rate/concurrency-limit + time-out outbound DeviantArt requests so bot floods | 33 | // Rate/concurrency-limit + time-out outbound DeviantArt requests so bot floods |
| 33 | // can't exhaust the process or get our egress IP banned by CloudFront/WAF. | 34 | // can't exhaust the process or get our egress IP banned by CloudFront/WAF. |