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", `