Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions jawsboot/jawsboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var assetsFS embed.FS
// included in the page head through [jaws.Jaws.GenerateHeadHTML]. The prefix may
// be absolute ("/static"), relative ("static") or empty; the returned URL path
// and the path component of the registered handler pattern are kept identical in
// all cases.
// all cases. [http.ServeMux] pattern syntax in prefix is treated as literal URL path data.
//
// Setup also registers [http.NotFoundHandler] (404) routes under prefix for the
// bundled bootstrap *.map sourcemap paths, quietly answering devtools probes for
Expand All @@ -48,13 +48,18 @@ func Setup(jw *jaws.Jaws, handleFn jaws.HandleFunc, prefix string) (urls []*url.
// so it is always a valid URL path; construct the URL directly rather
// than via the fallible url.Parse.
abspath := staticserve.EnsurePrefixSlash(path.Join(prefix, ss.Name))
urls = append(urls, &url.URL{Path: abspath})
handleFn(staticserve.NormalizeGET(abspath), ss)
u := &url.URL{Path: abspath}
urls = append(urls, u)
// Register the serialized path so ServeMux treats braces and other
// pattern syntax in the logical URL path as literal data.
handleFn(staticserve.NormalizeGET(u.String()), ss)
}
// Quietly 404 the predictable devtools source-map probes for the bundled
// assets; they are served only at their exact content-hashed paths.
handleFn(staticserve.NormalizeGET(path.Join(prefix, "bootstrap.bundle.min.js.map")), http.NotFoundHandler())
handleFn(staticserve.NormalizeGET(path.Join(prefix, "bootstrap.min.css.map")), http.NotFoundHandler())
for _, name := range []string{"bootstrap.bundle.min.js.map", "bootstrap.min.css.map"} {
u := &url.URL{Path: staticserve.EnsurePrefixSlash(path.Join(prefix, name))}
handleFn(staticserve.NormalizeGET(u.String()), http.NotFoundHandler())
}
}
return
}
112 changes: 112 additions & 0 deletions jawsboot/jawsboot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"path"
"strconv"
"strings"
Expand Down Expand Up @@ -232,6 +233,117 @@ func TestJawsBoot_SetupPrefixVariants(t *testing.T) {
}
}

func TestJawsBoot_SetupLiteralBracePrefixes(t *testing.T) {
assets := expectedStaticAssets(t, testAssetsFS, "assets/static", "")
for _, tc := range []struct {
name string
prefix string
outsidePrefix string
}{
{name: "absolute partial segment", prefix: "/static{assets}"},
{name: "relative partial segment", prefix: "static{assets}"},
{name: "complete wildcard segment", prefix: "/{assets}", outsidePrefix: "/outside"},
} {
t.Run(tc.name, func(t *testing.T) {
const fallbackStatus = http.StatusTeapot
mux := http.NewServeMux()
mux.HandleFunc("GET /", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(fallbackStatus)
})
jw, err := jaws.New()
if err != nil {
t.Fatal(err)
}
t.Cleanup(jw.Close)

var (
urls []*url.URL
setupErr error
panicValue any
)
func() {
defer func() { panicValue = recover() }()
urls, setupErr = jawsboot.Setup(jw, mux.Handle, tc.prefix)
}()
if panicValue != nil {
t.Fatalf("Setup(%q) panicked: %v", tc.prefix, panicValue)
}
if setupErr != nil {
t.Fatal(setupErr)
}
if len(urls) == 0 {
t.Fatal("Setup returned no URLs")
}
if got, want := len(urls), len(assets); got != want {
t.Errorf("Setup returned %d asset URLs, want %d", got, want)
}
returnedURLs := make(map[string]bool, len(urls))
for _, u := range urls {
returnedURLs[u.String()] = true
}

for _, exp := range assets {
assetPath := staticserve.EnsurePrefixSlash(path.Join(tc.prefix, exp.ss.Name))
assetURL := (&url.URL{Path: assetPath}).String()
if !returnedURLs[assetURL] {
t.Errorf("Setup(%q) did not return expected asset URL %q", tc.prefix, assetURL)
}
delete(returnedURLs, assetURL)
r := httptest.NewRequest(http.MethodGet, assetURL, nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, r)
if rr.Code != http.StatusOK {
t.Errorf("GET %q = %d, want 200", assetURL, rr.Code)
}
if _, pattern := mux.Handler(r); pattern != staticserve.NormalizeGET(assetURL) {
t.Errorf("GET %q matched pattern %q, want literal pattern %q",
assetURL, pattern, staticserve.NormalizeGET(assetURL))
}

if tc.outsidePrefix != "" {
outsideURI := path.Join(tc.outsidePrefix, exp.ss.Name)
outsideRequest := httptest.NewRequest(http.MethodGet, outsideURI, nil)
outsideRecorder := httptest.NewRecorder()
mux.ServeHTTP(outsideRecorder, outsideRequest)
if outsideRecorder.Code != fallbackStatus {
t.Errorf("GET %q = %d, want fallback status %d",
outsideURI, outsideRecorder.Code, fallbackStatus)
}
}
}
for unexpectedURL := range returnedURLs {
t.Errorf("Setup(%q) returned unexpected asset URL %q", tc.prefix, unexpectedURL)
}

for _, name := range []string{"bootstrap.bundle.min.js.map", "bootstrap.min.css.map"} {
mapPath := staticserve.EnsurePrefixSlash(path.Join(tc.prefix, name))
mapURL := (&url.URL{Path: mapPath}).String()
r := httptest.NewRequest(http.MethodGet, mapURL, nil)
if _, pattern := mux.Handler(r); pattern != staticserve.NormalizeGET(mapURL) {
t.Errorf("GET %q matched pattern %q, want literal 404 pattern %q",
mapURL, pattern, staticserve.NormalizeGET(mapURL))
}
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, r)
if rr.Code != http.StatusNotFound {
t.Errorf("GET %q = %d, want 404", mapURL, rr.Code)
}

if tc.outsidePrefix != "" {
outsideURI := path.Join(tc.outsidePrefix, name)
outsideRequest := httptest.NewRequest(http.MethodGet, outsideURI, nil)
outsideRecorder := httptest.NewRecorder()
mux.ServeHTTP(outsideRecorder, outsideRequest)
if outsideRecorder.Code != fallbackStatus {
t.Errorf("GET %q = %d, want fallback status %d",
outsideURI, outsideRecorder.Code, fallbackStatus)
}
}
}
})
}
}

// TestJawsBoot_SetupReturnedURLs pins jawsboot.Setup's exported (urls, err) contract
// directly, independently of jaws.Setup's wrapping: every returned URL is absolute
// and resolves to a handler registered via the supplied HandleFunc, for absolute,
Expand Down
Loading