snipe/tests/ui/test_easter_eggs.py
pyr0ball 6ec0f957b9 feat(snipe): auction support + easter eggs (Konami, The Steal, de-emphasis)
Auction metadata:
- Listing model gains buying_format + ends_at fields
- Migration 002 adds columns to existing databases
- scraper.py: parse s-item__time-left → absolute ends_at ISO timestamp
- normaliser.py: extract buyingOptions + itemEndDate from Browse API
- store.py: save/get updated for new fields

Easter eggs (app/ui/components/easter_eggs.py):
- Konami code detector (JS → URL param → Streamlit rerun)
- Web Audio API snipe call synthesis, gated behind sidebar checkbox
  (disabled by default for safety/accessibility)
- "The Steal" gold shimmer: trust ≥ 90, price 15–30% below market,
  no suspicious_price flag
- Auction de-emphasis: soft caption when > 1h remaining

UI updates:
- listing_row: steal banner + auction notice per row
- Search: inject CSS, check snipe mode, "Ending soon" sort option,
  pass market_price from comp cache to row renderer
- app.py: Konami detector + audio enable/disable sidebar toggle

Tests: 22 new tests (72 total, all green)
2026-03-25 14:27:02 -07:00

122 lines
4.3 KiB
Python

"""Tests for easter egg helpers (pure logic — no Streamlit calls)."""
from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
import pytest
from app.db.models import Listing, TrustScore
from app.ui.components.easter_eggs import is_steal, auction_hours_remaining
def _listing(**kwargs) -> Listing:
defaults = dict(
platform="ebay",
platform_listing_id="1",
title="Test",
price=800.0,
currency="USD",
condition="used",
seller_platform_id="seller1",
url="https://ebay.com/itm/1",
buying_format="fixed_price",
ends_at=None,
)
defaults.update(kwargs)
return Listing(**defaults)
def _trust(score: int, flags: list[str] | None = None) -> TrustScore:
return TrustScore(
listing_id=1,
composite_score=score,
account_age_score=20,
feedback_count_score=20,
feedback_ratio_score=20,
price_vs_market_score=20,
category_history_score=score - 80 if score >= 80 else 0,
red_flags_json=json.dumps(flags or []),
)
# ---------------------------------------------------------------------------
# is_steal
# ---------------------------------------------------------------------------
class TestIsSteal:
def test_qualifies_when_high_trust_and_20_pct_below(self):
listing = _listing(price=840.0) # 16% below 1000
trust = _trust(92)
assert is_steal(listing, trust, market_price=1000.0) is True
def test_fails_when_trust_below_90(self):
listing = _listing(price=840.0)
trust = _trust(89)
assert is_steal(listing, trust, market_price=1000.0) is False
def test_fails_when_discount_too_deep(self):
# 35% below market — suspicious, not a steal
listing = _listing(price=650.0)
trust = _trust(95)
assert is_steal(listing, trust, market_price=1000.0) is False
def test_fails_when_discount_too_shallow(self):
# 10% below market — not enough of a deal
listing = _listing(price=900.0)
trust = _trust(95)
assert is_steal(listing, trust, market_price=1000.0) is False
def test_fails_when_suspicious_price_flag(self):
listing = _listing(price=840.0)
trust = _trust(92, flags=["suspicious_price"])
assert is_steal(listing, trust, market_price=1000.0) is False
def test_fails_when_no_market_price(self):
listing = _listing(price=840.0)
trust = _trust(92)
assert is_steal(listing, trust, market_price=None) is False
def test_fails_when_no_trust(self):
listing = _listing(price=840.0)
assert is_steal(listing, None, market_price=1000.0) is False
def test_boundary_15_pct(self):
listing = _listing(price=850.0) # exactly 15% below 1000
trust = _trust(92)
assert is_steal(listing, trust, market_price=1000.0) is True
def test_boundary_30_pct(self):
listing = _listing(price=700.0) # exactly 30% below 1000
trust = _trust(92)
assert is_steal(listing, trust, market_price=1000.0) is True
# ---------------------------------------------------------------------------
# auction_hours_remaining
# ---------------------------------------------------------------------------
class TestAuctionHoursRemaining:
def _auction_listing(self, hours_ahead: float) -> Listing:
ends = (datetime.now(timezone.utc) + timedelta(hours=hours_ahead)).isoformat()
return _listing(buying_format="auction", ends_at=ends)
def test_returns_hours_for_active_auction(self):
listing = self._auction_listing(3.0)
h = auction_hours_remaining(listing)
assert h is not None
assert 2.9 < h < 3.1
def test_returns_none_for_fixed_price(self):
listing = _listing(buying_format="fixed_price")
assert auction_hours_remaining(listing) is None
def test_returns_none_when_no_ends_at(self):
listing = _listing(buying_format="auction", ends_at=None)
assert auction_hours_remaining(listing) is None
def test_returns_zero_for_ended_auction(self):
ends = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
listing = _listing(buying_format="auction", ends_at=ends)
h = auction_hours_remaining(listing)
assert h == 0.0