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:
223
tests/e2e.py
Normal file
223
tests/e2e.py
Normal 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)
|
||||
Reference in New Issue
Block a user