161 lines
6.9 KiB
Go
161 lines
6.9 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||
)
|
||
|
||
func TestChangePasswordUpdatesPrimarySuperAdminAndInvalidatesSession(t *testing.T) {
|
||
directory := t.TempDir()
|
||
cfg := config.Default(directory)
|
||
cfg.SetupReady = true
|
||
cfg.Admin.Username = "admin"
|
||
cfg.Admin.Email = "admin@example.test"
|
||
cfg.Admin.PasswordHash = httpUserPasswordHash(t, "AdminPass123")
|
||
cfg.DNS.VerificationState = "verified"
|
||
cfg.Web.Hostname = "scert.example.test"
|
||
configPath := filepath.Join(directory, "config", "scert.json")
|
||
if err := cfg.Save(configPath); err != nil {
|
||
t.Fatalf("保存改密测试配置失败:%v", err)
|
||
}
|
||
t.Setenv("SCERT_WEB_ENABLED", "false")
|
||
server := New(Dependencies{Config: cfg, ConfigPath: configPath, Repository: &stubDomainRepository{}})
|
||
login := loginAuthTest(t, server, "admin", "AdminPass123", false)
|
||
cookie := login.Result().Cookies()[0]
|
||
|
||
response := performChangePasswordRequest(t, server, cookie, `{"current_password":"AdminPass123","new_password":"NewAdmin123","confirm_password":"NewAdmin123"}`)
|
||
if response.Code != http.StatusOK {
|
||
t.Fatalf("超级管理员改密失败:%d %s", response.Code, response.Body.String())
|
||
}
|
||
var payload struct {
|
||
Authenticated bool `json:"authenticated"`
|
||
RequiresLogin bool `json:"requires_login"`
|
||
}
|
||
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
||
t.Fatalf("解析改密响应失败:%v", err)
|
||
}
|
||
if payload.Authenticated || !payload.RequiresLogin {
|
||
t.Fatalf("改密响应未要求重新登录:%+v", payload)
|
||
}
|
||
|
||
oldSession := authSessionRequest(server, cookie)
|
||
if oldSession.Code != http.StatusUnauthorized {
|
||
t.Fatalf("改密后旧会话应失效,实际为 %d:%s", oldSession.Code, oldSession.Body.String())
|
||
}
|
||
oldLogin := performLoginRequest(t, server, "admin", "AdminPass123")
|
||
if oldLogin.Code != http.StatusUnauthorized {
|
||
t.Fatalf("改密后旧密码不应登录成功,实际为 %d", oldLogin.Code)
|
||
}
|
||
newLogin := loginAuthTest(t, server, "admin", "NewAdmin123", false)
|
||
if newLogin.Code != http.StatusOK {
|
||
t.Fatalf("改密后新密码登录失败:%d %s", newLogin.Code, newLogin.Body.String())
|
||
}
|
||
|
||
saved, err := config.Load(configPath)
|
||
if err != nil {
|
||
t.Fatalf("读取改密后的配置失败:%v", err)
|
||
}
|
||
if saved.Admin.PasswordHash == cfg.Admin.PasswordHash || saved.Admin.PasswordHash == "" {
|
||
t.Fatal("改密后的配置没有保存新的密码摘要")
|
||
}
|
||
if saved.Web.Enabled != cfg.Web.Enabled {
|
||
t.Fatal("修改密码不应把环境变量覆盖值写回配置文件")
|
||
}
|
||
}
|
||
|
||
func TestChangePasswordUpdatesDatabaseUser(t *testing.T) {
|
||
directory := t.TempDir()
|
||
cfg := config.Default(directory)
|
||
cfg.SetupReady = true
|
||
cfg.Admin.Username = "admin"
|
||
cfg.Admin.Email = "admin@example.test"
|
||
cfg.Admin.PasswordHash = httpUserPasswordHash(t, "AdminPass123")
|
||
cfg.DNS.VerificationState = "verified"
|
||
cfg.Web.Hostname = "scert.example.test"
|
||
cfg.Database.DSN = filepath.Join(directory, "data", "change-password.db")
|
||
db, err := database.Open(t.Context(), cfg.Database)
|
||
if err != nil {
|
||
t.Fatalf("打开数据库改密测试数据库失败:%v", err)
|
||
}
|
||
defer db.Close()
|
||
if err := database.Migrate(t.Context(), db.DB, "sqlite"); err != nil {
|
||
t.Fatalf("迁移数据库改密测试数据库失败:%v", err)
|
||
}
|
||
users := repository.NewSQLUserRepository(db.DB, "sqlite")
|
||
if err := users.CreateUser(t.Context(), repository.User{
|
||
ID: "operator-1", Username: "operator", Email: "operator@example.test",
|
||
PasswordHash: httpUserPasswordHash(t, "Operator123"), Role: repository.UserRoleAdmin, Status: repository.UserStatusActive,
|
||
}); err != nil {
|
||
t.Fatalf("创建数据库改密测试用户失败:%v", err)
|
||
}
|
||
server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{}, Users: users})
|
||
login := loginAuthTest(t, server, "operator", "Operator123", false)
|
||
if login.Code != http.StatusOK {
|
||
t.Fatalf("数据库用户登录失败:%d %s", login.Code, login.Body.String())
|
||
}
|
||
cookie := login.Result().Cookies()[0]
|
||
response := performChangePasswordRequest(t, server, cookie, `{"current_password":"Operator123","new_password":"NewOper123","confirm_password":"NewOper123"}`)
|
||
if response.Code != http.StatusOK {
|
||
t.Fatalf("数据库用户改密失败:%d %s", response.Code, response.Body.String())
|
||
}
|
||
if oldSession := authSessionRequest(server, cookie); oldSession.Code != http.StatusUnauthorized {
|
||
t.Fatalf("数据库用户改密后旧会话应失效,实际为 %d", oldSession.Code)
|
||
}
|
||
if newLogin := loginAuthTest(t, server, "operator@example.test", "NewOper123", false); newLogin.Code != http.StatusOK {
|
||
t.Fatalf("数据库用户改密后新密码登录失败:%d %s", newLogin.Code, newLogin.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestChangePasswordRejectsInvalidInput(t *testing.T) {
|
||
directory := t.TempDir()
|
||
cfg := config.Default(directory)
|
||
cfg.SetupReady = true
|
||
cfg.Admin.Username = "admin"
|
||
cfg.Admin.Email = "admin@example.test"
|
||
cfg.Admin.PasswordHash = httpUserPasswordHash(t, "AdminPass123")
|
||
cfg.DNS.VerificationState = "verified"
|
||
cfg.Web.Hostname = "scert.example.test"
|
||
cfgPath := filepath.Join(directory, "config", "scert.json")
|
||
if err := cfg.Save(cfgPath); err != nil {
|
||
t.Fatalf("保存非法改密测试配置失败:%v", err)
|
||
}
|
||
server := New(Dependencies{Config: cfg, ConfigPath: cfgPath, Repository: &stubDomainRepository{}})
|
||
login := loginAuthTest(t, server, "admin", "AdminPass123", false)
|
||
cookie := login.Result().Cookies()[0]
|
||
response := performChangePasswordRequest(t, server, cookie, `{"current_password":"wrong-password","new_password":"short","confirm_password":"different"}`)
|
||
if response.Code != http.StatusBadRequest {
|
||
t.Fatalf("非法改密请求应先返回参数错误,实际为 %d:%s", response.Code, response.Body.String())
|
||
}
|
||
if !strings.Contains(response.Body.String(), "两次输入的新密码不一致") {
|
||
t.Fatalf("非法改密响应未说明确认密码错误:%s", response.Body.String())
|
||
}
|
||
}
|
||
|
||
func performChangePasswordRequest(t *testing.T, server *Server, cookie *http.Cookie, body string) *httptest.ResponseRecorder {
|
||
t.Helper()
|
||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
request.AddCookie(cookie)
|
||
response := httptest.NewRecorder()
|
||
server.Handler().ServeHTTP(response, request)
|
||
return response
|
||
}
|
||
|
||
func performLoginRequest(t *testing.T, server *Server, identifier, password string) *httptest.ResponseRecorder {
|
||
t.Helper()
|
||
body := `{"identifier":"` + identifier + `","password":"` + password + `","remember":false}`
|
||
request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
response := httptest.NewRecorder()
|
||
server.Handler().ServeHTTP(response, request)
|
||
return response
|
||
}
|