Clock_out_Time_Calculator/tests/test_crash_handler.py
KINDNICK ff71886fd7 v2.7.0: i18n 100% + 런타임 retranslate + 테스트 +47 + 폴리싱
- i18n 사전 100% (break/overtime/leave/clockin) — 50+ 신규 키
- 런타임 재번역 인프라 (ui/i18n_runtime.py) — 재시작 없이 메인 UI 적용
- MealController 분리 — 점심/저녁 토글을 컨트롤러로 추출
- 통합 테스트 +15 (S36-S52: 온보딩/salary/CSV/notification dedupe 등)
- pytest 신규 4종 + i18n_runtime 테스트 (총 122 케이스, 90→122)
- README/INSTALL/CLAUDE/AGENTS v2.6+ 아키텍처 반영
2026-04-30 19:30:47 +09:00

111 lines
3.5 KiB
Python

"""
utils.crash_handler 단위 테스트.
GUI 다이얼로그는 호출하지 않음 (테스트 주체는 _log_crash + _send_to_gitea).
"""
import os
import sys
import tempfile
from unittest.mock import patch, MagicMock
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.database import Database
from utils.crash_handler import _log_crash, _send_to_gitea
@pytest.fixture
def db():
p = os.path.join(tempfile.gettempdir(), 'clockout_crash_ut.db')
if os.path.exists(p):
os.remove(p)
d = Database(p)
yield d
try:
os.remove(p)
except OSError:
pass
class TestLogCrash:
def test_creates_table_and_inserts(self, db):
_log_crash(db, 'TestExc', 'msg', 'Traceback ...', 'v2.6.0')
conn = db.get_connection()
cur = conn.cursor()
cur.execute("SELECT exception_type, message, app_version FROM crash_log")
row = cur.fetchone()
conn.close()
assert row[0] == 'TestExc'
assert row[1] == 'msg'
assert row[2] == 'v2.6.0'
def test_table_idempotent_creation(self, db):
# 두 번 호출해도 두 행이 들어가야 (CREATE TABLE IF NOT EXISTS)
_log_crash(db, 'A', 'a', 't', 'v1')
_log_crash(db, 'B', 'b', 't', 'v1')
conn = db.get_connection()
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM crash_log")
count = cur.fetchone()[0]
conn.close()
assert count == 2
def test_silent_on_db_error(self, db):
# 잘못된 DB 객체를 줘도 예외 전파 안 됨 (안 그러면 후킹 자체가 죽음)
broken = MagicMock()
broken.get_connection.side_effect = RuntimeError('boom')
# raise되면 안 됨
_log_crash(broken, 'X', 'x', 'tb', 'v')
class TestSendToGitea:
@patch('utils.crash_handler.urllib.request.urlopen')
def test_success(self, mock_urlopen):
resp = MagicMock()
resp.status = 201
resp.__enter__.return_value = resp
resp.__exit__.return_value = False
mock_urlopen.return_value = resp
ok = _send_to_gitea('fake_token', 'title', 'body')
assert ok is True
req = mock_urlopen.call_args[0][0]
# PAT 헤더 확인
assert req.headers.get('Authorization') == 'token fake_token'
# User-Agent 위장
assert 'Mozilla' in req.headers.get('User-agent', '')
@patch('utils.crash_handler.urllib.request.urlopen')
def test_4xx_returns_false(self, mock_urlopen):
resp = MagicMock()
resp.status = 401
resp.__enter__.return_value = resp
resp.__exit__.return_value = False
mock_urlopen.return_value = resp
assert _send_to_gitea('bad', 't', 'b') is False
@patch('utils.crash_handler.urllib.request.urlopen')
def test_network_error(self, mock_urlopen):
import urllib.error
mock_urlopen.side_effect = urllib.error.URLError('boom')
assert _send_to_gitea('t', 't', 'b') is False
@patch('utils.crash_handler.urllib.request.urlopen')
def test_payload_json(self, mock_urlopen):
import json as _json
resp = MagicMock()
resp.status = 201
resp.__enter__.return_value = resp
resp.__exit__.return_value = False
mock_urlopen.return_value = resp
_send_to_gitea('tok', 'TITLE', 'BODY')
req = mock_urlopen.call_args[0][0]
body = _json.loads(req.data.decode('utf-8'))
assert body['title'] == 'TITLE'
assert body['body'] == 'BODY'