1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "fmt"
10 "image/png"
11 "net/http"
12 "net/http/httptest"
13 "strings"
14 "testing"
15
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18)
19
20// The web app manifest makes the site installable: every page links it, and
21// its icons are served assets.
22func TestWebAppManifest(t *testing.T) {
23 t.Parallel()
24 srv := newBareServer(t)
25 u := srv.assets.urls
26 man, ok := srv.assets.byName[strings.TrimPrefix(u.Manifest, StaticPrefix)]
27 require.True(t, ok)
28 assert.Equal(t, "application/manifest+json", man.typ)
29
30 var m webManifest
31 require.NoError(t, json.Unmarshal(man.body, &m))
32 assert.Equal(t, "standalone", m.Display)
33 assert.Equal(t, "/", m.StartURL)
34 assert.Equal(t, "/", m.Scope)
35 // The id keeps the identity of the app when the hashed URL of the manifest
36 // changes with a release.
37 assert.Equal(t, "/", m.ID)
38 assert.NotEmpty(t, m.Description)
39 assert.NotContains(t, string(man.body), "prefer_related_applications")
40 require.Len(t, m.Icons, len(appIcons))
41 for _, ic := range m.Icons {
42 a, ok := srv.assets.byName[strings.TrimPrefix(ic.Src, StaticPrefix)]
43 require.True(t, ok, ic.Src)
44 assert.Equal(t, "image/png", a.typ)
45 }
46 _, ok = srv.assets.byName[strings.TrimPrefix(u.AppleIcon, StaticPrefix)]
47 assert.True(t, ok)
48
49 // Chrome asks for a 192 and a 512 icon with purpose any.
50 var any192, any512 bool
51 for _, ic := range m.Icons {
52 any192 = any192 || (ic.Sizes == "192x192" && ic.Purpose == "any")
53 any512 = any512 || (ic.Sizes == "512x512" && ic.Purpose == "any")
54 }
55 assert.True(t, any192 && any512)
56
57 page := renderToString(t, Shell(Layout{Assets: u}, nil))
58 assert.Contains(t, page, `<link rel="manifest" href="`+u.Manifest+`">`)
59 assert.Contains(t, page, `<link rel="apple-touch-icon" href="`+u.AppleIcon+`">`)
60 assert.Contains(t, page, `data-install`)
61}
62
63// The icons are exactly the size the manifest declares: Chrome does not
64// install from an icon that is smaller than it says.
65func TestAppIconSizes(t *testing.T) {
66 t.Parallel()
67 for _, ic := range appIcons {
68 b, err := staticFS.ReadFile("static/app/" + ic.file)
69 require.NoError(t, err)
70 cfg, err := png.DecodeConfig(bytes.NewReader(b))
71 require.NoError(t, err, ic.file)
72 assert.Equal(t, ic.sizes, fmt.Sprintf("%dx%d", cfg.Width, cfg.Height), ic.file)
73 }
74}
75
76// The service worker is served from the root scope, is revalidated on each
77// check, and answers a conditional request with 304.
78func TestServiceWorkerRoute(t *testing.T) {
79 t.Parallel()
80 srv := newBareServer(t)
81 get := func(inm string) *httptest.ResponseRecorder {
82 rec := httptest.NewRecorder()
83 r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, ServiceWorkerPath, nil)
84 if inm != "" {
85 r.Header.Set("If-None-Match", inm)
86 }
87 srv.ServeHTTP(rec, r)
88 return rec
89 }
90 rec := get("")
91 require.Equal(t, http.StatusOK, rec.Code)
92 h := rec.Header()
93 assert.Equal(t, "text/javascript; charset=utf-8", h.Get("Content-Type"))
94 assert.Equal(t, "no-cache", h.Get("Cache-Control"), "a cached worker would never update")
95 assert.Equal(t, "/", h.Get("Service-Worker-Allowed"))
96 assert.NotEmpty(t, h.Get("ETag"))
97 assert.Contains(t, rec.Body.String(), "addEventListener('fetch'")
98 assert.NotContains(t, rec.Body.String(), "caches.", "nothing authenticated is stored")
99
100 again := get(h.Get("ETag"))
101 assert.Equal(t, http.StatusNotModified, again.Code)
102
103 // The registered script is the one served.
104 assert.Contains(t, staticJS(t, "ermite.js"), "register('"+ServiceWorkerPath+"'")
105}
106
107func staticJS(t *testing.T, name string) string {
108 t.Helper()
109 b, err := staticFS.ReadFile("static/js/" + name)
110 require.NoError(t, err)
111 return string(b)
112}