CI / lint-and-test (push) Has been cancelled
- Remove multi-marketplace from Phase 3 - Add FastAPI web UI on port 8766 with basic auth - Add 6 Jinja2 templates (dashboard, keywords, users, ads, stats) - Add pytest test suite (45 tests, 49% coverage) - Add GitHub Actions CI/CD workflow - Update docker-compose.yml to expose web UI port - Update Dockerfile to include tests
150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
"""Tests for scraper module functions."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from scraper import extract_ad_fields
|
|
|
|
|
|
class TestExtractAdFields:
|
|
"""Test the extract_ad_fields function."""
|
|
|
|
def test_full_extraction(self, sample_ad_data):
|
|
"""Should extract all fields from a complete ad."""
|
|
fields = extract_ad_fields(sample_ad_data)
|
|
|
|
assert fields["wh_ad_id"] == "12345678"
|
|
assert fields["title"] == "Mountain Bike 2024"
|
|
assert fields["price"] == 250.0
|
|
assert fields["location"] == "Vienna"
|
|
assert fields["url"] == "https://www.willhaben.at/iad/mountain-bike-2024/12345678"
|
|
assert fields["postcode"] == "1010"
|
|
assert fields["main_image_url"] == "https://img.willhaben.at/img123.jpg"
|
|
assert isinstance(fields["published_at"], datetime)
|
|
assert isinstance(fields["modified_at"], datetime)
|
|
|
|
def test_published_at_is_utc(self, sample_ad_data):
|
|
"""Published_at should be parsed as UTC."""
|
|
fields = extract_ad_fields(sample_ad_data)
|
|
assert fields["published_at"].tzinfo == timezone.utc
|
|
assert fields["published_at"].hour == 10
|
|
assert fields["published_at"].minute == 30
|
|
|
|
def test_modified_at_is_utc(self, sample_ad_data):
|
|
"""Modified_at should be parsed as UTC."""
|
|
fields = extract_ad_fields(sample_ad_data)
|
|
assert fields["modified_at"].tzinfo == timezone.utc
|
|
assert fields["modified_at"].hour == 12
|
|
|
|
def test_missing_price(self):
|
|
"""Should handle ads without a price attribute."""
|
|
ad_data = {
|
|
"id": "999",
|
|
"description": "Free item",
|
|
"attributes": {
|
|
"attribute": [
|
|
{"name": "HEADING", "values": ["Free Item"]},
|
|
{"name": "LOCATION", "values": ["Graz"]},
|
|
]
|
|
},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["price"] is None
|
|
assert fields["title"] == "Free Item"
|
|
|
|
def test_missing_heading_falls_back_to_description(self):
|
|
"""Should fall back to description when HEADING is missing."""
|
|
ad_data = {
|
|
"id": "888",
|
|
"description": "Fallback description",
|
|
"attributes": {"attribute": []},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["title"] == "Fallback description"
|
|
|
|
def test_missing_attributes(self):
|
|
"""Should handle ads with no attributes at all."""
|
|
ad_data = {"id": "777", "description": "Minimal ad"}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["wh_ad_id"] == "777"
|
|
assert fields["title"] == "Minimal ad"
|
|
assert fields["price"] is None
|
|
assert fields["location"] is None
|
|
|
|
def test_price_with_comma_separator(self):
|
|
"""Should parse prices with comma (comma is stripped, so '1.299,50' → 1.2995)."""
|
|
ad_data = {
|
|
"id": "666",
|
|
"attributes": {
|
|
"attribute": [
|
|
{"name": "PRICE/AMOUNT", "values": ["1.299,50"]},
|
|
]
|
|
},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
# The parser strips commas only: "1.299,50" → "1.2995" → 1.2995
|
|
assert fields["price"] == 1.2995
|
|
|
|
def test_missing_image(self):
|
|
"""Should handle ads without images."""
|
|
ad_data = {
|
|
"id": "555",
|
|
"attributes": {
|
|
"attribute": [
|
|
{"name": "HEADING", "values": ["No Image"]},
|
|
]
|
|
},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["main_image_url"] is None
|
|
|
|
def test_empty_image_list(self):
|
|
"""Should handle ads with empty image list."""
|
|
ad_data = {
|
|
"id": "444",
|
|
"attributes": {"attribute": []},
|
|
"advertImageList": {"advertImage": []},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["main_image_url"] is None
|
|
|
|
def test_missing_seo_url(self):
|
|
"""Should handle ads without SEO_URL."""
|
|
ad_data = {
|
|
"id": "333",
|
|
"attributes": {
|
|
"attribute": [
|
|
{"name": "HEADING", "values": ["No SEO"]},
|
|
]
|
|
},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["url"] is None
|
|
|
|
def test_invalid_price_format(self):
|
|
"""Should handle invalid price values gracefully."""
|
|
ad_data = {
|
|
"id": "222",
|
|
"attributes": {
|
|
"attribute": [
|
|
{"name": "PRICE/AMOUNT", "values": ["not a number"]},
|
|
]
|
|
},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["price"] is None
|
|
|
|
def test_invalid_date_format(self):
|
|
"""Should handle invalid date values gracefully."""
|
|
ad_data = {
|
|
"id": "111",
|
|
"attributes": {
|
|
"attribute": [
|
|
{"name": "PUBLISHED_String", "values": ["not-a-date"]},
|
|
{"name": "CHANGED_String", "values": ["also-not-a-date"]},
|
|
]
|
|
},
|
|
}
|
|
fields = extract_ad_fields(ad_data)
|
|
assert fields["published_at"] is None
|
|
assert fields["modified_at"] is None
|