Files
sm-user-plugin/classes/last_seen_test.go
Jakub Zych 493a3ff47c test: unit tests for the admin class functions, the models and the admin registration
- classes: every admin action for its changed-row count and idempotency, the ban across several throttle rows, the cleanup of a permanent delete
- classes: the privileged list and IsPrivilegedMember (D-30), also through a handle that carries conditions
- classes: the five-minute boundary of last_seen
- models: PermissionSet round trip, the slug table shared with the SPA, Fillable and Rules, fields that never marshal, the group filter value
- TestAdminRegistration: permissions, navigation, embedded files and phrase keys in en and pl; the admin secret is required
2026-10-05 15:15:28 +02:00

125 lines
3.7 KiB
Go

package classes
import (
"context"
"testing"
"time"
"git.golem15.com/golem15/sm-user-plugin/models"
"gorm.io/gorm"
)
// contextCancelled returns a context that is already cancelled, so a query
// through it fails.
func contextCancelled(t *testing.T) (context.Context, context.CancelFunc) {
t.Helper()
ctx, cancel := context.WithCancel(t.Context())
cancel()
return ctx, cancel
}
// TestTouchLastSeen pins the five-minute rule of users.last_seen on both
// sides of the boundary, for an active and a deactivated user (D-17).
func TestTouchLastSeen(t *testing.T) {
ctx := t.Context()
db := throttleDB(t)
user := models.User{Email: "touch@example.test", IsActivated: true}
gone := models.User{Email: "touch-gone@example.test", IsActivated: true, DeletedAt: gorm.DeletedAt{Time: time.Now(), Valid: true}}
for _, u := range []*models.User{&user, &gone} {
if err := db.Omit("Groups").Create(u).Error; err != nil {
t.Fatal(err)
}
}
read := func(id uint) *time.Time {
t.Helper()
var row struct{ LastSeen *time.Time }
if err := db.Raw(`SELECT last_seen FROM users WHERE id = ?`, id).Scan(&row).Error; err != nil {
t.Fatal(err)
}
return row.LastSeen
}
set := func(id uint, age time.Duration) time.Time {
t.Helper()
at := time.Now().Add(-age).Truncate(time.Microsecond)
if err := db.Exec(`UPDATE users SET last_seen = ? WHERE id = ?`, at, id).Error; err != nil {
t.Fatal(err)
}
return at
}
updatedAt := func(id uint) time.Time {
t.Helper()
var at time.Time
if err := db.Raw(`SELECT updated_at FROM users WHERE id = ?`, id).Scan(&at).Error; err != nil {
t.Fatal(err)
}
return at
}
for _, id := range []uint{user.ID, gone.ID} {
if read(id) != nil {
t.Fatalf("user %d starts with last_seen", id)
}
stamped := updatedAt(id)
started := time.Now().Add(-time.Second)
if err := TouchLastSeen(ctx, db, id); err != nil {
t.Fatal(err)
}
first := read(id)
if first == nil || first.Before(started) {
t.Fatalf("user %d: a NULL last_seen was not stamped: %v", id, first)
}
if !updatedAt(id).Equal(stamped) {
t.Fatalf("user %d: the stamp touched updated_at", id)
}
// Just inside the window: untouched. Just outside: stamped again.
for _, tc := range []struct {
age time.Duration
stamped bool
}{
{0, false}, {4*time.Minute + 55*time.Second, false},
{5*time.Minute + 5*time.Second, true}, {24 * time.Hour, true},
} {
was := set(id, tc.age)
if err := TouchLastSeen(ctx, db, id); err != nil {
t.Fatal(err)
}
now := read(id)
if now == nil || now.Equal(was) == tc.stamped {
t.Fatalf("user %d, last seen %s ago: stamped = %v, want %v", id, tc.age, !now.Equal(was), tc.stamped)
}
}
}
// One user's stamp never moves another's.
other := set(gone.ID, time.Hour)
if err := TouchLastSeen(ctx, db, user.ID); err != nil {
t.Fatal(err)
}
if got := read(gone.ID); got == nil || !got.Equal(other) {
t.Fatal("a stamp moved another user's last_seen")
}
// An unknown id and the zero id are no error; a missing handle and a
// failed write are.
if err := TouchLastSeen(ctx, db, 987654); err != nil {
t.Fatal(err)
}
if err := TouchLastSeen(ctx, db, 0); err != nil {
t.Fatal(err)
}
if err := TouchLastSeen(ctx, nil, user.ID); err == nil {
t.Fatal("TouchLastSeen without a handle")
}
cancelled, cancel := contextCancelled(t)
defer cancel()
set(user.ID, time.Hour)
if err := TouchLastSeen(cancelled, db, user.ID); err == nil {
t.Fatal("TouchLastSeen swallowed a failed write")
}
// The handle's own conditions do not leak into the update.
if err := TouchLastSeen(ctx, db.Where("1 = 0"), user.ID); err != nil {
t.Fatal(err)
}
if got := read(user.ID); got == nil || time.Since(*got) > time.Minute {
t.Fatalf("a scoped handle kept the stamp from being written: %v", got)
}
}