- 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
125 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|