- 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>
186 lines
6 KiB
Python
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 |