You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.6 KiB
127 lines
4.6 KiB
"""Synthetic checks for source-safe local import discovery.""" |
|
|
|
import os |
|
from hashlib import sha256 |
|
from pathlib import Path |
|
|
|
from django.test import override_settings |
|
|
|
from ingestion import guided_imports |
|
from ingestion.guided_imports import discover_imports |
|
|
|
|
|
HEADER = b"Data,Dependencia,Historico,Documento,Tipo,Valor\n" |
|
|
|
|
|
def test_discovery_returns_only_safe_metadata_for_direct_supported_files(tmp_path): |
|
bank = HEADER + b"01/02/2026,,SYNTHETIC BANK ROW,,,12.34\n" |
|
full = b"1 - SYNTHETIC OWNER\n" + b"15.02.2026" + b"SYNTHETIC CARD ROW".ljust(37) + b"BR 12,34 0,00\n" |
|
nested = tmp_path / "nested" |
|
nested.mkdir() |
|
(tmp_path / "bank.csv").write_bytes(bank) |
|
(tmp_path / "card.txt").write_bytes(full) |
|
(nested / "hidden.csv").write_bytes(bank) |
|
|
|
with override_settings(IMPORT_ROOT=tmp_path): |
|
rows = discover_imports() |
|
|
|
assert [(row.name, row.profile, row.sections, row.code) for row in rows] == [ |
|
("bank.csv", "BANK_CSV_V1", ("1",), None), |
|
("card.txt", "OUROCARD_FULL_V1", ("1",), None), |
|
] |
|
assert [row.document_hash for row in rows] == [sha256(bank).hexdigest(), sha256(full).hexdigest()] |
|
assert all(not hasattr(row, "source_text") for row in rows) |
|
|
|
|
|
def test_discovery_reports_safe_codes_for_direct_unsupported_or_invalid_files(tmp_path): |
|
(tmp_path / "unknown.ofx").write_bytes(b"SYNTHETIC OFX") |
|
(tmp_path / "statement.pdf").write_bytes(b"%PDF-SYNTHETIC") |
|
(tmp_path / "invalid.txt").write_bytes(b"SYNTHETIC UNRECOGNIZED TEXT") |
|
(tmp_path / "oversize.csv").write_bytes(b"x" * (10 * 1024 * 1024 + 1)) |
|
|
|
with override_settings(IMPORT_ROOT=tmp_path): |
|
rows = discover_imports() |
|
|
|
assert [(row.name, row.profile, row.sections, row.code) for row in rows] == [ |
|
("invalid.txt", None, (), "UNRECOGNIZED_LAYOUT"), |
|
("oversize.csv", None, (), "INPUT_TOO_LARGE"), |
|
("statement.pdf", None, (), "UNSUPPORTED_FILE"), |
|
("unknown.ofx", None, (), "UNSUPPORTED_FILE"), |
|
] |
|
assert all(not hasattr(row, "source_text") for row in rows) |
|
|
|
|
|
def test_discovery_ignores_symlinks_and_non_files(tmp_path): |
|
bank = HEADER + b"01/02/2026,,SYNTHETIC BANK ROW,,,12.34\n" |
|
target = tmp_path / "target.csv" |
|
target.write_bytes(bank) |
|
(tmp_path / "linked.csv").symlink_to(target) |
|
(tmp_path / "directory.csv").mkdir() |
|
|
|
with override_settings(IMPORT_ROOT=tmp_path): |
|
rows = discover_imports() |
|
|
|
assert [row.name for row in rows] == ["target.csv"] |
|
|
|
|
|
def test_discovery_marks_a_post_listing_symlink_replacement_unreadable(tmp_path, monkeypatch): |
|
bank = HEADER + b"01/02/2026,,SYNTHETIC BANK ROW,,,12.34\n" |
|
candidate = tmp_path / "candidate.csv" |
|
replacement = tmp_path / "replacement.csv" |
|
candidate.write_bytes(bank) |
|
replacement.write_bytes(bank) |
|
original_open = os.open |
|
replaced = False |
|
|
|
def replace_then_open(value, flags, *args, **kwargs): |
|
nonlocal replaced |
|
if not replaced and Path(value) == candidate: |
|
replaced = True |
|
candidate.unlink() |
|
candidate.symlink_to(replacement) |
|
return original_open(value, flags, *args, **kwargs) |
|
|
|
monkeypatch.setattr(guided_imports, "os", os, raising=False) |
|
monkeypatch.setattr(guided_imports.os, "open", replace_then_open) |
|
|
|
with override_settings(IMPORT_ROOT=tmp_path): |
|
rows = discover_imports() |
|
|
|
assert [(row.name, row.profile, row.sections, row.code) for row in rows] == [ |
|
("candidate.csv", None, (), "UNREADABLE_FILE"), |
|
("replacement.csv", "BANK_CSV_V1", ("1",), None), |
|
] |
|
|
|
|
|
def test_discovery_returns_empty_results_when_listing_the_import_root_fails(tmp_path, monkeypatch): |
|
root = tmp_path / "unavailable" |
|
root.mkdir() |
|
original_iterdir = Path.iterdir |
|
|
|
def delayed_failure(self): |
|
if self == root.resolve(): |
|
def entries(): |
|
raise OSError("SYNTHETIC LISTING FAILURE") |
|
yield |
|
return entries() |
|
return original_iterdir(self) |
|
|
|
monkeypatch.setattr(Path, "iterdir", delayed_failure) |
|
|
|
with override_settings(IMPORT_ROOT=root): |
|
assert discover_imports() == () |
|
|
|
|
|
def test_discovery_returns_empty_results_when_resolving_the_import_root_fails(tmp_path, monkeypatch): |
|
root = tmp_path / "unresolvable" |
|
original_resolve = Path.resolve |
|
|
|
def resolution_failure(self, *args, **kwargs): |
|
if self == root: |
|
raise RuntimeError("SYNTHETIC RESOLUTION FAILURE") |
|
return original_resolve(self, *args, **kwargs) |
|
|
|
monkeypatch.setattr(Path, "resolve", resolution_failure) |
|
|
|
with override_settings(IMPORT_ROOT=root): |
|
assert discover_imports() == ()
|
|
|