Files
spa-server/spa/handler_test.go
T

138 lines
4.2 KiB
Go
Raw Permalink Normal View History

package spa
import (
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"code.nochebuena.dev/einherjar/contracts/logging"
"code.nochebuena.dev/einherjar/core/logz"
)
func discardLogger() logging.Logger { return logz.New(logz.Config{Writer: io.Discard}) }
// newTestHandler builds a handler over a temp dir seeded with one file of each
// caching class plus index.html.
func newTestHandler(t *testing.T) http.Handler {
t.Helper()
dir := t.TempDir()
for name, body := range map[string]string{
"index.html": "<!doctype html><title>app</title>",
"main.4f8a2b1c.js": "console.log(1)",
"favicon.ico": "icon",
"ngsw.json": "{}",
"manifest.webmanifest": `{"name":"app"}`,
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return NewHandler(discardLogger(), dir)
}
func get(h http.Handler, target, accept string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, target, nil)
if accept != "" {
req.Header.Set("Accept", accept)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestCacheControl(t *testing.T) {
h := newTestHandler(t)
cases := []struct {
target, want string
}{
{"/main.4f8a2b1c.js", "public, max-age=31536000, immutable"}, // content-hashed
{"/favicon.ico", "public, max-age=3600"}, // verbatim, not hashed
{"/ngsw.json", "no-cache"}, // service-worker manifest
}
for _, c := range cases {
rec := get(h, c.target, "*/*")
if rec.Code != http.StatusOK {
t.Errorf("%s: status = %d, want 200", c.target, rec.Code)
continue
}
if got := rec.Header().Get("Cache-Control"); got != c.want {
t.Errorf("%s: Cache-Control = %q, want %q", c.target, got, c.want)
}
}
}
func TestIndexIsNoCache(t *testing.T) {
// The SPA shell (served for a navigation route) must revalidate every load.
rec := get(newTestHandler(t), "/dashboard", "text/html")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if got := rec.Header().Get("Cache-Control"); got != "no-cache" {
t.Errorf("index Cache-Control = %q, want no-cache", got)
}
}
func TestFallback_RouteServesIndex(t *testing.T) {
rec := get(newTestHandler(t), "/deep/route", "text/html,application/xhtml+xml")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 (index.html for a route)", rec.Code)
}
if body := rec.Body.String(); body == "" || body[0] != '<' {
t.Errorf("expected index.html body, got %q", body)
}
}
func TestFallback_MissingAssetIs404(t *testing.T) {
// A missing file with an extension must not be masked as HTML.
rec := get(newTestHandler(t), "/chunk.9f9f9f9f.js", "*/*")
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404 for a missing asset", rec.Code)
}
}
func TestFallback_NonHTMLAcceptIs404(t *testing.T) {
// A typed non-HTML client asking for a missing route gets 404, not the shell.
rec := get(newTestHandler(t), "/api/thing", "application/json")
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404 for a non-HTML client", rec.Code)
}
}
func TestFallback_RootServesIndex(t *testing.T) {
rec := get(newTestHandler(t), "/", "text/html")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 for /", rec.Code)
}
}
func TestManifestContentType(t *testing.T) {
// Without the mime registration this is sniffed as text/plain; with it the
// PWA manifest carries its registered type.
rec := get(newTestHandler(t), "/manifest.webmanifest", "*/*")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/manifest+json") {
t.Errorf("Content-Type = %q, want application/manifest+json", ct)
}
}
func TestIsHashed(t *testing.T) {
hashed := []string{"main.4f8a2b1c.js", "index-DkJf3x9a.js", "styles-4NDEUD5S.css", "app.a1b2c3d4.mjs"}
plain := []string{"favicon.ico", "index.html", "logo.png", "about.js", "main.js", "vendor.css"}
for _, n := range hashed {
if !isHashed(n) {
t.Errorf("isHashed(%q) = false, want true", n)
}
}
for _, n := range plain {
if isHashed(n) {
t.Errorf("isHashed(%q) = true, want false", n)
}
}
}