Files
silo-server/internal/userdb/conformance_test.go
T
QuickandClaude Fable 5 440a75b189 refactor(jellycompat): give DisplayPreferences its own table
The Jellyfin DisplayPreferences blobs rode the legacy user_settings
key/value table under synthetic jellycompat:* keys, which forced the
legacy settings API to carry a prefix carve-out in its otherwise-closed
unknown-key gate. They are the compat subsystem's storage, not user
settings: the contract neither validates nor resolves them.

Move them to a dedicated jellycompat_displayprefs table in both
backends, keyed by (prefs id, client) per user, with the blob stored as
opaque text served back byte-for-byte (deliberately not jsonb, which
would re-serialize it). The data-copy migrations — per-user SQLite V16
and a paired SQL + Go goose migration for Postgres — are transactional
and harmless to re-run, and both drive their key parsing and row
classification from the new internal/jellycompat/displayprefs package
so the backends cannot diverge, following the internal/settingsmigrate
precedent. A jellycompat:* row that does not parse as a DisplayPrefs
key (only ever writable through the removed carve-out) is recorded in
user_setting_migration_rejects rather than silently deleted.

With the last non-settings tenant gone, the jellycompatSettingPrefix
carve-out is deleted: the legacy settings endpoints now refuse
jellycompat:* keys like any other unknown key and never surface them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 13:32:41 +00:00

71 lines
2.3 KiB
Go

package userdb
import (
"context"
"database/sql"
"testing"
"time"
"github.com/Silo-Server/silo-server/internal/userstore"
"github.com/Silo-Server/silo-server/internal/userstore/storetest"
)
func newConformanceStore(t *testing.T) userstore.UserStore {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if err := InitSchema(db); err != nil {
t.Fatalf("InitSchema: %v", err)
}
return NewSQLiteUserStore(db)
}
// TestSQLiteProgressSince runs the offline-sync progress-reconciliation
// conformance test (invariant 1) against the real SQLite backend, exercising the
// synced_seq stamping triggers and event_at LWW comparison.
func TestSQLiteProgressSince(t *testing.T) {
storetest.RunProgressSince(t, newConformanceStore)
}
// TestSQLiteSettingValues runs the canonical settings-contract storage
// conformance tests against the per-user SQLite backend. The Postgres backend
// runs the same suite in internal/userstore/pgstore, which is what keeps the two
// from drifting on scope identity, partial uniqueness and delete behavior.
func TestSQLiteSettingValues(t *testing.T) {
storetest.RunSettingValues(t, newConformanceStore)
}
// TestSQLiteJellycompatDisplayPrefs runs the Jellyfin DisplayPreferences
// storage conformance tests against the per-user SQLite backend; the Postgres
// backend runs the same suite in internal/userstore/pgstore.
func TestSQLiteJellycompatDisplayPrefs(t *testing.T) {
storetest.RunJellycompatDisplayPrefs(t, newConformanceStore)
}
func TestSQLiteAddFavoriteAtReportsInsertion(t *testing.T) {
ctx := context.Background()
store := newConformanceStore(t)
if err := store.CreateProfile(ctx, userstore.Profile{ID: "p1", Name: "Test"}); err != nil {
t.Fatalf("CreateProfile: %v", err)
}
addedAt := time.Date(2026, time.July, 16, 12, 0, 0, 0, time.UTC)
inserted, err := store.AddFavoriteAt(ctx, "p1", "movie-1", addedAt)
if err != nil {
t.Fatalf("first AddFavoriteAt: %v", err)
}
if !inserted {
t.Fatal("first AddFavoriteAt reported no insertion")
}
inserted, err = store.AddFavoriteAt(ctx, "p1", "movie-1", addedAt)
if err != nil {
t.Fatalf("duplicate AddFavoriteAt: %v", err)
}
if inserted {
t.Fatal("duplicate AddFavoriteAt reported an insertion")
}
}