From 6cfc75e3b8d3d56ae6604355af4106c05d522b19 Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Sun, 26 Apr 2026 17:59:30 +0900 Subject: [PATCH] =?UTF-8?q?test:=20E2E=20=ED=85=8C=EC=8A=A4=ED=8A=B8=2010?= =?UTF-8?q?=EA=B0=9C=20(=ED=95=B5=EC=8B=AC=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/e2e.py — 컨테이너에서 직접 실행: 1. roster.json 로드 (34명, 7팀, 한지승 포함, 김태현 제외) 2. 투표 마감 토글 3. 기본 winner (priority 충돌 없음) 4. priority 1팀 모든 분야 1위 → 다음 팀에 자동 이양 5. 동률 status='tie' 6. tie_break 적용 시 status='ok' 7. voter_name UNIQUE 강제 8. 팀 제목 영속 9. archive 파일 생성 10. archive 동률 미해결 시 skip 실행: docker cp tests/e2e.py hackathon-vote:/tmp/e2e.py docker exec hackathon-vote python3 /tmp/e2e.py 결과: 10/10 통과 Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e.py | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 tests/e2e.py diff --git a/tests/e2e.py b/tests/e2e.py new file mode 100644 index 0000000..9e47cc1 --- /dev/null +++ b/tests/e2e.py @@ -0,0 +1,223 @@ +""" +E2E 테스트 — 컨테이너에서 직접 실행. +사용: docker exec hackathon-vote python3 /tmp/e2e.py +""" +import os +import sys +import tempfile + +# 테스트용 격리 DB +TEST_DB = tempfile.mktemp(suffix=".db") +os.environ["VOTE_DB"] = TEST_DB +os.environ["ADMIN_TOKEN"] = "test" + +sys.path.insert(0, "/app") + +# import after env set +from app import ( # noqa: E402 + get_conn, + get_participants, + get_teams, + compute_winners, + save_tie_break, + clear_tie_break, + set_voting_open, + is_voting_open, + set_title, + get_titles, + archive_results, +) + + +# ---- helpers ---- + +def reset_db(): + """테스트 DB 초기화.""" + if os.path.exists(TEST_DB): + os.unlink(TEST_DB) + conn = get_conn() + conn.close() + + +def insert_vote(name, emp_id, team, fun, polish, utility): + conn = get_conn() + conn.execute( + "INSERT INTO votes (voter_name, employee_id, voter_team, " + "fun_team, polish_team, utility_team, created_at) " + "VALUES (?, ?, ?, ?, ?, ?, datetime('now'))", + (name, emp_id, team, fun, polish, utility), + ) + conn.commit() + conn.close() + + +def vote_count(): + conn = get_conn() + n = conn.execute("SELECT COUNT(*) FROM votes").fetchone()[0] + conn.close() + return n + + +# ---- tests ---- + +PASS = "✅" +FAIL = "❌" +results = [] + + +def test(name, fn): + try: + fn() + results.append((PASS, name)) + print(f"{PASS} {name}") + except AssertionError as e: + results.append((FAIL, f"{name}: {e}")) + print(f"{FAIL} {name}: {e}") + except Exception as e: + results.append((FAIL, f"{name}: UNEXPECTED {type(e).__name__}: {e}")) + print(f"{FAIL} {name}: UNEXPECTED {type(e).__name__}: {e}") + + +def t_roster_load(): + parts = get_participants() + teams = get_teams() + assert len(parts) == 34, f"기대 34명, 실제 {len(parts)}" + assert len(teams) == 7, f"기대 7팀, 실제 {len(teams)}" + assert "한지승" in parts, "한지승 명단에 없음" + assert "김태현" not in parts, "김태현 진행요원이 명단에 있음" + + +def t_voting_toggle(): + reset_db() + assert is_voting_open() is True, "기본 open" + set_voting_open(False) + assert is_voting_open() is False, "마감 적용 안 됨" + set_voting_open(True) + assert is_voting_open() is True, "재개 적용 안 됨" + + +def t_simple_winners_no_priority_collision(): + """팀1=fun, 팀2=polish, 팀3=utility 각각 명확한 1위. priority 충돌 없음.""" + reset_db() + # 팀1에 5표 (fun), 팀2에 5표 (polish), 팀3에 5표 (utility) + for i in range(5): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀2", "팀3") + winners, _ = compute_winners() + assert winners["fun_team"]["status"] == "ok" + assert winners["fun_team"]["team"] == "팀1" + assert winners["polish_team"]["team"] == "팀2" + assert winners["utility_team"]["team"] == "팀3" + + +def t_priority_one_team_all_first(): + """팀1이 모든 분야 1위 → 팀1은 utility만, polish/fun은 다음 팀.""" + reset_db() + # 팀1에 모든 분야 5표, 팀2에 polish/fun 3표, 팀3에 fun 2표 + for i in range(5): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀1", "팀1") + for i in range(5, 8): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀2", "팀2", "팀1") + for i in range(8, 10): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀3", "팀3", "팀2") + winners, _ = compute_winners() + # 팀1이 utility 1위 → 팀1 수상 + assert winners["utility_team"]["team"] == "팀1", winners["utility_team"] + # 팀1 제외 → polish 1위 = 팀2 (3+5표=8 wait, 팀1=5, 팀2=3+...) 다시: + # polish 분포: 팀1=5, 팀2=3, 팀3=2 → 팀1 제외 후 팀2 + assert winners["polish_team"]["team"] == "팀2", winners["polish_team"] + # fun: 팀1=5, 팀2=3, 팀3=2 → 팀1, 팀2 제외 후 팀3 + assert winners["fun_team"]["team"] == "팀3", winners["fun_team"] + + +def t_tie_pending(): + """팀1, 팀2 utility 동률 → status='tie'.""" + reset_db() + for i in range(3): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1") + for i in range(3, 6): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2") + winners, _ = compute_winners() + assert winners["utility_team"]["status"] == "tie", winners["utility_team"] + assert set(winners["utility_team"]["tied"]) == {"팀1", "팀2"} + + +def t_tie_break_random(): + """동률 → save_tie_break으로 결정 → status='ok'.""" + reset_db() + for i in range(3): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1") + for i in range(3, 6): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2") + save_tie_break("utility_team", "팀1", "random") + winners, _ = compute_winners() + assert winners["utility_team"]["status"] == "ok" + assert winners["utility_team"]["team"] == "팀1" + assert winners["utility_team"]["method"] == "random" + clear_tie_break("utility_team") + + +def t_unique_voter(): + """같은 voter_name 두 번 INSERT 차단.""" + reset_db() + insert_vote("홍길동", "E1", "팀1", "팀2", "팀3", "팀4") + try: + insert_vote("홍길동", "E2", "팀1", "팀5", "팀6", "팀7") + raise AssertionError("중복 INSERT 통과 (UNIQUE 위반 예상)") + except Exception as e: + # IntegrityError 기대 + assert "UNIQUE" in str(e) or "unique" in str(e).lower() + + +def t_titles_persist(): + reset_db() + set_title("팀1", "Slack 자동 분류기") + titles = get_titles() + assert titles["팀1"] == "Slack 자동 분류기" + + +def t_archive_writes_file(): + """ceremony archive — 모든 winners ok 시 파일 생성.""" + reset_db() + # 명확한 1위 만들기 + for i in range(5): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀2", "팀3") + archive_dir = os.path.dirname(TEST_DB) or "." + before = set(f for f in os.listdir(archive_dir) if f.startswith("results_")) + path = archive_results() + assert path is not None, "archive 결과 None" + assert os.path.exists(path) + after = set(f for f in os.listdir(archive_dir) if f.startswith("results_")) + assert len(after) > len(before), "archive 파일 생성 안 됨" + # 정리 + os.unlink(path) + + +def t_archive_skips_when_pending(): + """동률 미해결 시 archive 안 함.""" + reset_db() + for i in range(3): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1") + for i in range(3, 6): + insert_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2") + path = archive_results() + assert path is None, f"동률 미해결인데 archive 됨: {path}" + + +# ---- run ---- + +if __name__ == "__main__": + print(f"# E2E 테스트 (DB={TEST_DB})\n") + test("roster.json 로드 (34명, 7팀)", t_roster_load) + test("투표 마감 토글", t_voting_toggle) + test("기본 winner (priority 충돌 없음)", t_simple_winners_no_priority_collision) + test("priority 1팀 모든 분야 1위", t_priority_one_team_all_first) + test("동률 status='tie'", t_tie_pending) + test("tie_break 적용 시 status='ok'", t_tie_break_random) + test("voter_name UNIQUE 강제", t_unique_voter) + test("팀 제목 영속", t_titles_persist) + test("archive 파일 생성", t_archive_writes_file) + test("archive 동률 미해결 시 skip", t_archive_skips_when_pending) + + fails = sum(1 for r, _ in results if r == FAIL) + print(f"\n# 합계: {len(results)} 중 통과 {len(results) - fails}, 실패 {fails}") + sys.exit(0 if fails == 0 else 1)