Notifications: staff turn them on per device from a new Notifications page, pick each type (bans, gags, mutes and silences added or lifted, server down and back up) and quiet hours. A watcher polls SimpleAdmin's tables, so in-game penalties are sent too. Web Push with VAPID and RFC 8291 encryption in internal/push, standard library only. Tables in sql/push.sql, created at startup; the VAPID key is made on first start and kept in saw_settings. Penalty notifications link to the public list searching for the player (#bans?q=<steamid64>). The Android app delegates notifications (versionCode 3). Caching: pages, scripts and styles are served with Cache-Control: no-cache and an ETag, so a deploy shows up at once instead of after Cloudflare's default 4-hour browser cache.
82 lines
3 KiB
Go
82 lines
3 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPWA(t *testing.T) {
|
|
h := (&Server{site: defaultSite()}).Handler()
|
|
get := func(path string) *httptest.ResponseRecorder {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("%s: status %d", path, rec.Code)
|
|
}
|
|
return rec
|
|
}
|
|
|
|
for path, start := range map[string]string{"/manifest.webmanifest": "/", "/admin.webmanifest": "/admin"} {
|
|
rec := get(path)
|
|
if ct := rec.Header().Get("Content-Type"); ct != "application/manifest+json" {
|
|
t.Errorf("%s: content type %q", path, ct)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &m); err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
if m.StartURL != start || m.ID != start || m.Scope != "/" || m.Display != "standalone" {
|
|
t.Errorf("%s: %+v", path, m)
|
|
}
|
|
// Every icon must exist, or Chrome won't offer to install.
|
|
for _, icon := range m.Icons {
|
|
if ct := get(icon.Src).Header().Get("Content-Type"); ct != icon.Type {
|
|
t.Errorf("%s: %s is %q, manifest says %q", path, icon.Src, ct, icon.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
if ct := get("/sw.js").Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") {
|
|
t.Errorf("/sw.js: content type %q", ct)
|
|
}
|
|
get("/static/offline.html")
|
|
}
|
|
|
|
func TestAssetLinks(t *testing.T) {
|
|
serve := func(cfg Config) *httptest.ResponseRecorder {
|
|
rec := httptest.NewRecorder()
|
|
(&Server{cfg: cfg, site: defaultSite()}).Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/.well-known/assetlinks.json", nil))
|
|
return rec
|
|
}
|
|
if rec := serve(Config{AndroidCerts: []string{""}}); rec.Code != http.StatusNotFound {
|
|
t.Errorf("unset: status %d", rec.Code)
|
|
}
|
|
rec := serve(Config{AndroidPackage: "sh.zio.cs2", AndroidCerts: []string{" aa:bb ", "", "CC:DD"}})
|
|
want := `[{"relation":["delegate_permission/common.handle_all_urls"],"target":{"namespace":"android_app","package_name":"sh.zio.cs2","sha256_cert_fingerprints":["AA:BB","CC:DD"]}}]`
|
|
if got := strings.TrimSpace(rec.Body.String()); rec.Code != http.StatusOK || got != want {
|
|
t.Errorf("status %d, body %s", rec.Code, got)
|
|
}
|
|
}
|
|
|
|
// TestAssetCaching checks embedded files must be revalidated, and answer 304 while unchanged.
|
|
func TestAssetCaching(t *testing.T) {
|
|
h := (&Server{site: defaultSite()}).Handler()
|
|
for _, path := range []string{"/", "/static/admin.js", "/static/styles.css", "/sw.js"} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
|
tag := rec.Header().Get("ETag")
|
|
if rec.Code != http.StatusOK || rec.Header().Get("Cache-Control") != "no-cache" || tag == "" {
|
|
t.Fatalf("%s: status %d, Cache-Control %q, ETag %q", path, rec.Code, rec.Header().Get("Cache-Control"), tag)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("If-None-Match", tag)
|
|
rec = httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotModified {
|
|
t.Errorf("%s with its ETag: status %d, want 304", path, rec.Code)
|
|
}
|
|
}
|
|
}
|