Files
memby/server/internal/store/logins_test.go
T

103 lines
3.5 KiB
Go
Raw Normal View History

2026-08-14 09:40:03 +12:00
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
}