93 lines
3.4 KiB
Go
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())
|
|
}
|
|
})
|
|
}
|
|
}
|