feat: Implement Walmart scraper and integrate with existing architecture

- Added Walmart scraper to scrape product data from Walmart.com, including category pages and product details.
- Introduced a stealth browser module to handle bot protection and improve scraping reliability.
- Created a SQLite database for tracking product history, price changes, stock events, and user favorites.
- Developed a Discord bot for user interaction, allowing location setting and stock checking at local stores.
- Implemented a favorites system to manage priority products and categories with custom notification settings.
- Added news aggregation module to fetch and analyze Pokemon TCG news from various sources.
- Created tools for API discovery and monitoring, including a backend monitor for detecting new products.
- Added unit tests for database operations, product filtering, and API endpoints to ensure functionality.
- Enhanced existing modules with improved error handling and logging for better maintainability.
This commit is contained in:
2026-03-27 23:08:09 -04:00
parent cddae24e34
commit 8d382e723f
64 changed files with 15433 additions and 443 deletions
+300
View File
@@ -0,0 +1,300 @@
"""
Unit tests for database operations.
"""
import pytest
import tempfile
import os
from datetime import datetime, timedelta
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.database import Database
@pytest.fixture
def db():
"""Create a temporary database for testing"""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
database = Database(db_path)
yield database
# Cleanup
os.unlink(db_path)
class TestProductOperations:
"""Tests for product CRUD operations"""
def test_create_product(self, db):
"""Test creating a new product"""
product_id = db.get_or_create_product(
url="https://example.com/product/123",
name="Test Pokemon Card",
site="pokemoncenter",
product_id="123",
price="$19.99",
in_stock=True
)
assert product_id is not None
assert product_id > 0
def test_get_product_by_url(self, db):
"""Test retrieving product by URL"""
url = "https://example.com/product/456"
db.get_or_create_product(
url=url,
name="Pikachu ETB",
site="target",
in_stock=True
)
product = db.get_product_by_url(url)
assert product is not None
assert product['name'] == "Pikachu ETB"
assert product['site'] == "target"
def test_update_product_price(self, db):
"""Test updating product price"""
product_id = db.get_or_create_product(
url="https://example.com/product/789",
name="Booster Box",
site="bestbuy",
price="$99.99",
in_stock=True
)
# Update price
db.update_product_price(product_id, "$89.99")
product = db.get_product(product_id)
assert product['current_price'] == "$89.99"
# Check price history was recorded
history = db.get_price_history(product_id, days=1)
assert len(history) >= 1
def test_update_product_stock(self, db):
"""Test updating product stock status"""
product_id = db.get_or_create_product(
url="https://example.com/product/stock-test",
name="Limited Edition",
site="pokemoncenter",
in_stock=True
)
# Mark out of stock
db.update_product_stock(product_id, False)
product = db.get_product(product_id)
assert product['in_stock'] == 0 # SQLite stores as 0/1
def test_category_detection(self, db):
"""Test automatic category detection from product name"""
test_cases = [
("Pokemon Scarlet Elite Trainer Box", "ETB"),
("Charizard Booster Bundle", "Booster Bundle"),
("Display Booster Box 36 Packs", "Booster Box"),
("Sleeved Booster Pack", "Booster Pack"),
("Premium Collection Box", "Collection Box"),
("Pokemon Trading Card Tin", "Tin"),
("Card Binder Album", "Accessories"),
("Random Pokemon Item", "Other"),
]
for name, expected_category in test_cases:
product_id = db.get_or_create_product(
url=f"https://example.com/{name.replace(' ', '-')}",
name=name,
site="test",
in_stock=True
)
product = db.get_product(product_id)
assert product['category'] == expected_category, f"Failed for: {name}"
class TestStockEvents:
"""Tests for stock event tracking"""
def test_record_stock_event(self, db):
"""Test recording stock events"""
product_id = db.get_or_create_product(
url="https://example.com/event-test",
name="Event Test Product",
site="test",
in_stock=True
)
db.record_stock_event(product_id, "new_drop")
db.record_stock_event(product_id, "out_of_stock")
db.record_stock_event(product_id, "restock")
events = db.get_recent_events(limit=10)
# Should have 3 events
product_events = [e for e in events if e['product_id'] == product_id]
assert len(product_events) == 3
def test_events_today_count(self, db):
"""Test counting events from today"""
product_id = db.get_or_create_product(
url="https://example.com/today-test",
name="Today Test",
site="test",
in_stock=True
)
db.record_stock_event(product_id, "new_drop")
db.record_stock_event(product_id, "restock")
new_drops = db.get_events_today("new_drop")
restocks = db.get_events_today("restock")
assert new_drops >= 1
assert restocks >= 1
def test_selling_rate_calculation(self, db):
"""Test that selling rate can be calculated from events"""
product_id = db.get_or_create_product(
url="https://example.com/sellrate-test",
name="Fast Seller",
site="pokemoncenter",
in_stock=True
)
# Simulate: drop -> sold out
db.record_stock_event(product_id, "new_drop")
db.record_stock_event(product_id, "out_of_stock")
# Get stock duration stats
stats = db.get_stock_duration_stats()
# Should return at least an empty list (stats depend on timing)
assert isinstance(stats, list)
class TestPriceHistory:
"""Tests for price history tracking"""
def test_record_price_change(self, db):
"""Test that price changes are recorded"""
product_id = db.get_or_create_product(
url="https://example.com/price-test",
name="Price Test",
site="test",
price="$49.99",
in_stock=True
)
# Record initial price
db.record_price(product_id, "$49.99")
# Change price
db.record_price(product_id, "$39.99")
history = db.get_price_history(product_id, days=1)
# Should have at least 2 price points
assert len(history) >= 1
def test_duplicate_price_not_recorded(self, db):
"""Test that same price is not recorded multiple times"""
product_id = db.get_or_create_product(
url="https://example.com/dup-price-test",
name="Dup Price Test",
site="test",
in_stock=True
)
# Record same price multiple times
db.record_price(product_id, "$29.99")
db.record_price(product_id, "$29.99")
db.record_price(product_id, "$29.99")
history = db.get_price_history(product_id, days=1)
# Should only have 1 entry
assert len(history) == 1
class TestDashboardStats:
"""Tests for dashboard statistics"""
def test_get_dashboard_stats(self, db):
"""Test getting dashboard stats summary"""
# Add some test data
for i in range(5):
product_id = db.get_or_create_product(
url=f"https://example.com/stats-{i}",
name=f"Stats Product {i}",
site="pokemoncenter",
in_stock=(i % 2 == 0)
)
db.record_stock_event(product_id, "new_drop")
stats = db.get_dashboard_stats()
assert 'total_products' in stats
assert 'in_stock_count' in stats
assert 'new_drops_today' in stats
assert stats['total_products'] >= 5
def test_get_site_stats(self, db):
"""Test per-site statistics"""
sites = ['target', 'bestbuy', 'pokemoncenter']
for site in sites:
db.get_or_create_product(
url=f"https://{site}.com/test",
name=f"{site} Product",
site=site,
in_stock=True
)
stats = db.get_site_stats()
assert len(stats) == 3
site_names = [s['site'] for s in stats]
assert 'target' in site_names
assert 'bestbuy' in site_names
class TestFavorites:
"""Tests for favorites functionality"""
def test_add_favorite(self, db):
"""Test adding a favorite"""
fav_id = db.add_favorite(
fav_type="product",
value="https://example.com/fav-product",
display_name="My Favorite Card",
priority="high"
)
assert fav_id is not None
favorite = db.get_favorite(fav_id)
assert favorite['display_name'] == "My Favorite Card"
def test_check_is_favorite(self, db):
"""Test checking if product is favorited"""
url = "https://example.com/check-fav"
# Not a favorite yet
result = db.check_is_favorite(url=url)
assert result is None
# Add as favorite
db.add_favorite(fav_type="product", value=url)
# Now should be found
result = db.check_is_favorite(url=url)
assert result is not None
if __name__ == "__main__":
pytest.main([__file__, "-v"])