This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/functional/test_l402_flow.py
Claude c91e02e7c5 test: add functional test suite with real fixtures, no mocking
Three-tier functional test infrastructure:
- CLI tests via Typer CliRunner (timmy, timmy-serve, self-tdd)
- Dashboard integration tests with real TestClient, real SQLite, real
  coordinator (no patch/mock — Ollama offline = graceful degradation)
- Docker compose container-level tests (gated by FUNCTIONAL_DOCKER=1)
- End-to-end L402 payment flow with real mock-lightning backend

42 new tests (8 Docker tests skipped without FUNCTIONAL_DOCKER=1).
All 849 tests pass.

https://claude.ai/code/session_01WU4h3cQQiouMwmgYmAgkMM
2026-02-25 00:46:22 +00:00

107 lines
4.0 KiB
Python

"""Functional test for the full L402 payment flow.
Uses the real mock-lightning backend (LIGHTNING_BACKEND=mock) — no patching.
This exercises the entire payment lifecycle a real client would go through:
1. Hit protected endpoint → get 402 + invoice + macaroon
2. "Pay" the invoice (settle via mock backend)
3. Present macaroon:preimage → get access
"""
import pytest
class TestL402PaymentFlow:
"""End-to-end L402 payment lifecycle."""
def test_unprotected_endpoints_work(self, serve_client):
"""Status and health don't require payment."""
resp = serve_client.get("/serve/status")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "active"
assert data["price_sats"] == 100
health = serve_client.get("/health")
assert health.status_code == 200
def test_chat_without_payment_returns_402(self, serve_client):
"""Hitting /serve/chat without an L402 token gives 402."""
resp = serve_client.post(
"/serve/chat",
json={"message": "hello"},
)
assert resp.status_code == 402
data = resp.json()
assert data["error"] == "Payment Required"
assert data["code"] == "L402"
assert "macaroon" in data
assert "invoice" in data
assert "payment_hash" in data
assert data["amount_sats"] == 100
# WWW-Authenticate header should be present
assert "WWW-Authenticate" in resp.headers
assert "L402" in resp.headers["WWW-Authenticate"]
def test_chat_with_garbage_token_returns_402(self, serve_client):
resp = serve_client.post(
"/serve/chat",
json={"message": "hello"},
headers={"Authorization": "L402 garbage:token"},
)
assert resp.status_code == 402
def test_full_payment_lifecycle(self, serve_client):
"""Complete flow: get challenge → pay → access."""
from timmy_serve.payment_handler import payment_handler
# Step 1: Hit protected endpoint, get 402 challenge
challenge_resp = serve_client.post(
"/serve/chat",
json={"message": "hello"},
)
assert challenge_resp.status_code == 402
challenge = challenge_resp.json()
macaroon = challenge["macaroon"]
payment_hash = challenge["payment_hash"]
# Step 2: "Pay" the invoice via the mock backend's auto-settle
# The mock backend settles invoices when you provide the correct preimage.
# Get the preimage from the mock backend's internal state.
invoice = payment_handler.get_invoice(payment_hash)
assert invoice is not None
preimage = invoice.preimage # mock backend exposes this
# Step 3: Present macaroon:preimage to access the endpoint
resp = serve_client.post(
"/serve/chat",
json={"message": "hello after paying"},
headers={"Authorization": f"L402 {macaroon}:{preimage}"},
)
# The chat will fail because Ollama isn't running, but the
# L402 middleware should let us through (status != 402).
# We accept 200 (success) or 500 (Ollama offline) — NOT 402.
assert resp.status_code != 402
def test_create_invoice_via_api(self, serve_client):
"""POST /serve/invoice creates a real invoice."""
resp = serve_client.post(
"/serve/invoice",
json={"amount_sats": 500, "memo": "premium access"},
)
assert resp.status_code == 200
data = resp.json()
assert data["amount_sats"] == 500
assert data["payment_hash"]
assert data["payment_request"]
def test_status_reflects_invoices(self, serve_client):
"""Creating invoices should be reflected in /serve/status."""
serve_client.post("/serve/invoice", json={"amount_sats": 100, "memo": "test"})
serve_client.post("/serve/invoice", json={"amount_sats": 200, "memo": "test2"})
resp = serve_client.get("/serve/status")
data = resp.json()
assert data["total_invoices"] >= 2