Files
zip/static_internal_test.go
a0825689f3 zip: Static file handler + composable Middleware (With/Chain) — additive (#6)
Two additive capabilities on the zip surface, neither touching the existing
Use(Handler...) c.Next model.

Static(fs.FS, ...StaticOption) — leaf file handler for a wildcard route.
Serves the "*" subpath from any fs.FS (embed.FS or os.DirFS); path.Clean +
fs.ValidPath reject traversal fail-closed (404); a missing file calls c.Next()
so a later route or SPA catch-all wins, never a 500. Sets
Content-Type/Length/Last-Modified, honours HEAD and If-Modified-Since (304).
WithIndex / WithStripPrefix options. fasthttp owns the streamed file's close.

Middleware = func(Handler) Handler, Chain(mw...), and App.With(mw...) — chi's
per-route wrapping idiom. With wraps the leaf at registration (not via c.Next),
Chain composes left-to-right. Coexists with Use: Use = global/prefix stack
(declaration order), With/Chain = per-route explicit wrapping.

Tests: serves file (content/type/len) over embed.FS AND os.DirFS, HEAD,
traversal blocked, missing->Next->catch-all, specificity beats the wildcard,
index, strip-prefix, 304; Chain/With execution order + the cloud
rateLimit+CSRF order and short-circuit proof. Suite green except the two
pre-existing macOS-host failures (TestListen_DualTransport,
TestMCP_FreeToolSurface).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-11 04:46:05 -07:00

36 lines
1.3 KiB
Go

package zip
import "testing"
// staticClean is the fail-closed traversal guard, tested directly so the
// security property holds regardless of what URL normalization the transport
// does upstream: any ".." escape or non-fs.ValidPath input is rejected, while
// in-root navigation ("a/../b") collapses and is allowed.
func TestStaticClean_FailClosed(t *testing.T) {
cases := []struct {
sub, index string
wantName string
wantOK bool
}{
{"main.css", "", "main.css", true},
{"a/b/c.js", "", "a/b/c.js", true},
{"a/../b.js", "", "b.js", true}, // in-root nav collapses — allowed
{"./x.css", "", "x.css", true}, // leading . collapses
{"", "", "", true}, // root, no index → nothing to serve
{"", "index.html", "index.html", true}, // root → index
{"/", "", "", true}, // just a slash → root
{"..", "", "", false}, // bare escape
{"../secret", "", "", false}, // escape one level
{"../../etc/passwd", "", "", false},
{"a/../../secret", "", "", false}, // escapes after collapse
{"sub/../../secret", "", "", false},
}
for _, c := range cases {
name, ok := staticClean(c.sub, c.index)
if ok != c.wantOK || name != c.wantName {
t.Errorf("staticClean(%q, %q) = (%q, %v), want (%q, %v)",
c.sub, c.index, name, ok, c.wantName, c.wantOK)
}
}
}