Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-29 10:03:32 +02:00
commit f8ed33df83
206 changed files with 44165 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package web
import (
"io"
"io/fs"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
)
func TestAssetHandlerServesThePackagedFiles(t *testing.T) {
cases := map[string]string{
"/admin/assets/admin.css": "text/css",
"/admin/assets/fonts.css": "text/css",
"/admin/assets/graphis.svg": "image/svg+xml",
"/admin/assets/fonts/ubuntu-normal-400-latin.woff2": "font/woff2",
}
for path, wantType := range cases {
rec := httptest.NewRecorder()
AssetHandler(rec, httptest.NewRequest(http.MethodGet, path, nil))
response := rec.Result()
if response.StatusCode != http.StatusOK {
t.Fatalf("%s: status %d", path, response.StatusCode)
}
if got := response.Header.Get("Content-Type"); !strings.HasPrefix(got, wantType) {
t.Errorf("%s: content type %q, want %q", path, got, wantType)
}
body, err := io.ReadAll(response.Body)
if err != nil || len(body) == 0 {
t.Fatalf("%s: empty body: %v", path, err)
}
}
}
func TestAssetHandlerRejectsUnknownAndTraversal(t *testing.T) {
for _, path := range []string{
"/admin/assets/",
"/admin/assets/nope.css",
"/admin/assets/../etc/passwd",
"/admin/assets/fonts/../admin.css",
} {
rec := httptest.NewRecorder()
AssetHandler(rec, httptest.NewRequest(http.MethodGet, path, nil))
if code := rec.Result().StatusCode; code != http.StatusOK && code != http.StatusNotFound {
t.Fatalf("%s: status %d", path, code)
}
// Only the packaged files are a 200; a re-served parent is a hit
// only if the tail resolves inside the set after Clean.
if path == "/admin/assets/" || path == "/admin/assets/nope.css" {
if rec.Result().StatusCode != http.StatusNotFound {
t.Errorf("%s: want 404, got %d", path, rec.Result().StatusCode)
}
}
}
}
func TestAssetHandlerRevalidatesWithTheETag(t *testing.T) {
rec := httptest.NewRecorder()
AssetHandler(rec, httptest.NewRequest(http.MethodGet, "/admin/assets/admin.css", nil))
etag := rec.Result().Header.Get("ETag")
if etag == "" {
t.Fatal("no ETag on the asset")
}
if got := rec.Result().Header.Get("Cache-Control"); !strings.Contains(got, "must-revalidate") {
t.Errorf("cache-control %q, want revalidation", got)
}
second := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/admin/assets/admin.css", nil)
request.Header.Set("If-None-Match", etag)
AssetHandler(second, request)
if code := second.Result().StatusCode; code != http.StatusNotModified {
t.Fatalf("revalidation status %d, want 304", code)
}
}
func TestEveryTemplateGlyphReferenceResolves(t *testing.T) {
sprite := assets["graphis.svg"]
if sprite == nil {
t.Fatal("the sprite is not packaged")
}
symbol := regexp.MustCompile(`<symbol id="([^"]+)"`)
defined := map[string]bool{}
for _, m := range symbol.FindAllStringSubmatch(string(sprite.body), -1) {
defined[m[1]] = true
}
ref := regexp.MustCompile(`icons\.svg#([a-z0-9-]+)"`)
// The date-picker builds its glyphs in JavaScript, so its ids are not
// literal in the template; they are named here to keep them covered.
jsRefs := []string{"chevron-left", "chevron-right", "calendar"}
tpls, err := fs.ReadDir(TemplateFS(), "templates")
if err != nil {
t.Fatalf("templates unreadable: %v", err)
}
for _, entry := range tpls {
body, err := TemplateFS().ReadFile("templates/" + entry.Name())
if err != nil {
t.Fatalf("%s: %v", entry.Name(), err)
}
for _, m := range ref.FindAllStringSubmatch(string(body), -1) {
if !defined[m[1]] {
t.Errorf("%s: references the glyph %q, which the sprite does not define", entry.Name(), m[1])
}
}
}
for _, name := range jsRefs {
if !defined[name] {
t.Errorf("the date picker references the glyph %q, which the sprite does not define", name)
}
}
}
func TestSecurityHeadersLetAssetsBeCached(t *testing.T) {
handler := SecurityHeaders(false)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "ok")
}))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/admin/assets/admin.css", nil))
if got := rec.Result().Header.Get("Cache-Control"); got != "" {
t.Errorf("asset cache-control = %q, want the handler to own it", got)
}
if strings.Contains(rec.Result().Header.Get("Content-Security-Policy"), "nonce-") {
t.Error("asset response carries a page CSP with a nonce")
}
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/admin/", nil))
if got := rec.Result().Header.Get("Cache-Control"); got != "no-store" {
t.Errorf("admin page cache-control = %q, want no-store", got)
}
if !strings.Contains(rec.Result().Header.Get("Content-Security-Policy"), "nonce-") {
t.Error("admin page CSP lost its nonce")
}
}