package httpapi import ( "encoding/json" "io" "io/fs" "log/slog" "os" "path/filepath" "strings" "testing" "testing/fstest" "time" "github.com/kristianwind/verdande/internal/config" "github.com/kristianwind/verdande/internal/store" ) // serverWithWeb builds the real router around a frontend, which newTestServerWith // cannot do — it passes nil for the web filesystem, because every other test here // is about the API. The renaming only happens when there is something to rename. func serverWithWeb(t *testing.T, edition string, web fs.FS) *Server { t.Helper() db, err := store.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { db.Close() }) cfg := &config.Config{ BaseURL: "http://localhost", DataDir: t.TempDir(), SessionTTL: 24 * time.Hour, Edition: edition, } return New(cfg, db, slog.New(slog.NewTextHandler(io.Discard, nil)), web) } // The shell as the frontend build produces it, cut down to the parts that matter // here: the two anchors that get renamed, and the inline theme script that must // not be touched. TestAppHTMLStillCarriesTheAnchors below is what keeps this // honest against the real file. const builtShell = ` verdande ` const builtManifest = `{ "name": "verdande", "short_name": "verdande", "description": "Opgaver og projekter, delt.", "start_url": "/", "display": "standalone", "background_color": "#111312", "theme_color": "#111312", "lang": "da", "icons": [ { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ` func builtWeb() fstest.MapFS { return fstest.MapFS{ "index.html": {Data: []byte(builtShell)}, "manifest.webmanifest": {Data: []byte(builtManifest)}, "_app/immutable/app.js": {Data: []byte("// app")}, } } // TestRenamingTheShellLeavesTheStorageKeysAlone is the one this file exists for. // // The product name and the localStorage prefix are the same word in app.html, and // they are not the same thing: the prefix is read back by the app itself, so // renaming it here and nowhere else sets the theme from one key and reads it from // another. The symptom is the white flash on load that the inline script exists to // prevent — which is to say, no error, nothing in a log, and nothing a test that // merely checked the title would see. func TestRenamingTheShellLeavesTheStorageKeysAlone(t *testing.T) { shell, manifest, err := rebrandedAssets(builtWeb(), "urd", "urd — noter, delt.", "Noter, delt.") if err != nil { t.Fatalf("rebrandedAssets: %v", err) } got := string(shell) for _, want := range []string{ "urd", `content="urd — noter, delt."`, } { if !strings.Contains(got, want) { t.Errorf("the shell does not contain %q", want) } } // Named values rather than "no occurrences of verdande": the three keys are // what has to survive, so the assertion says the three keys. for _, want := range []string{ `localStorage.getItem('verdande:theme')`, `localStorage.getItem('verdande:look')`, `look !== 'verdande'`, } { if !strings.Contains(got, want) { t.Errorf("renaming the shell changed a storage key: %q is gone", want) } } if strings.Contains(got, "urd:theme") || strings.Contains(got, "urd:look") { t.Error("renaming the shell rewrote a localStorage prefix; the app reads the verdande: ones") } gotManifest := string(manifest) for _, want := range []string{`"name": "urd"`, `"short_name": "urd"`, `"description": "Noter, delt."`} { if !strings.Contains(gotManifest, want) { t.Errorf("the manifest does not contain %q", want) } } } // TestRenamingTheShellKeepsTheCSPHashValid pins the relationship between the two // files that is easy to break and impossible to notice: the policy names a hash of // the inline script, and the renamed shell is what gets served. If a replacement // ever reached inside the