package app import ( "context" "database/sql" "database/sql/driver" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) type passwordChangeDB struct { oauthTestDB hash, pending string revoked, pendingRevoked bool failedWrite bool } type passwordChangeConnector struct{ db *passwordChangeDB } func (c passwordChangeConnector) Connect(context.Context) (driver.Conn, error) { return c.db, nil } func (passwordChangeConnector) Driver() driver.Driver { return oauthTestDriver{} } func (s *passwordChangeDB) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) { s.pending, s.pendingRevoked = s.hash, s.revoked return s, nil } func (s *passwordChangeDB) Commit() error { s.hash, s.revoked = s.pending, s.pendingRevoked return nil } func (s *passwordChangeDB) Rollback() error { s.pending = ""; return nil } func (s *passwordChangeDB) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { if strings.Contains(query, "SELECT password_hash FROM admin_users") { return oauthRow(s.hash), nil } return s.oauthTestDB.QueryContext(ctx, query, args) } func (s *passwordChangeDB) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { switch { case strings.HasPrefix(query, "UPDATE admin_users SET password_hash="): s.pending = args[0].Value.(string) case strings.HasPrefix(query, "UPDATE admin_sessions SET revoked_at="): if s.failedWrite { return nil, fmt.Errorf("session write failed") } s.pendingRevoked = true case strings.HasPrefix(query, "INSERT INTO admin_audit_logs"): default: return s.oauthTestDB.ExecContext(ctx, query, args) } return driver.RowsAffected(1), nil } func TestAdminPasswordChangeWithoutStrengthRules(t *testing.T) { for _, password := range []string{"1", "lowercase", "中文", "OldPassword123!", strings.Repeat("长", 80)} { t.Run(fmt.Sprintf("bytes-%d", len(password)), func(t *testing.T) { original, err := hashPassword("OldPassword123!") if err != nil { t.Fatal(err) } store := &passwordChangeDB{hash: original} db := sql.OpenDB(passwordChangeConnector{store}) t.Cleanup(func() { _ = db.Close() }) a := &App{db: db} request := func(current, next string) *httptest.ResponseRecorder { payload, _ := json.Marshal(map[string]string{"currentPassword": current, "newPassword": next}) r := httptest.NewRequest(http.MethodPut, "/admin/v1/me/password", strings.NewReader(string(payload))) r = r.WithContext(context.WithValue(r.Context(), identityKey{}, identity{ID: 7, Role: "admin"})) w := httptest.NewRecorder() a.adminChangePassword(w, r) return w } if w := request("wrong", password); w.Code != http.StatusBadRequest || store.hash != original { t.Fatal("incorrect old password accepted") } if w := request("OldPassword123!", ""); w.Code != http.StatusBadRequest { t.Fatal("empty new password accepted") } store.failedWrite = true if w := request("OldPassword123!", password); w.Code != http.StatusInternalServerError || store.hash != original { t.Fatal("failed session revocation did not roll back the password") } store.failedWrite = false w := request("OldPassword123!", password) if w.Code != http.StatusOK || !store.revoked || !checkPassword(store.hash, password) { t.Fatalf("password change failed: %d %s", w.Code, w.Body.String()) } }) } }