Skip to content

Commit 904b5d7

Browse files
yoon6yoclaude
andauthored
fix: E2E 도구를 변경 요청으로 합쳐진 비밀번호 변경과 새 변경 요청 경로에 맞춤 (#264)
* fix: E2E 도구를 변경 요청으로 합쳐진 비밀번호 변경과 새 변경 요청 경로에 맞춤 admin_be가 비밀번호 변경의 상태를 change_request로 옮기고 변경 요청 경로를 바꿔, 도구가 없는 컬럼과 없는 경로를 부르게 된다. - 비밀번호 변경 상태를 change_request에서 읽고 적용 완료를 FULFILLED로 기다림 - 초기화 때 PASSWORD 변경 요청 행도 지움 - 그룹 추가 변경 요청을 /api/users/me/change-requests로 내고 사유를 100자 이상으로 채움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: harness 시험 신청의 사용 목적을 200자 기준에 맞춤 admin_be가 사용 목적을 200자 이상 받게 된 뒤(be#682) E2E·측정의 신청 등록이 400으로 실패했다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 895a9ca commit 904b5d7

6 files changed

Lines changed: 30 additions & 21 deletions

File tree

‎harness/e2e/catalog.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,15 @@ cases:
158158
- approve: r1
159159
- wait: {r1: FULFILLED}
160160
- reset_password: a
161-
- wait_password: {a: APPLIED}
161+
- wait_password: {a: FULFILLED}
162162
- expect_password: {r1: changed}
163163

164164
- id: C10
165165
title: 리눅스 계정이 없는 사용자의 비밀번호 초기화 — 반영할 컨테이너가 없어 작업 없이 바로 적용된다
166166
steps:
167167
- user: a
168168
- reset_password: a
169-
- wait_password: {a: APPLIED}
169+
- wait_password: {a: FULFILLED}
170170
- expect_user: {a: {account: NONE, uid: none}}
171171

172172
- id: C11

‎harness/e2e/observe.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ def user_row(cluster, user_id):
2323

2424

2525
def password_reset_status(cluster, user_id):
26-
"""이 사용자의 가장 최근 비밀번호 재설정 신청 상태(PENDING·PROCESSING·APPLIED·DENIED). 없으면 None."""
27-
rows = cluster.sql(f"SELECT status FROM password_reset_requests WHERE user_id={int(user_id)} "
28-
"ORDER BY password_reset_request_id DESC LIMIT 1;")
26+
"""이 사용자의 가장 최근 비밀번호 변경 요청 상태(PENDING·PROCESSING·FULFILLED·DENIED). 없으면 None.
27+
상태는 변경 요청(change_request, 종류 PASSWORD)에 있고, password_reset_requests 는 그 요청을 가리킨다."""
28+
rows = cluster.sql("SELECT c.status FROM password_reset_requests r "
29+
"JOIN change_request c ON c.change_request_id = r.change_request_id "
30+
f"WHERE r.user_id={int(user_id)} ORDER BY r.password_reset_request_id DESC LIMIT 1;")
2931
return rows[0][0] if rows else None
3032

3133

‎harness/e2e/resetter.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,14 @@ def _delete_homes(self, names):
152152
def _delete_rows(self):
153153
users = f"SELECT user_id FROM users WHERE ubuntu_username LIKE '{self.prefix}%' OR email = '{self.admin_email}'"
154154
reqs = f"SELECT request_id FROM requests WHERE user_id IN ({users})"
155-
# 비밀번호 재설정 신청은 users를 가리킨다. 이 표가 생기기 전의 admin_be가 떠 있는 스택에서도 돌 수 있게
155+
# 비밀번호 변경 요청은 change_request(종류 PASSWORD, 대상 신청 없음)와 그 요청을 가리키는
156+
# password_reset_requests 두 곳에 있다. 이 표가 생기기 전의 admin_be가 떠 있는 스택에서도 돌 수 있게
156157
# 표가 있을 때만 지운다.
157158
has_resets = self.cluster.sql("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() "
158159
"AND table_name = 'password_reset_requests';")
159-
resets = (f"DELETE FROM password_reset_requests WHERE user_id IN (SELECT * FROM ({users}) t) "
160-
f"OR reviewed_by IN (SELECT * FROM ({users}) t);\n") if has_resets else ""
160+
resets = (f"DELETE FROM password_reset_requests WHERE user_id IN (SELECT * FROM ({users}) t);\n"
161+
f"DELETE FROM change_request WHERE request_id IS NULL "
162+
f"AND requested_by IN (SELECT * FROM ({users}) t);\n") if has_resets else ""
161163
# 그룹 작업은 users·change_request·groups 를 가리킨다. E2E 공용 그룹은 admin_be 기록만 지운다 — 인프라의
162164
# 그룹(AD·원장·팀 디렉터리)은 지울 수단이 없어 남기고, 다음 실행의 생성 작업이 그대로 이어받는다.
163165
# 다른 실행의 사용자가 아직 속해 있으면 그 실행이 쓰는 중이므로 그룹 행은 두고 간다.

‎harness/e2e/runner.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from .ports import safe
1717
from .resetter import admin_email, e2e_group_name, run_prefix
1818

19-
# admin_be는 사용 목적을 50자 이상 받는다(be#633).
20-
PURPOSE_FILLER = "자동 점검용 시험 신청입니다. 컨테이너 생성과 회수 흐름이 정상인지 확인합니다."
19+
# admin_be는 사용 목적을 200자 이상 받는다(be#682). 모자란 만큼 마침표로 채운다.
20+
PURPOSE_FILLER = "자동 점검용 시험 신청입니다. 컨테이너 생성과 회수 흐름이 정상인지 확인합니다.".ljust(200, ".")
2121
# 포트 용도는 신청자가 자유롭게 적는 글이다. 실제 신청처럼 한글·공백·괄호를 넣어, 이 글이 쿠버네티스 이름에
2222
# 들어가 생성이 실패하는 일(2026-10-06 operation)을 모든 사례가 함께 지킨다.
2323
PORT_REQUESTS = [{"internalPort": 5173, "usagePurpose": "프론트엔드 (React, Vite)"}]
@@ -337,7 +337,7 @@ def do_reset_password(self, arg):
337337
body={"newPassword": secrets.token_urlsafe(24)}, expect=expect)
338338

339339
def do_wait_password(self, arg):
340-
"""가장 최근 비밀번호 재설정 신청이 원하는 상태(PENDING·PROCESSING·APPLIED·DENIED)가 될 때까지 기다린다.
340+
"""가장 최근 비밀번호 변경 요청이 원하는 상태(PENDING·PROCESSING·FULFILLED·DENIED)가 될 때까지 기다린다.
341341
초기화는 컨테이너에 반영하는 작업만 등록하고 돌아오므로, 적용은 그 작업이 끝난 뒤다."""
342342
timeout = arg.get("timeout", self.run.wait_timeout)
343343
for alias, wanted in arg.items():
@@ -407,9 +407,11 @@ def do_request_group(self, arg):
407407
raise StepFailed(f"그룹 {self.run.group_name}이 admin_be에 없음")
408408
if group["gid"] is None:
409409
raise StepFailed(f"그룹 {self.run.group_name}이 아직 승인 대기 — 이 그룹을 고른 신청이 먼저 승인돼야 함")
410-
self._call("POST", f"/api/requests/{self._req(alias)}/change", as_user=self._owner(alias),
411-
body={"changeType": "GROUP", "newValue": json.dumps([group["gid"]]),
412-
"reason": f"e2e {self.case} 공용 그룹 추가"})
410+
# 사유는 100자 이상이어야 받는다(admin_be SingleChangeRequestDTO).
411+
self._call("POST", "/api/users/me/change-requests", as_user=self._owner(alias),
412+
body={"requestId": int(self._req(alias)), "changeType": "GROUP",
413+
"newValue": json.dumps([group["gid"]]),
414+
"reason": f"e2e {self.case} 공용 그룹 추가".ljust(100, ".")})
413415
rows = self.run.cluster.sql("SELECT MAX(change_request_id) FROM change_request "
414416
f"WHERE request_id={int(self._req(alias))} AND change_type='GROUP';")
415417
self.memo[f"{arg['as']}.change"] = int(rows[0][0])

‎harness/measure_ports.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import system
1919
from sha512_crypt import sha512_crypt
2020

21-
# admin_be는 사용 목적을 50자 이상 받는다(be#633).
22-
MEASURE_PURPOSE = "measure 자동 측정용 시험 신청입니다. 컨테이너 생성과 회수에 걸리는 시간을 잽니다."
21+
# admin_be는 사용 목적을 200자 이상 받는다(be#682). 모자란 만큼 마침표로 채운다.
22+
MEASURE_PURPOSE = "measure 자동 측정용 시험 신청입니다. 컨테이너 생성과 회수에 걸리는 시간을 잽니다.".ljust(200, ".")
2323

2424
TOKEN_TTL_SEC = 300
2525
_ALLOWED = (str, int, float, type(None))

‎harness/test_e2e_runner.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,14 @@ def _fulfilled_cluster():
148148

149149

150150
PASSWORD_STEPS = [{"user": "a"}, {"apply": {"user": "a", "as": "r1"}}, {"reset_password": "a"},
151-
{"wait_password": {"a": "APPLIED"}}]
151+
{"wait_password": {"a": "FULFILLED"}}]
152152

153153

154154
def test_password_reset_passes_when_pod_and_record_agree_on_a_new_hash():
155155
cluster, api = _fulfilled_cluster(), FakeApi()
156156
recorded = iter(["OLD", "NEW"])
157157
cluster.on(r"SELECT IFNULL\(ubuntu_password_hash", lambda _: [(next(recorded),)])
158-
cluster.on(r"FROM password_reset_requests", [("APPLIED",)])
158+
cluster.on(r"FROM password_reset_requests", [("FULFILLED",)])
159159
cluster.on(r"getent shadow", "NEW\n")
160160
run = make_run(cluster, api)
161161

@@ -174,7 +174,7 @@ def test_password_reset_fails_when_the_pod_kept_the_old_hash():
174174
cluster = _fulfilled_cluster()
175175
recorded = iter(["OLD", "NEW"])
176176
cluster.on(r"SELECT IFNULL\(ubuntu_password_hash", lambda _: [(next(recorded),)])
177-
cluster.on(r"FROM password_reset_requests", [("APPLIED",)])
177+
cluster.on(r"FROM password_reset_requests", [("FULFILLED",)])
178178
cluster.on(r"getent shadow", "OLD\n")
179179

180180
result = make_run(cluster, FakeApi()).run_case(
@@ -192,7 +192,7 @@ def test_password_reset_that_never_applies_is_a_failure():
192192
result = make_run(cluster, FakeApi()).run_case({"id": "C09", "steps": PASSWORD_STEPS}, allow_faults=False)
193193

194194
assert result["result"] == "FAIL" and result["step"] == 4
195-
assert "APPLIED" in result["error"] and "PENDING" in result["error"]
195+
assert "FULFILLED" in result["error"] and "PENDING" in result["error"]
196196

197197

198198
def test_rejected_password_reset_must_leave_the_password_alone():
@@ -352,6 +352,9 @@ def test_resetter_deletes_password_reset_rows_before_users_only_where_the_table_
352352
deletes = next(c for c in cluster.calls if "START TRANSACTION" in c)
353353
if table_exists:
354354
assert deletes.index("DELETE FROM password_reset_requests") < deletes.index("DELETE FROM users")
355-
assert "reviewed_by IN" in deletes
355+
# 상태·검토자는 변경 요청(종류 PASSWORD, 대상 신청 없음)에 있다. 재설정 행이 그 요청을 가리키므로 뒤에 지운다.
356+
password_changes = "DELETE FROM change_request WHERE request_id IS NULL"
357+
assert deletes.index("DELETE FROM password_reset_requests") < deletes.index(password_changes)
358+
assert deletes.index(password_changes) < deletes.index("DELETE FROM users")
356359
else:
357360
assert "password_reset_requests" not in deletes

0 commit comments

Comments
 (0)