271 lines
8.4 KiB
Python
271 lines
8.4 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from article_data_manager.importers.rollcalc_json import (
|
|
RollCalcFileError,
|
|
RollCalcJsonSyntaxError,
|
|
RollCalcValidationError,
|
|
load_rollcalc_articles,
|
|
)
|
|
|
|
|
|
def write_json(path: Path, payload: object) -> Path:
|
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def valid_article(**overrides: object) -> dict[str, object]:
|
|
article: dict[str, object] = {
|
|
"nr": "214700",
|
|
"name": "Example Article",
|
|
"thickness": 6.722,
|
|
"area_weight": 0.0,
|
|
"core_type": 0.0,
|
|
}
|
|
article.update(overrides)
|
|
return article
|
|
|
|
|
|
def test_loads_valid_file_with_one_article(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article()])
|
|
|
|
articles = load_rollcalc_articles(path)
|
|
|
|
assert len(articles) == 1
|
|
assert articles[0].nr == "214700"
|
|
assert articles[0].name == "Example Article"
|
|
assert articles[0].thickness == 6.722
|
|
assert articles[0].area_weight == 0.0
|
|
assert articles[0].core_type == 0.0
|
|
|
|
|
|
def test_loads_valid_file_with_multiple_articles(tmp_path: Path) -> None:
|
|
path = write_json(
|
|
tmp_path / "article-data.json",
|
|
[
|
|
valid_article(nr="100001"),
|
|
valid_article(nr="100002"),
|
|
],
|
|
)
|
|
|
|
articles = load_rollcalc_articles(path)
|
|
|
|
assert [article.nr for article in articles] == ["100001", "100002"]
|
|
|
|
|
|
def test_preserves_source_order(tmp_path: Path) -> None:
|
|
path = write_json(
|
|
tmp_path / "article-data.json",
|
|
[
|
|
valid_article(nr="300003"),
|
|
valid_article(nr="100001"),
|
|
valid_article(nr="200002"),
|
|
],
|
|
)
|
|
|
|
articles = load_rollcalc_articles(path)
|
|
|
|
assert [article.nr for article in articles] == ["300003", "100001", "200002"]
|
|
|
|
|
|
def test_preserves_leading_zero_in_article_number(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(nr="000123")])
|
|
|
|
articles = load_rollcalc_articles(path)
|
|
|
|
assert articles[0].nr == "000123"
|
|
|
|
|
|
def test_numeric_integer_is_stored_as_float(tmp_path: Path) -> None:
|
|
path = write_json(
|
|
tmp_path / "article-data.json",
|
|
[valid_article(thickness=5, area_weight=350, core_type=1)],
|
|
)
|
|
|
|
article = load_rollcalc_articles(path)[0]
|
|
|
|
assert article.thickness == 5.0
|
|
assert article.area_weight == 350.0
|
|
assert article.core_type == 1.0
|
|
assert isinstance(article.thickness, float)
|
|
assert isinstance(article.area_weight, float)
|
|
assert isinstance(article.core_type, float)
|
|
|
|
|
|
def test_accepts_and_ignores_unknown_fields(tmp_path: Path) -> None:
|
|
path = write_json(
|
|
tmp_path / "article-data.json",
|
|
[valid_article(width=6.0, unknown_nested={"value": "ignored"})],
|
|
)
|
|
|
|
article = load_rollcalc_articles(path)[0]
|
|
|
|
assert not hasattr(article, "width")
|
|
assert not hasattr(article, "unknown_nested")
|
|
assert article.to_dict() == {
|
|
"nr": "214700",
|
|
"name": "Example Article",
|
|
"thickness": 6.722,
|
|
"area_weight": 0.0,
|
|
"core_type": 0.0,
|
|
}
|
|
|
|
|
|
def test_empty_json_array_returns_empty_list(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [])
|
|
|
|
assert load_rollcalc_articles(path) == []
|
|
|
|
|
|
def test_missing_file_raises_file_error(tmp_path: Path) -> None:
|
|
path = tmp_path / "missing.json"
|
|
|
|
with pytest.raises(RollCalcFileError, match="does not exist"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_directory_path_raises_file_error(tmp_path: Path) -> None:
|
|
with pytest.raises(RollCalcFileError, match="regular file"):
|
|
load_rollcalc_articles(tmp_path)
|
|
|
|
|
|
def test_invalid_utf8_raises_file_error(tmp_path: Path) -> None:
|
|
path = tmp_path / "article-data.json"
|
|
path.write_bytes(b"\xff\xfe\xfa")
|
|
|
|
with pytest.raises(RollCalcFileError, match="UTF-8"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_empty_file_raises_json_syntax_error(tmp_path: Path) -> None:
|
|
path = tmp_path / "article-data.json"
|
|
path.write_text("", encoding="utf-8")
|
|
|
|
with pytest.raises(RollCalcJsonSyntaxError, match="invalid JSON"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_syntactically_invalid_json_raises_json_syntax_error(tmp_path: Path) -> None:
|
|
path = tmp_path / "article-data.json"
|
|
path.write_text("[}", encoding="utf-8")
|
|
|
|
with pytest.raises(RollCalcJsonSyntaxError, match="invalid JSON"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_json_root_must_be_array(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", {"nr": "214700"})
|
|
|
|
with pytest.raises(RollCalcValidationError, match="root must be an array"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_array_entry_must_be_object(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(), "invalid"])
|
|
|
|
with pytest.raises(RollCalcValidationError, match="index 1: entry must be an object"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_missing_required_field(tmp_path: Path) -> None:
|
|
article = valid_article()
|
|
del article["name"]
|
|
path = write_json(tmp_path / "article-data.json", [article])
|
|
|
|
with pytest.raises(RollCalcValidationError, match="missing required field 'name'"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_nr_must_be_string(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(nr=214700)])
|
|
|
|
with pytest.raises(RollCalcValidationError, match="field 'nr' must be a string, got int"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_nr_must_not_be_empty(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(nr="")])
|
|
|
|
with pytest.raises(RollCalcValidationError, match="field 'nr' must not be empty"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_nr_must_not_contain_surrounding_whitespace(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(nr=" 000123")])
|
|
|
|
with pytest.raises(RollCalcValidationError, match="leading or trailing whitespace"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_name_must_be_string(tmp_path: Path) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(name=123)])
|
|
|
|
with pytest.raises(RollCalcValidationError, match="field 'name' must be a string"):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
@pytest.mark.parametrize("field_name", ["thickness", "area_weight", "core_type"])
|
|
def test_numeric_field_must_not_be_string(tmp_path: Path, field_name: str) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(**{field_name: "6.722"})])
|
|
|
|
with pytest.raises(
|
|
RollCalcValidationError,
|
|
match=f"field '{field_name}' must be a JSON number, got str",
|
|
):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
@pytest.mark.parametrize("field_name", ["thickness", "area_weight", "core_type"])
|
|
def test_numeric_field_must_not_be_null(tmp_path: Path, field_name: str) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(**{field_name: None})])
|
|
|
|
with pytest.raises(
|
|
RollCalcValidationError,
|
|
match=f"field '{field_name}' must be a JSON number, got null",
|
|
):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
@pytest.mark.parametrize("field_name", ["thickness", "area_weight", "core_type"])
|
|
def test_numeric_field_must_not_be_boolean(tmp_path: Path, field_name: str) -> None:
|
|
path = write_json(tmp_path / "article-data.json", [valid_article(**{field_name: True})])
|
|
|
|
with pytest.raises(
|
|
RollCalcValidationError,
|
|
match=f"field '{field_name}' must be a JSON number, got bool",
|
|
):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_duplicate_article_number_raises_validation_error(tmp_path: Path) -> None:
|
|
path = write_json(
|
|
tmp_path / "article-data.json",
|
|
[valid_article(nr="00001"), valid_article(nr="1"), valid_article(nr="00001")],
|
|
)
|
|
|
|
with pytest.raises(
|
|
RollCalcValidationError,
|
|
match="Duplicate article number '00001' at index 2; first occurrence at index 0",
|
|
):
|
|
load_rollcalc_articles(path)
|
|
|
|
|
|
def test_multiple_errors_report_first_deterministically(tmp_path: Path) -> None:
|
|
path = write_json(
|
|
tmp_path / "article-data.json",
|
|
[
|
|
valid_article(name=123, thickness="6.722"),
|
|
valid_article(nr="214700"),
|
|
],
|
|
)
|
|
|
|
with pytest.raises(RollCalcValidationError) as exc_info:
|
|
load_rollcalc_articles(path)
|
|
|
|
message = str(exc_info.value)
|
|
assert "index 0" in message
|
|
assert "field 'name'" in message
|
|
assert "thickness" not in message
|