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