""" 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'