agenda/tests/test_airbnb.py
Edward Betts 567f3b0208 Refactor Airbnb parsing into reusable library
- Move all parsing logic from parse_airbnb.py to agenda/airbnb.py
- Update parse_airbnb.py to use the new library module
- Add comprehensive tests in tests/test_airbnb.py covering all functions
- Maintain backward compatibility for the command-line interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 10:35:26 +02:00

186 lines
6 KiB
Python

"""Tests for agenda.airbnb module."""
import pytest
from datetime import datetime
from zoneinfo import ZoneInfo
from unittest.mock import Mock, patch, mock_open
from agenda.airbnb import (
build_datetime,
list_to_dict,
extract_country_code,
walk_tree,
get_ui_state,
get_reservation_data,
get_price_from_reservation,
parse_multiple_files,
)
class TestBuildDatetime:
def test_build_datetime_utc(self):
result = build_datetime("2025-07-28", "15:30", "UTC")
expected = datetime(2025, 7, 28, 15, 30, tzinfo=ZoneInfo("UTC"))
assert result == expected
def test_build_datetime_local_timezone(self):
result = build_datetime("2025-12-25", "09:00", "Europe/London")
expected = datetime(2025, 12, 25, 9, 0, tzinfo=ZoneInfo("Europe/London"))
assert result == expected
class TestListToDict:
def test_list_to_dict_even_items(self):
items = ["key1", "value1", "key2", "value2"]
result = list_to_dict(items)
expected = {"key1": "value1", "key2": "value2"}
assert result == expected
def test_list_to_dict_empty_list(self):
result = list_to_dict([])
assert result == {}
def test_list_to_dict_single_pair(self):
items = ["name", "John"]
result = list_to_dict(items)
assert result == {"name": "John"}
class TestExtractCountryCode:
def test_extract_country_code_uk(self):
address = "123 Main Street, London, United Kingdom"
result = extract_country_code(address)
assert result == "gb"
def test_extract_country_code_france(self):
address = "456 Rue de la Paix, Paris, France"
result = extract_country_code(address)
assert result == "fr"
def test_extract_country_code_usa(self):
address = "789 Broadway, New York, United States"
result = extract_country_code(address)
assert result == "us"
def test_extract_country_code_not_found(self):
address = "123 Unknown Street, Mystery City"
result = extract_country_code(address)
assert result is None
def test_extract_country_code_case_insensitive(self):
address = "123 Main Street, UNITED KINGDOM"
result = extract_country_code(address)
assert result == "gb"
class TestWalkTree:
def test_walk_tree_dict_found(self):
data = {"level1": {"level2": {"target": "found"}}}
result = walk_tree(data, "target")
assert result == "found"
def test_walk_tree_dict_not_found(self):
data = {"level1": {"level2": {"other": "value"}}}
result = walk_tree(data, "target")
assert result is None
def test_walk_tree_list_found(self):
data = [{"other": "value"}, {"target": "found"}]
result = walk_tree(data, "target")
assert result == "found"
def test_walk_tree_nested_list_dict(self):
data = [{"level1": [{"target": "found"}]}]
result = walk_tree(data, "target")
assert result == "found"
def test_walk_tree_empty_data(self):
result = walk_tree({}, "target")
assert result is None
class TestGetPriceFromReservation:
def test_get_price_from_reservation_valid(self):
reservation = {
"payment_summary": {"subtitle": "Total cost: £150.00"}
}
result = get_price_from_reservation(reservation)
assert result == "150.00"
def test_get_price_from_reservation_different_amount(self):
reservation = {
"payment_summary": {"subtitle": "Total cost: £89.99"}
}
result = get_price_from_reservation(reservation)
assert result == "89.99"
class TestParseMultipleFiles:
@patch('agenda.airbnb.extract_booking_from_html')
def test_parse_multiple_files_single_file(self, mock_extract):
mock_booking = {
"type": "apartment",
"operator": "airbnb",
"name": "Test Apartment",
"booking_reference": "ABC123"
}
mock_extract.return_value = mock_booking
result = parse_multiple_files(["test1.html"])
assert len(result) == 1
assert result[0] == mock_booking
mock_extract.assert_called_once_with("test1.html")
@patch('agenda.airbnb.extract_booking_from_html')
def test_parse_multiple_files_multiple_files(self, mock_extract):
mock_booking1 = {"booking_reference": "ABC123"}
mock_booking2 = {"booking_reference": "DEF456"}
mock_extract.side_effect = [mock_booking1, mock_booking2]
result = parse_multiple_files(["test2.html", "test1.html"])
assert len(result) == 2
assert result[0] == mock_booking1
assert result[1] == mock_booking2
@patch('agenda.airbnb.extract_booking_from_html')
def test_parse_multiple_files_empty_list(self, mock_extract):
result = parse_multiple_files([])
assert result == []
mock_extract.assert_not_called()
class TestGetUiState:
@patch('lxml.html.etree')
def test_get_ui_state_with_mock_tree(self, mock_etree):
mock_tree = Mock()
mock_tree.xpath.return_value = ['{"test": [["uiState", {"key": "value"}]]}']
with patch('agenda.airbnb.walk_tree') as mock_walk:
mock_walk.return_value = [["key", "value"]]
result = get_ui_state(mock_tree)
assert result == {"key": "value"}
mock_tree.xpath.assert_called_once_with('//*[@id="data-injector-instances"]/text()')
class TestGetReservationData:
def test_get_reservation_data(self):
ui_state = {
"reservation": {
"scheduled_event": {
"rows": [
{"id": "row1", "data": "value1"},
{"id": "row2", "data": "value2"}
]
}
}
}
result = get_reservation_data(ui_state)
expected = {
"row1": {"id": "row1", "data": "value1"},
"row2": {"id": "row2", "data": "value2"}
}
assert result == expected