feat(products): import_validate → draft, preview records, discard + TEL-1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""products service — drafts: validate/preview/discard (PUC-2/3/3a/5a; INV-11)."""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
from app.domains import products
|
||||
from app.platform import db
|
||||
|
||||
GOOD_CSV = b"Handle,Title,Vendor,Variant Price\nmoon-mug,Moon Mug,Acme,18.00\nstar-tee,Star Tee,Acme,24.00\n"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def migrated_conn(fresh_db_url):
|
||||
with psycopg.connect(fresh_db_url) as conn:
|
||||
db.migrate(conn)
|
||||
yield conn
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def merchant(migrated_conn):
|
||||
acct = migrated_conn.execute(
|
||||
"INSERT INTO account (email) VALUES ('m@example.com') RETURNING id").fetchone()[0]
|
||||
sf = migrated_conn.execute(
|
||||
"INSERT INTO storefront (name) VALUES ('Shop') RETURNING id").fetchone()[0]
|
||||
migrated_conn.execute(
|
||||
"INSERT INTO storefront_membership (account_id, storefront_id) VALUES (%s,%s)", (acct, sf))
|
||||
migrated_conn.commit()
|
||||
return {"account_id": acct, "storefront_id": sf}
|
||||
|
||||
|
||||
def test_import_validate_creates_draft_with_summary(migrated_conn, merchant):
|
||||
draft = products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
assert draft["dialect"] == "canonical"
|
||||
assert draft["summary"] == {"adds": 2, "updates": 0, "unchanged": 0, "errors": 0}
|
||||
assert draft["expires_at"]
|
||||
|
||||
|
||||
def test_validate_writes_nothing_to_catalog_inv11(migrated_conn, merchant):
|
||||
products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
assert migrated_conn.execute("SELECT count(*) FROM product").fetchone()[0] == 0
|
||||
assert migrated_conn.execute("SELECT count(*) FROM variant").fetchone()[0] == 0
|
||||
|
||||
|
||||
def test_file_rejection_leaves_no_draft(migrated_conn, merchant):
|
||||
with pytest.raises(products.FileRejected):
|
||||
products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "bad.csv",
|
||||
b"Vendor,Price\nAcme,1\n")
|
||||
assert migrated_conn.execute("SELECT count(*) FROM import_draft").fetchone()[0] == 0
|
||||
|
||||
|
||||
def test_records_paging_and_kind_filter(migrated_conn, merchant):
|
||||
draft = products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
recs = products.get_draft_records(migrated_conn, merchant["storefront_id"], draft["id"])
|
||||
assert [r["handle"] for r in recs] == ["moon-mug", "star-tee"]
|
||||
adds = products.get_draft_records(
|
||||
migrated_conn, merchant["storefront_id"], draft["id"], kind="add", limit=1)
|
||||
assert len(adds) == 1 and adds[0]["kind"] == "add"
|
||||
|
||||
|
||||
def test_discard_deletes_no_trace_puc3a(migrated_conn, merchant):
|
||||
draft = products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
products.discard_draft(migrated_conn, merchant["storefront_id"], draft["id"])
|
||||
assert migrated_conn.execute("SELECT count(*) FROM import_draft").fetchone()[0] == 0
|
||||
products.discard_draft(migrated_conn, merchant["storefront_id"], draft["id"]) # idempotent
|
||||
|
||||
|
||||
def test_draft_scoped_to_storefront_inv14(migrated_conn, merchant):
|
||||
draft = products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
other_sf = migrated_conn.execute(
|
||||
"INSERT INTO storefront (name) VALUES ('Other') RETURNING id").fetchone()[0]
|
||||
migrated_conn.commit()
|
||||
with pytest.raises(products.DraftNotFound):
|
||||
products.get_draft(migrated_conn, other_sf, draft["id"])
|
||||
|
||||
|
||||
def test_expired_draft_raises_and_lazily_deletes(migrated_conn, merchant):
|
||||
draft = products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
migrated_conn.execute(
|
||||
"UPDATE import_draft SET expires_at = now() - interval '1 minute' WHERE id = %s",
|
||||
(draft["id"],))
|
||||
migrated_conn.commit()
|
||||
with pytest.raises(products.DraftExpired):
|
||||
products.get_draft(migrated_conn, merchant["storefront_id"], draft["id"])
|
||||
assert migrated_conn.execute("SELECT count(*) FROM import_draft").fetchone()[0] == 0
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def telemetry_propagation():
|
||||
"""create_app() sets propagate=False on the parent "ecomm" logger
|
||||
(main._ensure_app_logging), which hides ecomm.telemetry records from caplog's
|
||||
root-logger handler whenever an API test ran first. Restore propagation here."""
|
||||
lg = logging.getLogger("ecomm")
|
||||
prior = lg.propagate
|
||||
lg.propagate = True
|
||||
yield
|
||||
lg.propagate = prior
|
||||
|
||||
|
||||
def test_tel1_emitted(migrated_conn, merchant, caplog, telemetry_propagation):
|
||||
with caplog.at_level(logging.INFO, logger="ecomm.telemetry"):
|
||||
products.import_validate(
|
||||
migrated_conn, merchant["storefront_id"], merchant["account_id"], "cat.csv", GOOD_CSV)
|
||||
events = [json.loads(r.message) for r in caplog.records if r.name == "ecomm.telemetry"]
|
||||
assert any(
|
||||
e["event"] == "import_draft_created" and e["adds"] == 2 and e["row_count"] == 2
|
||||
and "duration_ms" in e and e["unknown_columns_count"] == 0
|
||||
for e in events
|
||||
)
|
||||
Reference in New Issue
Block a user