""" utils.csv_importer 단위 테스트. """ import os import sys import tempfile from datetime import datetime from pathlib import Path import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from utils.csv_importer import parse_csv, _normalize_row, _normalize_time, import_records from core.database import Database class TestNormalizeTime: def test_hh_mm_to_hh_mm_ss(self): assert _normalize_time('09:00', 'clock_in') == '09:00:00' def test_hh_mm_ss_unchanged(self): assert _normalize_time('09:00:00', 'clock_in') == '09:00:00' def test_empty_raises(self): with pytest.raises(ValueError): _normalize_time('', 'clock_in') def test_invalid_format_raises(self): with pytest.raises(ValueError): _normalize_time('foo', 'clock_in') with pytest.raises(ValueError): _normalize_time('25:00', 'clock_in') class TestNormalizeRow: def test_basic_row(self): row = { 'date': '2026-04-01', 'clock_in': '09:00', 'clock_out': '18:00', 'lunch_minutes': '60', 'memo': '메모', } out = _normalize_row(row) assert out['date'] == '2026-04-01' assert out['clock_in'] == '09:00:00' assert out['clock_out'] == '18:00:00' assert out['lunch_minutes'] == 60 assert out['memo'] == '메모' def test_optional_clock_out(self): row = {'date': '2026-04-01', 'clock_in': '09:00', 'clock_out': '', 'lunch_minutes': '0', 'memo': ''} out = _normalize_row(row) assert out['clock_out'] is None def test_invalid_date(self): row = {'date': 'not-a-date', 'clock_in': '09:00', 'clock_out': '', 'lunch_minutes': '0', 'memo': ''} with pytest.raises(ValueError): _normalize_row(row) def test_negative_lunch_minutes(self): row = {'date': '2026-04-01', 'clock_in': '09:00', 'clock_out': '', 'lunch_minutes': '-30', 'memo': ''} with pytest.raises(ValueError): _normalize_row(row) class TestParseCsv: def _write(self, content: str) -> str: f = tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, suffix='.csv', newline='') f.write(content) f.close() return f.name def test_valid_csv(self): path = self._write( "date,clock_in,clock_out,lunch_minutes,memo\n" "2026-04-01,09:00,18:00,60,첫째날\n" "2026-04-02,09:30:00,17:30:00,30,단축\n" ) try: rows = parse_csv(path) assert len(rows) == 2 assert rows[0]['lunch_minutes'] == 60 assert rows[1]['memo'] == '단축' finally: os.remove(path) def test_utf8_bom(self): # 엑셀 저장본 호환 path = self._write('\ufeff' + "date,clock_in,clock_out,lunch_minutes,memo\n" "2026-04-01,09:00,18:00,60,첫째날\n" ) try: rows = parse_csv(path) assert len(rows) == 1 finally: os.remove(path) def test_missing_required_header(self): path = self._write("date,memo\n2026-04-01,foo\n") try: with pytest.raises(ValueError) as exc: parse_csv(path) assert 'clock_in' in str(exc.value) finally: os.remove(path) def test_file_not_found(self): with pytest.raises(FileNotFoundError): parse_csv('/nonexistent/file.csv') def test_line_number_in_error(self): path = self._write( "date,clock_in,clock_out,lunch_minutes,memo\n" "2026-04-01,09:00,18:00,60,ok\n" "bad-date,09:00,18:00,60,broken\n" ) try: with pytest.raises(ValueError) as exc: parse_csv(path) assert '줄 3' in str(exc.value) finally: os.remove(path) class TestImportRecords: def _db(self): p = tempfile.NamedTemporaryFile(delete=False, suffix='.db') p.close() db = Database(p.name) db.save_settings({ 'work_minutes': '480', 'lunch_duration_minutes': '60', 'dinner_duration_minutes': '60', }) return db, p.name def test_overwrite_clears_overtime_usage(self): """CSV 덮어쓰기 시 overtime_usage도 삭제되어 잔액이 일관성을 유지해야 함.""" db, path = self._db() try: date_str = '2026-04-01' # 기존 기록 + 연장근무 사용 기록 생성 wid = db.add_work_record(date_str, '09:00:00') db.update_clock_out(date_str, '20:00:00', 11.0, 120, 120) db.add_overtime_earned(wid, 120, date_str) db.add_overtime_usage(wid, 30, date_str, '테스트') # 덮어쓰기 전 잔액 balance_before = db.get_total_overtime_balance() assert balance_before == 90 # 120 적립 - 30 사용 rows = [{ 'date': date_str, 'clock_in': '09:00:00', 'clock_out': '18:00:00', 'lunch_minutes': 60, 'dinner_minutes': 0, 'memo': '', }] import_records(db, rows, on_conflict='overwrite') # 덮어쓰기 후 연장근무 사용 기록은 삭제되어야 함 with db._conn() as conn: cur = conn.cursor() cur.execute("SELECT COUNT(*) FROM overtime_usage WHERE date = ?", (date_str,)) assert cur.fetchone()[0] == 0 balance_after = db.get_total_overtime_balance() assert balance_after == 0 # 새 기록은 연장근무 없음 finally: try: os.remove(path) except OSError: pass