Add flight comparator web app with full scan pipeline
Full-stack flight price scanner built on fast-flights v3 (SOCS cookie bypass): Backend (FastAPI + SQLite): - REST API with rate limiting, Pydantic v2 validation, paginated responses - Scan pipeline: resolves airports, queries every day in the window, saves individual flights + aggregate route stats to SQLite - Background async scan processor with real-time progress tracking - Airport search endpoint backed by OpenFlights dataset - Daily scan window (all dates, not monthly samples) Frontend (React 19 + TypeScript + Tailwind CSS v4): - Dashboard with live scan status and recent scans - Create scan form: country mode or specific airports (searchable dropdown) - Scan detail page with expandable route rows showing individual flights (date, airline, departure, arrival, price) loaded on demand - AirportSearch component with debounced live search and multi-select Database: - scans → routes → flights schema with FK cascade and auto-update triggers - Migrations for schema evolution (relaxed country constraint) Tests: - 74 tests: unit + integration, isolated per-test SQLite DB - Confirmed flight fixtures in tests/confirmed_flights.json (50 real flights, BDS→FMM Ryanair + BDS→DUS Eurowings, scraped Feb 2026) - Integration tests parametrized from confirmed routes Docker: - Multi-stage builds, Compose orchestration, Nginx reverse proxy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
195
flight-comparator/tests/conftest.py
Normal file
195
flight-comparator/tests/conftest.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Test fixtures and configuration for Flight Radar Web App tests.
|
||||
|
||||
This module provides reusable fixtures for testing the API.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sqlite3
|
||||
import os
|
||||
import tempfile
|
||||
from fastapi.testclient import TestClient
|
||||
from typing import Generator
|
||||
|
||||
# Import the FastAPI app
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from api_server import app, rate_limiter, log_buffer
|
||||
from database import get_connection
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_db_path() -> Generator[str, None, None]:
|
||||
"""Create a temporary database for testing."""
|
||||
# Create temporary database
|
||||
fd, path = tempfile.mkstemp(suffix=".db")
|
||||
os.close(fd)
|
||||
|
||||
# Set environment variable to use test database
|
||||
original_db = os.environ.get('DATABASE_PATH')
|
||||
os.environ['DATABASE_PATH'] = path
|
||||
|
||||
yield path
|
||||
|
||||
# Cleanup
|
||||
if original_db:
|
||||
os.environ['DATABASE_PATH'] = original_db
|
||||
else:
|
||||
os.environ.pop('DATABASE_PATH', None)
|
||||
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def clean_database(test_db_path):
|
||||
"""Provide a clean database for each test."""
|
||||
# Initialize database with schema
|
||||
conn = sqlite3.connect(test_db_path)
|
||||
conn.execute("PRAGMA foreign_keys = ON") # Enable foreign keys
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Read and execute schema
|
||||
schema_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database', 'schema.sql')
|
||||
with open(schema_path, 'r') as f:
|
||||
schema_sql = f.read()
|
||||
cursor.executescript(schema_sql)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
yield test_db_path
|
||||
|
||||
# Clean up all tables after test
|
||||
conn = sqlite3.connect(test_db_path)
|
||||
conn.execute("PRAGMA foreign_keys = ON") # Enable foreign keys for cleanup
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM routes")
|
||||
cursor.execute("DELETE FROM scans")
|
||||
# Note: flight_searches and flight_results are not in web app schema
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client(clean_database) -> TestClient:
|
||||
"""Provide a test client for the FastAPI app."""
|
||||
# Clear rate limiter for each test
|
||||
rate_limiter.requests.clear()
|
||||
|
||||
# Clear log buffer for each test
|
||||
log_buffer.clear()
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_scan_data():
|
||||
"""Provide sample scan data for testing."""
|
||||
return {
|
||||
"origin": "BDS",
|
||||
"country": "DE",
|
||||
"start_date": "2026-04-01",
|
||||
"end_date": "2026-06-30",
|
||||
"seat_class": "economy",
|
||||
"adults": 2
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_route_data():
|
||||
"""Provide sample route data for testing."""
|
||||
return {
|
||||
"scan_id": 1,
|
||||
"destination": "MUC",
|
||||
"destination_name": "Munich Airport",
|
||||
"destination_city": "Munich",
|
||||
"flight_count": 45,
|
||||
"airlines": '["Lufthansa", "Ryanair"]',
|
||||
"min_price": 89.99,
|
||||
"max_price": 299.99,
|
||||
"avg_price": 150.50
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_scan(clean_database, sample_scan_data):
|
||||
"""Create a test scan in the database."""
|
||||
def _create_scan(**kwargs):
|
||||
# Merge with defaults
|
||||
data = {**sample_scan_data, **kwargs}
|
||||
|
||||
conn = sqlite3.connect(clean_database)
|
||||
conn.execute("PRAGMA foreign_keys = ON") # Enable foreign keys
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO scans (origin, country, start_date, end_date, status, seat_class, adults)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
data['origin'],
|
||||
data['country'],
|
||||
data['start_date'],
|
||||
data['end_date'],
|
||||
data.get('status', 'pending'),
|
||||
data['seat_class'],
|
||||
data['adults']
|
||||
))
|
||||
|
||||
scan_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return scan_id
|
||||
|
||||
return _create_scan
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_route(clean_database, sample_route_data):
|
||||
"""Create a test route in the database."""
|
||||
def _create_route(**kwargs):
|
||||
# Merge with defaults
|
||||
data = {**sample_route_data, **kwargs}
|
||||
|
||||
conn = sqlite3.connect(clean_database)
|
||||
conn.execute("PRAGMA foreign_keys = ON") # Enable foreign keys
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO routes (scan_id, destination, destination_name, destination_city,
|
||||
flight_count, airlines, min_price, max_price, avg_price)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
data['scan_id'],
|
||||
data['destination'],
|
||||
data['destination_name'],
|
||||
data['destination_city'],
|
||||
data['flight_count'],
|
||||
data['airlines'],
|
||||
data['min_price'],
|
||||
data['max_price'],
|
||||
data['avg_price']
|
||||
))
|
||||
|
||||
route_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return route_id
|
||||
|
||||
return _create_route
|
||||
|
||||
|
||||
# Marker helpers for categorizing tests
|
||||
def pytest_configure(config):
|
||||
"""Configure custom pytest markers."""
|
||||
config.addinivalue_line("markers", "unit: Unit tests (fast, isolated)")
|
||||
config.addinivalue_line("markers", "integration: Integration tests (slower)")
|
||||
config.addinivalue_line("markers", "slow: Slow tests")
|
||||
config.addinivalue_line("markers", "database: Tests that interact with database")
|
||||
config.addinivalue_line("markers", "api: Tests for API endpoints")
|
||||
Reference in New Issue
Block a user