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