2026-08-06 22:33:56 +12:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/ponzischeme89/memby/server/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func documentJSON(t *testing.T, values map[string]any) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
raw, err := json.Marshal(normalizePreferences(values))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("marshal document: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return raw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The whole point of the history table: one line per setting, in the words the catalogue
|
|
|
|
|
// uses, rather than two JSON documents for somebody to compare by eye.
|
|
|
|
|
func TestPreferenceChangesDescribesOneSetting(t *testing.T) {
|
|
|
|
|
before := normalizePreferences(map[string]any{"homeCardDensity": "standard"})
|
|
|
|
|
after := normalizePreferences(map[string]any{"homeCardDensity": "large"})
|
|
|
|
|
|
|
|
|
|
changes := preferenceChanges(before, after)
|
|
|
|
|
if len(changes) != 1 {
|
|
|
|
|
t.Fatalf("got %d changes, want 1: %+v", len(changes), changes)
|
|
|
|
|
}
|
|
|
|
|
if changes[0].Key != "homeCardDensity" {
|
|
|
|
|
t.Errorf("key = %q, want homeCardDensity", changes[0].Key)
|
|
|
|
|
}
|
|
|
|
|
if changes[0].Before != "Standard" || changes[0].After != "Large" {
|
|
|
|
|
t.Errorf("got %q → %q, want Standard → Large", changes[0].Before, changes[0].After)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A television pushing back the document it already held is an ordinary event. Listing it
|
|
|
|
|
// as a change would fill the history with rows nobody made.
|
|
|
|
|
func TestPreferenceChangesIgnoresIdenticalDocuments(t *testing.T) {
|
|
|
|
|
document := normalizePreferences(map[string]any{"subtitlesEnabled": false})
|
|
|
|
|
if changes := preferenceChanges(document, document); len(changes) != 0 {
|
|
|
|
|
t.Fatalf("got %d changes for an unchanged document: %+v", len(changes), changes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A key the catalogue no longer knows is not a change anybody can act on, and a value
|
|
|
|
|
// stored before a setting existed must not read as one appearing out of nothing.
|
|
|
|
|
func TestPreferenceChangesIgnoresUnknownKeys(t *testing.T) {
|
|
|
|
|
before := map[string]any{"retiredSetting": "one"}
|
|
|
|
|
after := map[string]any{"retiredSetting": "two"}
|
|
|
|
|
if changes := preferenceChanges(before, after); len(changes) != 0 {
|
|
|
|
|
t.Fatalf("an unknown key produced changes: %+v", changes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Order is the setting for a multi, so a reordered list is a change even though the same
|
|
|
|
|
// rows are ticked.
|
|
|
|
|
func TestPreferenceChangesNoticesReorderedRows(t *testing.T) {
|
|
|
|
|
before := normalizePreferences(map[string]any{
|
|
|
|
|
"homeSections": []any{"continue", "favorites", "latest"}})
|
|
|
|
|
after := normalizePreferences(map[string]any{
|
|
|
|
|
"homeSections": []any{"favorites", "continue", "latest"}})
|
|
|
|
|
changes := preferenceChanges(before, after)
|
|
|
|
|
if len(changes) != 1 || changes[0].Key != "homeSections" {
|
|
|
|
|
t.Fatalf("got %+v, want one homeSections change", changes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPreferenceValueLabelWordsEveryKind(t *testing.T) {
|
|
|
|
|
for _, testCase := range []struct {
|
|
|
|
|
key string
|
|
|
|
|
value any
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{"showTitleLogo", true, "On"},
|
|
|
|
|
{"showTitleLogo", false, "Off"},
|
|
|
|
|
{"homeArtworkStyle", "backdrop", "Backdrops"},
|
|
|
|
|
{"seekIntervalSeconds", 30, "30 seconds"},
|
|
|
|
|
{"forYouMinutes", 0, "No limit"},
|
|
|
|
|
{"homeSections", []string{"latest", "continue"}, "Latest movies, Continue watching"},
|
|
|
|
|
{"homeHiddenRows", []string{}, "None"},
|
|
|
|
|
{"homeHiddenRows", []string{"recommended"}, "recommended"},
|
2026-08-17 11:41:36 +12:00
|
|
|
{"profileInitials", "MC", "MC"},
|
|
|
|
|
{"profileInitials", "", "Automatic"},
|
2026-08-06 22:33:56 +12:00
|
|
|
} {
|
|
|
|
|
definition, ok := preferenceDefinitionFor(testCase.key)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("%s is not in the catalogue", testCase.key)
|
|
|
|
|
}
|
|
|
|
|
got := preferenceValueLabel(definition, normalizePreference(definition, testCase.value))
|
|
|
|
|
if got != testCase.want {
|
|
|
|
|
t.Errorf("%s = %q, want %q", testCase.key, got, testCase.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The history arrives newest first, so a row's diff is against the element *after* it. Get
|
|
|
|
|
// this backwards and every line in the table reads inverted.
|
|
|
|
|
func TestAdminRevisionsDiffsAgainstThePrecedingRevision(t *testing.T) {
|
|
|
|
|
history := []store.PreferenceRevision{
|
|
|
|
|
{Revision: 3, Preferences: documentJSON(t, map[string]any{"homeCardDensity": "large"}),
|
|
|
|
|
Source: "admin", CreatedAt: time.Now()},
|
|
|
|
|
{Revision: 2, Preferences: documentJSON(t, map[string]any{"homeCardDensity": "compact"}),
|
|
|
|
|
Source: "device", DeviceName: "Lounge", CreatedAt: time.Now()},
|
|
|
|
|
{Revision: 1, Preferences: documentJSON(t, nil), Source: "device", CreatedAt: time.Now()},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows := adminRevisions(history, 3)
|
|
|
|
|
if len(rows) != 3 {
|
|
|
|
|
t.Fatalf("got %d rows, want 3", len(rows))
|
|
|
|
|
}
|
|
|
|
|
if !rows[0].Current {
|
|
|
|
|
t.Error("the newest revision is not marked current")
|
|
|
|
|
}
|
|
|
|
|
if len(rows[0].Changes) != 1 || rows[0].Changes[0].Before != "Compact" ||
|
|
|
|
|
rows[0].Changes[0].After != "Large" {
|
|
|
|
|
t.Fatalf("r3 changes = %+v, want compact → large", rows[0].Changes)
|
|
|
|
|
}
|
|
|
|
|
// The oldest entry held has nothing below it, so it is labelled rather than diffed
|
|
|
|
|
// against the defaults — which would claim decisions nobody made.
|
|
|
|
|
if !rows[2].Initial || len(rows[2].Changes) != 0 {
|
|
|
|
|
t.Errorf("r1 initial = %v with %d changes, want true with 0",
|
|
|
|
|
rows[2].Initial, len(rows[2].Changes))
|
|
|
|
|
}
|
|
|
|
|
if rows[0].Author != "Admin console" || rows[1].Author != "Lounge" {
|
|
|
|
|
t.Errorf("authors = %q, %q; want Admin console, Lounge", rows[0].Author, rows[1].Author)
|
|
|
|
|
}
|
|
|
|
|
// A write from before the history recorded a device name must not be attributed to
|
|
|
|
|
// an invented one.
|
|
|
|
|
if rows[2].Author != "A television" {
|
|
|
|
|
t.Errorf("nameless device author = %q, want A television", rows[2].Author)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A restore is announced as what it is, and the row it came from stays named.
|
|
|
|
|
func TestAdminRevisionsCarriesRestoreProvenance(t *testing.T) {
|
|
|
|
|
history := []store.PreferenceRevision{
|
|
|
|
|
{Revision: 5, Preferences: documentJSON(t, nil), Source: "admin", RestoredFrom: 2},
|
|
|
|
|
{Revision: 4, Preferences: documentJSON(t, nil), Source: "device"},
|
|
|
|
|
}
|
|
|
|
|
rows := adminRevisions(history, 5)
|
|
|
|
|
if rows[0].RestoredFrom != 2 {
|
|
|
|
|
t.Errorf("restoredFrom = %d, want 2", rows[0].RestoredFrom)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// "Behind" is what the page colours by, and a set holding something newer than the current
|
|
|
|
|
// revision is an anomaly rather than a negative number to render.
|
|
|
|
|
func TestBehindByNeverGoesNegative(t *testing.T) {
|
|
|
|
|
for _, testCase := range []struct{ current, held, want int64 }{
|
|
|
|
|
{7, 7, 0}, {7, 4, 3}, {7, 9, 0}, {7, 0, 7},
|
|
|
|
|
} {
|
|
|
|
|
if got := behindBy(testCase.current, testCase.held); got != testCase.want {
|
|
|
|
|
t.Errorf("behindBy(%d, %d) = %d, want %d",
|
|
|
|
|
testCase.current, testCase.held, got, testCase.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|