Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- [CLI] NewsDOM JSON 출력물이 스키마(`ParseResponse`)를 준수하는지 검증하는 `tools/validate_dom.py` 도구를 추가했습니다.

### Security
- 전역 500 에러 응답에도 표준 보안 헤더를 적용하여 예외 경로에서 header 누락을 방지
- MinerU subprocess argv 생성 시 `-`로 시작하는 option-like 인자를 거부하여 argument injection 위험을 낮춤
Expand Down
99 changes: 99 additions & 0 deletions tests/test_tools_validate_dom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from __future__ import annotations

import json
from pathlib import Path
import pytest
import runpy
import sys
import warnings

from tools import validate_dom


@pytest.fixture
def mock_valid_json_file(tmp_path: Path) -> Path:
json_path = tmp_path / "valid.json"
data = {
"document_id": "doc_123",
"pages": [],
"quality": {"status": "success", "parser": "mineru", "warnings": []},
}
json_path.write_text(json.dumps(data), encoding="utf-8")
return json_path


@pytest.fixture
def mock_invalid_schema_json_file(tmp_path: Path) -> Path:
json_path = tmp_path / "invalid_schema.json"
# Missing document_id which is required in ParseResponse
data = {"pages": []}
json_path.write_text(json.dumps(data), encoding="utf-8")
return json_path


@pytest.fixture
def mock_invalid_format_json_file(tmp_path: Path) -> Path:
json_path = tmp_path / "invalid_format.json"
json_path.write_text("{ this is not a valid json ", encoding="utf-8")
return json_path


def test_validate_dom_success(mock_valid_json_file, capsys):
validate_dom.main([str(mock_valid_json_file)])
out = capsys.readouterr().out
assert "Validation successful" in out


def test_validate_dom_invalid_schema(mock_invalid_schema_json_file, capsys):
with pytest.raises(SystemExit) as e:
validate_dom.main([str(mock_invalid_schema_json_file)])
assert e.value.code == 1
assert "Schema validation failed" in capsys.readouterr().err


def test_validate_dom_invalid_format(mock_invalid_format_json_file, capsys):
with pytest.raises(SystemExit) as e:
validate_dom.main([str(mock_invalid_format_json_file)])
assert e.value.code == 1
assert "Invalid JSON format" in capsys.readouterr().err


def test_validate_dom_not_found(tmp_path, capsys):
with pytest.raises(SystemExit) as e:
validate_dom.main([str(tmp_path / "missing.json")])
assert e.value.code == 1
assert "File not found" in capsys.readouterr().err


def test_validate_dom_wrong_ext(tmp_path, capsys):
txt = tmp_path / "wrong.txt"
txt.write_text("test", encoding="utf-8")
with pytest.raises(SystemExit) as e:
validate_dom.main([str(txt)])
assert e.value.code == 1
assert "must be a .json file" in capsys.readouterr().err


def test_validate_dom_main_execution(monkeypatch, mock_valid_json_file):
monkeypatch.setattr(sys, "argv", ["validate_dom.py", str(mock_valid_json_file)])
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
runpy.run_module("tools.validate_dom", run_name="__main__")


def test_validate_dom_help_exit(capsys):
with pytest.raises(SystemExit) as e:
validate_dom.main(["--help"])
assert e.value.code == 0
assert "Validate a NewsDOM JSON output" in capsys.readouterr().out


def test_validate_dom_interrupt(monkeypatch, mock_valid_json_file):
def mock_validate(*args, **kwargs):
raise KeyboardInterrupt()

monkeypatch.setattr(validate_dom, "validate_dom", mock_validate)

with pytest.raises(SystemExit) as e:
validate_dom.main([str(mock_valid_json_file)])
assert e.value.code == 130
63 changes: 63 additions & 0 deletions tools/validate_dom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

from pydantic import ValidationError

# We inject sys.path inside main() to avoid global side-effects during test imports.


def validate_dom(json_path: Path) -> Any:
"""Validate DOM JSON against ParseResponse schema."""
if not json_path.is_file():
raise FileNotFoundError(f"File not found or is not a file: {json_path}")
if json_path.suffix.lower() != ".json":
raise ValueError("File must be a .json file.")

try:
data = json.loads(json_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format: {e}") from e

from newsdom_api.schemas import ParseResponse

try:
validated_model = ParseResponse.model_validate(data)
except ValidationError as e:
raise ValueError(f"Schema validation failed:\n{e}") from e

return validated_model


def main(argv: list[str] | None = None) -> None:
"""Run validate_dom main entry point."""
_REPO_ROOT = Path(__file__).resolve().parents[1]
_SRC_ROOT = _REPO_ROOT / "src"
if str(_SRC_ROOT) not in sys.path: # pragma: no cover
sys.path.insert(0, str(_SRC_ROOT))

parser = argparse.ArgumentParser(
description="Validate a NewsDOM JSON output against the ParseResponse schema."
)
parser.add_argument("input", type=Path, help="Path to the JSON DOM file.")

args = parser.parse_args(argv)

try:
validate_dom(args.input)
print(
"Validation successful: The JSON file strictly matches the ParseResponse schema."
)
except KeyboardInterrupt:
sys.exit(130)
except (FileNotFoundError, ValueError, OSError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
Comment on lines +50 to +59


if __name__ == "__main__": # pragma: no cover
main()
Loading