Files
mesh-catalog/modules/mesh-mail-web/internal/web/static_test.go
T
jochen 7e83b99615 issue 1859: serve one script and one stylesheet from the page's own origin (1859.2)
ADR 0515 §6: /static/app.js and /static/app.css are built in and served with their types and nosniff; the
policy allows script and style from the origin alone and connect-src 'self'; no page carries an inline script,
a style or a handler. The script posts the page's forms without a reload and says a failure on the page.
2026-10-22 20:04:26 +02:00

78 lines
3.4 KiB
Go

package web
// Issue 1859/AC4 (novox/hq ADR 0515 §6): one script and one stylesheet, from the page's own origin; the
// content security policy allows script and style from it alone, and connect-src 'self'; no inline script
// and no style attribute on any page.
import (
"context"
"encoding/json"
"net/http"
"regexp"
"strings"
"testing"
"time"
)
var (
inlineScript = regexp.MustCompile(`(?i)<script(\s[^>]*)?>`)
styleAttr = regexp.MustCompile(`(?i)\sstyle\s*=|<style[\s>]|\son[a-z]+\s*=`)
)
// 1859/AC4.
func TestEveryPageLoadsScriptAndStyleFromItsOwnOriginAlone(t *testing.T) {
b := (&readingStandIn{list: fullList()}).bundle(time.Date(2026, 10, 22, 7, 45, 0, 0, time.UTC))
accountsRaw, _ := accountsAnswerJSON(map[string]any{"account": "work", "kind": "imap", "address": "op@work.example", "state": "current"})
b.Accounts = func(context.Context) (json.RawMessage, error) { return accountsRaw, nil }
b.AccountBucket = (&fakeAccountsBucket{}).get
b.MailLessons = seatAskJSON(t, map[string]any{"lessons": []any{}})
b.MailGroups = seatAskJSON(t, map[string]any{"groups": []any{}})
b.Search = seatAskJSON(t, map[string]any{"found": []any{}})
b.Read = seatAskJSON(t, map[string]any{"account": "work", "folder": "INBOX", "uid": 101, "subject": "s", "text": "t"})
s := NewServer(testHost, nil, b)
for _, path := range []string{"/", "/all", "/folder?account=work&folder=INBOX", "/search?q=x", "/mail?account=work&folder=INBOX&uid=101", "/write"} {
code, body, h := get(t, s, path)
csp := h.Get("Content-Security-Policy")
for _, want := range []string{"script-src 'self'", "style-src 'self'", "connect-src 'self'", "default-src 'self'"} {
if !strings.Contains(csp, want) {
t.Fatalf("%s: the policy %q does not hold %q", path, csp, want)
}
}
for _, tag := range inlineScript.FindAllString(body, -1) {
if !strings.Contains(tag, `src="/static/app.js"`) {
t.Fatalf("%s: a script that is not /static/app.js: %s", path, tag)
}
}
if m := styleAttr.FindString(body); m != "" {
t.Fatalf("%s (status %d): an inline style or handler %q:\n%s", path, code, m, body)
}
}
_, body, _ := get(t, s, "/")
if !strings.Contains(body, `<link rel="stylesheet" href="/static/app.css">`) || !strings.Contains(body, `<script src="/static/app.js" defer></script>`) {
t.Fatalf("the list page does not load the one stylesheet and the one script:\n%s", body)
}
for path, typ := range map[string]string{"/static/app.js": "text/javascript; charset=utf-8", "/static/app.css": "text/css; charset=utf-8"} {
code, body, h := get(t, s, path)
if code != http.StatusOK || h.Get("Content-Type") != typ || h.Get("X-Content-Type-Options") != "nosniff" || len(body) < 100 {
t.Fatalf("%s: status %d, type %q, nosniff %q, %d bytes; want 200, %s, nosniff, the file", path, code,
h.Get("Content-Type"), h.Get("X-Content-Type-Options"), len(body), typ)
}
}
if code, _, _ := get(t, s, "/static/other.js"); code != http.StatusNotFound {
t.Fatalf("/static/other.js: status %d, want 404 — the two files alone are served", code)
}
}
// 1859/AC4: the script talks only to its own origin: no address of another in it.
func TestTheScriptNamesNoOtherOrigin(t *testing.T) {
js, err := staticFiles.ReadFile("static/app.js")
if err != nil {
t.Fatal(err)
}
for _, word := range []string{"http://", "https://", "//cdn", "eval(", "innerHTML ="} {
if strings.Contains(string(js), word) {
t.Fatalf("app.js holds %q", word)
}
}
}