package store import ( "database/sql" "path/filepath" "testing" ) // Cans feature removal (2026-10-16): opening a store on a pre-removal // database must drop the paste_cans table (only when empty) and the // pastes.can_id column. A non-empty paste_cans table is left in place. func TestMigrateDropsCansSchema(t *testing.T) { dir := t.TempDir() legacy := func(name string, canRows int) string { db, err := sql.Open("sqlite", filepath.Join(dir, name)) if err != nil { t.Fatalf("open raw db: %v", err) } defer db.Close() stmts := []string{ `CREATE TABLE pastes ( id TEXT PRIMARY KEY, custom_slug TEXT UNIQUE, content TEXT NOT NULL, content_type TEXT NOT NULL DEFAULT 'text/plain', language TEXT, title TEXT, password_hash TEXT, expires_at INTEGER, burn_after_read INTEGER DEFAULT 0, visibility TEXT NOT NULL DEFAULT 'public', can_id TEXT, created_at INTEGER NOT NULL, deleted_at INTEGER, view_count INTEGER NOT NULL DEFAULT 0, deletion_token TEXT )`, `CREATE TABLE paste_cans ( id TEXT PRIMARY KEY, title TEXT, description TEXT, visibility TEXT NOT NULL DEFAULT 'public', password_hash TEXT, created_at INTEGER NOT NULL, deleted_at INTEGER, expires_at INTEGER )`, `ALTER TABLE paste_cans ADD COLUMN viewer_id TEXT`, `INSERT INTO pastes (id, content, content_type, visibility, can_id, created_at) VALUES ('p1', 'x', 'text/plain', 'public', NULL, 1)`, } for _, q := range stmts { if _, err := db.Exec(q); err != nil { t.Fatalf("seed %q: %v", q, err) } } for i := 0; i < canRows; i++ { if _, err := db.Exec(`INSERT INTO paste_cans (id, created_at) VALUES (?, 1)`, string(rune('a'+i))); err != nil { t.Fatalf("seed can row: %v", err) } } return filepath.Join(dir, name) } intCol := func(s *Store, table, col string) int { var n int s.db.QueryRow(`SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?`, table, col).Scan(&n) return n } tableExists := func(s *Store, table string) bool { var n int s.db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&n) return n > 0 } // Empty paste_cans: table and column both dropped. s1, err := OpenStore(legacy("empty.db", 0)) if err != nil { t.Fatalf("open: %v", err) } defer s1.db.Close() if tableExists(s1, "paste_cans") { t.Error("paste_cans still exists after migrate (was empty; should be dropped)") } if intCol(s1, "pastes", "can_id") != 0 { t.Error("pastes.can_id still exists after migrate; should be dropped") } // The store still works end to end. if _, err := s1.CreatePaste(&Paste{Content: "hello", Visibility: "public"}); err != nil { t.Fatalf("create paste on migrated db: %v", err) } if got := s1.QueryInt(`SELECT COUNT(*) FROM pastes`); got != 2 { t.Errorf("pastes count = %d, want 2", got) } // Non-empty paste_cans: table kept, can_id still dropped. s2, err := OpenStore(legacy("full.db", 2)) if err != nil { t.Fatalf("open: %v", err) } defer s2.db.Close() if !tableExists(s2, "paste_cans") { t.Error("non-empty paste_cans was dropped; must be kept until cleaned") } if intCol(s2, "pastes", "can_id") != 0 { t.Error("pastes.can_id still exists after migrate; should be dropped") } // Fresh database: neither the table nor the column is ever created. s3, err := OpenStore(filepath.Join(dir, "fresh.db")) if err != nil { t.Fatalf("open fresh: %v", err) } defer s3.db.Close() if tableExists(s3, "paste_cans") { t.Error("fresh db created paste_cans") } if intCol(s3, "pastes", "can_id") != 0 { t.Error("fresh db created pastes.can_id") } }