"""§6.5.1 row validation — every row-error rule has a fixture (SD-0002 §6.8).""" from decimal import Decimal import pytest from app.domains.products.codec import parse_csv from app.domains.products.validate import build_products def _products(*lines: str): return build_products(parse_csv(("\n".join(lines) + "\n").encode())) def _errors(*lines: str): return [e for p in _products(*lines) for e in p.errors] def test_simple_product_parses_clean(): [p] = _products( "Handle,Title,Vendor,Tags,Status,Published,Variant Price,Variant SKU", "moon-mug,Moon Mug,Acme,\"kitchen, mugs\",active,TRUE,18.00,SKU-1", ) assert p.valid and p.handle == "moon-mug" and p.title == "Moon Mug" assert p.fields["tags"] == ["kitchen", "mugs"] assert p.fields["status"] == "active" and p.fields["published"] is True [v] = p.variants assert v.options == (None, None, None) assert v.fields["price"] == Decimal("18.00") and v.fields["sku"] == "SKU-1" def test_option_product_groups_consecutive_rows(): [p] = _products( "Handle,Title,Option1 Name,Option1 Value,Variant Price", "tee,Tee,Size,S,24.00", "tee,,,M,24.00", "tee,,,L,26.00", ) assert p.valid and p.option_names == ("Size", None, None) assert [v.options[0] for v in p.variants] == ["S", "M", "L"] def test_image_only_rows_and_dedupe(): [p] = _products( "Handle,Title,Image Src,Image Position,Image Alt Text", "mug,Mug,https://x/a.jpg,1,front", "mug,,https://x/b.jpg,2,back", "mug,,https://x/a.jpg,3,dupe", ) assert p.valid assert [(i.source_url, i.position) for i in p.images] == [ ("https://x/a.jpg", 1), ("https://x/b.jpg", 2), ] def test_variant_image_joins_product_images(): [p] = _products( "Handle,Title,Variant Image", "mug,Mug,https://x/v.jpg", ) assert p.variants[0].fields["variant_image"] == "https://x/v.jpg" assert [i.source_url for i in p.images] == ["https://x/v.jpg"] def test_description_sanitized_inv15(): [p] = _products( "Handle,Title,Description", 'mug,Mug,"
hi
"', ) html = p.fields["description_html"] assert "" in html and "script" not in html and "onclick" not in html def test_blank_cell_clears_absent_column_missing(): [p] = _products("Handle,Title,Vendor", "mug,Mug,") assert p.fields["vendor"] is None # present-but-empty == clear assert "status" not in p.fields # absent column == untouched @pytest.mark.parametrize( "header,row,column,fragment", [ ("Handle,Title", ",NoHandle", "Handle", "needs a Handle"), ("Handle,Title", "Bad_Handle!,T", "Handle", "isn't a valid handle"), ("Handle,Title", "mug,", "Title", "missing its Title"), ("Handle,Title,Type", "mug,Mug,kit_virtual", "Type", "kits arrive"), ("Handle,Title,Component 1 SKU", "mug,Mug,ABC", "Component 1 SKU", "kits arrive"), ("Handle,Title,Status", "mug,Mug,live", "Status", "is not a status"), ("Handle,Title,Published", "mug,Mug,YES", "Published", "is not TRUE or FALSE"), ("Handle,Title,Variant Price", 'mug,Mug,"12,50"', "Variant Price", "is not a price"), ("Handle,Title,Variant Cost", "mug,Mug,-3", "Variant Cost", "is not a price"), ("Handle,Title,Variant Weight", "mug,Mug,heavy", "Variant Weight", "is not a number"), ("Handle,Title,Variant Inventory Qty", "mug,Mug,3.5", "Variant Inventory Qty", "is not a whole number"), ("Handle,Title,Variant Position", "mug,Mug,0", "Variant Position", "is not a position"), ("Handle,Title,Option1 Value", "mug,Mug,Red", "Option1 Value", "no Option1 Name"), ], ) def test_row_error_rules(header, row, column, fragment): errors = _errors(header, row) assert any(e.column == column and fragment in e.message for e in errors), errors def test_missing_option_value_for_named_option(): errors = _errors( "Handle,Title,Option1 Name,Option1 Value,Variant SKU", "tee,Tee,Size,S,A", "tee,,,,B", ) assert any("missing its Option1 Value" in e.message and e.line_number == 3 for e in errors) def test_duplicate_option_combo_is_error(): errors = _errors( "Handle,Title,Option1 Name,Option1 Value", "tee,Tee,Size,S", "tee,,,S", ) assert any("duplicate variant" in e.message for e in errors) def test_second_variant_on_no_option_product_is_error(): errors = _errors( "Handle,Title,Variant SKU", "mug,Mug,A", "mug,,B", ) assert any("without options can have only one variant" in e.message for e in errors) def test_non_consecutive_handle_is_error(): errors = _errors( "Handle,Title", "mug,Mug", "tee,Tee", "mug,", ) assert any("must be consecutive" in e.message and e.line_number == 4 for e in errors) def test_no_data_row_is_error(): errors = _errors( "Handle,Title,Variant SKU,Image Src", "mug,Mug,A,", "mug,,,", ) assert any("no variant or image data" in e.message for e in errors) def test_errors_poison_their_product_only(): products = _products( "Handle,Title,Variant Price", "good-mug,Mug,10.00", "bad-tee,Tee,not-a-price", ) by_handle = {p.handle: p for p in products} assert by_handle["good-mug"].valid assert not by_handle["bad-tee"].valid def test_all_errors_collected_not_first_only(): [p] = _products( "Handle,Title,Status,Variant Price", "mug,,bogus,abc", ) assert len(p.errors) == 3 # missing Title + bad status + bad price