feat(console): add database abstraction layer with tests
Implement console/db.py with Database class providing all data access methods for the console application, plus 36 passing tests in console/tests/test_db.py covering vehicle navigation, parts catalog, search, VIN cache, stats, manufacturers, and admin CRUD operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
273
console/tests/test_db.py
Normal file
273
console/tests/test_db.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Tests for the Database abstraction layer.
|
||||
|
||||
All tests run against the real SQLite database at vehicle_database/vehicle_database.db.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from console.db import Database
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def db():
|
||||
"""Provide a shared Database instance for all tests in this module."""
|
||||
return Database()
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Vehicle navigation
|
||||
# =========================================================================
|
||||
|
||||
class TestGetBrands:
|
||||
def test_returns_nonempty_list(self, db):
|
||||
brands = db.get_brands()
|
||||
assert isinstance(brands, list)
|
||||
assert len(brands) > 0
|
||||
|
||||
def test_each_brand_has_name_key(self, db):
|
||||
brands = db.get_brands()
|
||||
for b in brands:
|
||||
assert "name" in b
|
||||
|
||||
def test_each_brand_has_id_and_country(self, db):
|
||||
brands = db.get_brands()
|
||||
for b in brands:
|
||||
assert "id" in b
|
||||
assert "country" in b
|
||||
|
||||
|
||||
class TestGetModels:
|
||||
def test_no_filter_returns_nonempty(self, db):
|
||||
models = db.get_models()
|
||||
assert isinstance(models, list)
|
||||
assert len(models) > 0
|
||||
|
||||
def test_filter_by_uppercase_brand(self, db):
|
||||
models = db.get_models(brand="TOYOTA")
|
||||
assert isinstance(models, list)
|
||||
assert len(models) > 0
|
||||
|
||||
def test_filter_by_lowercase_brand(self, db):
|
||||
"""Brand filtering must be case-insensitive."""
|
||||
models = db.get_models(brand="toyota")
|
||||
assert isinstance(models, list)
|
||||
assert len(models) > 0
|
||||
|
||||
def test_each_model_has_id_and_name(self, db):
|
||||
models = db.get_models()
|
||||
for m in models[:5]:
|
||||
assert "id" in m
|
||||
assert "name" in m
|
||||
|
||||
|
||||
class TestGetYears:
|
||||
def test_returns_list(self, db):
|
||||
years = db.get_years()
|
||||
assert isinstance(years, list)
|
||||
assert len(years) > 0
|
||||
|
||||
def test_filter_by_brand(self, db):
|
||||
years = db.get_years(brand="TOYOTA")
|
||||
assert isinstance(years, list)
|
||||
assert len(years) > 0
|
||||
|
||||
def test_each_year_has_id_and_year(self, db):
|
||||
years = db.get_years()
|
||||
for y in years[:5]:
|
||||
assert "id" in y
|
||||
assert "year" in y
|
||||
|
||||
|
||||
class TestGetEngines:
|
||||
def test_returns_list(self, db):
|
||||
engines = db.get_engines()
|
||||
assert isinstance(engines, list)
|
||||
assert len(engines) > 0
|
||||
|
||||
def test_filter_by_brand(self, db):
|
||||
engines = db.get_engines(brand="TOYOTA")
|
||||
assert isinstance(engines, list)
|
||||
assert len(engines) > 0
|
||||
|
||||
|
||||
class TestGetModelYearEngine:
|
||||
def test_returns_list(self, db):
|
||||
result = db.get_model_year_engine(
|
||||
brand="TOYOTA", model="Corolla", year=2020, engine_id=None
|
||||
)
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Parts catalog
|
||||
# =========================================================================
|
||||
|
||||
class TestGetCategories:
|
||||
def test_returns_exactly_12(self, db):
|
||||
categories = db.get_categories()
|
||||
assert isinstance(categories, list)
|
||||
assert len(categories) == 12
|
||||
|
||||
def test_each_has_expected_keys(self, db):
|
||||
categories = db.get_categories()
|
||||
for c in categories:
|
||||
assert "id" in c
|
||||
assert "name" in c
|
||||
|
||||
|
||||
class TestGetGroups:
|
||||
def test_returns_nonempty_for_known_category(self, db):
|
||||
groups = db.get_groups(category_id=2)
|
||||
assert isinstance(groups, list)
|
||||
assert len(groups) > 0
|
||||
|
||||
def test_each_group_has_name(self, db):
|
||||
groups = db.get_groups(category_id=2)
|
||||
for g in groups:
|
||||
assert "name" in g
|
||||
|
||||
|
||||
class TestGetParts:
|
||||
def test_returns_list(self, db):
|
||||
parts = db.get_parts()
|
||||
assert isinstance(parts, list)
|
||||
assert len(parts) > 0
|
||||
|
||||
def test_pagination(self, db):
|
||||
page1 = db.get_parts(page=1, per_page=5)
|
||||
page2 = db.get_parts(page=2, per_page=5)
|
||||
assert len(page1) <= 5
|
||||
assert len(page2) <= 5
|
||||
# Pages should contain different items (if enough data)
|
||||
if page1 and page2:
|
||||
ids1 = {p["id"] for p in page1}
|
||||
ids2 = {p["id"] for p in page2}
|
||||
assert ids1.isdisjoint(ids2)
|
||||
|
||||
|
||||
class TestGetPart:
|
||||
def test_returns_dict_with_oem_part_number(self, db):
|
||||
part = db.get_part(1)
|
||||
assert isinstance(part, dict)
|
||||
assert "oem_part_number" in part
|
||||
|
||||
def test_includes_group_and_category_info(self, db):
|
||||
part = db.get_part(1)
|
||||
assert "group_name" in part
|
||||
assert "category_name" in part
|
||||
|
||||
def test_nonexistent_returns_none(self, db):
|
||||
part = db.get_part(999999)
|
||||
assert part is None
|
||||
|
||||
|
||||
class TestGetAlternatives:
|
||||
def test_returns_list(self, db):
|
||||
alts = db.get_alternatives(1)
|
||||
assert isinstance(alts, list)
|
||||
|
||||
|
||||
class TestGetCrossReferences:
|
||||
def test_returns_list(self, db):
|
||||
refs = db.get_cross_references(1)
|
||||
assert isinstance(refs, list)
|
||||
|
||||
|
||||
class TestGetVehiclesForPart:
|
||||
def test_returns_list(self, db):
|
||||
vehicles = db.get_vehicles_for_part(1)
|
||||
assert isinstance(vehicles, list)
|
||||
assert len(vehicles) > 0
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Search
|
||||
# =========================================================================
|
||||
|
||||
class TestSearchParts:
|
||||
def test_returns_results_for_brake(self, db):
|
||||
results = db.search_parts("brake")
|
||||
assert isinstance(results, list)
|
||||
assert len(results) > 0
|
||||
|
||||
def test_each_result_has_expected_keys(self, db):
|
||||
results = db.search_parts("brake")
|
||||
for r in results[:3]:
|
||||
assert "id" in r
|
||||
assert "name" in r
|
||||
assert "oem_part_number" in r
|
||||
|
||||
|
||||
class TestSearchPartNumber:
|
||||
def test_returns_results_for_04465(self, db):
|
||||
results = db.search_part_number("04465")
|
||||
assert isinstance(results, list)
|
||||
assert len(results) > 0
|
||||
|
||||
def test_each_result_has_match_type(self, db):
|
||||
results = db.search_part_number("04465")
|
||||
for r in results:
|
||||
assert "match_type" in r
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# VIN cache
|
||||
# =========================================================================
|
||||
|
||||
class TestVinCache:
|
||||
def test_get_nonexistent_vin_returns_none(self, db):
|
||||
result = db.get_vin_cache("00000000000000000")
|
||||
assert result is None
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Stats
|
||||
# =========================================================================
|
||||
|
||||
class TestGetStats:
|
||||
def test_returns_dict_with_required_keys(self, db):
|
||||
stats = db.get_stats()
|
||||
assert isinstance(stats, dict)
|
||||
assert "brands" in stats
|
||||
assert "models" in stats
|
||||
assert "parts" in stats
|
||||
|
||||
def test_counts_are_positive(self, db):
|
||||
stats = db.get_stats()
|
||||
assert stats["brands"] > 0
|
||||
assert stats["models"] > 0
|
||||
assert stats["parts"] > 0
|
||||
|
||||
def test_includes_top_brands(self, db):
|
||||
stats = db.get_stats()
|
||||
assert "top_brands" in stats
|
||||
assert isinstance(stats["top_brands"], list)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Manufacturers
|
||||
# =========================================================================
|
||||
|
||||
class TestGetManufacturers:
|
||||
def test_returns_nonempty_list(self, db):
|
||||
manufacturers = db.get_manufacturers()
|
||||
assert isinstance(manufacturers, list)
|
||||
assert len(manufacturers) > 0
|
||||
|
||||
def test_each_has_name(self, db):
|
||||
manufacturers = db.get_manufacturers()
|
||||
for m in manufacturers:
|
||||
assert "name" in m
|
||||
assert "id" in m
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Admin CRUD — smoke tests
|
||||
# =========================================================================
|
||||
|
||||
class TestCrossrefsPaginated:
|
||||
def test_returns_list(self, db):
|
||||
refs = db.get_crossrefs_paginated(page=1, per_page=5)
|
||||
assert isinstance(refs, list)
|
||||
assert len(refs) <= 5
|
||||
Reference in New Issue
Block a user