package user import ( "context" "encoding/json" "net/http" "slices" "sync" "testing" "git.golem15.com/golem15/sm-user-plugin/classes" "git.golem15.com/golem15/sm-user-plugin/models" "gorm.io/gorm" ) const permResetTwoFactor = "golem15.users.reset_two_factor" // fakeTwoFactorAdmin stands in for a two-factor plugin's service. type fakeTwoFactorAdmin struct { mu sync.Mutex has bool resets []uint txs []*gorm.DB } func (f *fakeTwoFactorAdmin) HasTwoFactor(_ context.Context, tx *gorm.DB, _ uint) (bool, error) { if tx == nil { return false, nil } f.mu.Lock() defer f.mu.Unlock() return f.has, nil } func (f *fakeTwoFactorAdmin) Reset(_ context.Context, tx *gorm.DB, user *models.User) error { f.mu.Lock() defer f.mu.Unlock() f.resets = append(f.resets, user.ID) f.txs = append(f.txs, tx) f.has = false return nil } // recordAction returns the offered record action named name from the show // response of user id, or nil. func (e *adminEnv) recordAction(t *testing.T, token string, id uint, name string) map[string]any { t.Helper() rec := e.expect(t, http.StatusOK, http.MethodGet, userPath(id, ""), token, nil) var meta struct { Actions []map[string]any `json:"actions"` } if err := json.Unmarshal(adminDecode(t, rec).Meta, &meta); err != nil { t.Fatal(err) } for _, action := range meta.Actions { if action["name"] == name { return action } } return nil } // TestResetTwoFactorSlotSmoke pins the D-15 slot: reset_two_factor is // offered only when a classes.TwoFactorAdmin is published and the user has // a second factor, only to an administrator holding // golem15.users.reset_two_factor, with the danger confirmation, and running // it calls Reset once inside the action's transaction. func TestResetTwoFactorSlotSmoke(t *testing.T) { if testing.Short() { t.Skip("Postgres") } env := newAdminEnv(t) token := env.admin(t, permAccessUsers, permResetTwoFactor) plain := env.admin(t, permAccessUsers) user := env.seedUser(t, "r2fa", nil) actionPath := userPath(user.ID, "/actions/reset_two_factor") // No two-factor plugin: never offered, never runs. if slices.Contains(env.offered(t, token, user.ID), "reset_two_factor") { t.Fatal("reset_two_factor offered without a published TwoFactorAdmin") } if rec := env.call(t, http.MethodPost, actionPath, token, map[string]any{}); rec.Code == http.StatusOK { t.Fatalf("reset_two_factor ran without a TwoFactorAdmin: %s", rec.Body.String()) } svc := &fakeTwoFactorAdmin{has: true} if err := env.app.Publish[classes.TwoFactorAdmin](svc); err != nil { t.Fatal(err) } action := env.recordAction(t, token, user.ID, "reset_two_factor") if action == nil { t.Fatalf("reset_two_factor not offered: %v", env.offered(t, token, user.ID)) } if action["danger"] != true || action["label"] != "Reset 2FA" { t.Fatalf("offered action = %v", action) } // An administrator with only access_users neither sees nor runs it. if slices.Contains(env.offered(t, plain, user.ID), "reset_two_factor") { t.Fatal("reset_two_factor offered without its permission") } env.expect(t, http.StatusForbidden, http.MethodPost, actionPath, plain, map[string]any{}) if len(svc.resets) != 0 { t.Fatal("a refused administrator reached Reset") } env.expect(t, http.StatusOK, http.MethodPost, actionPath, token, map[string]any{}) if len(svc.resets) != 1 || svc.resets[0] != user.ID || svc.txs[0] == nil { t.Fatalf("Reset calls = %v (tx %v)", svc.resets, svc.txs) } // Nothing left to reset: no longer offered. if slices.Contains(env.offered(t, token, user.ID), "reset_two_factor") { t.Fatal("reset_two_factor offered for a user without a second factor") } }