mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 01:38:17 +00:00
136 lines
4.8 KiB
Go
136 lines
4.8 KiB
Go
package core
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestHTMLCompression locks the response-compression policy:
|
|
// - server-rendered HTML is gzipped when the client accepts it, on every host
|
|
// - it carries Vary: Accept-Encoding, so a shared cache can't hand a gzip body
|
|
// to a client that didn't ask for one (the public root pages are cacheable)
|
|
// - a client that doesn't accept gzip still gets valid, readable HTML
|
|
// - pre-compressed static assets are NOT re-compressed — they keep the brotli
|
|
// they were built with at startup
|
|
//
|
|
// Regression for the pre-release audit finding that HTML was served uncompressed
|
|
// (the org directory shipped ~31 KB of markup per request).
|
|
func TestHTMLCompression(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, ts, h := splitServer(t)
|
|
|
|
get := func(t *testing.T, host, path, acceptEncoding string) *http.Response {
|
|
t.Helper()
|
|
req, err := http.NewRequest(http.MethodGet, ts.URL+path, nil)
|
|
if err != nil {
|
|
t.Fatalf("request: %v", err)
|
|
}
|
|
req.Host = host
|
|
// Set explicitly (and never leave it empty): Go's transport adds gzip
|
|
// automatically and transparently decompresses, which would hide the header
|
|
// we're asserting on.
|
|
req.Header.Set("Accept-Encoding", acceptEncoding)
|
|
resp, err := noRedirect().Do(req)
|
|
if err != nil {
|
|
t.Fatalf("get %s%s: %v", host, path, err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
t.Run("html is gzipped on every host", func(t *testing.T) {
|
|
for _, page := range []struct{ name, host, path string }{
|
|
{"root landing", h.root, "/"},
|
|
{"root directory", h.root, "/orgs"},
|
|
{"root docs", h.root, "/docs"},
|
|
{"auth login", h.auth, "/login"},
|
|
{"app 404", h.app, "/nonexistent-page"},
|
|
} {
|
|
resp := get(t, page.host, page.path, "gzip")
|
|
if enc := resp.Header.Get("Content-Encoding"); enc != "gzip" {
|
|
resp.Body.Close()
|
|
t.Errorf("%s Content-Encoding = %q, want gzip", page.name, enc)
|
|
continue
|
|
}
|
|
if vary := resp.Header.Values("Vary"); !hasToken(vary, "Accept-Encoding") {
|
|
t.Errorf("%s Vary = %q, want it to include Accept-Encoding", page.name, vary)
|
|
}
|
|
// The body must be real gzip that decodes to the page.
|
|
zr, err := gzip.NewReader(resp.Body)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
t.Errorf("%s: body is not valid gzip: %v", page.name, err)
|
|
continue
|
|
}
|
|
body, err := io.ReadAll(zr)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
t.Errorf("%s: gunzip: %v", page.name, err)
|
|
continue
|
|
}
|
|
if !strings.Contains(string(body), "</html>") {
|
|
t.Errorf("%s: decompressed body isn't a complete page (%d bytes)", page.name, len(body))
|
|
}
|
|
}
|
|
})
|
|
|
|
// A client that can't decompress must still get a usable page.
|
|
t.Run("identity still served uncompressed", func(t *testing.T) {
|
|
resp := get(t, h.root, "/orgs", "identity")
|
|
defer resp.Body.Close()
|
|
if enc := resp.Header.Get("Content-Encoding"); enc != "" {
|
|
t.Fatalf("Content-Encoding = %q, want empty for an identity request", enc)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("read body: %v", err)
|
|
}
|
|
if !strings.Contains(string(body), "</html>") {
|
|
t.Fatalf("identity body isn't a complete page (%d bytes)", len(body))
|
|
}
|
|
})
|
|
|
|
// Compression must actually pay for itself on a real page.
|
|
t.Run("compression meaningfully shrinks the directory page", func(t *testing.T) {
|
|
plain := get(t, h.root, "/orgs", "identity")
|
|
rawBody, err := io.ReadAll(plain.Body)
|
|
plain.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("read identity body: %v", err)
|
|
}
|
|
zipped := get(t, h.root, "/orgs", "gzip")
|
|
gzBody, err := io.ReadAll(zipped.Body)
|
|
zipped.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("read gzip body: %v", err)
|
|
}
|
|
if len(gzBody) >= len(rawBody) {
|
|
t.Fatalf("gzip body (%d) not smaller than identity (%d)", len(gzBody), len(rawBody))
|
|
}
|
|
// Markup compresses far better than this; a floor of 50% catches a silent
|
|
// regression (e.g. compression quietly disabled) without being brittle.
|
|
ratio := float64(len(gzBody)) / float64(len(rawBody))
|
|
if ratio > 0.5 {
|
|
t.Errorf("gzip only got %.0f%% of original (%d → %d bytes); expected under 50%%",
|
|
ratio*100, len(rawBody), len(gzBody))
|
|
}
|
|
t.Logf("/orgs: %d bytes → %d bytes gzipped (%.0f%%)", len(rawBody), len(gzBody), ratio*100)
|
|
})
|
|
|
|
// Static assets are pre-compressed at startup with brotli; the HTML compressor
|
|
// must not touch them (double-encoding would corrupt the response).
|
|
t.Run("pre-compressed assets are not re-compressed", func(t *testing.T) {
|
|
asset := fingerprintedAsset(t, ts, h.root, "/", "ui")
|
|
resp := get(t, h.root, asset, "br, gzip")
|
|
defer resp.Body.Close()
|
|
if enc := resp.Header.Get("Content-Encoding"); enc != "br" {
|
|
t.Fatalf("%s Content-Encoding = %q, want br (the startup-built variant)", asset, enc)
|
|
}
|
|
if strings.Contains(resp.Header.Get("Content-Encoding"), ",") {
|
|
t.Fatalf("%s was double-encoded: %q", asset, resp.Header.Get("Content-Encoding"))
|
|
}
|
|
})
|
|
}
|