#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2026 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import datetime as dtm from zoneinfo import ZoneInfo import pytest from telegram import ( BusinessConnection, BusinessIntro, BusinessLocation, BusinessMessagesDeleted, BusinessOpeningHours, BusinessOpeningHoursInterval, Chat, Location, Sticker, User, ) from telegram._business import BusinessBotRights from telegram._utils.datetime import UTC, to_timestamp from tests.auxil.slots import mro_slots class BusinessTestBase: id_ = "123" user = User(123, "test_user", False) user_chat_id = 123 date = dtm.datetime.now(tz=UTC).replace(microsecond=0) can_change_gift_settings = True can_convert_gifts_to_stars = True can_delete_all_messages = True can_delete_sent_messages = True can_edit_bio = True can_edit_name = True can_edit_profile_photo = True can_edit_username = True can_manage_stories = True can_read_messages = True can_reply = True can_transfer_and_upgrade_gifts = True can_transfer_stars = True can_view_gifts_and_stars = True is_enabled = True message_ids = (123, 321) business_connection_id = "123" chat = Chat(123, "test_chat") title = "Business Title" message = "Business description" sticker = Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR) address = "address" location = Location(-23.691288, 46.788279) opening_minute = 0 closing_minute = 60 time_zone_name = "Country/City" opening_hours = [ BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60) ] @pytest.fixture(scope="module") def business_bot_rights(): return BusinessBotRights( can_change_gift_settings=BusinessTestBase.can_change_gift_settings, can_convert_gifts_to_stars=BusinessTestBase.can_convert_gifts_to_stars, can_delete_all_messages=BusinessTestBase.can_delete_all_messages, can_delete_sent_messages=BusinessTestBase.can_delete_sent_messages, can_edit_bio=BusinessTestBase.can_edit_bio, can_edit_name=BusinessTestBase.can_edit_name, can_edit_profile_photo=BusinessTestBase.can_edit_profile_photo, can_edit_username=BusinessTestBase.can_edit_username, can_manage_stories=BusinessTestBase.can_manage_stories, can_read_messages=BusinessTestBase.can_read_messages, can_reply=BusinessTestBase.can_reply, can_transfer_and_upgrade_gifts=BusinessTestBase.can_transfer_and_upgrade_gifts, can_transfer_stars=BusinessTestBase.can_transfer_stars, can_view_gifts_and_stars=BusinessTestBase.can_view_gifts_and_stars, ) @pytest.fixture(scope="module") def business_connection(business_bot_rights): return BusinessConnection( BusinessTestBase.id_, BusinessTestBase.user, BusinessTestBase.user_chat_id, BusinessTestBase.date, BusinessTestBase.is_enabled, rights=business_bot_rights, ) @pytest.fixture(scope="module") def business_messages_deleted(): return BusinessMessagesDeleted( BusinessTestBase.business_connection_id, BusinessTestBase.chat, BusinessTestBase.message_ids, ) @pytest.fixture(scope="module") def business_intro(): return BusinessIntro( BusinessTestBase.title, BusinessTestBase.message, BusinessTestBase.sticker, ) @pytest.fixture(scope="module") def business_location(): return BusinessLocation( BusinessTestBase.address, BusinessTestBase.location, ) @pytest.fixture(scope="module") def business_opening_hours_interval(): return BusinessOpeningHoursInterval( BusinessTestBase.opening_minute, BusinessTestBase.closing_minute, ) @pytest.fixture(scope="module") def business_opening_hours(): return BusinessOpeningHours( BusinessTestBase.time_zone_name, BusinessTestBase.opening_hours, ) class TestBusinessBotRightsWithoutRequest(BusinessTestBase): def test_slot_behaviour(self, business_bot_rights): inst = business_bot_rights for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" def test_to_dict(self, business_bot_rights): rights_dict = business_bot_rights.to_dict() assert isinstance(rights_dict, dict) assert rights_dict["can_reply"] is self.can_reply assert rights_dict["can_read_messages"] is self.can_read_messages assert rights_dict["can_delete_sent_messages"] is self.can_delete_sent_messages assert rights_dict["can_delete_all_messages"] is self.can_delete_all_messages assert rights_dict["can_edit_name"] is self.can_edit_name assert rights_dict["can_edit_bio"] is self.can_edit_bio assert rights_dict["can_edit_profile_photo"] is self.can_edit_profile_photo assert rights_dict["can_edit_username"] is self.can_edit_username assert rights_dict["can_change_gift_settings"] is self.can_change_gift_settings assert rights_dict["can_view_gifts_and_stars"] is self.can_view_gifts_and_stars assert rights_dict["can_convert_gifts_to_stars"] is self.can_convert_gifts_to_stars assert rights_dict["can_transfer_and_upgrade_gifts"] is self.can_transfer_and_upgrade_gifts assert rights_dict["can_transfer_stars"] is self.can_transfer_stars assert rights_dict["can_manage_stories"] is self.can_manage_stories def test_de_json(self): json_dict = { "can_reply": self.can_reply, "can_read_messages": self.can_read_messages, "can_delete_sent_messages": self.can_delete_sent_messages, "can_delete_all_messages": self.can_delete_all_messages, "can_edit_name": self.can_edit_name, "can_edit_bio": self.can_edit_bio, "can_edit_profile_photo": self.can_edit_profile_photo, "can_edit_username": self.can_edit_username, "can_change_gift_settings": self.can_change_gift_settings, "can_view_gifts_and_stars": self.can_view_gifts_and_stars, "can_convert_gifts_to_stars": self.can_convert_gifts_to_stars, "can_transfer_and_upgrade_gifts": self.can_transfer_and_upgrade_gifts, "can_transfer_stars": self.can_transfer_stars, "can_manage_stories": self.can_manage_stories, } rights = BusinessBotRights.de_json(json_dict, None) assert rights.can_reply is self.can_reply assert rights.can_read_messages is self.can_read_messages assert rights.can_delete_sent_messages is self.can_delete_sent_messages assert rights.can_delete_all_messages is self.can_delete_all_messages assert rights.can_edit_name is self.can_edit_name assert rights.can_edit_bio is self.can_edit_bio assert rights.can_edit_profile_photo is self.can_edit_profile_photo assert rights.can_edit_username is self.can_edit_username assert rights.can_change_gift_settings is self.can_change_gift_settings assert rights.can_view_gifts_and_stars is self.can_view_gifts_and_stars assert rights.can_convert_gifts_to_stars is self.can_convert_gifts_to_stars assert rights.can_transfer_and_upgrade_gifts is self.can_transfer_and_upgrade_gifts assert rights.can_transfer_stars is self.can_transfer_stars assert rights.can_manage_stories is self.can_manage_stories assert rights.api_kwargs == {} assert isinstance(rights, BusinessBotRights) def test_equality(self): rights1 = BusinessBotRights( can_reply=self.can_reply, ) rights2 = BusinessBotRights( can_reply=True, ) rights3 = BusinessBotRights( can_reply=True, can_read_messages=self.can_read_messages, ) assert rights1 == rights2 assert hash(rights1) == hash(rights2) assert rights1 is not rights2 assert rights1 != rights3 assert hash(rights1) != hash(rights3) class TestBusinessConnectionWithoutRequest(BusinessTestBase): def test_slots(self, business_connection): bc = business_connection for attr in bc.__slots__: assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot" def test_de_json(self, business_bot_rights): json_dict = { "id": self.id_, "user": self.user.to_dict(), "user_chat_id": self.user_chat_id, "date": to_timestamp(self.date), "is_enabled": self.is_enabled, "rights": business_bot_rights.to_dict(), } bc = BusinessConnection.de_json(json_dict, None) assert bc.id == self.id_ assert bc.user == self.user assert bc.user_chat_id == self.user_chat_id assert bc.date == self.date assert bc.is_enabled == self.is_enabled assert bc.rights == business_bot_rights assert bc.api_kwargs == {} assert isinstance(bc, BusinessConnection) def test_de_json_localization(self, offline_bot, raw_bot, tz_bot, business_bot_rights): json_dict = { "id": self.id_, "user": self.user.to_dict(), "user_chat_id": self.user_chat_id, "date": to_timestamp(self.date), "is_enabled": self.is_enabled, "rights": business_bot_rights.to_dict(), } chat_bot = BusinessConnection.de_json(json_dict, offline_bot) chat_bot_raw = BusinessConnection.de_json(json_dict, raw_bot) chat_bot_tz = BusinessConnection.de_json(json_dict, tz_bot) # comparing utcoffsets because comparing tzinfo objects is not reliable date_offset = chat_bot_tz.date.utcoffset() date_offset_tz = tz_bot.defaults.tzinfo.utcoffset(chat_bot_tz.date.replace(tzinfo=None)) assert chat_bot.date.tzinfo == UTC assert chat_bot_raw.date.tzinfo == UTC assert date_offset_tz == date_offset def test_to_dict(self, business_connection, business_bot_rights): bc_dict = business_connection.to_dict() assert isinstance(bc_dict, dict) assert bc_dict["id"] == self.id_ assert bc_dict["user"] == self.user.to_dict() assert bc_dict["user_chat_id"] == self.user_chat_id assert bc_dict["date"] == to_timestamp(self.date) assert bc_dict["is_enabled"] == self.is_enabled assert bc_dict["rights"] == business_bot_rights.to_dict() def test_equality(self, business_bot_rights): bc1 = BusinessConnection( self.id_, self.user, self.user_chat_id, self.date, self.is_enabled, rights=business_bot_rights, ) bc2 = BusinessConnection( self.id_, self.user, self.user_chat_id, self.date, self.is_enabled, rights=business_bot_rights, ) bc3 = BusinessConnection( "321", self.user, self.user_chat_id, self.date, self.is_enabled, rights=business_bot_rights, ) bc4 = BusinessConnection( self.id_, self.user, self.user_chat_id, self.date, self.is_enabled, rights=BusinessBotRights(), ) assert bc1 == bc2 assert hash(bc1) == hash(bc2) assert bc1 != bc3 assert hash(bc1) != hash(bc3) assert bc1 != bc4 assert hash(bc1) != hash(bc4) class TestBusinessMessagesDeleted(BusinessTestBase): def test_slots(self, business_messages_deleted): bmd = business_messages_deleted for attr in bmd.__slots__: assert getattr(bmd, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(bmd)) == len(set(mro_slots(bmd))), "duplicate slot" def test_to_dict(self, business_messages_deleted): bmd_dict = business_messages_deleted.to_dict() assert isinstance(bmd_dict, dict) assert bmd_dict["message_ids"] == list(self.message_ids) assert bmd_dict["business_connection_id"] == self.business_connection_id assert bmd_dict["chat"] == self.chat.to_dict() def test_de_json(self): json_dict = { "business_connection_id": self.business_connection_id, "chat": self.chat.to_dict(), "message_ids": self.message_ids, } bmd = BusinessMessagesDeleted.de_json(json_dict, None) assert bmd.business_connection_id == self.business_connection_id assert bmd.chat == self.chat assert bmd.message_ids == self.message_ids assert bmd.api_kwargs == {} assert isinstance(bmd, BusinessMessagesDeleted) def test_equality(self): bmd1 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) bmd2 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) bmd3 = BusinessMessagesDeleted("1", Chat(4, "random"), [321, 123]) assert bmd1 == bmd2 assert hash(bmd1) == hash(bmd2) assert bmd1 != bmd3 assert hash(bmd1) != hash(bmd3) class TestBusinessIntroWithoutRequest(BusinessTestBase): def test_slot_behaviour(self, business_intro): intro = business_intro for attr in intro.__slots__: assert getattr(intro, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(intro)) == len(set(mro_slots(intro))), "duplicate slot" def test_to_dict(self, business_intro): intro_dict = business_intro.to_dict() assert isinstance(intro_dict, dict) assert intro_dict["title"] == self.title assert intro_dict["message"] == self.message assert intro_dict["sticker"] == self.sticker.to_dict() def test_de_json(self): json_dict = { "title": self.title, "message": self.message, "sticker": self.sticker.to_dict(), } intro = BusinessIntro.de_json(json_dict, None) assert intro.title == self.title assert intro.message == self.message assert intro.sticker == self.sticker assert intro.api_kwargs == {} assert isinstance(intro, BusinessIntro) def test_equality(self): intro1 = BusinessIntro(self.title, self.message, self.sticker) intro2 = BusinessIntro(self.title, self.message, self.sticker) intro3 = BusinessIntro("Other Business", self.message, self.sticker) assert intro1 == intro2 assert hash(intro1) == hash(intro2) assert intro1 is not intro2 assert intro1 != intro3 assert hash(intro1) != hash(intro3) class TestBusinessLocationWithoutRequest(BusinessTestBase): def test_slot_behaviour(self, business_location): inst = business_location for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" def test_to_dict(self, business_location): blc_dict = business_location.to_dict() assert isinstance(blc_dict, dict) assert blc_dict["address"] == self.address assert blc_dict["location"] == self.location.to_dict() def test_de_json(self): json_dict = { "address": self.address, "location": self.location.to_dict(), } blc = BusinessLocation.de_json(json_dict, None) assert blc.address == self.address assert blc.location == self.location assert blc.api_kwargs == {} assert isinstance(blc, BusinessLocation) def test_equality(self): blc1 = BusinessLocation(self.address, self.location) blc2 = BusinessLocation(self.address, self.location) blc3 = BusinessLocation("Other Address", self.location) assert blc1 == blc2 assert hash(blc1) == hash(blc2) assert blc1 is not blc2 assert blc1 != blc3 assert hash(blc1) != hash(blc3) class TestBusinessOpeningHoursIntervalWithoutRequest(BusinessTestBase): def test_slot_behaviour(self, business_opening_hours_interval): inst = business_opening_hours_interval for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" def test_to_dict(self, business_opening_hours_interval): bohi_dict = business_opening_hours_interval.to_dict() assert isinstance(bohi_dict, dict) assert bohi_dict["opening_minute"] == self.opening_minute assert bohi_dict["closing_minute"] == self.closing_minute def test_de_json(self): json_dict = { "opening_minute": self.opening_minute, "closing_minute": self.closing_minute, } bohi = BusinessOpeningHoursInterval.de_json(json_dict, None) assert bohi.opening_minute == self.opening_minute assert bohi.closing_minute == self.closing_minute assert bohi.api_kwargs == {} assert isinstance(bohi, BusinessOpeningHoursInterval) def test_equality(self): bohi1 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) bohi2 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) bohi3 = BusinessOpeningHoursInterval(61, 100) assert bohi1 == bohi2 assert hash(bohi1) == hash(bohi2) assert bohi1 is not bohi2 assert bohi1 != bohi3 assert hash(bohi1) != hash(bohi3) @pytest.mark.parametrize( ("opening_minute", "expected"), [ # openings per docstring (8 * 60, (0, 8, 0)), (24 * 60, (1, 0, 0)), (6 * 24 * 60, (6, 0, 0)), ], ) def test_opening_time(self, opening_minute, expected): bohi = BusinessOpeningHoursInterval(opening_minute, -0) opening_time = bohi.opening_time assert opening_time == expected cached = bohi.opening_time assert cached is opening_time @pytest.mark.parametrize( ("closing_minute", "expected"), [ # closings per docstring (20 * 60 + 30, (0, 20, 30)), (2 * 24 * 60 - 1, (1, 23, 59)), (7 * 24 * 60 - 2, (6, 23, 58)), ], ) def test_closing_time(self, closing_minute, expected): bohi = BusinessOpeningHoursInterval(-0, closing_minute) closing_time = bohi.closing_time assert closing_time == expected cached = bohi.closing_time assert cached is closing_time class TestBusinessOpeningHoursWithoutRequest(BusinessTestBase): def test_slot_behaviour(self, business_opening_hours): inst = business_opening_hours for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" def test_to_dict(self, business_opening_hours): boh_dict = business_opening_hours.to_dict() assert isinstance(boh_dict, dict) assert boh_dict["time_zone_name"] == self.time_zone_name assert boh_dict["opening_hours"] == [opening.to_dict() for opening in self.opening_hours] def test_de_json(self): json_dict = { "time_zone_name": self.time_zone_name, "opening_hours": [opening.to_dict() for opening in self.opening_hours], } boh = BusinessOpeningHours.de_json(json_dict, None) assert boh.time_zone_name == self.time_zone_name assert boh.opening_hours == tuple(self.opening_hours) assert boh.api_kwargs == {} assert isinstance(boh, BusinessOpeningHours) def test_equality(self): boh1 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) boh2 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) boh3 = BusinessOpeningHours("Other/Timezone", self.opening_hours) assert boh1 == boh2 assert hash(boh1) == hash(boh2) assert boh1 is not boh2 assert boh1 != boh3 assert hash(boh1) != hash(boh3) class TestBusinessOpeningHoursGetOpeningHoursForDayWithoutRequest: @pytest.fixture def sample_opening_hours(self): # Monday 8am-8:30pm (480-1230) # Tuesday 24 hours (1440-2879) # Sunday 12am-11:58pm (8640-10078) intervals = [ BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours BusinessOpeningHoursInterval(8640, 10078), # Sunday 12am-11:58pm ] return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) def test_monday_opening_hours(self, sample_opening_hours): # Test for Monday test_date = dtm.date(2023, 11, 6) # Monday time_zone = ZoneInfo("UTC") result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( ( dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), ), ) assert result == expected def test_tuesday_24_hours(self, sample_opening_hours): # Test for Tuesday (24 hours) test_date = dtm.date(2023, 11, 7) # Tuesday time_zone = ZoneInfo("UTC") result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( ( dtm.datetime(2023, 11, 7, 0, 0, tzinfo=time_zone), dtm.datetime(2023, 11, 7, 23, 59, tzinfo=time_zone), ), ) assert result == expected def test_sunday_opening_hours(self, sample_opening_hours): # Test for Sunday test_date = dtm.date(2023, 11, 12) # Sunday time_zone = ZoneInfo("UTC") result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( ( dtm.datetime(2023, 11, 12, 0, 0, tzinfo=time_zone), dtm.datetime(2023, 11, 12, 23, 58, tzinfo=time_zone), ), ) assert result == expected def test_day_with_no_opening_hours(self, sample_opening_hours): # Test for Wednesday (no opening hours defined) test_date = dtm.date(2023, 11, 8) # Wednesday time_zone = ZoneInfo("UTC") result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) assert result == () def test_multiple_intervals_same_day(self): # Test with multiple intervals on the same day intervals = [ # unsorted on purpose to check that the sorting works (even though this is # currently undocumented behaviour) BusinessOpeningHoursInterval(900, 1230), # Monday 3pm-8:30pm BusinessOpeningHoursInterval(480, 720), # Monday 8am-12pm ] opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) test_date = dtm.date(2023, 11, 6) # Monday time_zone = ZoneInfo("UTC") result = opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( ( dtm.datetime(2023, 11, 6, 8, 0, tzinfo=time_zone), dtm.datetime(2023, 11, 6, 12, 0, tzinfo=time_zone), ), ( dtm.datetime(2023, 11, 6, 15, 0, tzinfo=time_zone), dtm.datetime(2023, 11, 6, 20, 30, tzinfo=time_zone), ), ) assert result == expected @pytest.mark.parametrize("input_type", [str, ZoneInfo]) def test_timezone_conversion(self, sample_opening_hours, input_type): # Test that timezone is properly applied test_date = dtm.date(2023, 11, 6) # Monday time_zone = input_type("America/New_York") zone_info = ZoneInfo("America/New_York") result = sample_opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( ( dtm.datetime(2023, 11, 6, 3, 0, tzinfo=zone_info), dtm.datetime(2023, 11, 6, 15, 30, tzinfo=zone_info), ), ) assert result == expected assert result[0][0].tzinfo == zone_info assert result[0][1].tzinfo == zone_info def test_timezone_conversation_changing_date(self): # test for the edge case where the returned time is on a different date in the target # timezone than in the business timezone intervals = [ BusinessOpeningHoursInterval(60, 120), # Monday 1am-2am UTC ] opening_hours = BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) test_date = dtm.date(2023, 11, 6) # Monday time_zone = ZoneInfo("America/New_York") # UTC-5, so 1am UTC is 8pm previous day result = opening_hours.get_opening_hours_for_day(test_date, time_zone) expected = ( ( dtm.datetime(2023, 11, 5, 20, 0, tzinfo=time_zone), dtm.datetime(2023, 11, 5, 21, 0, tzinfo=time_zone), ), ) assert result == expected def test_no_timezone_provided(self, sample_opening_hours): # Test when no timezone is provided test_date = dtm.date(2023, 11, 6) # Monday result = sample_opening_hours.get_opening_hours_for_day(test_date) expected = ( ( dtm.datetime( 2023, 11, 6, 8, 0, tzinfo=ZoneInfo(sample_opening_hours.time_zone_name), ), dtm.datetime( 2023, 11, 6, 20, 30, tzinfo=ZoneInfo(sample_opening_hours.time_zone_name), ), ), ) assert result == expected class TestBusinessOpeningHoursIsOpenWithoutRequest: @pytest.fixture def sample_opening_hours(self): # Monday 8am-8:30pm (480-1230) # Tuesday 24 hours (1440-2879) # Sunday 12am-11:59pm (8640-10079) intervals = [ BusinessOpeningHoursInterval(480, 1230), # Monday 8am-8:30pm UTC BusinessOpeningHoursInterval(1440, 2879), # Tuesday 24 hours UTC BusinessOpeningHoursInterval(8640, 10079), # Sunday 12am-11:59pm UTC ] return BusinessOpeningHours(time_zone_name="UTC", opening_hours=intervals) def test_is_open_during_business_hours(self, sample_opening_hours): # Monday 10am UTC (within 8am-8:30pm) dt = dtm.datetime(2023, 11, 6, 10, 0, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is True def test_is_open_at_opening_time(self, sample_opening_hours): # Monday exactly 8am UTC dt = dtm.datetime(2023, 11, 6, 8, 0, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is True def test_is_closed_at_closing_time(self, sample_opening_hours): # Monday exactly 8:30pm UTC (closing time is exclusive) dt = dtm.datetime(2023, 11, 6, 20, 30, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is False def test_is_closed_outside_business_hours(self, sample_opening_hours): # Monday 7am UTC (before opening) dt = dtm.datetime(2023, 11, 6, 7, 0, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is False def test_is_open_24h_day(self, sample_opening_hours): # Tuesday 3am UTC (24h opening) dt = dtm.datetime(2023, 11, 7, 3, 0, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is True def test_is_closed_on_day_with_no_hours(self, sample_opening_hours): # Wednesday (no opening hours) dt = dtm.datetime(2023, 11, 8, 12, 0, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is False def test_timezone_conversion(self, sample_opening_hours): # Monday 5am EDT is 10am UTC (should be open) dt = dtm.datetime(2023, 11, 6, 5, 0, tzinfo=ZoneInfo("America/New_York")) assert sample_opening_hours.is_open(dt) is True # Monday 2am EDT is 7am UTC (should be closed) dt = dtm.datetime(2023, 11, 6, 2, 0, tzinfo=ZoneInfo("America/New_York")) assert sample_opening_hours.is_open(dt) is False def test_naive_datetime_uses_business_timezone(self, sample_opening_hours): # Naive datetime - should be interpreted as UTC (business timezone) dt = dtm.datetime(2023, 11, 6, 10, 0) # 10am naive assert sample_opening_hours.is_open(dt) is True def test_boundary_conditions(self, sample_opening_hours): # Sunday 11:58pm UTC (should be open) dt = dtm.datetime(2023, 11, 12, 23, 58, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is True # Sunday 11:59pm UTC (should be closed) dt = dtm.datetime(2023, 11, 12, 23, 59, tzinfo=ZoneInfo("UTC")) assert sample_opening_hours.is_open(dt) is False