Align admin account e2e with must-change password gate
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:
2026-10-12 03:54:59 +00:00
co-authored by Cursor
parent 1ba7225e83
commit 47463a9623
+43 -9
View File
@@ -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 {