package server import ( "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // postForm posts an application/x-www-form-urlencoded body to the router. func postForm(router http.Handler, path string, form url.Values) *httptest.ResponseRecorder { req := httptest.NewRequest("POST", path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() router.ServeHTTP(w, req) return w } func TestSubscriptionsPageRenders(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() req := httptest.NewRequest("GET", "/subscriptions", nil) w := httptest.NewRecorder() srv.Router().ServeHTTP(w, req) if w.Code != http.StatusOK { body, _ := io.ReadAll(w.Body) t.Fatalf("expected 200, got %d: %s", w.Code, body) } if !strings.Contains(w.Body.String(), "Create Subscription") { t.Error("subscriptions page missing create form") } } func TestCreateSubscriptionLifecycle(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() // Valid create redirects (303) and persists. w := postForm(router, "/subscriptions", url.Values{ "name": {"My Sub"}, "url": {"https://example.com/playlist"}, "output_dir": {"subscriptions/mine"}, "refresh_mode": {"overwrite"}, "schedule_kind": {"daily"}, }) if w.Code != http.StatusSeeOther { t.Fatalf("create: expected 303, got %d", w.Code) } // It now appears on the listing with its details. req := httptest.NewRequest("GET", "/subscriptions", nil) lw := httptest.NewRecorder() router.ServeHTTP(lw, req) body := lw.Body.String() for _, want := range []string{"My Sub", "subscriptions/mine", "Next run:"} { if !strings.Contains(body, want) { t.Errorf("listing missing %q", want) } } } // hasErrorFlash reports whether the response set an error flash cookie (the // Post/Redirect/Get feedback mechanism for a rejected form). func hasErrorFlash(w *httptest.ResponseRecorder) bool { for _, c := range w.Result().Cookies() { if c.Name == "flash" { if v, err := url.QueryUnescape(c.Value); err == nil { return strings.HasPrefix(v, "error|") } } } return false } func TestCreateSubscriptionValidation(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Router() // A rejected form redirects back (303) with an error flash rather than a raw // 400, and must not persist anything. cases := []struct { name string form url.Values }{ {"empty output_dir", url.Values{ "url": {"https://example.com/x"}, "schedule_kind": {"daily"}, "output_dir": {""}, }}, {"bad cron", url.Values{ "url": {"https://example.com/x"}, "output_dir": {"foo"}, "schedule_kind": {"cron"}, "cron_expr": {"definitely not cron"}, }}, } for _, tc := range cases { w := postForm(router, "/subscriptions", tc.form) if w.Code != http.StatusSeeOther { t.Errorf("%s: expected 303 redirect, got %d", tc.name, w.Code) } if !hasErrorFlash(w) { t.Errorf("%s: expected an error flash cookie", tc.name) } } // None of the rejected forms should have created a subscription. req := httptest.NewRequest("GET", "/subscriptions", nil) lw := httptest.NewRecorder() router.ServeHTTP(lw, req) if !strings.Contains(lw.Body.String(), "No subscriptions yet") { t.Error("rejected forms should not have persisted any subscription") } }