diff --git a/Gemfile.lock b/Gemfile.lock index 88db2978..04363f1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,6 +279,7 @@ GEM PLATFORMS arm64-darwin-21 arm64-darwin-23 + arm64-darwin-24 x64-mingw-ucrt x86_64-darwin-21 x86_64-darwin-22 diff --git a/tools/tests/automation_prepare_adhoc_availability_test.py b/tools/tests/automation_prepare_adhoc_availability_test.py new file mode 100644 index 00000000..899490aa --- /dev/null +++ b/tools/tests/automation_prepare_adhoc_availability_test.py @@ -0,0 +1,254 @@ +import tempfile +import os +import sys +import pytest +import pandas as pd +from ruamel.yaml import YAML +from automation_prepare_adhoc_availability import ( + get_available_mentor_sort, + get_unavailable_mentor_sort, + get_availability_update_dict, + update_mentor_availability, + MONTHS_MAP, + TYPE_LONG_TERM, + TYPE_AD_HOC, + TYPE_BOTH +) + +yaml = YAML() + + +class TestGetAvailableMentorSort: + def test_new_mentor_with_full_availability_returns_500(self): + mentor = {'name': 'Test Mentor', 'hours': 2} + current_availability = [4, 5, 6] + + result = get_available_mentor_sort(mentor, current_availability) + + assert result == 500 + + def test_mentor_with_more_than_3_hours_returns_500(self): + mentor = {'name': 'Test Mentor', 'hours': 4} + current_availability = [4] + + result = get_available_mentor_sort(mentor, current_availability) + + assert result == 500 + + def test_mentor_with_3_or_less_hours_returns_200(self): + mentor = {'name': 'Test Mentor', 'hours': 3} + current_availability = [4] + + result = get_available_mentor_sort(mentor, current_availability) + + assert result == 200 + + def test_mentor_with_1_hour_returns_200(self): + mentor = {'name': 'Test Mentor', 'hours': 1} + current_availability = [4] + + result = get_available_mentor_sort(mentor, current_availability) + + assert result == 200 + + +class TestGetUnavailableMentorSort: + def test_disabled_mentor_returns_1(self): + mentor = {'name': 'Test Mentor', 'disabled': True, 'type': TYPE_BOTH} + + result = get_unavailable_mentor_sort(mentor) + + assert result == 1 + + def test_long_term_mentor_returns_10(self): + mentor = {'name': 'Test Mentor', 'disabled': False, 'type': TYPE_LONG_TERM} + + result = get_unavailable_mentor_sort(mentor) + + assert result == 10 + + def test_ad_hoc_mentor_returns_100(self): + mentor = {'name': 'Test Mentor', 'disabled': False, 'type': TYPE_AD_HOC} + + result = get_unavailable_mentor_sort(mentor) + + assert result == 100 + + def test_both_type_mentor_returns_100(self): + mentor = {'name': 'Test Mentor', 'disabled': False, 'type': TYPE_BOTH} + + result = get_unavailable_mentor_sort(mentor) + + assert result == 100 + + +class TestGetAvailabilityUpdateDict: + def test_returns_dict_with_mentor_hours(self): + data = { + 'Mentor Name': ['Alice Smith', 'Bob Jones'], + 'Availability (Hours)': [5, 3] + } + df = pd.DataFrame(data) + + result = get_availability_update_dict(df) + + assert result['Alice Smith'] == 5 + assert result['Bob Jones'] == 3 + + def test_empty_hours_returns_none(self): + data = { + 'Mentor Name': ['Alice Smith'], + 'Availability (Hours)': [''] + } + df = pd.DataFrame(data) + + result = get_availability_update_dict(df) + + assert result['Alice Smith'] is None + + def test_nan_hours_returns_none(self): + data = { + 'Mentor Name': ['Alice Smith'], + 'Availability (Hours)': [pd.NA] + } + df = pd.DataFrame(data) + + result = get_availability_update_dict(df) + + assert result['Alice Smith'] is None + + def test_strips_whitespace_from_names(self): + data = { + 'Mentor Name': [' Alice Smith '], + 'Availability (Hours)': [4] + } + df = pd.DataFrame(data) + + result = get_availability_update_dict(df) + + assert 'Alice Smith' in result + + +class TestUpdateMentorAvailability: + def test_updates_mentor_availability_from_xlsx(self, monkeypatch): + + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as xlsx_file: + xlsx_path = xlsx_file.name + + with tempfile.NamedTemporaryFile(suffix='.yml', mode='w', delete=False) as yml_file: + yml_path = yml_file.name + + try: + df = pd.DataFrame({ + 'Mentor Name': ['Alice Smith'], + 'Availability (Hours)': [5] + }) + df.to_excel(xlsx_path, index=False) + + mentors = [ + { + 'name': 'Alice Smith', + 'hours': 2, + 'availability': [4, 5], + 'sort': 200, + 'type': TYPE_AD_HOC, + 'disabled': False + }, + { + 'name': 'Bob Jones', + 'hours': 3, + 'availability': [4], + 'sort': 100, + 'type': TYPE_LONG_TERM, + 'disabled': False + } + ] + + with open(yml_path, 'w') as f: + yaml.dump(mentors, f) + + update_mentor_availability(4, xlsx_path, yml_path) + + with open(yml_path, 'r') as f: + result = yaml.load(f) + + alice = next(m for m in result if m['name'] == 'Alice Smith') + assert alice['hours'] == 5 + assert alice['availability'] == [4] + assert alice['sort'] == 500 + + bob = next(m for m in result if m['name'] == 'Bob Jones') + assert bob['availability'] == [] + assert bob['sort'] == 10 + + finally: + os.remove(xlsx_path) + os.remove(yml_path) + + def test_mentor_not_in_xlsx_becomes_unavailable(self, monkeypatch): + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as xlsx_file: + xlsx_path = xlsx_file.name + + with tempfile.NamedTemporaryFile(suffix='.yml', mode='w', delete=False) as yml_file: + yml_path = yml_file.name + + try: + df = pd.DataFrame({ + 'Mentor Name': ['Alice Smith'], + 'Availability (Hours)': [5] + }) + df.to_excel(xlsx_path, index=False) + + mentors = [ + {'name': 'Alice Smith', 'hours': 2, 'availability': [4], 'sort': 200, 'type': TYPE_AD_HOC, 'disabled': False}, + {'name': 'Bob Jones', 'hours': 3, 'availability': [4], 'sort': 200, 'type': TYPE_BOTH, 'disabled': False} + ] + + with open(yml_path, 'w') as f: + yaml.dump(mentors, f) + + update_mentor_availability(4, xlsx_path, yml_path) + + with open(yml_path, 'r') as f: + result = yaml.load(f) + + bob = next(m for m in result if m['name'] == 'Bob Jones') + assert bob['availability'] == [] + assert bob['sort'] == 100 + + finally: + os.remove(xlsx_path) + os.remove(yml_path) + + def test_keeps_existing_hours_when_xlsx_hours_empty(self): + with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as xlsx_file: + xlsx_path = xlsx_file.name + + with tempfile.NamedTemporaryFile(suffix='.yml', mode='w', delete=False) as yml_file: + yml_path = yml_file.name + + try: + df = pd.DataFrame({ + 'Mentor Name': ['Alice Smith'], + 'Availability (Hours)': [''] + }) + df.to_excel(xlsx_path, index=False) + + mentors = [ + {'name': 'Alice Smith', 'hours': 3, 'availability': [4], 'sort': 200, 'type': TYPE_AD_HOC, 'disabled': False} + ] + + with open(yml_path, 'w') as f: + yaml.dump(mentors, f) + + update_mentor_availability(4, xlsx_path, yml_path) + + with open(yml_path, 'r') as f: + result = yaml.load(f) + + alice = result[0] + assert alice['hours'] == 3 + + finally: + os.remove(xlsx_path) + os.remove(yml_path) \ No newline at end of file diff --git a/tools/tests/download_image_test.py b/tools/tests/download_image_test.py new file mode 100644 index 00000000..8b583f2f --- /dev/null +++ b/tools/tests/download_image_test.py @@ -0,0 +1,93 @@ +import os +import sys +import io +import tempfile +import pytest +import builtins +import requests +from unittest import mock + +import download_image + + +class TestDownloadImage: + def test_successful_download_creates_file(self, tmp_path, monkeypatch): + monkeypatch.setattr(download_image, "IMAGE_FILE_PATH", str(tmp_path)) + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.content = b"fake image data" + mock_response.raise_for_status = mock.Mock() + monkeypatch.setattr(requests, "get", mock.Mock(return_value=mock_response)) + + url = "https://example.com/test.jpeg" + mentor_name = "Alice Smith" + image_path = download_image.download_image(url, mentor_name) + + assert image_path.endswith("alice_smith.jpeg") + assert os.path.exists(image_path) + with open(image_path, "rb") as f: + assert f.read() == b"fake image data" + + def test_download_failure_returns_none(self, tmp_path, monkeypatch): + monkeypatch.setattr(download_image, "IMAGE_FILE_PATH", str(tmp_path)) + + monkeypatch.setattr(requests, "get", mock.Mock(side_effect=requests.exceptions.RequestException("network error"))) + + result = download_image.download_image("https://badurl.com/image.jpeg", "Bob") + assert result is None + + def test_directory_created_if_not_exists(self, tmp_path, monkeypatch): + image_dir = tmp_path / "new_images" + monkeypatch.setattr(download_image, "IMAGE_FILE_PATH", str(image_dir)) + + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.content = b"123" + mock_response.raise_for_status = mock.Mock() + monkeypatch.setattr(requests, "get", mock.Mock(return_value=mock_response)) + + result = download_image.download_image("https://example.com/img.jpg", "John Doe") + assert os.path.exists(image_dir) + assert result.endswith("john_doe.jpeg") + + def test_filename_sanitization(self, tmp_path, monkeypatch): + monkeypatch.setattr(download_image, "IMAGE_FILE_PATH", str(tmp_path)) + + mock_response = mock.Mock() + mock_response.content = b"data" + mock_response.raise_for_status = mock.Mock() + monkeypatch.setattr(requests, "get", mock.Mock(return_value=mock_response)) + + result = download_image.download_image("https://example.com/img.jpg", "Alice Smith-Jones") + assert "alice_smith-jones.jpeg" in result + + +class TestRunAutomation: + def test_run_automation_success(self, tmp_path, monkeypatch, capsys): + monkeypatch.setattr(sys, "argv", ["download_image.py", "https://example.com/test.jpg", "Charlie"]) + + fake_path = str(tmp_path / "charlie.jpeg") + monkeypatch.setattr(download_image, "download_image", mock.Mock(return_value=fake_path)) + + download_image.run_automation() + + captured = capsys.readouterr() + assert "Image saved to" in captured.out + download_image.download_image.assert_called_once() + + def test_run_automation_failure(self, monkeypatch, capsys): + monkeypatch.setattr(sys, "argv", ["download_image.py", "https://example.com/fail.jpg", "David"]) + monkeypatch.setattr(download_image, "download_image", mock.Mock(return_value=None)) + + download_image.run_automation() + captured = capsys.readouterr() + assert "Failed to download the image." in captured.out + + def test_run_automation_no_args(self, monkeypatch, caplog): + monkeypatch.setattr(sys, "argv", ["download_image.py"]) + caplog.set_level("INFO") + + download_image.run_automation() + + assert "Add parameters for download" in caplog.text diff --git a/tools/tests/meetup_import_test.py b/tools/tests/meetup_import_test.py new file mode 100644 index 00000000..32696b89 --- /dev/null +++ b/tools/tests/meetup_import_test.py @@ -0,0 +1,126 @@ +import tempfile +import os +import pytest +import yaml +from unittest import mock +import requests +from meetup_import import ( + clean_name, + get_hosts_and_speakers, + clean_description, + get_formatted_event_description, + get_event_image_url, + to_literal_str, + to_quoted_str, + LiteralString, + QuotedString, + NoQuoteString, + get_event_key, + get_existing_event_keys, + load_existing_events_from_file, + process_meetup_data +) + +def test_clean_name_variants(): + assert clean_name('**Alice**') == 'Alice' + assert clean_name('_Alice_') == 'Alice' + assert clean_name('[Alice](https://x.com)') == 'Alice' + assert clean_name('Alice | Other') == 'Alice' + assert clean_name(' Alice ') == 'Alice' + +def test_get_hosts_and_speakers_variants(): + text = """Host: Alice + Co-host: Bob + Speaker: Dr. Jane Doe""" + host, speaker = get_hosts_and_speakers(text) + assert 'Alice' in host and 'Bob' in host + assert 'Dr. Jane Doe' in speaker + +def test_get_hosts_and_speakers_empty(): + host, speaker = get_hosts_and_speakers('') + assert host == '' and speaker == '' + +def test_clean_description_removes_formatting(): + text = '**Bold** [link](https://x.com) café 😀' + result = clean_description(text) + assert '**' not in result + assert 'https' not in result + assert 'cafe' in result + assert '😀' not in result + +def test_get_formatted_event_description_variants(): + desc1 = 'This is first. This is second.' + desc2 = 'Women Coding Community presents event.' + desc3 = 'Single sentence' + assert get_formatted_event_description(desc1) == 'This is first.' + assert 'Women Coding Community' not in get_formatted_event_description(desc2) + assert get_formatted_event_description(desc3).endswith('.') + +def test_get_event_image_url_with_og_image(monkeypatch): + mock_html = ''' +
+ + + ''' + mock_response = mock.Mock() + mock_response.content = mock_html.encode() + monkeypatch.setattr(requests, "get", mock.Mock(return_value=mock_response)) + + url = get_event_image_url("https://meetup.com/event/123") + assert url == "https://example.com/event-image.jpg" + +def test_get_event_image_url_fallback_to_img_tag(monkeypatch): + mock_html = ''' + +
+
+ '''
+ mock_response = mock.Mock()
+ mock_response.content = mock_html.encode()
+ monkeypatch.setattr(requests, "get", mock.Mock(return_value=mock_response))
+
+ url = get_event_image_url("https://meetup.com/event/456")
+ assert url == "https://example.com/fallback.jpg"
+
+def test_to_literal_and_quoted_str():
+ assert isinstance(to_literal_str('Line 1\nLine 2'), LiteralString)
+ assert isinstance(to_literal_str('Simple text'), str)
+ assert isinstance(to_quoted_str('Hello!'), QuotedString)
+ assert isinstance(to_quoted_str('Simple'), NoQuoteString)
+
+def test_get_event_key():
+ event = {'title': ' Talk ', 'date': 'JAN 1, 2025'}
+ assert get_event_key(event) == 'Talk - JAN 1, 2025'
+
+def test_get_existing_event_keys():
+ events = [{'title': 'A', 'date': '1'}, {'title': 'B', 'date': '2'}]
+ keys = get_existing_event_keys(events)
+ assert len(keys) == 2 and all(isinstance(k, str) for k in keys)
+
+def test_load_existing_events_from_file(tmp_path):
+ events = [{'title': 'E1', 'date': 'D1'}]
+ path = tmp_path / 'data.yml'
+ with open(path, 'w') as f:
+ yaml.dump(events, f)
+ loaded = load_existing_events_from_file(path)
+ assert loaded[0]['title'] == 'E1'
+
+def test_load_existing_events_handles_missing_file():
+ result = load_existing_events_from_file('/no/such/file.yml')
+ assert result == []
+
+def test_process_meetup_data_fields():
+ meetup = {
+ 'title': 'Event\nTitle',
+ 'description': 'Desc\nline',
+ 'expiration': '20250101',
+ 'host': 'Host',
+ 'speaker': 'Speaker',
+ 'image': {'path': 'https://img', 'alt': 'Alt'},
+ 'link': {'path': 'https://x', 'title': 'View'}
+ }
+ result = process_meetup_data(meetup)
+ assert isinstance(result['title'], (LiteralString, str))
+ assert isinstance(result['expiration'], QuotedString)
+ assert isinstance(result['image']['path'], (QuotedString, NoQuoteString))
+ assert isinstance(result['link']['title'], (QuotedString, NoQuoteString))
\ No newline at end of file