oem_crawler/tests/test_raw_storage.py
2026-11-07 16:53:30 +01:00

206 lines
6.3 KiB
Python

"""
Tests for RawStorageManager, external compressed storage, and raw extraction toggle.
"""
from __future__ import annotations
import gzip
import json
import shutil
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from oem_crawler.config import settings
from oem_crawler.crawlers.audi.parser import parse_audi_vehicle
from oem_crawler.crawlers.bmw.parser import parse_bmw_vehicle
from oem_crawler.db.models import Base, Vehicle, VehicleOption
from oem_crawler.db.pipeline import StockIngestionPipeline
from oem_crawler.db.schemas import (
OptionSchema,
VehicleCondition,
VehicleIngestSchema,
VehicleStatus,
)
from oem_crawler.storage import RawStorageManager
@pytest.fixture
def temp_raw_dir(tmp_path):
orig_dir = settings.RAW_STORAGE_DIR
orig_enabled = settings.ENABLE_RAW_EXTRACTION
raw_dir = tmp_path / "raw_storage"
settings.RAW_STORAGE_DIR = str(raw_dir)
settings.ENABLE_RAW_EXTRACTION = True
yield raw_dir
settings.RAW_STORAGE_DIR = orig_dir
settings.ENABLE_RAW_EXTRACTION = orig_enabled
def test_raw_storage_manager_save_and_load(temp_raw_dir):
payload = {"brand": "BMW", "specs": {"engine": "v8", "power": 390}}
timestamp = datetime(2026, 11, 7, 12, 0, 0, tzinfo=timezone.utc)
# 1. Save
rel_path = RawStorageManager.save_payload(
brand="BMW",
oem_vehicle_id="VIN-123456",
payload=payload,
timestamp=timestamp,
)
assert rel_path == "bmw/2026-11-07/VIN-123456.json.gz"
full_path = temp_raw_dir / rel_path
assert full_path.exists()
# 2. Verify compression
with gzip.open(full_path, "rt", encoding="utf-8") as f:
loaded_raw = json.load(f)
assert loaded_raw == payload
# 3. Load via Manager
loaded = RawStorageManager.load_payload(rel_path)
assert loaded == payload
# 4. Delete
deleted = RawStorageManager.delete_payload(rel_path)
assert deleted is True
assert not full_path.exists()
def test_raw_storage_manager_disabled(temp_raw_dir):
settings.ENABLE_RAW_EXTRACTION = False
payload = {"key": "val"}
rel_path = RawStorageManager.save_payload(
brand="AUDI",
oem_vehicle_id="AUDI-001",
payload=payload,
)
assert rel_path is None
assert not (temp_raw_dir / "audi").exists()
def test_parser_include_raw_toggle():
# BMW parser with include_raw=False
raw_bmw = {
"documentId": "TEST-BMW-RAW",
"vehicleSpecification": {
"modelAndOption": {
"model": {"modelName": "BMW 330i"},
"modelRange": {"description": {"de_DE": "3er"}},
}
},
"pricing": {"price": {"value": 55000}},
"extra_huge_catalog": {"items": [1, 2, 3] * 1000},
}
veh_with_raw = parse_bmw_vehicle(raw_bmw, include_raw=True)
assert veh_with_raw.raw_payload == raw_bmw
veh_no_raw = parse_bmw_vehicle(raw_bmw, include_raw=False)
assert veh_no_raw.raw_payload == {}
assert veh_no_raw.model_name == "BMW 330i"
assert veh_no_raw.series == "3er"
def test_pipeline_ingestion_with_raw_storage(temp_raw_dir, tmp_path):
db_file = tmp_path / "test_stock.db"
db_url = f"sqlite:///{db_file}"
engine = create_engine(db_url)
Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)
pipeline = StockIngestionPipeline(session_factory=session_factory)
test_vehicle = VehicleIngestSchema(
brand="BMW",
oem_vehicle_id="BMW-INGEST-01",
series="3er",
model_name="320d",
condition=VehicleCondition.NEW,
fuel_type="diesel",
mileage_km=0,
price_gross=Decimal("45000.00"),
status=VehicleStatus.IN_STOCK,
options=[
OptionSchema(option_code="S01", name="Sport Line", raw_data={"pr": "S01"}),
],
raw_payload={"full_spec": "all details here", "code": "BMW-INGEST-01"},
)
def stream():
yield test_vehicle
run = pipeline.run_crawler_pipeline(
brand="BMW",
crawler_name="test-crawler",
vehicle_stream=stream(),
market_country="DE",
)
assert run.items_inserted == 1
with session_factory() as session:
veh = session.query(Vehicle).filter_by(oem_vehicle_id="BMW-INGEST-01").first()
assert veh is not None
# In-database raw_payload is kept empty to save disk space
assert veh.raw_payload == {}
# Relative path points to the compressed file
assert veh.raw_payload_path is not None
assert veh.raw_payload_path.startswith("bmw/")
assert veh.raw_payload_path.endswith(".json.gz")
# Options raw_data in database is kept empty
opt = session.query(VehicleOption).filter_by(vehicle_id=veh.id).first()
assert opt.name == "Sport Line"
assert opt.raw_data == {}
# Loading via helper method reads from disk seamlessly
recovered_payload = veh.get_raw_payload()
assert recovered_payload == {"full_spec": "all details here", "code": "BMW-INGEST-01"}
def test_restore_database_from_raw(temp_raw_dir, tmp_path):
from oem_crawler.tools.restore_from_raw import restore_database
# Create dummy raw payload files in temp_raw_dir
payload = {
"documentId": "BMW-RESTORE-99",
"vehicleSpecification": {
"modelAndOption": {
"model": {"modelName": "BMW 520d"},
"modelRange": {"description": {"de_DE": "5er"}},
}
},
"pricing": {"price": {"value": 62000}},
}
RawStorageManager.save_payload(
brand="bmw",
oem_vehicle_id="BMW-RESTORE-99",
payload=payload,
)
db_file = tmp_path / "restored.db"
db_url = f"sqlite:///{db_file}"
# Restore into fresh DB
restore_database(
raw_dir=temp_raw_dir,
db_url=db_url,
target_brands=["bmw"],
)
engine = create_engine(db_url)
session_factory = sessionmaker(bind=engine)
with session_factory() as session:
veh = session.query(Vehicle).filter_by(oem_vehicle_id="BMW-RESTORE-99").first()
assert veh is not None
assert veh.model_name == "BMW 520d"
assert veh.price_gross == Decimal("62000")
assert veh.raw_payload_path.startswith("bmw/")
assert veh.raw_payload == {}