This commit is contained in:
ponzischeme89
2026-08-19 06:57:59 +12:00
parent 8c847c59b8
commit 2b43b9ef12
94 changed files with 6359 additions and 778 deletions
+129
View File
@@ -0,0 +1,129 @@
package store
import (
"strings"
"testing"
"time"
)
// notificationWhere is the one predicate the log, its totals, its daily chart and the page
// count all read with. It is worth pinning hard for two reasons: a placeholder numbered
// wrong is a query that either fails or, worse, filters on the wrong argument, and a clause
// that drifts between the four readers is a page whose total disagrees with its own table.
func TestNotificationWhereIsEmptyByDefault(t *testing.T) {
where, args := notificationWhere(NotificationLogFilter{})
if where != "TRUE" {
t.Fatalf("where = %q, want an unfiltered predicate", where)
}
if len(args) != 0 {
t.Fatalf("args = %v, want none", args)
}
}
func TestNotificationWhereNumbersPlaceholdersInOrder(t *testing.T) {
from := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
to := time.Date(2026, 8, 19, 0, 0, 0, 0, time.UTC)
where, args := notificationWhere(NotificationLogFilter{
UserID: "u1",
Kinds: []string{"show-return", "watch-time-week"},
Channels: []string{"in-app"},
Statuses: []string{"failed", "skipped"},
Sources: []string{"watch-time-digest"},
Query: "bear",
From: from,
To: to,
})
// One placeholder per argument, in the order the arguments are appended. The search
// clause reuses its placeholder across four columns, which is why the count of distinct
// placeholders is what matters rather than the count of "$".
for i := range args {
marker := "$" + itoa(i+1)
if !strings.Contains(where, marker) {
t.Fatalf("clause %q never uses %s; the arguments and the placeholders disagree", where, marker)
}
}
if len(args) != 8 {
t.Fatalf("args = %d, want 8", len(args))
}
if args[0] != "u1" {
t.Fatalf("args[0] = %v, want the user id first", args[0])
}
if args[len(args)-1] != "%bear%" {
t.Fatalf("args[last] = %v, want the wrapped search term", args[len(args)-1])
}
if args[6] != to {
t.Fatalf("args[6] = %v, want the upper bound", args[6])
}
}
// Every filter combines with AND. A page whose controls quietly ORed together would answer
// a different question from the one the filter bar describes.
func TestNotificationWhereCombinesWithAnd(t *testing.T) {
where, _ := notificationWhere(NotificationLogFilter{
UserID: "u1", Statuses: []string{"failed"},
})
if strings.Contains(where, " OR emby_user_id") {
t.Fatalf("clause %q ORs its filters together", where)
}
if strings.Count(where, " AND ") != 2 {
t.Fatalf("clause %q does not AND both filters", where)
}
}
// An empty list is not a filter. Sending `ANY('{}')` would match nothing, so a page whose
// dropdown is on "any" would show an empty table.
func TestNotificationWhereIgnoresEmptyLists(t *testing.T) {
where, args := notificationWhere(NotificationLogFilter{
Kinds: []string{}, Channels: nil, Statuses: []string{}, Query: " ",
})
if where != "TRUE" || len(args) != 0 {
t.Fatalf("where = %q args = %v; an unset filter must not narrow anything", where, args)
}
}
// The search box covers the four columns an operator half-remembers something from, and it
// must use one placeholder for all of them — repeating the argument four times would put
// the later filters' placeholders out of step.
func TestNotificationWhereSearchesFourColumnsWithOneArgument(t *testing.T) {
where, args := notificationWhere(NotificationLogFilter{Query: "timeout"})
for _, column := range []string{"title ILIKE", "body ILIKE", "detail ILIKE", "username ILIKE"} {
if !strings.Contains(where, column) {
t.Errorf("search does not cover %s", column)
}
}
if len(args) != 1 {
t.Fatalf("args = %d, want one shared search argument", len(args))
}
if strings.Count(where, "$1") != 4 {
t.Fatalf("clause %q does not reuse $1 across all four columns", where)
}
}
// The retention constant is what the console derives its widest window from, so a change to
// one that is not a change to the other would offer a range the prune has already emptied.
func TestNotificationRetentionIsWholeDays(t *testing.T) {
if NotificationRetention%(24*time.Hour) != 0 {
t.Fatalf("retention %v is not a whole number of days", NotificationRetention)
}
if days := int(NotificationRetention / (24 * time.Hour)); days != 90 {
t.Fatalf("retention = %d days, want 90", days)
}
}
func TestClampTextBoundsAStoredString(t *testing.T) {
if got := clampText("short"); got != "short" {
t.Fatalf("clampText shortened an ordinary string to %q", got)
}
long := strings.Repeat("é", notificationTextLimit+50)
got := clampText(long)
// Counted in runes, not bytes: a body in Japanese must not be cut at a third of an
// English one's length, and never mid-character.
if runes := []rune(got); len(runes) != notificationTextLimit+1 {
t.Fatalf("clamped to %d runes, want %d plus the ellipsis", len(runes), notificationTextLimit)
}
if !strings.HasSuffix(got, "…") {
t.Fatal("a clamped string does not say that it was clamped")
}
}