test: E2E 테스트 10개 (핵심 비즈니스 로직)

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) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-04-26 17:59:30 +09:00
parent bf4d3e73cc
commit 6cfc75e3b8

223
tests/e2e.py Normal file
View File

@@ -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)