favoritter/internal/store/fave_test.go

223 lines
5.8 KiB
Go
Raw Normal View History

feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
// 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")
feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
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")
feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
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)
}
}
feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
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")
feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
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")
feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:55:22 +02:00
}
// 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))
}
}