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
30 changes: 17 additions & 13 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ type SetupFunc = func(jw *Jaws, handleFn HandleFunc, prefix string) (urls []*url

// makeAbsPath returns a copy of u with prefix prepended to relative paths.
//
// When a non-empty prefix is applied the result is made absolute (a leading slash is
// ensured), so the head URL matches the always-absolute handler pattern that
// [staticserve.NormalizeGET] registers for a [staticserve.StaticServe] extra. A
// relative prefix would otherwise leave the URL relative, which a browser resolves
// against the current page and so fails to load on any non-root page.
// When a non-empty prefix is applied, the joined path is slash-rooted. An empty
// prefix preserves a relative URL.
func makeAbsPath(prefix string, u *url.URL) *url.URL {
if u != nil {
copied := *u
Expand All @@ -48,6 +45,13 @@ func makeAbsPath(prefix string, u *url.URL) *url.URL {
// It calls [Jaws.GenerateHeadHTML] with the final list of URLs, with any
// relative URL paths prefixed with prefix.
//
// [staticserve.StaticServe] extras are local resources. Their generated URLs
// are slash-rooted so they match their registered handlers, including when
// prefix is empty. Other relative URL extras remain relative with an empty
// prefix. Each [staticserve.StaticServe.Name] is treated as a literal path,
// not as a pre-escaped URL; percent signs in a name are escaped as literal
// percent signs.
//
// If handleFn is nil, Setup generates head HTML from the configured resources
// without registering any handlers.
func (jw *Jaws) Setup(handleFn HandleFunc, prefix string, extras ...any) (err error) {
Expand All @@ -59,14 +63,14 @@ func (jw *Jaws) Setup(handleFn HandleFunc, prefix string, extras ...any) (err er

handleStaticServe := func(ss *staticserve.StaticServe) {
if ss != nil {
u, urlErr := url.Parse(ss.Name)
err = errors.Join(err, urlErr)
if u != nil {
u = makeAbsPath(prefix, u)
urls = append(urls, u)
if handleFn != nil {
setupHandleFn(staticserve.NormalizeGET(u.String()), ss)
}
assetPath := ss.Name
if !path.IsAbs(assetPath) {
assetPath = path.Join(prefix, assetPath)
}
u := &url.URL{Path: path.Join("/", assetPath)}
urls = append(urls, u)
if handleFn != nil {
setupHandleFn(staticserve.NormalizeGET(u.String()), ss)
}
}
}
Expand Down
132 changes: 122 additions & 10 deletions setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jaws

import (
"net/http"
"net/http/httptest"
"net/url"
"path"
"strings"
Expand Down Expand Up @@ -182,22 +183,133 @@ func TestJaws_SetupDoesNotPrefixProtocolRelativeURL(t *testing.T) {
func TestJaws_SetupEmptyPrefix(t *testing.T) {
ss := staticserve.Must("favicon.png", []byte("Hello"))

jw, _ := New()
jw, err := New()
if err != nil {
t.Fatal(err)
}
defer jw.Close()
mux := &testMux{}
_ = jw.Setup(mux.Handle, "", ss)
mux := http.NewServeMux()
if err = jw.Setup(mux.Handle, "", ss); err != nil {
t.Fatal(err)
}

if got := jw.FaviconURL(); got != ss.Name {
t.Errorf("unexpected favicon URL: %q", got)
headURL := jw.FaviconURL()
if want := "/" + ss.Name; headURL != want {
t.Fatalf("favicon URL = %q, want %q", headURL, want)
}
if len(mux.m) != 1 {
t.Fatalf("expected 1 handler, got %d", len(mux.m))
if !strings.Contains(jw.headPrefix, `href="`+headURL+`"`) {
t.Fatalf("head HTML %q does not contain favicon URL %q", jw.headPrefix, headURL)
}
for pattern := range mux.m {
if want := "GET /" + ss.Name; pattern != want {
t.Errorf("expected pattern %q, got %q", want, pattern)

pageURL, err := url.Parse("http://example.test/account/view")
if err != nil {
t.Fatal(err)
}
ref, err := url.Parse(headURL)
if err != nil {
t.Fatal(err)
}
resolved := pageURL.ResolveReference(ref)
if resolved.Path != headURL {
t.Fatalf("favicon URL %q resolves from nested page to %q", headURL, resolved.Path)
}

rr := httptest.NewRecorder()
mux.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, resolved.String(), nil))
if rr.Code != http.StatusOK {
t.Fatalf("GET %q = %d, want %d", resolved.Path, rr.Code, http.StatusOK)
}
if got := rr.Body.String(); got != "Hello" {
t.Fatalf("GET %q body = %q, want %q", resolved.Path, got, "Hello")
}
}

func TestJaws_SetupStaticServeEscapesName(t *testing.T) {
ss := staticserve.Must(`favicon:scheme {asset}#query?percent%\file.png`, []byte("Hello"))

jw, err := New()
if err != nil {
t.Fatal(err)
}
defer jw.Close()
mux := http.NewServeMux()
if err = jw.Setup(mux.Handle, "", ss); err != nil {
t.Fatal(err)
}

headURL := jw.FaviconURL()
if !strings.HasPrefix(headURL, "/favicon:scheme") {
t.Fatalf("favicon URL is not slash-rooted: %q", headURL)
}
for _, escaped := range []string{"%20", "%7Basset%7D", "%23", "%3F", "%25", "%5C"} {
if !strings.Contains(headURL, escaped) {
t.Errorf("favicon URL %q does not contain %q", headURL, escaped)
}
}

u, err := url.Parse(headURL)
if err != nil {
t.Fatal(err)
}
if u.RawQuery != "" || u.Fragment != "" {
t.Fatalf("favicon URL parsed with query %q and fragment %q", u.RawQuery, u.Fragment)
}

rr := httptest.NewRecorder()
mux.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, headURL, nil))
if rr.Code != http.StatusOK {
t.Fatalf("GET %q = %d, want %d", headURL, rr.Code, http.StatusOK)
}

wildcardURL := strings.Replace(headURL, "%7Basset%7D", "other", 1)
rr = httptest.NewRecorder()
mux.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, wildcardURL, nil))
if rr.Code != http.StatusNotFound {
t.Fatalf("GET wildcard candidate %q = %d, want %d", wildcardURL, rr.Code, http.StatusNotFound)
}
}

func TestJaws_SetupEmptyPrefixKeepsGenericRelativeURLs(t *testing.T) {
urlExtra, err := url.Parse("url.css")
if err != nil {
t.Fatal(err)
}
setupURL, err := url.Parse("setup.css")
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
extra any
want string
}{
{name: "string", extra: "string.css", want: "string.css"},
{name: "URL", extra: urlExtra, want: "url.css"},
{name: "SetupFunc", extra: SetupFunc(func(_ *Jaws, _ HandleFunc, _ string) (urls []*url.URL, err error) {
urls = append(urls, setupURL)
return
}), want: "setup.css"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
jw, err := New()
if err != nil {
t.Fatal(err)
}
defer jw.Close()
if err = jw.Setup(nil, "", tc.extra); err != nil {
t.Fatal(err)
}

if !strings.Contains(jw.headPrefix, `href="`+tc.want+`"`) {
t.Fatalf("head HTML %q does not contain relative URL %q", jw.headPrefix, tc.want)
}
if strings.Contains(jw.headPrefix, `href="/`+tc.want+`"`) {
t.Fatalf("head HTML %q slash-rooted relative URL %q", jw.headPrefix, tc.want)
}
})
}
}

func TestJaws_SetupKeepsMethodPattern(t *testing.T) {
Expand Down
Loading