favoritter/internal/store/fave_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

254 lines
6.6 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-or-later
package store
import (
"testing"
)
func TestFaveCRUD(t *testing.T) {
db := testDB(t)
users := NewUserStore(db)
faves := NewFaveStore(db)
tags := NewTagStore(db)
Argon2Memory = 1024
Argon2Time = 1
defer func() { Argon2Memory = 65536; Argon2Time = 3 }()
// Create a user first.
user, err := users.Create("testuser", "password123", "user")
if err != nil {
t.Fatalf("create user: %v", err)
}
// Create a fave.
fave, err := faves.Create(user.ID, "Blade Runner 2049", "https://example.com", "", "", "public")
if err != nil {
t.Fatalf("create fave: %v", err)
}
if fave.Description != "Blade Runner 2049" {
t.Errorf("description = %q, want %q", fave.Description, "Blade Runner 2049")
}
if fave.Username != "testuser" {
t.Errorf("username = %q, want %q", fave.Username, "testuser")
}
// Get by ID.
got, err := faves.GetByID(fave.ID)
if err != nil {
t.Fatalf("get fave: %v", err)
}
if got.Description != fave.Description {
t.Errorf("got description = %q, want %q", got.Description, fave.Description)
}
// Update.
err = faves.Update(fave.ID, "Blade Runner 2049 (Final Cut)", "https://example.com/br2049", "", "", "private")
if err != nil {
t.Fatalf("update fave: %v", err)
}
updated, _ := faves.GetByID(fave.ID)
if updated.Description != "Blade Runner 2049 (Final Cut)" {
t.Errorf("updated description = %q", updated.Description)
}
if updated.Privacy != "private" {
t.Errorf("updated privacy = %q, want private", updated.Privacy)
}
// Set tags.
err = tags.SetFaveTags(fave.ID, []string{"film", "sci-fi", "favoritt"})
if err != nil {
t.Fatalf("set tags: %v", err)
}
faveTags, err := tags.ForFave(fave.ID)
if err != nil {
t.Fatalf("for fave: %v", err)
}
if len(faveTags) != 3 {
t.Errorf("tag count = %d, want 3", len(faveTags))
}
// List by user.
list, total, err := faves.ListByUser(user.ID, 10, 0)
if err != nil {
t.Fatalf("list by user: %v", err)
}
if total != 1 || len(list) != 1 {
t.Errorf("list by user: total=%d, len=%d", total, len(list))
}
// Load tags for list.
err = faves.LoadTags(list)
if err != nil {
t.Fatalf("load tags: %v", err)
}
if len(list[0].Tags) != 3 {
t.Errorf("loaded tags = %d, want 3", len(list[0].Tags))
}
// Public list should be empty (fave is now private).
pubList, pubTotal, err := faves.ListPublicByUser(user.ID, 10, 0)
if err != nil {
t.Fatalf("list public: %v", err)
}
if pubTotal != 0 || len(pubList) != 0 {
t.Errorf("public list should be empty: total=%d, len=%d", pubTotal, len(pubList))
}
// Delete.
err = faves.Delete(fave.ID)
if err != nil {
t.Fatalf("delete fave: %v", err)
}
_, err = faves.GetByID(fave.ID)
if err != ErrFaveNotFound {
t.Errorf("deleted fave error = %v, want ErrFaveNotFound", err)
}
}
func TestFaveNotes(t *testing.T) {
db := testDB(t)
users := NewUserStore(db)
faves := NewFaveStore(db)
Argon2Memory = 1024
Argon2Time = 1
defer func() { Argon2Memory = 65536; Argon2Time = 3 }()
user, _ := users.Create("testuser", "password123", "user")
// Create with notes.
fave, err := faves.Create(user.ID, "Film", "https://example.com", "", "En fantastisk film", "public")
if err != nil {
t.Fatalf("create fave with notes: %v", err)
}
if fave.Notes != "En fantastisk film" {
t.Errorf("notes = %q, want %q", fave.Notes, "En fantastisk film")
}
// Update notes.
err = faves.Update(fave.ID, fave.Description, fave.URL, fave.ImagePath, "Oppdatert anmeldelse", fave.Privacy)
if err != nil {
t.Fatalf("update notes: %v", err)
}
updated, _ := faves.GetByID(fave.ID)
if updated.Notes != "Oppdatert anmeldelse" {
t.Errorf("updated notes = %q", updated.Notes)
}
// Notes appear in list queries.
list, _, _ := faves.ListByUser(user.ID, 10, 0)
if len(list) != 1 || list[0].Notes != "Oppdatert anmeldelse" {
t.Error("notes should be loaded in list queries")
}
// Empty notes by default.
fave2, _ := faves.Create(user.ID, "No notes", "", "", "", "public")
if fave2.Notes != "" {
t.Errorf("default notes = %q, want empty", fave2.Notes)
}
}
func TestUpdatePrivacy(t *testing.T) {
db := testDB(t)
users := NewUserStore(db)
faves := NewFaveStore(db)
Argon2Memory = 1024
Argon2Time = 1
defer func() { Argon2Memory = 65536; Argon2Time = 3 }()
user, _ := users.Create("testuser", "password123", "user")
fave, _ := faves.Create(user.ID, "Toggle me", "", "", "", "public")
// Toggle to private.
err := faves.UpdatePrivacy(fave.ID, "private")
if err != nil {
t.Fatalf("update privacy: %v", err)
}
got, _ := faves.GetByID(fave.ID)
if got.Privacy != "private" {
t.Errorf("privacy = %q, want private", got.Privacy)
}
// Toggle back to public.
faves.UpdatePrivacy(fave.ID, "public")
got, _ = faves.GetByID(fave.ID)
if got.Privacy != "public" {
t.Errorf("privacy = %q, want public", got.Privacy)
}
}
func TestListByTag(t *testing.T) {
db := testDB(t)
users := NewUserStore(db)
faves := NewFaveStore(db)
tags := NewTagStore(db)
Argon2Memory = 1024
Argon2Time = 1
defer func() { Argon2Memory = 65536; Argon2Time = 3 }()
user, _ := users.Create("testuser", "password123", "user")
// Create two public faves with overlapping tags.
f1, _ := faves.Create(user.ID, "Fave 1", "", "", "", "public")
f2, _ := faves.Create(user.ID, "Fave 2", "", "", "", "public")
f3, _ := faves.Create(user.ID, "Private Fave", "", "", "", "private")
tags.SetFaveTags(f1.ID, []string{"music", "jazz"})
tags.SetFaveTags(f2.ID, []string{"music", "rock"})
tags.SetFaveTags(f3.ID, []string{"music", "secret"})
// ListByTag only returns public faves.
list, total, err := faves.ListByTag("music", 10, 0)
if err != nil {
t.Fatalf("list by tag: %v", err)
}
if total != 2 {
t.Errorf("total = %d, want 2 (private fave should be excluded)", total)
}
if len(list) != 2 {
t.Errorf("len = %d, want 2", len(list))
}
}
func TestFavePagination(t *testing.T) {
db := testDB(t)
users := NewUserStore(db)
faves := NewFaveStore(db)
Argon2Memory = 1024
Argon2Time = 1
defer func() { Argon2Memory = 65536; Argon2Time = 3 }()
user, _ := users.Create("testuser", "password123", "user")
// Create 5 faves.
for i := 0; i < 5; i++ {
faves.Create(user.ID, "Fave "+string(rune('A'+i)), "", "", "", "public")
}
// Page 1 with limit 2.
page1, total, err := faves.ListByUser(user.ID, 2, 0)
if err != nil {
t.Fatalf("page 1: %v", err)
}
if total != 5 {
t.Errorf("total = %d, want 5", total)
}
if len(page1) != 2 {
t.Errorf("page 1 len = %d, want 2", len(page1))
}
// Page 3 with limit 2 should have 1 item.
page3, _, err := faves.ListByUser(user.ID, 2, 4)
if err != nil {
t.Fatalf("page 3: %v", err)
}
if len(page3) != 1 {
t.Errorf("page 3 len = %d, want 1", len(page3))
}
}