Files
canon/internal/store/store_test.go
T
cdnutterandClaude Opus 5.5 3923444b3b Rename project from TopDeck to Canon
- Module github.com/cdnutter/canon, binary/command cmd/canon, Docker
  image and compose project "canon"
- Environment variables now use the CANON_ prefix
- Default site title is "<Your name>'s Canon" (or "Canon" with no name);
  a Site title set in Settings still overrides it
- Existing topdeck.db (and WAL files) is renamed to canon.db on start
- README: upgrade notes, including moving the old compose volume

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:22:17 -07:00

210 lines
5.5 KiB
Go

package store
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func newTestStore(t *testing.T) *Store {
t.Helper()
s, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close() })
return s
}
func titles(t *testing.T, s *Store, listID int64) string {
t.Helper()
items, err := s.Items(context.Background(), listID, 0, 0)
if err != nil {
t.Fatal(err)
}
var out []string
for i, it := range items {
if it.Rank != i+1 {
t.Fatalf("rank gap: item %q has rank %d at position %d", it.Title, it.Rank, i+1)
}
out = append(out, it.Title)
}
return strings.Join(out, ",")
}
func TestRanking(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
l, err := s.CreateList(ctx, List{Title: "Top 5 Movies", Category: "movie", MaxItems: 5, Visible: true})
if err != nil {
t.Fatal(err)
}
if l.Slug != "movies" {
t.Errorf("slug = %q, want movies", l.Slug)
}
ids := map[string]int64{}
for _, title := range []string{"A", "B", "C", "D"} {
it, err := s.AddItem(ctx, Item{ListID: l.ID, Title: title}, 0)
if err != nil {
t.Fatal(err)
}
ids[title] = it.ID
}
if got := titles(t, s, l.ID); got != "A,B,C,D" {
t.Fatalf("append: %s", got)
}
e, err := s.AddItem(ctx, Item{ListID: l.ID, Title: "E"}, 2)
if err != nil {
t.Fatal(err)
}
ids["E"] = e.ID
if got := titles(t, s, l.ID); got != "A,E,B,C,D" {
t.Fatalf("insert at 2: %s", got)
}
var ve ValidationError
if _, err := s.AddItem(ctx, Item{ListID: l.ID, Title: "F"}, 0); !errors.As(err, &ve) {
t.Fatalf("expected full-list error, got %v", err)
}
steps := []struct {
id string
rank int
want string
}{
{"A", 5, "E,B,C,D,A"},
{"D", 1, "D,E,B,C,A"},
{"B", 99, "D,E,C,A,B"}, // clamped to the end
{"C", 0, "C,D,E,A,B"}, // clamped to the top
}
for _, st := range steps {
if err := s.MoveItem(ctx, ids[st.id], st.rank); err != nil {
t.Fatal(err)
}
if got := titles(t, s, l.ID); got != st.want {
t.Fatalf("move %s to %d: got %s, want %s", st.id, st.rank, got, st.want)
}
}
if _, err := s.DeleteItem(ctx, ids["E"]); err != nil {
t.Fatal(err)
}
if got := titles(t, s, l.ID); got != "C,D,A,B" {
t.Fatalf("delete: %s", got)
}
if _, err := s.UpdateList(ctx, List{ID: l.ID, Title: "x", Category: "movie", MaxItems: 3}); !errors.As(err, &ve) {
t.Fatalf("expected shrink error, got %v", err)
}
if _, err := s.UpdateList(ctx, List{ID: l.ID, Title: "x", Category: "tv", MaxItems: 5}); !errors.As(err, &ve) {
t.Fatalf("expected category change error, got %v", err)
}
}
func TestListLimits(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
for _, n := range []int{0, MaxListSize + 1} {
if _, err := s.CreateList(ctx, List{Title: "x", Category: "game", MaxItems: n}); err == nil {
t.Errorf("size %d accepted", n)
}
}
if _, err := s.CreateList(ctx, List{Title: "x", Category: "game", MaxItems: MaxListSize}); err != nil {
t.Errorf("max size rejected: %v", err)
}
if _, err := s.CreateList(ctx, List{Title: "x", Category: "podcast", MaxItems: 5}); err == nil {
t.Error("unknown category accepted")
}
// Second list in a category gets a unique slug.
l, err := s.CreateList(ctx, List{Title: "y", Category: "game", MaxItems: 5})
if err != nil {
t.Fatal(err)
}
if l.Slug != "video-games-2" {
t.Errorf("slug = %q", l.Slug)
}
}
func BenchmarkMoveInLargeList(b *testing.B) {
ctx := context.Background()
s, err := Open(b.TempDir())
if err != nil {
b.Fatal(err)
}
defer s.Close()
l, _ := s.CreateList(ctx, List{Title: "big", Category: "song", MaxItems: 5000})
var first int64
for i := 0; i < 5000; i++ {
it, err := s.AddItem(ctx, Item{ListID: l.ID, Title: fmt.Sprint(i)}, 0)
if err != nil {
b.Fatal(err)
}
if i == 0 {
first = it.ID
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
s.MoveItem(ctx, first, 5000-(i%2)*4999)
}
}
func TestCustomList(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
l, err := s.CreateList(ctx, List{Title: "Favorite Sports Teams", Category: "custom", MaxItems: 10, Art: "poster"})
if err != nil {
t.Fatal(err)
}
if l.Slug != "favorite-sports-teams" {
t.Errorf("custom slug = %q, want it from the title", l.Slug)
}
if l.Aspect() != "poster" {
t.Errorf("aspect = %q, want the list's own choice", l.Aspect())
}
l.Art = "sideways"
if l, err = s.UpdateList(ctx, l); err != nil {
t.Fatal(err)
}
if l.Art != "" || l.Aspect() != "square" {
t.Errorf("invalid art should fall back to the category default, got art=%q aspect=%q", l.Art, l.Aspect())
}
m, _ := s.CreateList(ctx, List{Title: "Top 10 Movies", Category: "movie", MaxItems: 10})
if m.Aspect() != "poster" {
t.Errorf("movie aspect = %q", m.Aspect())
}
}
func TestAdoptLegacyDB(t *testing.T) {
dir := t.TempDir()
s, err := Open(dir)
if err != nil {
t.Fatal(err)
}
if _, err := s.CreateList(context.Background(), List{Title: "Keep me", Category: "movie", MaxItems: 5}); err != nil {
t.Fatal(err)
}
s.Close()
// Pretend this data dir predates the rename.
for _, suffix := range []string{"", "-wal", "-shm"} {
os.Rename(filepath.Join(dir, "canon.db"+suffix), filepath.Join(dir, "topdeck.db"+suffix))
}
s, err = Open(dir)
if err != nil {
t.Fatal(err)
}
defer s.Close()
lists, err := s.Lists(context.Background(), true)
if err != nil || len(lists) != 1 || lists[0].Title != "Keep me" {
t.Fatalf("legacy data not adopted: %v %v", lists, err)
}
if _, err := os.Stat(filepath.Join(dir, "topdeck.db")); !os.IsNotExist(err) {
t.Errorf("topdeck.db should have been renamed")
}
}