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() }