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
This commit is contained in:
124
classes/last_seen_test.go
Normal file
124
classes/last_seen_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user