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 | 26 | for n, x := range a { |
| 27 | 27 | switch x { |
| 28 | 28 | case "-c", "--config": |
| 29 | if len(a) >= 2 { | |
| 29 | if n+1 < len(a) { | |
| 30 | 30 | CFG.cfg = a[n+1] |
| 31 | 31 | } else { |
| 32 | 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 | 20 | static.StaticPath = "../static" |
| 21 | 21 | static.CopyTemplatesToMemory() |
| 22 | 22 | LoadLanguages() |
| 23 | ParseTemplates() | |
| 23 | 24 | }) |
| 24 | 25 | } |
| 25 | 26 | |
| @@ -149,3 +150,15 @@ func TestErrorPageShowsOneEscapedLine(t *testing.T) { | ||
| 149 | 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 | 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 | 33 | println(msg) |
| 32 | 34 | os.Exit(code) |
| 33 | 35 | } |
| 36 | ||
| 34 | 37 | func try(e error) { |
| 35 | 38 | if e != nil { |
| 36 | 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 | |
| 160 | // if the template cannot be parsed. | |
| 161 | func (s skunkyart) ExecuteTemplate(file, dir string, data any) { | |
| 162 | var buf strings.Builder | |
| 163 | tmp := template.New(file) | |
| 164 | // T is bound to this request's language, so templates ask for a key and | |
| 165 | // never have to know which catalogue answered. | |
| 166 | tmp = tmp.Funcs(template.FuncMap{ | |
| 167 | "T": func(key string) string { return T(s.Lang, key) }, | |
| 168 | }) | |
| 169 | tmp, err := tmp.ParseFS(static.Templates, dir+"/*") | |
| 170 | if err != nil { | |
| 162 | // pageTemplates is every page template parsed once per language, by | |
| 163 | // ParseTemplates. One set per language because T is bound at parse time, so | |
| 164 | // templates ask for a key and never have to know which catalogue answered. | |
| 165 | var pageTemplates = map[string]*template.Template{} | |
| 166 | ||
| 167 | // ParseTemplates parses static/html once for each loaded language. Call it at | |
| 168 | // startup after LoadLanguages; a template that does not parse exits the | |
| 169 | // process, since it would otherwise be a 500 on every request for that page. | |
| 170 | func ParseTemplates() { | |
| 171 | langs := Languages() | |
| 172 | if len(langs) == 0 { | |
| 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 | 196 | s.Writer.WriteHeader(500) |
| 172 | wr(s.Writer, err.Error()) | |
| 197 | wr(s.Writer, "templates not parsed") | |
| 173 | 198 | return |
| 174 | 199 | } |
| 175 | try(tmp.Execute(&buf, &data)) | |
| 200 | var buf strings.Builder | |
| 201 | try(tmp.ExecuteTemplate(&buf, file, &data)) | |
| 176 | 202 | wr(s.Writer, buf.String()) |
| 177 | 203 | } |
| 178 | 204 | |
main.go +1
| @@ -28,6 +28,7 @@ func main() { | ||
| 28 | 28 | // After the copy, not before: the catalogues are assets, and ExecuteConfig |
| 29 | 29 | // runs while static/ is still unread. |
| 30 | 30 | app.LoadLanguages() |
| 31 | app.ParseTemplates() | |
| 31 | 32 | |
| 32 | 33 | // Rate/concurrency-limit + time-out outbound DeviantArt requests so bot floods |
| 33 | 34 | // can't exhaust the process or get our egress IP banned by CloudFront/WAF. |