Files
kefu/im/backend/internal/app/password_change_test.go
T
2026-09-03 08:38:17 +08:00

93 lines
3.4 KiB
Go

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