Files
article-data-manager/tests/unit/importers/test_rollcalc_json.py
2026-07-29 11:31:17 +02:00

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