"""§6.5.1 file-level codec — parse, caps, required columns (PUC-5a fixtures).""" import pytest from app.domains.products import FileRejected from app.domains.products.codec import parse_csv def _csv(*lines: str) -> bytes: return ("\n".join(lines) + "\n").encode() GOOD = _csv( "Handle,Title,Variant Price,Bogus Column", "moon-mug,Moon Mug,18.00,x", "star-tee,Star Tee,24.00,y", ) def test_parses_header_rows_and_unknown_columns(): parsed = parse_csv(GOOD) assert parsed.dialect == "canonical" assert parsed.unknown_columns == ["Bogus Column"] assert [r.line_number for r in parsed.rows] == [2, 3] assert parsed.rows[0].cells["Handle"] == "moon-mug" assert parsed.rows[0].cells["Variant Price"] == "18.00" assert "Bogus Column" not in parsed.rows[0].cells def test_bom_tolerated(): parsed = parse_csv(b"\xef\xbb\xbf" + GOOD) assert parsed.rows[0].cells["Handle"] == "moon-mug" def test_quoted_cells_rfc4180(): parsed = parse_csv(_csv("Handle,Title,Tags", 'mug,"The ""Best"" Mug","a, b"')) assert parsed.rows[0].cells["Title"] == 'The "Best" Mug' assert parsed.rows[0].cells["Tags"] == "a, b" def test_empty_rows_skipped_short_rows_padded(): parsed = parse_csv(_csv("Handle,Title,Vendor", "mug,Mug", "", ",,", "tee,Tee,Acme")) assert [r.cells["Handle"] for r in parsed.rows] == ["mug", "tee"] assert parsed.rows[0].cells["Vendor"] == "" @pytest.mark.parametrize( "data,code", [ (b"\xff\xfe\x00garbage\x00", "not_csv"), (b"", "not_csv"), (_csv("Title,Vendor", "Mug,Acme"), "missing_required_column"), (_csv("Handle,Vendor", "mug,Acme"), "missing_required_column"), ( _csv("Handle,Title", *(f"h{i},T{i}" for i in range(5001))), "too_many_rows", ), (b"Handle,Title\n" + b"x" * (10 * 1024 * 1024), "file_too_large"), ], ) def test_file_level_rejections(data, code): with pytest.raises(FileRejected) as exc: parse_csv(data) assert exc.value.code == code def test_missing_column_message_names_the_column(): with pytest.raises(FileRejected) as exc: parse_csv(_csv("Handle,Vendor", "mug,Acme")) assert "'Title'" in exc.value.message def test_parse_detects_and_maps_shopify(): data = ( "Handle,Title,Body (HTML),Cost per item,Variant Price,Variant Grams,Type,Gift Card\n" "mug,Moon Mug,
Grey
,9.50,18.00,300,Drinkware,false\n" ).encode("utf-8") parsed = parse_csv(data) assert parsed.dialect == "shopify" cells = parsed.rows[0].cells assert cells["Description"] == "Grey
" assert cells["Variant Cost"] == "9.50" assert cells["Variant Weight"] == "300" assert cells["Variant Weight Unit"] == "g" # synthesized # Type (free-text) and Gift Card warned, never mapped: assert "Type" in parsed.unknown_columns assert "Gift Card" in parsed.unknown_columns assert "product_type" not in str(cells) # Type never reached canonical def test_parse_canonical_unchanged(): data = b"Handle,Title,Description,Variant Price\nmug,Moon Mug,Grey,18.00\n" parsed = parse_csv(data) assert parsed.dialect == "canonical" assert parsed.rows[0].cells["Description"] == "Grey" assert parsed.unknown_columns == []