Files
hackerthon-vote/tests/e2e.py
th-kim0823 6c6929a505 feat: topics 헬퍼 (get_topics, update_topics)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:48:34 +09:00

304 lines
9.0 KiB
Python

"""
E2E 테스트 — JSON 단일 파일 기반.
실행: docker exec hackathon-vote python3 /tmp/e2e.py
"""
import os
import sys
import tempfile
import shutil
# 격리 데이터 파일 (실제 hackathon.json과 분리)
TEST_DATA = tempfile.mktemp(suffix=".json")
SEED_CANDIDATES = ["/app/data/hackathon.json", "/app/hackathon.json"]
seed = next((p for p in SEED_CANDIDATES if os.path.exists(p)), None)
if seed is None:
raise SystemExit(f"seed 파일 없음: {SEED_CANDIDATES}")
shutil.copy(seed, TEST_DATA)
os.environ["DATA_PATH"] = TEST_DATA
os.environ["ADMIN_TOKEN"] = "test"
sys.path.insert(0, "/app")
from app import ( # noqa: E402
load_data,
save_data,
insert_vote,
list_votes,
clear_votes,
get_participants,
get_teams,
compute_winners,
save_tie_break,
clear_tie_break,
set_voting_open,
is_voting_open,
set_title,
get_titles,
archive_results,
)
def reset_votes():
"""투표만 초기화 (people, settings 보존)."""
data = load_data()
data["votes"] = []
data["tie_breaks"] = {}
data["titles"] = {}
data["settings"]["voting_open"] = True
save_data(data)
def add_vote(name, emp, team, fun, polish, utility):
insert_vote(name, emp, team, {
"fun_team": fun,
"polish_team": polish,
"utility_team": utility,
})
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_load():
parts = get_participants()
teams = get_teams()
assert len(parts) == 34, f"기대 34명, 실제 {len(parts)}"
assert len(teams) == 7
assert "한지승" in parts
assert "김태현" not in parts
def t_voting_toggle():
reset_votes()
assert is_voting_open() is True
set_voting_open(False)
assert is_voting_open() is False
set_voting_open(True)
assert is_voting_open() is True
def t_simple_winners():
reset_votes()
for i in range(5):
add_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():
reset_votes()
for i in range(5):
add_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀1", "팀1")
for i in range(5, 8):
add_vote(f"V{i}", f"E{i}", "팀7", "팀2", "팀2", "팀1")
for i in range(8, 10):
add_vote(f"V{i}", f"E{i}", "팀7", "팀3", "팀3", "팀2")
winners, _ = compute_winners()
assert winners["utility_team"]["team"] == "팀1"
assert winners["polish_team"]["team"] == "팀2"
assert winners["fun_team"]["team"] == "팀3"
def t_tie_pending():
reset_votes()
for i in range(3):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1")
for i in range(3, 6):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2")
winners, _ = compute_winners()
assert winners["utility_team"]["status"] == "tie"
assert set(winners["utility_team"]["tied"]) == {"팀1", "팀2"}
def t_tie_break_random():
reset_votes()
for i in range(3):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1")
for i in range(3, 6):
add_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():
reset_votes()
add_vote("홍길동", "E1", "팀1", "팀2", "팀3", "팀4")
try:
add_vote("홍길동", "E2", "팀1", "팀5", "팀6", "팀7")
raise AssertionError("중복 INSERT 통과")
except ValueError as e:
assert str(e) == "DUPLICATE_VOTER"
def t_titles_persist():
reset_votes()
set_title("팀1", "Slack 자동 분류기")
titles = get_titles()
assert titles["팀1"] == "Slack 자동 분류기"
def t_archive_writes_file():
reset_votes()
for i in range(5):
add_vote(f"V{i}", f"E{i}", "팀7", "팀1", "팀2", "팀3")
archive_dir = os.path.dirname(TEST_DATA) 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)
os.unlink(path)
def t_archive_skip_pending():
reset_votes()
for i in range(3):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀1")
for i in range(3, 6):
add_vote(f"V{i}", f"E{i}", "팀7", "팀5", "팀6", "팀2")
path = archive_results()
assert path is None
def t_atomic_write():
"""save_data 후 load_data가 동일 데이터 반환."""
reset_votes()
data = load_data()
data["titles"]["팀1"] = "test atomic"
save_data(data)
fresh = load_data()
assert fresh["titles"]["팀1"] == "test atomic"
def t_vote_count():
reset_votes()
assert len(list_votes()) == 0
add_vote("A", "1", "팀1", "팀2", "팀3", "팀4")
assert len(list_votes()) == 1
clear_votes()
assert len(list_votes()) == 0
def t_empty_state_has_topics_and_stage():
from app import _empty_state
s = _empty_state()
assert s["settings"]["current_stage"] == "intro"
assert s["topics"] == {"categories": []}
def t_load_data_backfills_nested_settings():
"""기존 settings에 voting_open만 있을 때 current_stage 자동 보강."""
legacy = {
"people": [],
"settings": {"voting_open": True},
"titles": {},
"tie_breaks": {},
"votes": [],
}
save_data(legacy)
fresh = load_data()
assert fresh["settings"]["voting_open"] is True
assert fresh["settings"]["current_stage"] == "intro"
assert fresh["topics"] == {"categories": []}
def t_stage_helpers():
from app import get_stage, set_stage, can_accept_votes, load_data, save_data, _empty_state
save_data(_empty_state())
assert get_stage() == "intro"
assert can_accept_votes(load_data()) is False # intro → False
set_stage("vote")
d = load_data()
assert d["settings"]["current_stage"] == "vote"
assert d["settings"]["voting_open"] is True
assert can_accept_votes(d) is True
set_stage("topics")
d = load_data()
assert d["settings"]["current_stage"] == "topics"
assert d["settings"]["voting_open"] is True # 떠나도 voting_open 유지
assert can_accept_votes(d) is False # stage 다르면 False
# 명시적 마감 후 vote 다시 들어오면 voting_open 자동 True
d["settings"]["voting_open"] = False
save_data(d)
set_stage("vote")
assert can_accept_votes(load_data()) is True
# invalid stage → ValueError
try:
set_stage("invalid_stage")
raise AssertionError("expected ValueError")
except ValueError:
pass
def t_topics_helpers():
from app import get_topics, update_topics, load_data, save_data, _empty_state
save_data(_empty_state())
assert get_topics() == []
sample = [
{"id": "T1", "title": "테스트", "tagline": "tl", "tone": "tn",
"items": [f"item{i}" for i in range(10)]}
]
update_topics(sample)
assert get_topics() == sample
# atomic 갱신 확인
sample2 = [{"id": "T1", "title": "교체", "tagline": "tl", "tone": "tn",
"items": [f"x{i}" for i in range(10)]}]
update_topics(sample2)
assert get_topics() == sample2
if __name__ == "__main__":
print(f"# E2E (data={TEST_DATA})\n")
test("hackathon.json 로드 (34명, 7팀)", t_load)
test("투표 마감 토글", t_voting_toggle)
test("단순 winner", t_simple_winners)
test("priority 1팀 모든 분야 1위", t_priority_one_team_all_first)
test("동률 status='tie'", t_tie_pending)
test("tie_break 적용", 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_skip_pending)
test("atomic write", t_atomic_write)
test("clear_votes", t_vote_count)
test("empty_state 신규 키", t_empty_state_has_topics_and_stage)
test("load_data nested 키 backfill", t_load_data_backfills_nested_settings)
test("stage 헬퍼", t_stage_helpers)
test("topics 헬퍼", t_topics_helpers)
fails = sum(1 for r, _ in results if r == FAIL)
print(f"\n# {len(results)} 중 통과 {len(results) - fails}, 실패 {fails}")
os.unlink(TEST_DATA)
sys.exit(0 if fails == 0 else 1)