"""SLICE-3 storefront creation — service + endpoint scenario tests (SD-0001 §6.5 PUC-4/7).""" import re from contextlib import contextmanager import psycopg import pytest from fastapi.testclient import TestClient from app.main import create_app from app.domains import storefronts from app.platform import db @pytest.fixture() def migrated_conn(fresh_db_url): with psycopg.connect(fresh_db_url) as conn: db.migrate(conn) yield conn def _account_id(conn, email="merchant@example.com"): return conn.execute( "INSERT INTO account (email) VALUES (%s) RETURNING id", (email,) ).fetchone()[0] def test_create_storefront_with_name(migrated_conn): acct = _account_id(migrated_conn) sf = storefronts.create_storefront(migrated_conn, acct, "merchant@example.com", "Ben's Bets") assert sf.name == "Ben's Bets" assert storefronts.storefront_for(migrated_conn, acct) == sf def test_14_01_0026_blank_name_gets_generated_default(migrated_conn): acct = _account_id(migrated_conn) sf = storefronts.create_storefront(migrated_conn, acct, "merchant@example.com", None) assert sf.name == "merchant's storefront" def test_whitespace_name_treated_as_blank(migrated_conn): acct = _account_id(migrated_conn) sf = storefronts.create_storefront(migrated_conn, acct, "ben@bensbets.com", " ") assert sf.name == "ben's storefront" def test_second_create_refused_inv_4(migrated_conn): acct = _account_id(migrated_conn) storefronts.create_storefront(migrated_conn, acct, "merchant@example.com", "First") with pytest.raises(storefronts.AlreadyOwnsStorefront): storefronts.create_storefront(migrated_conn, acct, "merchant@example.com", "Second") def test_membership_row_is_owner(migrated_conn): acct = _account_id(migrated_conn) sf = storefronts.create_storefront(migrated_conn, acct, "merchant@example.com", None) role = migrated_conn.execute( "SELECT role FROM storefront_membership WHERE account_id = %s AND storefront_id = %s", (acct, sf.id), ).fetchone()[0] assert role == "owner" def test_storefront_for_none_when_absent(migrated_conn): acct = _account_id(migrated_conn) assert storefronts.storefront_for(migrated_conn, acct) is None # ── endpoint scenario tests (§6.4 POST /api/storefronts; corpus 14.01.*) ────────── @contextmanager def _signed_in_client(fresh_db_url, email="merchant@example.com"): with TestClient(create_app(database_url=fresh_db_url)) as client: client.post("/api/auth/request-code", json={"email": email}) code = re.search(r"\b(\d{6})\b", client.app.state.mailer.outbox[-1].body).group(1) client.post("/api/auth/verify", json={"email": email, "code": code}) yield client def test_14_01_0013_create_storefront_with_name(fresh_db_url): with _signed_in_client(fresh_db_url) as client: resp = client.post("/api/storefronts", json={"name": "Ben's Bets"}) assert resp.status_code == 201 assert resp.json()["name"] == "Ben's Bets" assert isinstance(resp.json()["id"], int) def test_14_01_0015_0016_nothing_but_a_name_is_asked(fresh_db_url): # No payment method, plan, or commitment: the request body needs nothing at all and # the response carries only the storefront — no plan/trial/billing fields. with _signed_in_client(fresh_db_url) as client: resp = client.post("/api/storefronts", json={}) assert resp.status_code == 201 assert set(resp.json().keys()) == {"id", "name"} def test_14_01_0025_create_lands_on_admin(fresh_db_url): # After create, the entry-routing answer carries the storefront -> admin (PUC-6). with _signed_in_client(fresh_db_url) as client: created = client.post("/api/storefronts", json={"name": "Ben's Bets"}).json() me = client.get("/api/auth/me").json() assert me["storefront"] == created def test_puc_05_returning_without_storefront_routes_to_create(fresh_db_url): with _signed_in_client(fresh_db_url) as client: me = client.get("/api/auth/me").json() assert me["storefront"] is None def test_14_01_0027_returning_with_storefront_straight_to_admin(fresh_db_url): # PUC-6: a returning merchant's verify response already carries the storefront. from datetime import datetime, timedelta, timezone email = "returning@example.com" with _signed_in_client(fresh_db_url, email) as client: client.post("/api/storefronts", json={"name": "Ben's Bets"}) # fresh login: backdate the consumed code to clear the 60s resend cooldown with psycopg.connect(fresh_db_url) as c: c.execute( "UPDATE auth_code SET created_at = %s WHERE email = %s", (datetime.now(timezone.utc) - timedelta(minutes=2), email), ) c.commit() client.post("/api/auth/request-code", json={"email": email}) code = re.search(r"\b(\d{6})\b", client.app.state.mailer.outbox[-1].body).group(1) resp = client.post("/api/auth/verify", json={"email": email, "code": code}) assert resp.status_code == 200 assert resp.json()["storefront"]["name"] == "Ben's Bets" assert resp.json()["created"] is False def test_puc_07_second_create_refused_409(fresh_db_url): with _signed_in_client(fresh_db_url) as client: client.post("/api/storefronts", json={"name": "First"}) resp = client.post("/api/storefronts", json={"name": "Second"}) assert resp.status_code == 409 assert resp.json()["error"]["code"] == "already_owns_storefront" def test_puc_08_admin_shell_answer(fresh_db_url): # The shell renders from /me alone: storefront name + signed-in email (§6.5 PUC-8). with _signed_in_client(fresh_db_url) as client: client.post("/api/storefronts", json={"name": "Ben's Bets"}) me = client.get("/api/auth/me").json() assert me["account"]["email"] == "merchant@example.com" assert me["storefront"]["name"] == "Ben's Bets" def test_create_storefront_requires_session(fresh_db_url): with TestClient(create_app(database_url=fresh_db_url)) as client: resp = client.post("/api/storefronts", json={"name": "Nope"}) assert resp.status_code == 401 assert resp.json()["error"]["code"] == "unauthenticated"