"""Tests for duty_teller.handlers.import_duty_schedule (import_duty_schedule_cmd, handover_time, document).""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from duty_teller.handlers import import_duty_schedule as mod from duty_teller.importers.duty_schedule import ( DutyScheduleParseError, DutyScheduleResult, ) from datetime import date def _make_update(message=None, effective_user=None): """Build minimal Update with message and effective_user.""" update = MagicMock() update.message = message update.effective_user = effective_user return update def _make_user(user_id=1, first_name="Admin", last_name="User", username="admin"): """Build minimal Telegram user.""" u = MagicMock() u.id = user_id u.first_name = first_name u.last_name = last_name u.username = username return u # --- import_duty_schedule_cmd --- @pytest.mark.asyncio async def test_import_duty_schedule_cmd_no_message_returns_early(): """import_duty_schedule_cmd: no update.message -> no reply, no user_data.""" update = _make_update(message=None, effective_user=_make_user()) context = MagicMock() context.user_data = {} await mod.import_duty_schedule_cmd(update, context) assert not context.user_data @pytest.mark.asyncio async def test_import_duty_schedule_cmd_no_effective_user_returns_early(): """import_duty_schedule_cmd: no effective_user -> no reply, no user_data.""" message = MagicMock() message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=None) context = MagicMock() context.user_data = {} await mod.import_duty_schedule_cmd(update, context) assert not context.user_data message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_import_duty_schedule_cmd_not_admin_replies_admin_only(): """import_duty_schedule_cmd: non-admin -> reply_text(import.admin_only).""" message = MagicMock() message.reply_text = AsyncMock() user = _make_user(42) update = _make_update(message=message, effective_user=user) context = MagicMock() context.user_data = {} with patch.object( mod, "is_admin_async", new_callable=AsyncMock, return_value=False ): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "t") as mock_t: mock_t.return_value = "Admin only" await mod.import_duty_schedule_cmd(update, context) message.reply_text.assert_called_once_with("Admin only") mock_t.assert_called_with("en", "import.admin_only") assert not context.user_data.get("awaiting_handover_time") @pytest.mark.asyncio async def test_import_duty_schedule_cmd_admin_sets_user_data_and_replies_handover_format(): """import_duty_schedule_cmd: admin -> user_data awaiting_handover_time=True, reply handover_format.""" message = MagicMock() message.reply_text = AsyncMock() user = _make_user(42) update = _make_update(message=message, effective_user=user) context = MagicMock() context.user_data = {} with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "t") as mock_t: mock_t.return_value = "Enter time e.g. 09:00 Europe/Moscow" await mod.import_duty_schedule_cmd(update, context) assert context.user_data["awaiting_handover_time"] is True message.reply_text.assert_called_once_with("Enter time e.g. 09:00 Europe/Moscow") mock_t.assert_called_with("en", "import.handover_format") # --- handle_handover_time_text --- @pytest.mark.asyncio async def test_handle_handover_time_text_no_message_returns_early(): """handle_handover_time_text: no message -> no reply.""" update = _make_update(message=None, effective_user=_make_user()) context = MagicMock() context.user_data = {"awaiting_handover_time": True} await mod.handle_handover_time_text(update, context) # Handler returns early; update.message is None so we only check user_data unchanged. assert context.user_data["awaiting_handover_time"] is True @pytest.mark.asyncio async def test_handle_handover_time_text_no_awaiting_flag_returns_early(): """handle_handover_time_text: not awaiting_handover_time -> no reply.""" message = MagicMock() message.text = "09:00 Europe/Moscow" message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = {} with patch.object(mod, "parse_handover_time"): await mod.handle_handover_time_text(update, context) message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_handle_handover_time_text_not_admin_returns_early(): """handle_handover_time_text: not admin -> no reply.""" message = MagicMock() message.text = "09:00 Europe/Moscow" message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = {"awaiting_handover_time": True} with patch.object( mod, "is_admin_async", new_callable=AsyncMock, return_value=False ): with patch.object(mod, "parse_handover_time"): await mod.handle_handover_time_text(update, context) message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_handle_handover_time_text_parse_error_replies_parse_time_error(): """handle_handover_time_text: parse_handover_time returns None -> reply parse_time_error.""" message = MagicMock() message.text = "invalid" message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = {"awaiting_handover_time": True} with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "parse_handover_time", return_value=None): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "t") as mock_t: mock_t.return_value = "Could not parse time" await mod.handle_handover_time_text(update, context) message.reply_text.assert_called_once_with("Could not parse time") mock_t.assert_called_with("en", "import.parse_time_error") assert context.user_data.get("awaiting_handover_time") is True @pytest.mark.asyncio async def test_handle_handover_time_text_success_sets_user_data_and_replies_send_json(): """handle_handover_time_text: parsed time -> user_data updated, reply send_json.""" message = MagicMock() message.text = " 09:00 Europe/Moscow " message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = {"awaiting_handover_time": True} with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "parse_handover_time", return_value=(6, 0)): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "t") as mock_t: mock_t.return_value = "Send JSON file" await mod.handle_handover_time_text(update, context) assert context.user_data["handover_utc_time"] == (6, 0) assert context.user_data["awaiting_handover_time"] is False assert context.user_data["awaiting_duty_schedule_file"] is True message.reply_text.assert_called_once_with("Send JSON file") mock_t.assert_called_with("en", "import.send_json") # --- handle_duty_schedule_document --- def _make_document(file_id="file-123", file_name="schedule.json"): """Build minimal Document with file_id and file_name.""" doc = MagicMock() doc.file_id = file_id doc.file_name = file_name return doc @pytest.mark.asyncio async def test_handle_duty_schedule_document_no_document_returns_early(): """handle_duty_schedule_document: no document -> no import.""" message = MagicMock() message.document = None update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = { "awaiting_duty_schedule_file": True, "handover_utc_time": (6, 0), } await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_handle_duty_schedule_document_not_awaiting_file_returns_early(): """handle_duty_schedule_document: not awaiting_duty_schedule_file -> no import.""" message = MagicMock() message.document = _make_document() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = {} await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_handle_duty_schedule_document_no_handover_time_returns_early(): """handle_duty_schedule_document: no handover_utc_time -> no import.""" message = MagicMock() message.document = _make_document() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = {"awaiting_duty_schedule_file": True} await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_handle_duty_schedule_document_not_admin_returns_early(): """handle_duty_schedule_document: not admin -> no import.""" message = MagicMock() message.document = _make_document() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = { "awaiting_duty_schedule_file": True, "handover_utc_time": (6, 0), } with patch.object( mod, "is_admin_async", new_callable=AsyncMock, return_value=False ): await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_handle_duty_schedule_document_non_json_replies_need_json(): """handle_duty_schedule_document: file not .json -> reply need_json.""" message = MagicMock() message.document = _make_document(file_name="data.txt") message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = { "awaiting_duty_schedule_file": True, "handover_utc_time": (6, 0), } with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "t") as mock_t: mock_t.return_value = "File must be .json" await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_called_once_with("File must be .json") mock_t.assert_called_with("en", "import.need_json") @pytest.mark.asyncio async def test_handle_duty_schedule_document_parse_error_replies_generic_and_clears_user_data(): """handle_duty_schedule_document: DutyScheduleParseError -> reply generic message (no str(e)), clear user_data.""" message = MagicMock() message.document = _make_document() message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = { "awaiting_duty_schedule_file": True, "handover_utc_time": (6, 0), } context.bot = MagicMock() mock_file = MagicMock() mock_file.download_as_bytearray = AsyncMock(return_value=bytearray(b"invalid")) context.bot.get_file = AsyncMock(return_value=mock_file) with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "parse_duty_schedule") as mock_parse: mock_parse.side_effect = DutyScheduleParseError("Bad JSON") with patch.object(mod, "t") as mock_t: mock_t.return_value = "The file could not be parsed." await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_called_once_with("The file could not be parsed.") mock_t.assert_called_with("en", "import.parse_error_generic") assert "awaiting_duty_schedule_file" not in context.user_data assert "handover_utc_time" not in context.user_data @pytest.mark.asyncio async def test_handle_duty_schedule_document_import_error_replies_generic_and_clears_user_data(): """handle_duty_schedule_document: run_import raises -> reply generic message (no str(e)), clear user_data.""" message = MagicMock() message.document = _make_document() message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = { "awaiting_duty_schedule_file": True, "handover_utc_time": (6, 0), } mock_file = MagicMock() mock_file.download_as_bytearray = AsyncMock(return_value=bytearray(b"{}")) context.bot = MagicMock() context.bot.get_file = AsyncMock(return_value=mock_file) result = DutyScheduleResult( start_date=date(2026, 2, 1), end_date=date(2026, 2, 28), entries=[], ) with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "parse_duty_schedule", return_value=result): with patch.object(mod, "session_scope") as mock_scope: mock_session = MagicMock() mock_scope.return_value.__enter__.return_value = mock_session mock_scope.return_value.__exit__.return_value = None with patch.object(mod, "run_import") as mock_run: mock_run.side_effect = ValueError("DB error") with patch.object(mod, "t") as mock_t: mock_t.return_value = "Import failed. Please try again." await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_called_once_with("Import failed. Please try again.") mock_t.assert_called_with("en", "import.import_error_generic") assert "awaiting_duty_schedule_file" not in context.user_data assert "handover_utc_time" not in context.user_data @pytest.mark.asyncio async def test_handle_duty_schedule_document_success_replies_done_and_clears_user_data(): """handle_duty_schedule_document: successful import -> reply done, clear user_data.""" message = MagicMock() message.document = _make_document() message.reply_text = AsyncMock() update = _make_update(message=message, effective_user=_make_user()) context = MagicMock() context.user_data = { "awaiting_duty_schedule_file": True, "handover_utc_time": (6, 0), } mock_file = MagicMock() mock_file.download_as_bytearray = AsyncMock(return_value=bytearray(b"{}")) context.bot = MagicMock() context.bot.get_file = AsyncMock(return_value=mock_file) result = DutyScheduleResult( start_date=date(2026, 2, 1), end_date=date(2026, 2, 28), entries=[], ) with patch.object(mod, "is_admin_async", new_callable=AsyncMock, return_value=True): with patch.object(mod, "get_lang", return_value="en"): with patch.object(mod, "parse_duty_schedule", return_value=result): with patch.object(mod, "session_scope") as mock_scope: mock_session = MagicMock() mock_scope.return_value.__enter__.return_value = mock_session mock_scope.return_value.__exit__.return_value = None with patch.object(mod, "run_import", return_value=(3, 5, 0, 1)): with patch.object(mod, "t") as mock_t: mock_t.side_effect = lambda lang, key, **kw: ( "Done: 3 users, 5 duties, 1 vacation (6 total)" if key == "import.done" else "" ) await mod.handle_duty_schedule_document(update, context) message.reply_text.assert_called_once() assert "done" in message.reply_text.call_args[0][0].lower() or "import.done" in str( message.reply_text.call_args ) assert "awaiting_duty_schedule_file" not in context.user_data assert "handover_utc_time" not in context.user_data