From c1568e540f31e23d3695477ef723c8e77e999038 Mon Sep 17 00:00:00 2001 From: zwitschi Date: Sun, 5 Oct 2025 16:03:41 +0200 Subject: [PATCH] feat: add test workflow and implement unit tests for main functionality --- .gitea/workflows/test-application.yaml | 39 +++++++++ requirements.txt | 1 + test_main.py | 111 +++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 .gitea/workflows/test-application.yaml create mode 100644 test_main.py diff --git a/.gitea/workflows/test-application.yaml b/.gitea/workflows/test-application.yaml new file mode 100644 index 0000000..2f8a103 --- /dev/null +++ b/.gitea/workflows/test-application.yaml @@ -0,0 +1,39 @@ +name: Test Application +run-name: ${{ gitea.actor }} tests the application +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Create and activate virtual environment + run: | + python -m venv venv + source venv/bin/activate + pip install -r requirements.txt + pip install pytest + + - name: Run tests + run: | + source venv/bin/activate + pytest --maxfail=1 --disable-warnings -q + + - name: Deactivate virtual environment + run: deactivate + + - name: Clean up virtual environment + run: rm -rf venv diff --git a/requirements.txt b/requirements.txt index c79f8f3..ce9097e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +pytest python-dotenv pytz requests diff --git a/test_main.py b/test_main.py new file mode 100644 index 0000000..22ef10f --- /dev/null +++ b/test_main.py @@ -0,0 +1,111 @@ +import io +import time +from unittest import mock + +import pytest + +import main + + +SAMPLE_TIMEZONE_CSV = """Etc/UTC,ZZ,UTC,0,0,0 +America/New_York,US,EST,0,-18000,0 +Europe/London,GB,BST,0,0,1 +""" + +SAMPLE_COUNTRY_CSV = """ZZ,Unknown +US,United States +GB,United Kingdom +""" + + +def test_load_timezones_and_countries(monkeypatch): + tzs = main.load_timezones() + countries = main.load_countries() + assert any(t['zone_name'] == 'America/New_York' for t in tzs) + assert any(c['country_code'] == 'US' for c in countries) + + +def test_get_tz_and_country_info(): + timezones = [{'zone_name': 'A/B', 'country_code': 'US'}] + countries = [{'country_code': 'US', 'country_name': 'United States'}] + assert main.get_tz_info('A/B', timezones)['zone_name'] == 'A/B' + assert main.get_country_info('US', countries)[ + 'country_name'] == 'United States' + assert main.get_tz_info('X/Y', timezones) is None + assert main.get_country_info('XX', countries) is None + + +def test_create_embed_all_types(monkeypatch): + # Prevent create_embed from trying to read actual CSV files by patching loaders + monkeypatch.setattr(main, 'load_timezones', lambda: [ + {'zone_name': 'Etc/UTC', 'country_code': 'ZZ'}]) + monkeypatch.setattr(main, 'load_countries', lambda: [ + {'country_code': 'ZZ', 'country_name': 'Nowhere'}]) + + # reminder + emb = main.create_embed('reminder') + assert emb['title'] == 'Reminder' + assert '5 minute' in emb['description'] + assert emb['color'] == 0xe67e22 + + # reminder_halftime + emb = main.create_embed('reminder_halftime') + assert emb['title'] == 'Reminder halftime' + assert 'Half-time in 5 minutes' in emb['description'] + + # halftime (should include image) + monkeypatch.setattr(main, 'where_is_it_420', lambda tzs, cs: []) + emb = main.create_embed('halftime') + assert emb['title'] == 'Halftime' + assert emb['image'] is not None + + # 420 (should include image and appended tz info string when list empty) + monkeypatch.setattr(main, 'where_is_it_420', lambda tzs, cs: []) + emb = main.create_embed('420') + assert emb['title'] == '420' + assert emb['image'] is not None + assert "not 4:20" in emb['description'] or "It's 4:20" in emb['description'] + + # unknown + emb = main.create_embed('nope') + assert emb['description'] == 'Unknown notification type' + + +def test_where_is_it_420(monkeypatch): + # Limit timezones to a predictable set + monkeypatch.setattr(main.pytz, 'all_timezones', ['Etc/UTC']) + + tzs = [{'zone_name': 'Etc/UTC', 'country_code': 'ZZ'}] + countries = [{'country_code': 'ZZ', 'country_name': 'Nowhere'}] + + class FakeDatetime: + @staticmethod + def now(tz): + class R: + hour = 4 + return R() + + monkeypatch.setattr(main, 'datetime', FakeDatetime) + monkeypatch.setattr(main, 'get_tz_info', lambda name, + t: tzs[0] if name == 'Etc/UTC' else None) + monkeypatch.setattr(main, 'get_country_info', lambda code, + c: countries[0] if code == 'ZZ' else None) + + res = main.where_is_it_420(tzs, countries) + assert res == ['Nowhere'] + + +def test_main_exits_quickly(monkeypatch): + # Patch send_notification so it doesn't perform network + monkeypatch.setattr(main, 'send_notification', lambda x: None) + # Make schedule.run_pending raise KeyboardInterrupt to exit loop + monkeypatch.setattr(main.schedule, 'run_pending', lambda: ( + _ for _ in ()).throw(KeyboardInterrupt())) + # Patch time.sleep to no-op + monkeypatch.setattr(main.time, 'sleep', lambda s: None) + # Ensure WEBHOOK_URL present to avoid early return + monkeypatch.setenv('DISCORD_WEBHOOK_URL', 'http://example.com/webhook') + main.WEBHOOK_URL = 'http://example.com/webhook' + + # Should exit quickly due to KeyboardInterrupt from run_pending + main.main()