package server import ( "net/http" "net/http/httptest" "net/url" "regexp" "strconv" "strings" "testing" ) // getWith issues a GET carrying the given cookies (so a flash set by a prior // POST is delivered to the page that renders it). func getWith(router http.Handler, path string, cookies []*http.Cookie) *httptest.ResponseRecorder { req := httptest.NewRequest("GET", path, nil) for _, c := range cookies { req.AddCookie(c) } w := httptest.NewRecorder() router.ServeHTTP(w, req) return w } func flash(w *httptest.ResponseRecorder) (kind, message string, ok bool) { for _, c := range w.Result().Cookies() { if c.Name == "flash" && c.Value != "" { if v, err := url.QueryUnescape(c.Value); err == nil { if k, m, found := strings.Cut(v, "|"); found { return k, m, true } } } } return "", "", false } var presetIDRe = regexp.MustCompile(`/settings/presets/(\d+)"`) // newestPresetID returns the highest preset id referenced on the settings page, // which (ids autoincrement) is the most recently created preset. func newestPresetID(t *testing.T, router http.Handler) string { t.Helper() body := getWith(router, "/settings", nil).Body.String() max := int64(-1) for _, m := range presetIDRe.FindAllStringSubmatch(body, -1) { if id, err := strconv.ParseInt(m[1], 10, 64); err == nil && id > max { max = id } } if max < 0 { t.Fatal("no preset id found on settings page") } return strconv.FormatInt(max, 10) } func validPresetForm(name string) url.Values { return url.Values{ "name": {name}, "format_mode": {"default"}, } } // A created preset shows a success flash exactly once, then the message is gone. func TestPresetCreateFlashShownOnce(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() w := postForm(router, "/settings/presets", validPresetForm("Archival")) if w.Code != http.StatusSeeOther { t.Fatalf("create preset: expected 303, got %d", w.Code) } kind, msg, ok := flash(w) if !ok || kind != "success" || !strings.Contains(msg, "created") { t.Fatalf("expected success flash, got kind=%q msg=%q ok=%v", kind, msg, ok) } // The redirected page carries the flash cookie and shows the message. page := getWith(router, "/settings", w.Result().Cookies()) if !strings.Contains(page.Body.String(), msg) { t.Error("settings page did not render the flash message") } if !strings.Contains(page.Body.String(), "Archival") { t.Error("created preset not shown on settings page") } // ...and it clears the cookie, so a subsequent load is clean. if _, _, stillSet := flash(page); stillSet { t.Error("flash cookie should have been cleared after being shown") } clean := getWith(router, "/settings", page.Result().Cookies()) if strings.Contains(clean.Body.String(), msg) { t.Error("flash message should not persist across loads") } } func TestPresetUpdateAndDelete(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() if w := postForm(router, "/settings/presets", validPresetForm("Original")); w.Code != http.StatusSeeOther { t.Fatalf("create: got %d", w.Code) } id := newestPresetID(t, router) if w := postForm(router, "/settings/presets/"+id, validPresetForm("Renamed")); w.Code != http.StatusSeeOther { t.Fatalf("update: got %d", w.Code) } if body := getWith(router, "/settings", nil).Body.String(); !strings.Contains(body, "Renamed") { t.Error("updated preset name not reflected") } w := postForm(router, "/settings/presets/"+id+"/delete", nil) if w.Code != http.StatusSeeOther { t.Fatalf("delete: got %d", w.Code) } if k, _, ok := flash(w); !ok || k != "success" { t.Error("expected success flash on delete") } if body := getWith(router, "/settings", nil).Body.String(); strings.Contains(body, "Renamed") { t.Error("deleted preset still present") } } // The create and edit preset forms share one partial; both must render. func TestSettingsRendersSharedPresetForm(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() if w := postForm(router, "/settings/presets", validPresetForm("Editable")); w.Code != http.StatusSeeOther { t.Fatalf("create: got %d", w.Code) } body := getWith(router, "/settings", nil).Body.String() for _, want := range []string{"Create Preset", "Update Preset", `name="format_mode"`} { if !strings.Contains(body, want) { t.Errorf("settings page missing %q", want) } } } func TestCreateDownloadRequiresURL(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() w := postForm(router, "/download", url.Values{"url": {""}}) if w.Code != http.StatusSeeOther { t.Fatalf("expected 303 redirect, got %d", w.Code) } if loc := w.Header().Get("Location"); loc != "/download" { t.Errorf("expected redirect to /download, got %q", loc) } if k, _, ok := flash(w); !ok || k != "error" { t.Error("expected an error flash for the missing URL") } } func TestCreateDownloadQueues(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() w := postForm(router, "/download", url.Values{"url": {"https://example.com/clip"}}) if w.Code != http.StatusSeeOther { t.Fatalf("expected 303, got %d", w.Code) } if loc := w.Header().Get("Location"); loc != "/queue" { t.Errorf("expected redirect to /queue, got %q", loc) } if k, _, ok := flash(w); !ok || k != "success" { t.Error("expected a success flash") } if body := getWith(router, "/queue", nil).Body.String(); !strings.Contains(body, "example.com/clip") { t.Error("queued download not shown on the queue page") } } func TestUpdateSettingsFlash(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() w := postForm(router, "/settings", url.Values{ "refresh_interval": {"7"}, "auto_refresh_library": {"1"}, "auto_refresh_downloads": {"1"}, }) if w.Code != http.StatusSeeOther { t.Fatalf("expected 303, got %d", w.Code) } k, msg, ok := flash(w) if !ok || k != "success" || !strings.Contains(msg, "saved") { t.Errorf("expected success flash, got kind=%q msg=%q ok=%v", k, msg, ok) } // The saved interval is reflected in the form. if body := getWith(router, "/settings", nil).Body.String(); !strings.Contains(body, `value="7"`) { t.Error("saved refresh interval not reflected in settings form") } } // A subscription's edit form is rendered by the shared partial with its values // populated. func TestSubscriptionEditFormRenders(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() if w := postForm(router, "/subscriptions", url.Values{ "name": {"News"}, "url": {"https://example.com/news"}, "output_dir": {"subscriptions/news"}, "refresh_mode": {"skip"}, "schedule_kind": {"weekly"}, }); w.Code != http.StatusSeeOther { t.Fatalf("create sub: got %d", w.Code) } body := getWith(router, "/subscriptions", nil).Body.String() for _, want := range []string{ "Update Subscription", // edit form present `value="https://example.com/news"`, // URL populated in edit form `value="subscriptions/news"`, // folder populated } { if !strings.Contains(body, want) { t.Errorf("subscriptions page missing %q", want) } } } func findCookie(w *httptest.ResponseRecorder, name string) (string, bool) { for _, c := range w.Result().Cookies() { if c.Name == name { return c.Value, true } } return "", false } // An explicit ?sort= is remembered, and a later request without one restores it. // The pages auto-refresh, so a sort order that did not persist would be reset // every few seconds. func TestSortOrderPersistsInCookie(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() cases := []struct { name, path, cookie, sort, selected string }{ {"queue", "/queue", "queue_sort", "status", `