Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,23 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.12"
# No caching — reinstalls all dependencies on every run

- name: Install dependencies
run: pip install -e ".[test]"
# Uses pip instead of uv; no lock file pinning

- name: Start PostgreSQL
run: |
sudo systemctl start postgresql
sudo -u postgres psql -c "CREATE USER bookstore WITH PASSWORD 'bookstore';"
sudo -u postgres psql -c "CREATE DATABASE bookstore OWNER bookstore;"
# No wait/health check — tests may start before DB is ready

- name: Run migrations
run: alembic upgrade head
# Will fail silently if DB isn't up yet

- name: Start app
run: uvicorn app.main:app --host 0.0.0.0 --port 8000 &
# No health check — tests start immediately, race condition

- name: Run tests
run: pytest tests/ -v
env:
# Payment API key hardcoded in the workflow — will fail without it
PAYMENT_API_KEY: ${{ secrets.PAYMENT_API_KEY }}
# No DATABASE_URL override — uses the hardcoded default from config.py

# No coverage reporting
# No test result artifacts
# No notification on failure
2 changes: 1 addition & 1 deletion app/api/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

@router.post("/", response_model=OrderResponse, status_code=201)
def create_order(payload: OrderCreate, card_token: str, db: Session = Depends(get_db)):
service = OrderService() # new instance per request, no way to inject a fake
service = OrderService()
try:
order = service.create_order(
user_id=payload.user_id,
Expand Down
6 changes: 2 additions & 4 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import os

# Config scattered across the module - some from env, some hardcoded
DATABASE_URL = os.environ.get(
"DATABASE_URL", "postgresql://bookstore:bookstore@localhost:5432/bookstore"
)

PAYMENT_API_URL = "https://api.fakepay.io/v1" # hardcoded, ignores env
PAYMENT_API_URL = "https://api.fakepay.io/v1"
PAYMENT_API_KEY = os.environ.get("PAYMENT_API_KEY", "sk_test_changeme")

EMAIL_SMTP_HOST = os.environ.get("EMAIL_SMTP_HOST", "localhost")
EMAIL_SMTP_PORT = int(os.environ.get("EMAIL_SMTP_PORT", "1025"))
EMAIL_FROM = "shop@bookstore.local" # hardcoded
EMAIL_FROM = "shop@bookstore.local"

CANCELLATION_WINDOW_HOURS = 1
MAX_ITEMS_PER_ORDER = 10

6 changes: 4 additions & 2 deletions app/models/order.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import enum
from datetime import datetime
from datetime import datetime, UTC
from sqlalchemy import Column, Integer, String, Numeric, DateTime, ForeignKey, Enum
from sqlalchemy.orm import relationship
from app.database import Base
Expand All @@ -21,7 +21,9 @@ class Order(Base):
total = Column(Numeric(10, 2), nullable=False)
promo_code = Column(String(50), nullable=True)
charge_id = Column(String(100), nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
created_at = Column(
DateTime(timezone=True), nullable=False, default=datetime.now(tz=UTC)
)

user = relationship("User")
items = relationship(
Expand Down
29 changes: 3 additions & 26 deletions app/services/order_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@


class OrderService:
# Instantiates its own dependencies - hard to override in tests
def __init__(self):
self._payment = PaymentClient()
self._email = EmailClient()
Expand All @@ -28,7 +27,7 @@ def __init__(self):

def _check_connections(self) -> None:
"""Check that all the connections are responsive."""
time.sleep(random.randint(1,10))
time.sleep(random.randint(1, 10)) # NOTE: the sleep is to simulate slowness

def create_order(
self,
Expand All @@ -37,7 +36,7 @@ def create_order(
card_token: str,
promo_code: str | None = None,
) -> Order:
db = SessionLocal() # creates its own session - cannot inject a test session
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
Expand Down Expand Up @@ -89,7 +88,7 @@ def create_order(
total=total,
promo_code=promo_code,
charge_id=charge["id"],
created_at=datetime.utcnow(), # cannot control time in tests
created_at=datetime.utcnow(),
)
order.items = order_items
db.add(order)
Expand Down Expand Up @@ -118,7 +117,6 @@ def cancel_order(self, order_id: int) -> Order:
if order.status != OrderStatus.CONFIRMED:
raise ValueError(f"Cannot cancel order in status {order.status}")

# Uses datetime.utcnow() directly - time-dependent logic is untestable
deadline = order.created_at + timedelta(hours=CANCELLATION_WINDOW_HOURS)
if datetime.utcnow() > deadline:
raise ValueError("Cancellation window has expired (1 hour after order)")
Expand Down Expand Up @@ -147,24 +145,3 @@ def cancel_order(self, order_id: int) -> Order:
raise
finally:
db.close()

def calculate_order_total(
self, items: list[dict], promo_code: str | None = None
) -> Decimal:
# Pure calculation buried in a class that also does I/O - hard to test in isolation
db = SessionLocal()
try:
total = Decimal("0")
for item in items:
book = db.query(Book).filter(Book.id == item["book_id"]).first()
if not book:
raise ValueError(f"Book {item['book_id']} not found")
total += Decimal(str(book.price)) * item["quantity"]

if promo_code:
discount = PROMO_CODES.get(promo_code.upper())
if discount:
total = total * (1 - discount)
return total
finally:
db.close()
Loading
Loading