| 1 | package pwa |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func exampleManifest() Manifest { |
| 12 | return Manifest{ID: "/app/", Name: "Example", StartURL: "/app/inbox", Scope: "/app/", |
| 13 | Icons: []Icon{{Src: "/icon.png", Sizes: "192x192", Type: "image/png"}}} |
| 14 | } |
| 15 | |
| 16 | func TestManifestIdentityAndHeaders(t *testing.T) { |
| 17 | m := exampleManifest() |
| 18 | m.Name = `Example "quoted" <app>` |
| 19 | h, err := m.Handler() |
| 20 | if err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | w := httptest.NewRecorder() |
| 24 | h.ServeHTTP(w, httptest.NewRequest("GET", "/manifest.webmanifest", nil)) |
| 25 | var got Manifest |
| 26 | if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if got.ID != m.ID || got.Name != m.Name || got.Display != "standalone" { |
| 30 | t.Fatalf("identity changed: %+v", got) |
| 31 | } |
| 32 | if w.Header().Get("Content-Type") != "application/manifest+json" || w.Header().Get("Cache-Control") != "no-cache" { |
| 33 | t.Fatal(w.Header()) |
| 34 | } |
| 35 | for _, method := range []string{"HEAD", "POST"} { |
| 36 | w := httptest.NewRecorder() |
| 37 | h.ServeHTTP(w, httptest.NewRequest(method, "/manifest.webmanifest", nil)) |
| 38 | if method == "HEAD" && w.Body.Len() != 0 { |
| 39 | t.Fatal("HEAD returned a body") |
| 40 | } |
| 41 | if method == "POST" && w.Code != http.StatusMethodNotAllowed { |
| 42 | t.Fatal("manifest accepted a mutation") |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func TestManifestRejectsScopeEscapes(t *testing.T) { |
| 48 | for _, start := range []string{"/other/", "//evil.example/", "/app/../outside", "/app/%2e%2e/outside", "/app/%5c../outside", "/app/./inbox", "/app/\n/", "https://example.com/app/"} { |
| 49 | t.Run(start, func(t *testing.T) { |
| 50 | m := exampleManifest() |
| 51 | m.StartURL = start |
| 52 | if _, err := m.Handler(); err == nil { |
| 53 | t.Fatalf("accepted ambiguous or out-of-scope start URL %q", start) |
| 54 | } |
| 55 | }) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestAssetsAreScriptsAndNeverDirectoryListings(t *testing.T) { |
| 60 | h := http.StripPrefix("/pwa", Assets()) |
| 61 | for _, path := range []string{"/pwa/client.mjs", "/pwa/worker.js"} { |
| 62 | w := httptest.NewRecorder() |
| 63 | h.ServeHTTP(w, httptest.NewRequest("GET", path, nil)) |
| 64 | if w.Code != 200 || !strings.HasPrefix(w.Header().Get("Content-Type"), "text/javascript") || w.Body.Len() == 0 { |
| 65 | t.Fatalf("%s: %d %v", path, w.Code, w.Header()) |
| 66 | } |
| 67 | } |
| 68 | w := httptest.NewRecorder() |
| 69 | h.ServeHTTP(w, httptest.NewRequest("GET", "/pwa/", nil)) |
| 70 | if w.Code != 404 { |
| 71 | t.Fatal("assets exposed a directory listing") |
| 72 | } |
| 73 | } |
| 74 | |