package store import ( "strings" "testing" "time" ) // The filter predicate is the one piece of this feature worth pinning without a database: // it is built by string concatenation with hand-numbered placeholders, and the failure it // can produce is not a compile error or a crash but a *wrong answer* — a page of somebody // else's sign-ins, or a total that does not match the rows above it. func TestLoginWhereIsEmptyForAnEmptyFilter(t *testing.T) { where, args := loginWhere(LoginFilter{}) if where != "TRUE" { t.Fatalf("an unfiltered history should match everything, got %q", where) } if len(args) != 0 { t.Fatalf("no filter means no arguments, got %v", args) } } func TestLoginWhereNumbersPlaceholdersInOrder(t *testing.T) { // Every argument must be referenced exactly once, by its own position. A duplicated or // skipped number is how one filter silently applies another's value. where, args := loginWhere(LoginFilter{ EmbyUserID: "user-1", DeviceID: "device-1", IPAddress: "10.0.0.9", From: time.Now().Add(-time.Hour), To: time.Now(), Method: LoginMethodPassword, }) if len(args) != 6 { t.Fatalf("expected six bound values, got %d (%v)", len(args), args) } for position := 1; position <= len(args); position++ { placeholder := "$" + itoa(position) if strings.Count(where, placeholder) != 1 { t.Fatalf("placeholder %s appears %d times in %q", placeholder, strings.Count(where, placeholder), where) } } } func TestLoginWhereQueryReusesOneArgumentAcrossThreeColumns(t *testing.T) { // The free-text control searches three columns from a single bound value. If it ever // bound three, every later placeholder would be numbered wrongly. where, args := loginWhere(LoginFilter{Query: "lounge"}) if len(args) != 1 { t.Fatalf("free text should bind one value, got %d", len(args)) } if strings.Count(where, "$1") != 3 { t.Fatalf("free text should search three columns, got %q", where) } } func TestLoginWhereOutcomeBindsNothing(t *testing.T) { // Outcome is a literal predicate rather than a bound value; if it ever consumed an // argument slot without appending to args, the numbering after it would be wrong. for _, outcome := range []string{"success", "failure"} { where, args := loginWhere(LoginFilter{Outcome: outcome, DeviceID: "device-1"}) if len(args) != 1 { t.Fatalf("%s: expected one bound value, got %d", outcome, len(args)) } if !strings.Contains(where, "$1") { t.Fatalf("%s: the device filter lost its placeholder: %q", outcome, where) } } } func TestLoginWhereIgnoresBlankAndWhitespaceFilters(t *testing.T) { // The console sends empty controls as empty strings. A blank that reached the query // would match no rows at all rather than every row, which reads as "this television // has never connected". where, args := loginWhere(LoginFilter{EmbyUserID: " ", DeviceID: "", IPAddress: "\t"}) if where != "TRUE" || len(args) != 0 { t.Fatalf("blank filters should not narrow anything, got %q %v", where, args) } } func TestLoginWhereUnknownOutcomeMatchesBoth(t *testing.T) { // An outcome the server does not recognise must mean "any", not "none": the filter is // a convenience, and a typo in a bookmarked URL should not empty the page. where, _ := loginWhere(LoginFilter{Outcome: "maybe"}) if strings.Contains(where, "success") { t.Fatalf("an unrecognised outcome should not filter, got %q", where) } } func itoa(value int) string { if value == 0 { return "0" } digits := "" for value > 0 { digits = string(rune('0'+value%10)) + digits value /= 10 } return digits }