from decimal import Decimal from unittest.mock import patch import pytest from django.urls import reverse from links.models import SiteSettings from pricemon.models import PriceAlert, PriceWatcher @pytest.fixture def watcher(): with patch('pricemon.tasks.schedule_watcher'): return PriceWatcher.objects.create( name='Test product', url='https://example.com/product', css_selector='.price', ) @pytest.fixture def alert(watcher): return PriceAlert.objects.create( watcher=watcher, old_price='100.00', new_price='80.00', drop_pct='20.00', ) @pytest.fixture def telegram_creds(): ss = SiteSettings.get() ss.telegram_bot_token = '123:abc' ss.telegram_chat_id = '999' ss.save() return ss @pytest.fixture def threshold_watcher(watcher): watcher.alert_price_threshold = Decimal('100.00') watcher.last_price = Decimal('120.00') watcher.save() return watcher @pytest.mark.django_db class TestPriceMonitorToggle: def test_dashboard_shows_disable_action_for_enabled_watcher(self, client, watcher): response = client.get(reverse('pricemon-dashboard')) assert response.status_code == 200 assert reverse('pricemon-toggle-enabled', args=[watcher.pk]) in response.content.decode() assert 'Disable monitoring' in response.content.decode() assert response.context['enabled_watchers_count'] == 1 def test_toggle_disables_watcher_and_unschedules_job(self, client, watcher): with patch('pricemon.tasks.unschedule_watcher') as unschedule_watcher: response = client.post(reverse('pricemon-toggle-enabled', args=[watcher.pk])) watcher.refresh_from_db() assert response.status_code == 302 assert response.url == reverse('pricemon-dashboard') assert watcher.enabled is False unschedule_watcher.assert_called_once() def test_toggle_enables_paused_watcher_and_schedules_job(self, client, watcher): with patch('pricemon.tasks.unschedule_watcher'): watcher.enabled = False watcher.save(update_fields=['enabled']) with patch('pricemon.tasks.schedule_watcher') as schedule_watcher: response = client.post(reverse('pricemon-toggle-enabled', args=[watcher.pk])) watcher.refresh_from_db() assert response.status_code == 302 assert watcher.enabled is True schedule_watcher.assert_called_once() def test_toggle_rejects_get_requests(self, client, watcher): response = client.get(reverse('pricemon-toggle-enabled', args=[watcher.pk])) assert response.status_code == 405 @pytest.mark.django_db class TestPriceMonitorAlertsPartial: def test_dashboard_renders_with_active_alert(self, client, watcher, alert): response = client.get(reverse('pricemon-dashboard')) assert response.status_code == 200 body = response.content.decode() assert 'alert-{}'.format(alert.pk) in body assert 'Price Drop Alerts' in body def test_alerts_partial_renders_i18n_tags(self, client, watcher, alert): response = client.get(reverse('pricemon-alerts-partial')) assert response.status_code == 200 body = response.content.decode() assert 'alert-{}'.format(alert.pk) in body assert 'View' in body and 'Dismiss' in body assert reverse('pricemon-dismiss-alert', args=[alert.pk]) in body @pytest.mark.django_db class TestPriceMonitorTelegram: def _check(self, watcher_pk): with patch('pricemon.scraper.fetch_price') as fetch, patch( 'pricemon.notifications.requests.post' ) as post: fetch.return_value = (Decimal('90.00'), '$90', '') post.return_value.status_code = 200 post.return_value.raise_for_status.return_value = None from pricemon.tasks import check_watcher check_watcher(watcher_pk) return post def test_sends_telegram_when_price_meets_threshold(self, threshold_watcher, telegram_creds): post = self._check(threshold_watcher.pk) post.assert_called_once() url = post.call_args[0][0] assert url.endswith(f'/bot{telegram_creds.telegram_bot_token}/sendMessage') body = post.call_args.kwargs['json'] assert body['chat_id'] == '999' assert body['parse_mode'] == 'MarkdownV2' assert 'Price Drop Alert' in body['text'] assert threshold_watcher.name in body['text'] assert PriceAlert.objects.count() == 1 def test_no_telegram_without_global_creds(self, threshold_watcher): SiteSettings.get().delete() with patch('pricemon.scraper.fetch_price') as fetch, patch( 'pricemon.notifications.requests.post' ) as post: fetch.return_value = (Decimal('90.00'), '$90', '') post.return_value.status_code = 200 post.return_value.raise_for_status.return_value = None from pricemon.tasks import check_watcher check_watcher(threshold_watcher.pk) post.assert_not_called() assert PriceAlert.objects.count() == 1 def test_no_alert_when_price_above_threshold(self, threshold_watcher, telegram_creds): with patch('pricemon.scraper.fetch_price') as fetch, patch( 'pricemon.notifications.requests.post' ) as post: fetch.return_value = (Decimal('110.00'), '$110', '') post.return_value.status_code = 200 post.return_value.raise_for_status.return_value = None from pricemon.tasks import check_watcher check_watcher(threshold_watcher.pk) post.assert_not_called() assert PriceAlert.objects.count() == 0 def test_no_alert_when_threshold_unset(self, watcher, telegram_creds): with patch('pricemon.scraper.fetch_price') as fetch, patch( 'pricemon.notifications.requests.post' ) as post: fetch.return_value = (Decimal('1.00'), '$1', '') post.return_value.status_code = 200 post.return_value.raise_for_status.return_value = None from pricemon.tasks import check_watcher check_watcher(watcher.pk) post.assert_not_called() assert PriceAlert.objects.count() == 0 def test_non_recurring_alert_skipped_if_undismissed( self, threshold_watcher, telegram_creds ): threshold_watcher.recurring_notification = False threshold_watcher.save() PriceAlert.objects.create( watcher=threshold_watcher, old_price='120.00', new_price='95.00', drop_pct='20.83', dismissed=False, ) with patch('pricemon.scraper.fetch_price') as fetch, patch( 'pricemon.notifications.requests.post' ) as post: fetch.return_value = (Decimal('90.00'), '$90', '') post.return_value.status_code = 200 post.return_value.raise_for_status.return_value = None from pricemon.tasks import check_watcher check_watcher(threshold_watcher.pk) post.assert_not_called() assert PriceAlert.objects.count() == 1 def test_telegram_failure_does_not_break_check(self, threshold_watcher, telegram_creds): with patch('pricemon.scraper.fetch_price') as fetch, patch( 'pricemon.notifications.requests.post' ) as post: fetch.return_value = (Decimal('90.00'), '$90', '') post.side_effect = Exception('Boom from Telegram') from pricemon.tasks import check_watcher check_watcher(threshold_watcher.pk) post.assert_called_once() assert PriceAlert.objects.count() == 1 threshold_watcher.refresh_from_db() assert threshold_watcher.last_price == Decimal('90.00')