174 lines
6.4 KiB
Python
174 lines
6.4 KiB
Python
"""
|
|
utils.holiday_api 단위 테스트.
|
|
|
|
실제 정부 API는 호출하지 않음 — 모두 urlopen mock.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from utils.holiday_api import (
|
|
fetch_korean_holidays, _parse_response, is_configured,
|
|
)
|
|
|
|
|
|
def _ok_response(items):
|
|
"""API 정상 응답 형식 빌드."""
|
|
return {
|
|
'response': {
|
|
'header': {'resultCode': '00', 'resultMsg': 'NORMAL SERVICE.'},
|
|
'body': {
|
|
'items': {'item': items} if items else {'item': []},
|
|
'numOfRows': 100,
|
|
'pageNo': 1,
|
|
'totalCount': len(items) if isinstance(items, list) else 1,
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
class TestParseResponse:
|
|
def test_multiple_items(self):
|
|
items = [
|
|
{'dateKind': '01', 'dateName': '근로자의 날', 'isHoliday': 'Y',
|
|
'locdate': 20260501, 'seq': 1},
|
|
{'dateKind': '01', 'dateName': '어린이날', 'isHoliday': 'Y',
|
|
'locdate': 20260505, 'seq': 1},
|
|
]
|
|
out = _parse_response(_ok_response(items))
|
|
assert len(out) == 2
|
|
assert out[0]['date'] == '2026-05-01'
|
|
assert out[0]['name'] == '근로자의 날'
|
|
assert out[0]['is_holiday'] is True
|
|
assert out[1]['date'] == '2026-05-05'
|
|
|
|
def test_single_item_as_dict(self):
|
|
# API가 결과 1개일 때 list가 아닌 dict로 반환하는 케이스
|
|
item = {'dateName': '근로자의 날', 'isHoliday': 'Y', 'locdate': 20260501}
|
|
data = {
|
|
'response': {
|
|
'header': {'resultCode': '00'},
|
|
'body': {'items': {'item': item}, 'totalCount': 1},
|
|
}
|
|
}
|
|
out = _parse_response(data)
|
|
assert len(out) == 1
|
|
assert out[0]['name'] == '근로자의 날'
|
|
|
|
def test_empty_year(self):
|
|
# totalCount=0 같은 정상 빈 응답
|
|
data = {
|
|
'response': {
|
|
'header': {'resultCode': '00'},
|
|
'body': {'items': '', 'totalCount': 0},
|
|
}
|
|
}
|
|
assert _parse_response(data) == []
|
|
|
|
def test_error_result_code(self):
|
|
data = {
|
|
'response': {
|
|
'header': {'resultCode': '30', 'resultMsg': 'SERVICE_KEY_IS_NOT_REGISTERED'},
|
|
'body': {},
|
|
}
|
|
}
|
|
assert _parse_response(data) is None
|
|
|
|
def test_isholiday_n_filtered_at_caller_level(self):
|
|
# 응답 자체엔 is_holiday=False도 포함됨 (예: 24절기). _parse는 그대로 반환,
|
|
# 실제 휴일 등록은 호출자가 is_holiday=True만 필터.
|
|
items = [{'dateName': '동지', 'isHoliday': 'N', 'locdate': 20261221}]
|
|
out = _parse_response(_ok_response(items))
|
|
assert len(out) == 1
|
|
assert out[0]['is_holiday'] is False
|
|
|
|
def test_locdate_str_form(self):
|
|
items = [{'dateName': '근로자의 날', 'isHoliday': 'Y', 'locdate': '20260501'}]
|
|
out = _parse_response(_ok_response(items))
|
|
assert out[0]['date'] == '2026-05-01'
|
|
|
|
def test_invalid_locdate_skipped(self):
|
|
items = [
|
|
{'dateName': '근로자의 날', 'isHoliday': 'Y', 'locdate': 20260501},
|
|
{'dateName': '잘못된 날짜', 'isHoliday': 'Y', 'locdate': 'abc'},
|
|
{'dateName': '짧은 날짜', 'isHoliday': 'Y', 'locdate': '202605'},
|
|
]
|
|
out = _parse_response(_ok_response(items))
|
|
assert len(out) == 1 # 정상 1개만
|
|
assert out[0]['name'] == '근로자의 날'
|
|
|
|
def test_missing_required_fields_skipped(self):
|
|
items = [
|
|
{'dateName': '근로자의 날', 'isHoliday': 'Y', 'locdate': 20260501},
|
|
{'isHoliday': 'Y', 'locdate': 20260505}, # name 없음
|
|
{'dateName': '신정', 'isHoliday': 'Y'}, # locdate 없음
|
|
]
|
|
out = _parse_response(_ok_response(items))
|
|
assert len(out) == 1
|
|
|
|
def test_malformed_response_returns_none(self):
|
|
# response 구조 자체가 깨진 경우
|
|
assert _parse_response({'random': 'data'}) is None or _parse_response({'random': 'data'}) == []
|
|
# 위는 implementation-dependent — 둘 다 합리적
|
|
# 정확히는: response 키 없음 → response={}, header={}, resultCode != '00' → None
|
|
assert _parse_response({}) is None
|
|
|
|
|
|
class TestFetchNetwork:
|
|
@patch('utils.holiday_api.urllib.request.urlopen')
|
|
def test_success(self, mock_urlopen):
|
|
items = [{'dateName': '근로자의 날', 'isHoliday': 'Y', 'locdate': 20260501}]
|
|
resp = MagicMock()
|
|
resp.read.return_value = json.dumps(_ok_response(items)).encode('utf-8')
|
|
resp.__enter__.return_value = resp
|
|
resp.__exit__.return_value = False
|
|
mock_urlopen.return_value = resp
|
|
|
|
out = fetch_korean_holidays(2026)
|
|
assert out is not None
|
|
assert len(out) == 1
|
|
assert out[0]['date'] == '2026-05-01'
|
|
|
|
# 요청 URL에 serviceKey + solYear=2026 + _type=json 포함되었는지
|
|
req = mock_urlopen.call_args[0][0]
|
|
assert 'serviceKey=' in req.full_url
|
|
assert 'solYear=2026' in req.full_url
|
|
assert '_type=json' in req.full_url
|
|
|
|
@patch('utils.holiday_api.urllib.request.urlopen')
|
|
def test_network_error_returns_none(self, mock_urlopen):
|
|
import urllib.error
|
|
mock_urlopen.side_effect = urllib.error.URLError('boom')
|
|
assert fetch_korean_holidays(2026) is None
|
|
|
|
@patch('utils.holiday_api.urllib.request.urlopen')
|
|
def test_timeout_returns_none(self, mock_urlopen):
|
|
mock_urlopen.side_effect = TimeoutError('slow')
|
|
assert fetch_korean_holidays(2026) is None
|
|
|
|
@patch('utils.holiday_api.urllib.request.urlopen')
|
|
def test_invalid_json_returns_none(self, mock_urlopen):
|
|
resp = MagicMock()
|
|
resp.read.return_value = b'<html>error</html>'
|
|
resp.__enter__.return_value = resp
|
|
resp.__exit__.return_value = False
|
|
mock_urlopen.return_value = resp
|
|
assert fetch_korean_holidays(2026) is None
|
|
|
|
|
|
class TestConfigured:
|
|
def test_key_set(self, monkeypatch):
|
|
import utils.holiday_api as _ha
|
|
monkeypatch.setattr(_ha, '_SERVICE_KEY', 'fa419259319e31d2fcd4f959e65da817fe2f19894bff340a63889db7a8ffac93')
|
|
assert _ha.is_configured() is True
|
|
|
|
def test_key_empty(self, monkeypatch):
|
|
import utils.holiday_api as _ha
|
|
monkeypatch.setattr(_ha, '_SERVICE_KEY', '')
|
|
assert _ha.is_configured() is False
|