231 lines
9.2 KiB
Go
231 lines
9.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The document a television applies must always be complete. A missing key is a setting
|
|
// with no value on the other end, which is how a launcher ends up with no rows.
|
|
func TestNormalizePreferencesFillsEveryKnownKey(t *testing.T) {
|
|
result := normalizePreferences(nil)
|
|
if len(result) != len(preferenceCatalogue) {
|
|
t.Fatalf("got %d keys, want %d", len(result), len(preferenceCatalogue))
|
|
}
|
|
for _, definition := range preferenceCatalogue {
|
|
if _, ok := result[definition.Key]; !ok {
|
|
t.Errorf("%s missing from a normalised document", definition.Key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizePreferencesDropsUnknownKeys(t *testing.T) {
|
|
result := normalizePreferences(map[string]any{"embyToken": "secret", "showTitleLogo": false})
|
|
if _, ok := result["embyToken"]; ok {
|
|
t.Error("an unknown key survived normalisation")
|
|
}
|
|
if result["showTitleLogo"] != false {
|
|
t.Errorf("showTitleLogo = %v, want false", result["showTitleLogo"])
|
|
}
|
|
}
|
|
|
|
func TestNormalizePreferencesBoundsAndNormalisesProfileInitials(t *testing.T) {
|
|
if got := normalizePreferences(map[string]any{"profileInitials": " mc "})["profileInitials"]; got != "MC" {
|
|
t.Errorf("profileInitials = %v, want MC", got)
|
|
}
|
|
for _, value := range []any{"MAT", "M\nC", 12} {
|
|
if got := normalizePreferences(map[string]any{"profileInitials": value})["profileInitials"]; got != "" {
|
|
t.Errorf("profileInitials for %v = %v, want automatic fallback", value, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A short name is a person's name, so unlike the initials beside it in the catalogue it
|
|
// keeps the case it was typed in. Folding it would have the launcher greeting somebody as
|
|
// MATT, which is the whole reason Uppercase is per-definition rather than per-kind.
|
|
func TestNormalizePreferencesKeepsShortNameCaseAndBoundsIt(t *testing.T) {
|
|
if got := normalizePreferences(map[string]any{"shortName": " Matt "})["shortName"]; got != "Matt" {
|
|
t.Errorf("shortName = %v, want Matt", got)
|
|
}
|
|
long := strings.Repeat("a", shortNameMaxLength+1)
|
|
for _, value := range []any{long, "Ma\ntt", 12} {
|
|
if got := normalizePreferences(map[string]any{"shortName": value})["shortName"]; got != "" {
|
|
t.Errorf("shortName for %v = %v, want the account-name fallback", value, got)
|
|
}
|
|
}
|
|
if got := normalizePreferences(nil)["shortName"]; got != "" {
|
|
t.Errorf("default shortName = %v, want blank", got)
|
|
}
|
|
}
|
|
|
|
// The short name is admin-owned like the initials, so a television saving an unrelated
|
|
// setting must not be what quietly clears it.
|
|
func TestDevicePreferenceWritePreservesAdminShortName(t *testing.T) {
|
|
stored, err := json.Marshal(normalizePreferences(map[string]any{"shortName": "Matt"}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
merged := preserveAdminPreferences(map[string]any{"showTitleLogo": false}, stored)
|
|
if normalizePreferences(merged)["shortName"] != "Matt" {
|
|
t.Errorf("shortName = %v, want preserved Matt", normalizePreferences(merged)["shortName"])
|
|
}
|
|
}
|
|
|
|
func TestDevicePreferenceWritePreservesAdminInitials(t *testing.T) {
|
|
stored, err := json.Marshal(normalizePreferences(map[string]any{"profileInitials": "MC"}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
merged := preserveAdminPreferences(map[string]any{
|
|
"profileInitials": "XX", "showTitleLogo": false,
|
|
}, stored)
|
|
normalised := normalizePreferences(merged)
|
|
if normalised["profileInitials"] != "MC" {
|
|
t.Errorf("profileInitials = %v, want preserved MC", normalised["profileInitials"])
|
|
}
|
|
if normalised["showTitleLogo"] != false {
|
|
t.Errorf("showTitleLogo = %v, want device change false", normalised["showTitleLogo"])
|
|
}
|
|
}
|
|
|
|
func TestNormalizePreferencesRejectsIllegalValues(t *testing.T) {
|
|
result := normalizePreferences(map[string]any{
|
|
"homeCardDensity": "enormous",
|
|
"showRatingsStrip": "yes",
|
|
"forYouMinutes": 45,
|
|
"homeSections": []any{"nonsense"},
|
|
})
|
|
if result["homeCardDensity"] != "standard" {
|
|
t.Errorf("homeCardDensity = %v, want the default", result["homeCardDensity"])
|
|
}
|
|
if result["showRatingsStrip"] != true {
|
|
t.Errorf("showRatingsStrip = %v, want the default", result["showRatingsStrip"])
|
|
}
|
|
if result["forYouMinutes"] != 0 {
|
|
t.Errorf("forYouMinutes = %v, want the default", result["forYouMinutes"])
|
|
}
|
|
// Every named row was unknown, which leaves nothing to draw — the default stands in
|
|
// rather than an empty launcher being honoured as a choice.
|
|
want := []string{"continue", "favorites", "latest"}
|
|
if !reflect.DeepEqual(result["homeSections"], want) {
|
|
t.Errorf("homeSections = %v, want %v", result["homeSections"], want)
|
|
}
|
|
}
|
|
|
|
// The order of a multi-select is the setting, not an implementation detail: it is the
|
|
// order the rows appear in on the launcher.
|
|
func TestNormalizePreferencesKeepsMultiOrderAndDedupes(t *testing.T) {
|
|
result := normalizePreferences(map[string]any{
|
|
"homeSections": []any{"latest", "continue", "latest", "unknown"},
|
|
})
|
|
want := []string{"latest", "continue"}
|
|
if !reflect.DeepEqual(result["homeSections"], want) {
|
|
t.Errorf("homeSections = %v, want %v", result["homeSections"], want)
|
|
}
|
|
}
|
|
|
|
// Free-form row ids are stored newline-separated on the television, so an id containing
|
|
// one would come back as two rows on the next sync.
|
|
func TestNormalizePreferencesRejectsNewlinesInRowIds(t *testing.T) {
|
|
result := normalizePreferences(map[string]any{
|
|
"homeRowOrder": []any{"recommended", "bad\nid", " ", "recommended", "latest"},
|
|
})
|
|
want := []string{"recommended", "latest"}
|
|
if !reflect.DeepEqual(result["homeRowOrder"], want) {
|
|
t.Errorf("homeRowOrder = %v, want %v", result["homeRowOrder"], want)
|
|
}
|
|
}
|
|
|
|
func TestNormalizePreferencesBoundsListLength(t *testing.T) {
|
|
ids := make([]any, 0, maxListEntries+50)
|
|
for i := 0; i < maxListEntries+50; i++ {
|
|
ids = append(ids, string(rune('a'+i%26))+string(rune('a'+i/26)))
|
|
}
|
|
result := normalizePreferences(map[string]any{"homeHiddenRows": ids})
|
|
if got := len(result["homeHiddenRows"].([]string)); got > maxListEntries {
|
|
t.Errorf("kept %d ids, want at most %d", got, maxListEntries)
|
|
}
|
|
}
|
|
|
|
// A default that is a slice must be copied out, or a caller mutating one user's document
|
|
// edits the catalogue for every user for the life of the process.
|
|
func TestNormalizePreferencesDoesNotShareSliceDefaults(t *testing.T) {
|
|
first := normalizePreferences(nil)
|
|
first["homeSections"].([]string)[0] = "tampered"
|
|
second := normalizePreferences(nil)
|
|
if second["homeSections"].([]string)[0] != "continue" {
|
|
t.Fatal("mutating one document changed the catalogue default")
|
|
}
|
|
}
|
|
|
|
// Numbers arrive from encoding/json as float64; a document that has been through the wire
|
|
// must normalise identically to one built in Go.
|
|
func TestNormalizePreferencesAcceptsJSONNumbers(t *testing.T) {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal([]byte(`{"forYouMinutes":60}`), &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := normalizePreferences(decoded)["forYouMinutes"]; got != 60 {
|
|
t.Errorf("forYouMinutes = %v, want 60", got)
|
|
}
|
|
}
|
|
|
|
// The client applies whatever comes back, so a round trip through storage must be a
|
|
// fixed point — otherwise two televisions could disagree about what they just agreed on.
|
|
func TestNormalizePreferencesIsIdempotentThroughJSON(t *testing.T) {
|
|
once := normalizePreferences(map[string]any{
|
|
"homeSections": []any{"favorites"}, "homeCardDensity": "large",
|
|
"hideWatchedMovies": true, "homeRowOrder": []any{"recommended"},
|
|
})
|
|
raw, err := json.Marshal(once)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
twice := decodePreferences(raw)
|
|
rawOnce, _ := json.Marshal(once)
|
|
rawTwice, _ := json.Marshal(twice)
|
|
if string(rawOnce) != string(rawTwice) {
|
|
t.Errorf("round trip changed the document:\n once: %s\ntwice: %s", rawOnce, rawTwice)
|
|
}
|
|
}
|
|
|
|
// The catalogue is the contract the admin console and the television both read. A default
|
|
// that is not itself a legal value would hand every new viewer something the editor
|
|
// cannot represent.
|
|
func TestPreferenceCatalogueDefaultsAreLegal(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for _, definition := range preferenceCatalogue {
|
|
if seen[definition.Key] {
|
|
t.Errorf("%s appears in the catalogue twice", definition.Key)
|
|
}
|
|
seen[definition.Key] = true
|
|
if definition.Name == "" || definition.Area == "" {
|
|
t.Errorf("%s needs a name and an area to render in the console", definition.Key)
|
|
}
|
|
normalised := normalizePreference(definition, defaultValue(definition))
|
|
if !reflect.DeepEqual(normalised, defaultValue(definition)) {
|
|
t.Errorf("%s default %v is not a legal value (normalises to %v)",
|
|
definition.Key, definition.Default, normalised)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The skip interval is a number from a fixed list, and the television normalises it a
|
|
// second time. Both ends have to agree on which values exist, or a viewer's choice comes
|
|
// back as the default the moment they change some other setting.
|
|
func TestNormalizePreferencesSeekInterval(t *testing.T) {
|
|
if got := normalizePreferences(map[string]any{"seekIntervalSeconds": 30})["seekIntervalSeconds"]; got != 30 {
|
|
t.Errorf("seekIntervalSeconds = %v, want 30", got)
|
|
}
|
|
// 15 is not offered; neither is a string. Both fall back rather than reaching a player
|
|
// as a step size nothing on the television knows how to label.
|
|
for _, value := range []any{15, "30", 0, nil} {
|
|
got := normalizePreferences(map[string]any{"seekIntervalSeconds": value})["seekIntervalSeconds"]
|
|
if got != 10 {
|
|
t.Errorf("seekIntervalSeconds for %v = %v, want the default 10", value, got)
|
|
}
|
|
}
|
|
}
|