0c7a76a305
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
"""§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,<p>Grey</p>,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"] == "<p>Grey</p>"
|
|
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 == []
|