favoritter/internal/handler/web_test.go
Ole-Morten Duesund b186fb4bc5 feat: add edit/delete buttons to list views and inline privacy toggle
Fave cards in the list and profile views now show edit, delete, and
privacy toggle buttons directly — no need to open the detail page first.

- New POST /faves/{id}/privacy route with HTMX privacy toggle partial
- New UpdatePrivacy store method for single-column update
- fave_list.html: edit link, HTMX delete, privacy toggle on every card
- profile.html: edit/delete for owner's own cards
- privacy_toggle.html: new HTMX partial that swaps inline on toggle
- CSS: compact .fave-card-actions styles

The existing handleFaveDelete already returns empty 200 for HTMX
requests, so hx-target="closest article" hx-swap="outerHTML" removes
the card from DOM seamlessly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:17:46 +02:00

1493 lines
42 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-or-later
package handler
import (
"bytes"
"encoding/csv"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// csrfToken performs a GET to extract a CSRF token cookie from the response.
func csrfToken(t *testing.T, mux *http.ServeMux, path string) string {
t.Helper()
req := httptest.NewRequest("GET", path, nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
token := extractCookie(rr, "csrf_token")
if token == "" {
t.Fatal("no csrf_token cookie from GET " + path)
}
return token
}
// postForm creates a POST request with form data and CSRF token.
func postForm(path string, csrf string, values url.Values, cookies ...*http.Cookie) *http.Request {
values.Set("csrf_token", csrf)
req := httptest.NewRequest("POST", path, strings.NewReader(values.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
for _, c := range cookies {
req.AddCookie(c)
}
return req
}
// --- Auth flows ---
func TestSignupOpenMode(t *testing.T) {
h, mux := testServer(t)
// Set signup mode to open.
h.deps.Settings.Update("Test", "", "open")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"newuser"},
"password": {"password123"},
"password_confirm": {"password123"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Errorf("signup: got %d, want 303\nbody: %s", rr.Code, rr.Body.String())
}
// Should have a session cookie (auto-login after signup).
if extractCookie(rr, "session") == "" {
t.Error("expected session cookie after signup")
}
}
func TestSignupDuplicate(t *testing.T) {
h, mux := testServer(t)
h.deps.Settings.Update("Test", "", "open")
h.deps.Users.Create("existing", "password123", "user")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"existing"},
"password": {"password123"},
"password_confirm": {"password123"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("duplicate signup: got %d, want 200 (re-render)", rr.Code)
}
if !strings.Contains(rr.Body.String(), "allerede i bruk") {
t.Error("should show duplicate username error")
}
}
func TestSignupClosedMode(t *testing.T) {
h, mux := testServer(t)
h.deps.Settings.Update("Test", "", "closed")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"newuser"},
"password": {"password123"},
"password_confirm": {"password123"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
body := rr.Body.String()
if !strings.Contains(body, "stengt") {
t.Error("closed signup should show 'stengt' message")
}
}
func TestSignupRequestMode(t *testing.T) {
h, mux := testServer(t)
h.deps.Settings.Update("Test", "", "requests")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"requester"},
"password": {"password123"},
"password_confirm": {"password123"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
body := rr.Body.String()
if !strings.Contains(body, "sendt") {
t.Error("request mode signup should show 'sendt' message")
}
// Verify a signup request was created.
requests, _ := h.deps.SignupRequests.ListPending()
if len(requests) != 1 {
t.Errorf("expected 1 pending request, got %d", len(requests))
}
}
func TestSignupInvalidUsername(t *testing.T) {
h, mux := testServer(t)
h.deps.Settings.Update("Test", "", "open")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"a"}, // Too short (min 2).
"password": {"password123"},
"password_confirm": {"password123"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Ugyldig brukernavn") {
t.Error("should show username validation error")
}
}
func TestSignupPasswordTooShort(t *testing.T) {
h, mux := testServer(t)
h.deps.Settings.Update("Test", "", "open")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"newuser"},
"password": {"short"},
"password_confirm": {"short"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "minst 8 tegn") {
t.Error("should show password length error")
}
}
func TestSignupPasswordMismatch(t *testing.T) {
h, mux := testServer(t)
h.deps.Settings.Update("Test", "", "open")
csrf := csrfToken(t, mux, "/signup")
form := url.Values{
"username": {"newuser"},
"password": {"password123"},
"password_confirm": {"different456"},
}
req := postForm("/signup", csrf, form)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "ikke like") {
t.Error("should show password mismatch error")
}
}
func TestLogout(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
csrf := csrfToken(t, mux, "/login")
req := postForm("/logout", csrf, url.Values{}, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Errorf("logout: got %d, want 303", rr.Code)
}
loc := rr.Header().Get("Location")
if !strings.Contains(loc, "/login") {
t.Errorf("logout redirect = %q, want /login", loc)
}
}
func TestPasswordResetFlow(t *testing.T) {
h, mux := testServer(t)
// Create user with must_reset flag.
user, _ := h.deps.Users.CreateWithReset("resetuser", "temppass", "user")
token, _ := h.deps.Sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
// Accessing /reset-password should render the form.
req := httptest.NewRequest("GET", "/reset-password", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("reset page: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Endre passord") {
t.Error("should render password reset form")
}
}
func TestPasswordResetSubmit(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.CreateWithReset("resetuser", "temppass", "user")
token, _ := h.deps.Sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
// Get CSRF token.
getReq := httptest.NewRequest("GET", "/reset-password", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{
"password": {"newpassword123"},
"password_confirm": {"newpassword123"},
}
req := postForm("/reset-password", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Errorf("reset submit: got %d, want 303\nbody: %s", rr.Code, rr.Body.String())
}
// Verify new password works.
_, err := h.deps.Users.Authenticate("resetuser", "newpassword123")
if err != nil {
t.Errorf("new password should work: %v", err)
}
}
// --- Fave CRUD (web) ---
func TestCreateFavePageRendering(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
req := httptest.NewRequest("GET", "/faves/new", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("new fave page: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Ny favoritt") {
t.Error("should render new fave form")
}
}
func TestCreateFaveSubmit(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
// Get CSRF token.
getReq := httptest.NewRequest("GET", "/faves/new", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
// POST multipart form (fave creation uses ParseMultipartForm).
var body bytes.Buffer
writer := multipart.NewWriter(&body)
writer.WriteField("csrf_token", csrf)
writer.WriteField("description", "Min nye favoritt")
writer.WriteField("url", "https://example.com")
writer.WriteField("privacy", "public")
writer.WriteField("tags", "test, go")
writer.Close()
req := httptest.NewRequest("POST", "/faves", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.AddCookie(cookie)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Errorf("create fave: got %d, want 303\nbody: %s", rr.Code, rr.Body.String())
}
// Verify redirect points to the new fave.
loc := rr.Header().Get("Location")
if !strings.Contains(loc, "/faves/") {
t.Errorf("redirect = %q, should point to /faves/{id}", loc)
}
}
func TestCreateFaveMissingDescription(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
getReq := httptest.NewRequest("GET", "/faves/new", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
var body bytes.Buffer
writer := multipart.NewWriter(&body)
writer.WriteField("csrf_token", csrf)
writer.WriteField("description", "")
writer.Close()
req := httptest.NewRequest("POST", "/faves", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.AddCookie(cookie)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Beskrivelse er påkrevd") {
t.Error("should show description required error")
}
}
func TestEditFaveNotOwner(t *testing.T) {
h, mux := testServer(t)
userA, _ := h.deps.Users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "", "public")
cookieB := loginUser(t, h, "userb", "pass123", "user")
req := httptest.NewRequest("GET", "/faves/"+faveIDStr(fave.ID)+"/edit", nil)
req.AddCookie(cookieB)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Errorf("edit by non-owner: got %d, want 403", rr.Code)
}
}
func TestDeleteFaveHTMX(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Delete me", "", "", "", "public")
token, _ := h.deps.Sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
getReq := httptest.NewRequest("GET", "/faves/"+faveIDStr(fave.ID), nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := httptest.NewRequest("DELETE", "/faves/"+faveIDStr(fave.ID), nil)
req.Header.Set("HX-Request", "true")
req.Header.Set("X-CSRF-Token", csrf)
req.AddCookie(cookie)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("HTMX delete: got %d, want 200", rr.Code)
}
}
func TestDeleteFaveNotOwner(t *testing.T) {
h, mux := testServer(t)
userA, _ := h.deps.Users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "", "public")
cookieB := loginUser(t, h, "userb", "pass123", "user")
getReq := httptest.NewRequest("GET", "/faves/"+faveIDStr(fave.ID), nil)
getReq.AddCookie(cookieB)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := httptest.NewRequest("DELETE", "/faves/"+faveIDStr(fave.ID), nil)
req.Header.Set("X-CSRF-Token", csrf)
req.AddCookie(cookieB)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Errorf("delete by non-owner: got %d, want 403", rr.Code)
}
}
// --- Admin ---
func TestAdminUserList(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
h.deps.Users.Create("regular", "pass123", "user")
req := httptest.NewRequest("GET", "/admin/users", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("admin users: got %d, want 200", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "admin") || !strings.Contains(body, "regular") {
t.Error("admin users page should list all users")
}
}
func TestAdminCreateUser(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{
"username": {"newuser"},
"role": {"user"},
}
req := postForm("/admin/users", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Bruker opprettet") {
t.Error("should show user created message")
}
// Verify user was created.
_, err := h.deps.Users.GetByUsername("newuser")
if err != nil {
t.Errorf("new user should exist: %v", err)
}
}
func TestAdminToggleDisabled(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
user, _ := h.deps.Users.Create("target", "pass123", "user")
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := postForm("/admin/users/"+faveIDStr(user.ID)+"/toggle-disabled", csrf, url.Values{}, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "deaktivert") {
t.Error("should show deactivated message")
}
}
func TestAdminCannotDisableSelf(t *testing.T) {
h, mux := testServer(t)
admin, _ := h.deps.Users.Create("admin", "pass123", "admin")
token, _ := h.deps.Sessions.Create(admin.ID)
cookie := &http.Cookie{Name: "session", Value: token}
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := postForm("/admin/users/"+faveIDStr(admin.ID)+"/toggle-disabled", csrf, url.Values{}, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "din egen konto") {
t.Error("should prevent self-disable")
}
}
func TestAdminSettings(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
// GET settings page.
getReq := httptest.NewRequest("GET", "/admin/settings", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
if getRR.Code != http.StatusOK {
t.Errorf("admin settings GET: got %d, want 200", getRR.Code)
}
csrf := extractCookie(getRR, "csrf_token")
// POST updated settings.
form := url.Values{
"site_name": {"Nytt Navn"},
"site_description": {"En beskrivelse"},
"signup_mode": {"closed"},
}
req := postForm("/admin/settings", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "lagret") {
t.Error("should show settings saved message")
}
// Verify settings were updated.
settings, _ := h.deps.Settings.Get()
if settings.SiteName != "Nytt Navn" {
t.Errorf("site_name = %q, want 'Nytt Navn'", settings.SiteName)
}
}
func TestAdminApproveSignupRequest(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
// Create a signup request.
h.deps.SignupRequests.Create("requester", "password123")
requests, _ := h.deps.SignupRequests.ListPending()
if len(requests) == 0 {
t.Fatal("expected pending request")
}
getReq := httptest.NewRequest("GET", "/admin/signup-requests", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{"action": {"approve"}}
req := postForm("/admin/signup-requests/"+faveIDStr(requests[0].ID), csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "godkjent") {
t.Error("should show approved message")
}
// User should now exist.
_, err := h.deps.Users.GetByUsername("requester")
if err != nil {
t.Errorf("approved user should exist: %v", err)
}
}
func TestAdminRejectSignupRequest(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
h.deps.SignupRequests.Create("rejectable", "password123")
requests, _ := h.deps.SignupRequests.ListPending()
getReq := httptest.NewRequest("GET", "/admin/signup-requests", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{"action": {"reject"}}
req := postForm("/admin/signup-requests/"+faveIDStr(requests[0].ID), csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "avvist") {
t.Error("should show rejected message")
}
}
func TestAdminTags(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
// Create a tag via a fave.
admin, _ := h.deps.Users.GetByUsername("admin")
fave, _ := h.deps.Faves.Create(admin.ID, "Test", "", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"testmerke"})
req := httptest.NewRequest("GET", "/admin/tags", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("admin tags: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "testmerke") {
t.Error("admin tags page should list tags")
}
}
// --- Feeds ---
func TestUserFeed(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("feeduser", "pass123", "user")
h.deps.Users.UpdateProfile(user.ID, "Feed User", "", "public", "public")
h.deps.Faves.Create(user.ID, "User fave", "", "", "", "public")
req := httptest.NewRequest("GET", "/u/feeduser/feed.xml", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("user feed: got %d, want 200", rr.Code)
}
ct := rr.Header().Get("Content-Type")
if !strings.Contains(ct, "atom+xml") {
t.Errorf("content-type = %q", ct)
}
if !strings.Contains(rr.Body.String(), "User fave") {
t.Error("user feed should contain fave")
}
}
func TestFeedExcludesPrivate(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("testuser", "pass123", "user")
h.deps.Faves.Create(user.ID, "Public fave", "", "", "", "public")
h.deps.Faves.Create(user.ID, "Secret fave", "", "", "", "private")
req := httptest.NewRequest("GET", "/feed.xml", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
body := rr.Body.String()
if !strings.Contains(body, "Public fave") {
t.Error("feed should contain public fave")
}
if strings.Contains(body, "Secret fave") {
t.Error("feed should NOT contain private fave")
}
}
func TestTagFeed(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Tagged fave", "", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"golang"})
req := httptest.NewRequest("GET", "/tags/golang/feed.xml", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("tag feed: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Tagged fave") {
t.Error("tag feed should contain tagged fave")
}
}
func TestLimitedProfileFeedReturns404(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("limited", "pass123", "user")
h.deps.Users.UpdateProfile(user.ID, "Limited", "", "limited", "public")
req := httptest.NewRequest("GET", "/u/limited/feed.xml", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("limited profile feed: got %d, want 404", rr.Code)
}
}
// --- Import/Export (web) ---
func TestExportJSON(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
user, _ := h.deps.Users.GetByUsername("testuser")
fave, _ := h.deps.Faves.Create(user.ID, "Export me", "https://example.com", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"test"})
req := httptest.NewRequest("GET", "/export/json", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("export JSON: got %d, want 200", rr.Code)
}
ct := rr.Header().Get("Content-Type")
if !strings.Contains(ct, "application/json") {
t.Errorf("content-type = %q", ct)
}
var faves []ExportFave
if err := json.Unmarshal(rr.Body.Bytes(), &faves); err != nil {
t.Fatalf("parse export: %v", err)
}
if len(faves) != 1 {
t.Fatalf("exported %d, want 1", len(faves))
}
if faves[0].Description != "Export me" {
t.Errorf("description = %q", faves[0].Description)
}
}
func TestExportCSV(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
user, _ := h.deps.Users.GetByUsername("testuser")
h.deps.Faves.Create(user.ID, "CSV fave", "", "", "", "public")
req := httptest.NewRequest("GET", "/export/csv", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("export CSV: got %d, want 200", rr.Code)
}
ct := rr.Header().Get("Content-Type")
if !strings.Contains(ct, "text/csv") {
t.Errorf("content-type = %q", ct)
}
reader := csv.NewReader(bytes.NewReader(rr.Body.Bytes()))
records, err := reader.ReadAll()
if err != nil {
t.Fatalf("parse CSV: %v", err)
}
// Header + 1 data row.
if len(records) != 2 {
t.Errorf("CSV rows = %d, want 2 (header + data)", len(records))
}
if records[0][0] != "description" {
t.Errorf("CSV header = %q, want 'description'", records[0][0])
}
}
func TestImportJSONFile(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
getReq := httptest.NewRequest("GET", "/import", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
// Build multipart form with JSON file.
var body bytes.Buffer
writer := multipart.NewWriter(&body)
writer.WriteField("csrf_token", csrf)
part, _ := writer.CreateFormFile("file", "import.json")
jsonData := `[{"description":"Imported 1","privacy":"public","tags":["go"]},{"description":"Imported 2"}]`
part.Write([]byte(jsonData))
writer.Close()
req := httptest.NewRequest("POST", "/import", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.AddCookie(cookie)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Importert 2 av 2") {
t.Errorf("import result: %s", rr.Body.String())
}
}
func TestImportCSVFile(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
getReq := httptest.NewRequest("GET", "/import", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
// Build multipart form with CSV file.
var body bytes.Buffer
writer := multipart.NewWriter(&body)
writer.WriteField("csrf_token", csrf)
part, _ := writer.CreateFormFile("file", "import.csv")
csvData := "description,url,privacy,tags\nCSV Fave,https://example.com,public,test\n"
part.Write([]byte(csvData))
writer.Close()
req := httptest.NewRequest("POST", "/import", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.AddCookie(cookie)
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrf})
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Importert 1 av 1") {
t.Errorf("import CSV result: %s", rr.Body.String())
}
}
// --- Profile ---
func TestProfileDisabledUser(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("disabled", "pass123", "user")
h.deps.Users.SetDisabled(user.ID, true)
req := httptest.NewRequest("GET", "/u/disabled", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("disabled profile: got %d, want 404", rr.Code)
}
}
func TestProfileOwnerSeesPrivateFaves(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("owner", "pass123", "user")
h.deps.Faves.Create(user.ID, "Private fave", "", "", "", "private")
token, _ := h.deps.Sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
req := httptest.NewRequest("GET", "/u/owner", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("own profile: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Private fave") {
t.Error("owner should see their own private faves on profile")
}
}
func TestProfileVisitorCannotSeePrivateFaves(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("owner", "pass123", "user")
h.deps.Users.UpdateProfile(user.ID, "Owner", "", "public", "public")
h.deps.Faves.Create(user.ID, "Only public", "", "", "", "public")
h.deps.Faves.Create(user.ID, "Hidden secret", "", "", "", "private")
req := httptest.NewRequest("GET", "/u/owner", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
body := rr.Body.String()
if !strings.Contains(body, "Only public") {
t.Error("visitor should see public fave")
}
if strings.Contains(body, "Hidden secret") {
t.Error("visitor should NOT see private fave")
}
}
// --- Tag browsing ---
func TestTagBrowse(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Tagged", "", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"golang"})
req := httptest.NewRequest("GET", "/tags/golang", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("tag browse: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Tagged") {
t.Error("tag browse should show tagged fave")
}
}
// --- Home page ---
func TestHomePageUnauthenticated(t *testing.T) {
_, mux := testServer(t)
req := httptest.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("home page: got %d, want 200", rr.Code)
}
}
func TestHomePageAuthenticated(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
user, _ := h.deps.Users.GetByUsername("testuser")
h.deps.Faves.Create(user.ID, "Home fave", "", "", "", "public")
req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("authenticated home: got %d, want 200", rr.Code)
}
}
// --- Settings ---
func TestSettingsPageRendering(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
req := httptest.NewRequest("GET", "/settings", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("settings page: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Innstillinger") {
t.Error("should render settings page")
}
}
func TestSettingsUpdateProfile(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
getReq := httptest.NewRequest("GET", "/settings", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{
"display_name": {"Nytt Navn"},
"bio": {"Min bio"},
"profile_visibility": {"public"},
"default_fave_privacy": {"private"},
}
req := postForm("/settings", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "lagret") {
t.Error("should show settings saved message")
}
user, _ := h.deps.Users.GetByUsername("testuser")
if user.DisplayName != "Nytt Navn" {
t.Errorf("display_name = %q", user.DisplayName)
}
}
func TestSettingsChangePassword(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
getReq := httptest.NewRequest("GET", "/settings", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{
"current_password": {"pass123"},
"new_password": {"newpass456"},
"confirm_password": {"newpass456"},
}
req := postForm("/settings/password", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "endret") {
t.Errorf("should show password changed: %s", rr.Body.String())
}
// Verify new password works.
_, err := h.deps.Users.Authenticate("testuser", "newpass456")
if err != nil {
t.Errorf("new password should work: %v", err)
}
}
func TestSettingsChangePasswordWrongCurrent(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
getReq := httptest.NewRequest("GET", "/settings", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{
"current_password": {"wrongpass"},
"new_password": {"newpass456"},
"confirm_password": {"newpass456"},
}
req := postForm("/settings/password", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "feil") {
t.Error("should show wrong password error")
}
}
// --- Tag autocomplete ---
// TestTagSuggestionsNoInlineHandlers is a regression test for the autocomplete click bug.
// Tag suggestions must NOT use inline event handlers (onclick, onmousedown) because
// they are blocked by CSP (script-src 'self'). Instead, they must use data-tag-name
// attributes and delegated event listeners in app.js.
func TestTagSuggestionsNoInlineHandlers(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Test", "", "", "", "public")
h.deps.Tags.SetFaveTags(fave.ID, []string{"golang", "goroutines"})
req := httptest.NewRequest("GET", "/tags/search?q=go", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("tag search: got %d, want 200", rr.Code)
}
body := rr.Body.String()
// Must NOT use inline handlers — they are blocked by CSP script-src 'self'.
if strings.Contains(body, "onclick=") {
t.Error("tag suggestions must not use onclick (blocked by CSP)")
}
if strings.Contains(body, "onmousedown=") {
t.Error("tag suggestions must not use onmousedown (blocked by CSP)")
}
if strings.Contains(body, "ontouchstart=") {
t.Error("tag suggestions must not use ontouchstart (blocked by CSP)")
}
// Must use data-tag-name for delegated event handling in app.js.
if !strings.Contains(body, "data-tag-name=") {
t.Error("tag suggestions must use data-tag-name attribute for delegated event handling")
}
// Verify the tag names are in the data attributes.
if !strings.Contains(body, `data-tag-name="golang"`) {
t.Error("suggestion should have data-tag-name with the tag name")
}
}
// TestTagSuggestionsDisplayName verifies that the "av" display name bug is fixed.
// The fave list should show the username as fallback when display_name is empty.
func TestDisplayNameFallbackToUsername(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
// Create a user WITHOUT a display name and a public fave.
user, _ := h.deps.Users.GetByUsername("testuser")
h.deps.Faves.Create(user.ID, "Test fave", "", "", "", "public")
// The home page shows "av <display_name>" — with no display name set,
// it should fall back to the username.
req := httptest.NewRequest("GET", "/", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
body := rr.Body.String()
// Should show "av testuser" (username as fallback), not just "av".
if !strings.Contains(body, "testuser") {
t.Error("home page should show username as fallback when display_name is empty")
}
}
// --- PWA ---
func TestManifestJSON(t *testing.T) {
_, mux := testServer(t)
req := httptest.NewRequest("GET", "/manifest.json", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("manifest: got %d, want 200", rr.Code)
}
ct := rr.Header().Get("Content-Type")
if !strings.Contains(ct, "manifest+json") {
t.Errorf("content-type = %q, want manifest+json", ct)
}
var manifest map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &manifest); err != nil {
t.Fatalf("parse manifest: %v", err)
}
if manifest["name"] != "Test" {
t.Errorf("name = %v, want Test", manifest["name"])
}
// share_target should exist with GET method.
st, ok := manifest["share_target"].(map[string]any)
if !ok {
t.Fatal("manifest missing share_target")
}
if st["method"] != "GET" {
t.Errorf("share_target method = %v, want GET", st["method"])
}
}
func TestServiceWorkerContent(t *testing.T) {
_, mux := testServer(t)
req := httptest.NewRequest("GET", "/sw.js", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("sw.js: got %d, want 200", rr.Code)
}
ct := rr.Header().Get("Content-Type")
if !strings.Contains(ct, "javascript") {
t.Errorf("content-type = %q, want javascript", ct)
}
cc := rr.Header().Get("Cache-Control")
if cc != "no-cache" {
t.Errorf("Cache-Control = %q, want no-cache", cc)
}
body := rr.Body.String()
if strings.Contains(body, "{{BASE_PATH}}") {
t.Error("sw.js should have BASE_PATH placeholder replaced")
}
if !strings.Contains(body, "CACHE_NAME") {
t.Error("sw.js should contain service worker code")
}
}
func TestShareRedirectsToFaveNew(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
req := httptest.NewRequest("GET", "/share?url=https://example.com&title=Test+Page", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Fatalf("share: got %d, want 303", rr.Code)
}
loc := rr.Header().Get("Location")
if !strings.Contains(loc, "/faves/new") {
t.Errorf("redirect = %q, should point to /faves/new", loc)
}
if !strings.Contains(loc, "url=https") {
t.Errorf("redirect = %q, should contain url param", loc)
}
if !strings.Contains(loc, "description=Test") {
t.Errorf("redirect = %q, should contain description from title", loc)
}
}
func TestShareTextFieldFallback(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
// Some Android apps put the URL in "text" instead of "url".
req := httptest.NewRequest("GET", "/share?text=Check+this+out+https://example.com/article", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
loc := rr.Header().Get("Location")
if !strings.Contains(loc, "url=https") {
t.Errorf("should extract URL from text field: %q", loc)
}
}
func TestShareRequiresLogin(t *testing.T) {
_, mux := testServer(t)
req := httptest.NewRequest("GET", "/share?url=https://example.com", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Errorf("unauthenticated share: got %d, want 303", rr.Code)
}
loc := rr.Header().Get("Location")
if !strings.Contains(loc, "/login") {
t.Errorf("should redirect to login: %q", loc)
}
}
func TestFaveNewPreFill(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
req := httptest.NewRequest("GET", "/faves/new?url=https://example.com&description=Shared+Page&notes=Great+article", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("fave new pre-fill: got %d, want 200", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "https://example.com") {
t.Error("URL should be pre-filled")
}
if !strings.Contains(body, "Shared Page") {
t.Error("description should be pre-filled")
}
if !strings.Contains(body, "Great article") {
t.Error("notes should be pre-filled")
}
}
// --- Privacy toggle ---
func TestTogglePrivacyOwner(t *testing.T) {
h, mux := testServer(t)
user, _ := h.deps.Users.Create("testuser", "pass123", "user")
fave, _ := h.deps.Faves.Create(user.ID, "Toggle me", "", "", "", "public")
token, _ := h.deps.Sessions.Create(user.ID)
cookie := &http.Cookie{Name: "session", Value: token}
getReq := httptest.NewRequest("GET", "/faves/"+faveIDStr(fave.ID), nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := postForm("/faves/"+faveIDStr(fave.ID)+"/privacy", csrf, url.Values{}, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("toggle privacy: got %d, want 200\nbody: %s", rr.Code, rr.Body.String())
}
// Should now be private.
updated, _ := h.deps.Faves.GetByID(fave.ID)
if updated.Privacy != "private" {
t.Errorf("privacy = %q, want private after toggle from public", updated.Privacy)
}
// Response should contain the toggle partial with "Privat".
if !strings.Contains(rr.Body.String(), "Privat") {
t.Error("toggle response should show new privacy state")
}
}
func TestTogglePrivacyNotOwner(t *testing.T) {
h, mux := testServer(t)
userA, _ := h.deps.Users.Create("usera", "pass123", "user")
fave, _ := h.deps.Faves.Create(userA.ID, "A's fave", "", "", "", "public")
cookieB := loginUser(t, h, "userb", "pass123", "user")
getReq := httptest.NewRequest("GET", "/faves/"+faveIDStr(fave.ID), nil)
getReq.AddCookie(cookieB)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := postForm("/faves/"+faveIDStr(fave.ID)+"/privacy", csrf, url.Values{}, cookieB)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Errorf("toggle by non-owner: got %d, want 403", rr.Code)
}
}
func TestFaveListShowsEditButton(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
user, _ := h.deps.Users.GetByUsername("testuser")
h.deps.Faves.Create(user.ID, "Editable fave", "", "", "", "public")
req := httptest.NewRequest("GET", "/faves", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
body := rr.Body.String()
if !strings.Contains(body, "Rediger") {
t.Error("fave list should show edit link")
}
if !strings.Contains(body, "Slett") {
t.Error("fave list should show delete button")
}
if !strings.Contains(body, "Offentlig") {
t.Error("fave list should show privacy toggle")
}
}
// --- Admin: role + delete ---
func TestAdminSetRoleSuccess(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
user, _ := h.deps.Users.Create("target", "pass123", "user")
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{"role": {"admin"}}
req := postForm("/admin/users/"+faveIDStr(user.ID)+"/role", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "endret til admin") {
t.Errorf("should show role changed: %s", rr.Body.String())
}
updated, _ := h.deps.Users.GetByID(user.ID)
if updated.Role != "admin" {
t.Errorf("role = %q, want admin", updated.Role)
}
}
func TestAdminSetRoleSelf(t *testing.T) {
h, mux := testServer(t)
admin, _ := h.deps.Users.Create("admin", "pass123", "admin")
token, _ := h.deps.Sessions.Create(admin.ID)
cookie := &http.Cookie{Name: "session", Value: token}
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
form := url.Values{"role": {"user"}}
req := postForm("/admin/users/"+faveIDStr(admin.ID)+"/role", csrf, form, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "din egen rolle") {
t.Error("should prevent changing own role")
}
}
func TestAdminDeleteUserSuccess(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "admin", "pass123", "admin")
user, _ := h.deps.Users.Create("deleteme", "pass123", "user")
h.deps.Faves.Create(user.ID, "Will be deleted", "", "", "", "public")
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := postForm("/admin/users/"+faveIDStr(user.ID)+"/delete", csrf, url.Values{}, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "permanent slettet") {
t.Errorf("should show deleted: %s", rr.Body.String())
}
// User should be gone.
_, err := h.deps.Users.GetByUsername("deleteme")
if err == nil {
t.Error("deleted user should not exist")
}
// Faves should be cascade-deleted.
faves, total, _ := h.deps.Faves.ListByUser(user.ID, 10, 0)
if total != 0 || len(faves) != 0 {
t.Error("faves should be cascade-deleted with user")
}
}
func TestAdminDeleteUserSelf(t *testing.T) {
h, mux := testServer(t)
admin, _ := h.deps.Users.Create("admin", "pass123", "admin")
token, _ := h.deps.Sessions.Create(admin.ID)
cookie := &http.Cookie{Name: "session", Value: token}
getReq := httptest.NewRequest("GET", "/admin/users", nil)
getReq.AddCookie(cookie)
getRR := httptest.NewRecorder()
mux.ServeHTTP(getRR, getReq)
csrf := extractCookie(getRR, "csrf_token")
req := postForm("/admin/users/"+faveIDStr(admin.ID)+"/delete", csrf, url.Values{}, cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "din egen konto") {
t.Error("should prevent self-deletion")
}
}
// --- Export page ---
func TestExportPageRendering(t *testing.T) {
h, mux := testServer(t)
cookie := loginUser(t, h, "testuser", "pass123", "user")
req := httptest.NewRequest("GET", "/export", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("export page: got %d, want 200", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Eksporter") {
t.Error("should render export page")
}
}