Files
zooqueenandHanzo Dev ab3fbe6436 Answer a static miss by what the path names
staticFiles had one answer for every miss and the wrong status for the one it
was configured to give: with errorPage404 set it served that page with HTTP 200,
for pages and assets alike, which is why the Sites plane declares it for no host
— 30KB of markup answering a request for a chunk, and a soft 404 on every dead
address.

It now answers the way cloud's own site plane does (apps/sites notFound): a page
miss gets the site's 404 page with status 404, JSON gets a JSON 404, and every
other file a bare 404. What decides is the media type the path names, read from
the same table that types a served file — so /models/glm-5.2 is a page, not a
file with the extension ".2", and that one predicate now gates the SPA shell too
(a dotted client route used to 404).

Registers .map and .webmanifest beside the fonts: the runtime image ships no
system mime.types, so an unregistered extension reads as "no type", and this
decision is made from the type.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-15 16:17:02 -07:00

149 lines
4.7 KiB
Go

package staticfiles
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hanzoai/ingress/pkg/config/dynamic"
)
// localRoot writes a small site to a temp dir and returns the path.
func localRoot(t *testing.T) string {
t.Helper()
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "assets"), 0o755); err != nil {
t.Fatal(err)
}
files := map[string]string{
"index.html": "<!doctype html><title>home</title>",
"assets/app.js": "export const v = 1;\n",
"404.html": "local not found",
}
for name, body := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return dir
}
func newLocal(t *testing.T, cfg dynamic.StaticFiles) http.Handler {
t.Helper()
h, err := New(context.Background(), http.NotFoundHandler(), cfg, "local")
if err != nil {
t.Fatalf("New: %v", err)
}
return h
}
// TestLocalOriginUnchanged proves the local-directory origin still serves files,
// resolves the root index, and derives content-type — unaffected by the S3 work.
func TestLocalOriginUnchanged(t *testing.T) {
srv := httptest.NewServer(newLocal(t, dynamic.StaticFiles{Root: localRoot(t)}))
defer srv.Close()
// Direct asset.
resp, err := srv.Client().Get(srv.URL + "/assets/app.js")
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK || !bytes.Contains(body, []byte("export const v")) {
t.Fatalf("local asset: status=%d body=%q", resp.StatusCode, body)
}
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "javascript") {
t.Fatalf("local asset content-type = %q", ct)
}
// Root resolves to index.html (via directory redirect, as it always has).
resp2, err := srv.Client().Get(srv.URL + "/")
if err != nil {
t.Fatal(err)
}
body2, _ := io.ReadAll(resp2.Body)
resp2.Body.Close()
if resp2.StatusCode != http.StatusOK || !bytes.Contains(body2, []byte("<title>home</title>")) {
t.Fatalf("local root: status=%d body=%q", resp2.StatusCode, body2)
}
}
// TestLocalSPAFallback proves the SPA fallback still works after serveFile was
// rerouted through the http.FileSystem (was os.Open).
func TestLocalSPAFallback(t *testing.T) {
srv := httptest.NewServer(newLocal(t, dynamic.StaticFiles{Root: localRoot(t), SPAMode: true}))
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + "/deep/client/route")
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK || !bytes.Contains(body, []byte("<title>home</title>")) {
t.Fatalf("local spa fallback: status=%d body=%q", resp.StatusCode, body)
}
}
// TestLocalErrorPage proves the custom 404 page still works after the refactor.
func TestLocalErrorPage(t *testing.T) {
srv := httptest.NewServer(newLocal(t, dynamic.StaticFiles{Root: localRoot(t), ErrorPage404: "404.html"}))
defer srv.Close()
resp, err := srv.Client().Get(srv.URL + "/nope")
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusNotFound || !bytes.Contains(body, []byte("local not found")) {
t.Fatalf("local error page: status=%d body=%q", resp.StatusCode, body)
}
}
// TestNosniffTypesAreRegistered proves every extension this plane serves gets a
// Content-Type. The pairing is what makes it matter: the handler sets
// `X-Content-Type-Options: nosniff`, so a missing type is not a browser falling
// back to a guess, it is a browser refusing to render. Go's built-in table stops
// at the web's first dozen extensions and the rest come from the system
// mime.types, which the runtime image does not ship — so this passed on a
// developer's box and failed in production until the types were registered here.
func TestNosniffTypesAreRegistered(t *testing.T) {
dir := t.TempDir()
for name, body := range map[string]string{
"readme.md": "# hello\n",
"font.woff2": "\x77OF2",
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
srv := httptest.NewServer(newLocal(t, dynamic.StaticFiles{Root: dir}))
defer srv.Close()
for path, want := range map[string]string{
"/readme.md": "text/markdown",
"/font.woff2": "font/woff2",
} {
resp, err := srv.Client().Get(srv.URL + path)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if got := resp.Header.Get("Content-Type"); !strings.HasPrefix(got, want) {
t.Errorf("%s: Content-Type %q, want %s — with nosniff the browser will not render this", path, got, want)
}
if resp.Header.Get("X-Content-Type-Options") != "nosniff" {
t.Errorf("%s: nosniff missing; this test is only meaningful beside it", path)
}
}
}