- 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
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user