Files
office_translator/tests/test_stripe_live_mode.py
sepehr 8f96ddfe71
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m31s
feat(admin): Stripe live-mode safety + one-click webhook setup
User said 'I want this to be automatic'. Three gaps were addressed.

Gap 1 — test key in production went undetected
  The admin page showed 'cle secrete OK' for both sk_test_ and sk_live_.
  If a misconfigured VPS kept its sk_test_… in production, the app
  would create real signup flow but never charge real cards. Added
  services.pricing_config:
    - stripe_mode() -> 'live' | 'test' | 'unknown' (key prefix)
    - is_test_mode_in_production() -> True if ENV=production AND sk_test_
  GET /admin/pricing now exposes {mode, is_test_mode_in_production, env}.
  POST /admin/pricing/setup-stripe and the new setup-webhook refuse
  to run in that state unless the admin passes {force: true}.

Gap 2 — webhook setup was 100% manual
  The admin had to go to Stripe Dashboard, create the endpoint, copy
  the whsec_, paste it back. Stripe API supports creating webhook
  endpoints programmatically, so the new endpoint
  POST /admin/pricing/setup-webhook does it all in one click:
    - derives the webhook URL from the request (X-Forwarded-Proto + Host)
    - calls stripe.WebhookEndpoint.create() (or .update() if the URL
      already exists) with the 6 events the backend actually handles
      (checkout.session.completed, customer.subscription.*, invoice.*)
    - persists the returned whsec_ to .env via _update_env_file
    - hot-reloads the runtime config (no restart needed)
  Refuses http:// URLs in live mode (Stripe requires https).

Gap 3 — obsolete script leaked a test secret
  scripts/stripe_setup.py contained a hardcoded sk_test_… in source.
  It had been replaced by POST /admin/pricing/setup-stripe but was
  still in the repo. Deleted via git rm. The key was also rolled: the
  user should rotate that sk_test_ in the Stripe Dashboard.

Frontend changes (admin pricing page):
  - LIVE / TEST / non-configure badges next to 'Statut Stripe'
  - ENV=... chip in the header
  - BLOCKING red banner if test mode detected in production
  - Stepped numbering: 1. Produits & prix / 2. Webhook Stripe
  - New 'Setup webhook auto' button
  - Auto-setup error 409 (TEST_MODE_IN_PRODUCTION) -> confirmation
    dialog to retry with force=true

11 new tests for stripe_mode() and is_test_mode_in_production(),
covering live key, test key, missing key, garbage key, whitespace,
ENV vs ENVIRONMENT alias, all 5 prod/dev combinations.

Total: 471 tests pass (was 460), zero regression.
2026-07-14 20:19:49 +02:00

104 lines
3.5 KiB
Python

"""
Tests for Stripe live-mode safety checks.
Covers the helpers added in `services/pricing_config.py`:
- stripe_mode() : detect 'live' / 'test' / 'unknown'
- is_test_mode_in_production() : True if ENV=production but sk_test_…
These are pure functions on env vars, so they don't need any
database or HTTP setup.
"""
import os
import importlib
def _reload_pricing_config():
"""Reload pricing_config with the current os.environ (no cache)."""
import services.pricing_config as pc
importlib.reload(pc)
return pc
class TestStripeMode:
"""stripe_mode() must return the correct value based on key prefix."""
def setup_method(self):
self._saved_env = os.environ.copy()
def teardown_method(self):
os.environ.clear()
os.environ.update(self._saved_env)
def test_live_key_returns_live(self):
os.environ["STRIPE_SECRET_KEY"] = "sk_live_abc123XYZ"
pc = _reload_pricing_config()
assert pc.stripe_mode() == "live"
def test_test_key_returns_test(self):
os.environ["STRIPE_SECRET_KEY"] = "sk_test_abc123XYZ"
pc = _reload_pricing_config()
assert pc.stripe_mode() == "test"
def test_missing_key_returns_unknown(self):
os.environ.pop("STRIPE_SECRET_KEY", None)
pc = _reload_pricing_config()
assert pc.stripe_mode() == "unknown"
def test_empty_key_returns_unknown(self):
os.environ["STRIPE_SECRET_KEY"] = ""
pc = _reload_pricing_config()
assert pc.stripe_mode() == "unknown"
def test_garbage_key_returns_unknown(self):
os.environ["STRIPE_SECRET_KEY"] = "totally-not-a-stripe-key"
pc = _reload_pricing_config()
assert pc.stripe_mode() == "unknown"
def test_key_with_whitespace_is_trimmed(self):
os.environ["STRIPE_SECRET_KEY"] = " sk_live_abc123XYZ "
pc = _reload_pricing_config()
assert pc.stripe_mode() == "live"
class TestIsTestModeInProduction:
"""is_test_mode_in_production() must catch the dangerous misconfig."""
def setup_method(self):
self._saved_env = os.environ.copy()
def teardown_method(self):
os.environ.clear()
os.environ.update(self._saved_env)
def test_test_key_in_prod_returns_true(self):
os.environ["ENV"] = "production"
os.environ["STRIPE_SECRET_KEY"] = "sk_test_xxx"
pc = _reload_pricing_config()
assert pc.is_test_mode_in_production() is True
def test_live_key_in_prod_returns_false(self):
os.environ["ENV"] = "production"
os.environ["STRIPE_SECRET_KEY"] = "sk_live_xxx"
pc = _reload_pricing_config()
assert pc.is_test_mode_in_production() is False
def test_test_key_in_dev_returns_false(self):
os.environ["ENV"] = "development"
os.environ["STRIPE_SECRET_KEY"] = "sk_test_xxx"
pc = _reload_pricing_config()
assert pc.is_test_mode_in_production() is False
def test_no_key_in_prod_returns_false(self):
os.environ["ENV"] = "production"
os.environ.pop("STRIPE_SECRET_KEY", None)
pc = _reload_pricing_config()
assert pc.is_test_mode_in_production() is False
def test_environment_alias_works(self):
"""ENVIRONMENT env var should also work (not just ENV)."""
os.environ.pop("ENV", None)
os.environ["ENVIRONMENT"] = "production"
os.environ["STRIPE_SECRET_KEY"] = "sk_test_xxx"
pc = _reload_pricing_config()
assert pc.is_test_mode_in_production() is True