Files
willhaben-tracker/worker/tests/test_scraper.py
Lago 8151c530da
CI / lint-and-test (push) Has been cancelled
feat: Phase 3 — web dashboard, testing, and CI/CD
- 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
2026-07-10 22:28:18 +02:00

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