Align admin account e2e with must-change password gate
ci / vet-and-test (pull_request) Failing after 1m25s
ci / vet-and-test (pull_request) Failing after 1m25s
Why: U4 middleware restricts must_change sessions to me/password/logout; the legacy e2e expected admin create before first password change. Evidence: go test ./internal/store/ ./internal/server/ ./internal/app/ Impact: TestAdminAccountFlow matches set-password + reloginRequired. Refs: docs/svc-deploy/u4-one-time-login-key.md §5–6 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -321,10 +321,29 @@ func TestAdminAccountFlow(t *testing.T) {
|
||||
return rec
|
||||
}
|
||||
|
||||
// 会话可访问 API;/api/me 携带身份与管理员位。
|
||||
// /api/me 允许 must_change;业务 API 拒绝(U4 设密门)。
|
||||
if rec := session("GET", "/api/me", ""); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"isAdmin":true`) {
|
||||
t.Fatalf("me via session: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec := session("POST", "/api/admin/users", `{"owner":"alice","name":"爱丽丝","password":"alice-init-01"}`); rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("must_change admin must be gated before create: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
// 首登改密:不要求 current;成功后销毁会话,须重新密码登录。
|
||||
if rec := session("POST", "/api/auth/password", `{"next":"admin-final-9"}`); rec.Code != http.StatusOK ||
|
||||
!strings.Contains(rec.Body.String(), `"reloginRequired":true`) {
|
||||
t.Fatalf("admin force set-password: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec := session("GET", "/api/me", ""); rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("admin session must die after set-password: %d", rec.Code)
|
||||
}
|
||||
req = httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"owner":"admin","password":"admin-final-9"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), `"mustChangePassword":true`) {
|
||||
t.Fatalf("admin relogin after set-password: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
tok = strings.Split(strings.Split(rec.Header().Get("Set-Cookie"), SessionCookie+"=")[1], ";")[0]
|
||||
|
||||
// 管理员建号(普通用户,初始密码)。
|
||||
if rec := session("POST", "/api/admin/users", `{"owner":"alice","name":"爱丽丝","password":"alice-init-01"}`); rec.Code != http.StatusOK {
|
||||
@@ -335,7 +354,7 @@ func TestAdminAccountFlow(t *testing.T) {
|
||||
t.Fatalf("duplicate owner must 409: %d", rec.Code)
|
||||
}
|
||||
|
||||
// alice 首登:mustChangePassword=true;改密后清除。
|
||||
// alice 首登:mustChangePassword=true;业务 API 被设密门拒绝。
|
||||
req = httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"owner":"alice","password":"alice-init-01"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -358,16 +377,19 @@ func TestAdminAccountFlow(t *testing.T) {
|
||||
if !strings.Contains(rec.Body.String(), `"mustChangePassword":true`) {
|
||||
t.Fatalf("alice first login must force change: %s", rec.Body.String())
|
||||
}
|
||||
// alice 非管理员:建号被拒 403。
|
||||
if rec := alice("POST", "/api/admin/users", `{"owner":"bob","password":"bob-init-001"}`); rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("non-admin create must 403: %d", rec.Code)
|
||||
t.Fatalf("must_change non-admin create must 403: %d", rec.Code)
|
||||
}
|
||||
// alice 改密(错误当前密码 → 401;成功 → must_change 清除)。
|
||||
if rec := alice("POST", "/api/auth/password", `{"current":"bad","next":"alice-final-9"}`); rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("wrong current password: %d", rec.Code)
|
||||
// must_change 改密忽略 current;短密码拒绝;成功后 reloginRequired。
|
||||
if rec := alice("POST", "/api/auth/password", `{"next":"short"}`); rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("short password must 400: %d", rec.Code)
|
||||
}
|
||||
if rec := alice("POST", "/api/auth/password", `{"current":"alice-init-01","next":"alice-final-9"}`); rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), `"mustChangePassword":true`) {
|
||||
t.Fatalf("alice change password: %d %s", rec.Code, rec.Body.String())
|
||||
if rec := alice("POST", "/api/auth/password", `{"current":"ignored","next":"alice-final-9"}`); rec.Code != http.StatusOK ||
|
||||
!strings.Contains(rec.Body.String(), `"reloginRequired":true`) {
|
||||
t.Fatalf("alice force set-password: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec := alice("GET", "/api/me", ""); rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("alice session must die after set-password: %d", rec.Code)
|
||||
}
|
||||
// 旧密码不能再登录;新密码可以且无需改密。
|
||||
req = httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"owner":"alice","password":"alice-init-01"}`))
|
||||
@@ -377,6 +399,18 @@ func TestAdminAccountFlow(t *testing.T) {
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("old password must fail after change: %d", rec.Code)
|
||||
}
|
||||
req = httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"owner":"alice","password":"alice-final-9"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), `"mustChangePassword":true`) {
|
||||
t.Fatalf("alice relogin: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
aliceTok = strings.Split(strings.Split(rec.Header().Get("Set-Cookie"), SessionCookie+"=")[1], ";")[0]
|
||||
// 已设密普通用户:建号仍 403(非管理员)。
|
||||
if rec := alice("POST", "/api/admin/users", `{"owner":"bob","password":"bob-init-001"}`); rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("non-admin create must 403: %d", rec.Code)
|
||||
}
|
||||
|
||||
// 管理员重置 alice 密码 → 其旧会话吊销 + 首登再改密。
|
||||
if rec := session("POST", "/api/admin/users/alice/password", `{"password":"reset-init-02"}`); rec.Code != http.StatusOK {
|
||||
|
||||
Reference in New Issue
Block a user