Files
sm-user-plugin/deactivating_event_test.go
Jakub Zych 9335e80342 test: RequestUser, EndRequestSession, AvatarURL and user.Deactivating
- TestRequestUser: bearer, token and auth_token cookies in guard order;
  expired, foreign, blacklisted, revoked by tokens_valid_after and
  deactivated tokens are anonymous; only infrastructure failures error
- TestEndRequestSession (no database): jti blacklisted until the token
  expires, forget cookie with the configured Domain and Path, best effort
- TestAvatarURL: thumbnail, broken-image and storage-failure fallbacks,
  gravatar with the size, the user payload's avatar_url unchanged
- TestDeactivatingEvent: one event per newly deactivated user inside the
  bulk action's transaction, a listener error rolls the action back
2026-10-19 21:40:41 +02:00

111 lines
3.5 KiB
Go

package user
import (
"context"
"errors"
"net/http"
"sync"
"testing"
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/sm-user-plugin/models"
"gorm.io/gorm"
)
// TestDeactivatingEvent pins user.Deactivating (D-05): the admin deactivate
// bulk action fires one event per newly deactivated user inside the
// action's transaction, already deactivated users fire none, a listener
// error rolls the whole action back, and the event name is the constant.
func TestDeactivatingEvent(t *testing.T) {
if classes.DeactivatingEventName != "user.Deactivating" {
t.Fatalf("DeactivatingEventName = %q", classes.DeactivatingEventName)
}
env := newAdminEnv(t)
token := env.admin(t, permAccessUsers)
var mu sync.Mutex
var fired []uint
var failFor uint
var txSawDeactivation = map[uint]bool{}
env.app.Events.Listen[*classes.DeactivatingEvent]("test.deactivating", func(ctx context.Context, e *classes.DeactivatingEvent) error {
mu.Lock()
defer mu.Unlock()
if e.User == nil || e.Tx == nil {
return errors.New("event without user or transaction")
}
fired = append(fired, e.User.ID)
// Inside the action's transaction the row is already deactivated.
var deleted int64
if err := e.Tx.Session(&gorm.Session{NewDB: true}).Unscoped().Model(&models.User{}).
Where("id = ? AND deleted_at IS NOT NULL", e.User.ID).Count(&deleted).Error; err != nil {
return err
}
txSawDeactivation[e.User.ID] = deleted == 1
if e.User.ID == failFor {
return errors.New("listener refuses")
}
return nil
})
reset := func() {
mu.Lock()
defer mu.Unlock()
fired, txSawDeactivation = nil, map[uint]bool{}
}
a, b := env.seedUser(t, "deact-a", nil), env.seedUser(t, "deact-b", nil)
if rec := env.bulk(t, token, "deactivate", a, b); rec.Code != http.StatusOK {
t.Fatalf("deactivate: %d %s", rec.Code, rec.Body.String())
}
mu.Lock()
if len(fired) != 2 || !txSawDeactivation[a.ID] || !txSawDeactivation[b.ID] {
t.Fatalf("events %v, seen in the transaction %v", fired, txSawDeactivation)
}
mu.Unlock()
for _, u := range []*models.User{a, b} {
if row := env.reload(t, u.ID); row == nil || !row.DeletedAt.Valid {
t.Fatalf("user %d not deactivated", u.ID)
}
}
// Already deactivated users fire nothing; a mixed selection fires only
// for the newly deactivated one.
reset()
c := env.seedUser(t, "deact-c", nil)
if rec := env.bulk(t, token, "deactivate", a, c); rec.Code != http.StatusOK {
t.Fatalf("mixed deactivate: %d %s", rec.Code, rec.Body.String())
}
mu.Lock()
if len(fired) != 1 || fired[0] != c.ID {
t.Fatalf("mixed selection fired %v, want only %d", fired, c.ID)
}
mu.Unlock()
// A listener error rolls the whole action back.
reset()
d, e := env.seedUser(t, "deact-d", nil), env.seedUser(t, "deact-e", nil)
failFor = e.ID
if rec := env.bulk(t, token, "deactivate", d, e); rec.Code < 400 {
t.Fatalf("deactivate with a refusing listener: %d %s", rec.Code, rec.Body.String())
}
failFor = 0
for _, u := range []*models.User{d, e} {
if row := env.reload(t, u.ID); row == nil || row.DeletedAt.Valid {
t.Fatalf("user %d deactivated although a listener refused", u.ID)
}
}
// Other bulk actions fire nothing.
reset()
if rec := env.bulk(t, token, "restore", a); rec.Code != http.StatusOK {
t.Fatalf("restore: %d %s", rec.Code, rec.Body.String())
}
if rec := env.bulk(t, token, "activate", d); rec.Code != http.StatusOK {
t.Fatalf("activate: %d %s", rec.Code, rec.Body.String())
}
mu.Lock()
if len(fired) != 0 {
t.Fatalf("restore and activate fired %v", fired)
}
mu.Unlock()
}