更新
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user