eeb601cbe2
All Go files lived flat in backend/ as one package main. Move store,
latest-chapter polling, sessions, HTTP middleware, the JSON API, the
userscript handler, and the web UI (with its templates/static assets)
into backend/internal/{store,latest,session,httpmw,api,userscript,web},
each with an exported API. main.go becomes the composition root wiring
them into newRouter; root-level tests cover the assembled router while
package-local tests cover unit behavior. Update Dockerfile/.dockerignore
for the new internal/ tree and CLAUDE.md to describe the layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
package userscript
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const testToken = "s3cret-token"
|
|
|
|
// sampleScript is a stand-in for the real userscript: a metadata block with a
|
|
// @version line, plus a body that must survive the rewrite untouched.
|
|
const sampleScript = `// ==UserScript==
|
|
// @name Manga Bookmark Sync
|
|
// @version 1.5.0
|
|
// @match https://asurascans.com/*
|
|
// ==/UserScript==
|
|
(function () { "use strict"; })();
|
|
`
|
|
|
|
// writeScript drops a userscript in a temp dir with a known mtime and returns
|
|
// its path plus the version string the handler is expected to stamp.
|
|
func writeScript(t *testing.T, body string) (path, wantVersion string) {
|
|
t.Helper()
|
|
path = filepath.Join(t.TempDir(), "manga-bookmark.user.js")
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatalf("write script: %v", err)
|
|
}
|
|
mod := time.Date(2026, 7, 28, 16, 42, 0, 0, time.UTC)
|
|
if err := os.Chtimes(path, mod, mod); err != nil {
|
|
t.Fatalf("chtimes: %v", err)
|
|
}
|
|
return path, "2026.07.28.1642"
|
|
}
|
|
|
|
// newTestMux registers Handler the same way main.go's router does, without
|
|
// pulling in the store or the rest of the app.
|
|
func newTestMux(token, path string) http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", Handler(token, path))
|
|
return mux
|
|
}
|
|
|
|
func getScript(t *testing.T, srv http.Handler, token string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/u/"+token+"/manga-bookmark.user.js", nil))
|
|
return rr
|
|
}
|
|
|
|
func TestUserscriptServedWithStampedVersion(t *testing.T) {
|
|
path, wantVersion := writeScript(t, sampleScript)
|
|
rr := getScript(t, newTestMux(testToken, path), testToken)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rr.Code)
|
|
}
|
|
if ct := rr.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") {
|
|
t.Errorf("Content-Type = %q, want text/javascript", ct)
|
|
}
|
|
if cc := rr.Header().Get("Cache-Control"); cc != "no-cache" {
|
|
t.Errorf("Cache-Control = %q, want no-cache", cc)
|
|
}
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, "// @version "+wantVersion) {
|
|
t.Errorf("body has no stamped version %q:\n%s", wantVersion, body)
|
|
}
|
|
if strings.Contains(body, "1.5.0") {
|
|
t.Errorf("body still carries the file's own version:\n%s", body)
|
|
}
|
|
// Everything outside the @version line is served verbatim.
|
|
if !strings.Contains(body, `(function () { "use strict"; })();`) {
|
|
t.Errorf("body was altered beyond the version line:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, "// @name Manga Bookmark Sync") {
|
|
t.Errorf("metadata block was altered:\n%s", body)
|
|
}
|
|
}
|
|
|
|
// The empty-token case ("/u//manga-bookmark.user.js") is covered at the
|
|
// router level (see backend's guardEmptyUserscriptToken): ServeMux 307s it to
|
|
// "/u/manga-bookmark.user.js" before this handler's own token check ever runs.
|
|
func TestUserscriptWrongTokenIs404(t *testing.T) {
|
|
path, _ := writeScript(t, sampleScript)
|
|
srv := newTestMux(testToken, path)
|
|
for _, tok := range []string{"wrong", testToken + "x", testToken[:3]} {
|
|
if got := getScript(t, srv, tok).Code; got != http.StatusNotFound {
|
|
t.Errorf("token %q: status = %d, want 404", tok, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUserscriptMissingFileIs404(t *testing.T) {
|
|
srv := newTestMux(testToken, filepath.Join(t.TempDir(), "absent.user.js"))
|
|
if got := getScript(t, srv, testToken).Code; got != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", got)
|
|
}
|
|
}
|
|
|
|
func TestUserscriptWithoutVersionLineServedUnmodified(t *testing.T) {
|
|
const noVersion = "// ==UserScript==\n// @name x\n// ==/UserScript==\nconsole.log(1);\n"
|
|
path, _ := writeScript(t, noVersion)
|
|
rr := getScript(t, newTestMux(testToken, path), testToken)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rr.Code)
|
|
}
|
|
if rr.Body.String() != noVersion {
|
|
t.Fatalf("body = %q, want it unmodified", rr.Body.String())
|
|
}
|
|
}
|