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.
 
 
 
 
 

149 lines
4.7 KiB

import json
from datetime import datetime, timezone as datetime_timezone
from io import StringIO
import pytest
from django.core.management import call_command
from django.core.management.base import CommandError
from finance.exports import ExportArtifact, ExportError, write_export
def synthetic_artifact(filename="gizmoduck-export-20260907T120000Z.zip"):
return ExportArtifact(
filename=filename,
content=b"synthetic zip bytes",
version=1,
row_counts={"accounts.csv": 2, "transactions.csv": 4},
)
@pytest.fixture
def export_root(tmp_path, settings):
settings.EXPORT_ROOT = tmp_path
return tmp_path
def test_write_export_generates_a_plain_name_and_publishes_complete_content(
export_root, monkeypatch
):
now = datetime(2026, 9, 7, 12, 0, tzinfo=datetime_timezone.utc)
monkeypatch.setattr("finance.exports.timezone.now", lambda: now)
monkeypatch.setattr("finance.exports.build_export", synthetic_artifact)
result = write_export()
assert result.name == "gizmoduck-export-20260907T120000Z.zip"
assert result.path == export_root / result.name
assert result.path.read_bytes() == b"synthetic zip bytes"
assert list(export_root.iterdir()) == [result.path]
@pytest.mark.parametrize(
"name",
[
"../outside.zip",
"nested/outside.zip",
"nested\\outside.zip",
"/tmp/outside.zip",
"C:\\outside.zip",
"export.txt",
".zip",
],
)
def test_write_export_rejects_non_plain_zip_names(export_root, name):
with pytest.raises(ExportError, match="invalid_name"):
write_export(name)
assert list(export_root.iterdir()) == []
def test_write_export_rejects_existing_target_before_snapshot(export_root, monkeypatch):
existing = export_root / "existing.zip"
existing.write_bytes(b"original")
def unexpected_snapshot():
raise AssertionError("snapshot must not be built")
monkeypatch.setattr("finance.exports.build_export", unexpected_snapshot)
with pytest.raises(ExportError, match="target_exists"):
write_export(existing.name)
assert existing.read_bytes() == b"original"
def test_write_export_rejects_destination_symlink(export_root, monkeypatch):
outside = export_root.parent / "outside-export.zip"
outside.write_bytes(b"outside original")
link = export_root / "linked.zip"
try:
link.symlink_to(outside)
except OSError:
pytest.skip("Symlink creation unavailable")
monkeypatch.setattr("finance.exports.build_export", synthetic_artifact)
with pytest.raises(ExportError, match="invalid_name"):
write_export(link.name)
assert outside.read_bytes() == b"outside original"
assert link.is_symlink()
def test_write_export_removes_reservation_and_temporary_file_on_snapshot_failure(
export_root, monkeypatch
):
def failed_snapshot():
raise RuntimeError("SYNTHETIC PRIVATE DATABASE DETAIL")
monkeypatch.setattr("finance.exports.build_export", failed_snapshot)
with pytest.raises(ExportError, match="export_failed") as caught:
write_export("failed.zip")
assert "PRIVATE" not in str(caught.value)
assert list(export_root.iterdir()) == []
def test_write_export_cleans_up_if_atomic_publication_fails(export_root, monkeypatch):
monkeypatch.setattr("finance.exports.build_export", synthetic_artifact)
def failed_replace(_source, _destination):
raise OSError("SYNTHETIC PRIVATE FILESYSTEM DETAIL")
monkeypatch.setattr("finance.exports.os.replace", failed_replace)
with pytest.raises(ExportError, match="publication_failed") as caught:
write_export("failed.zip")
assert "PRIVATE" not in str(caught.value)
assert list(export_root.iterdir()) == []
def test_export_command_prints_only_safe_package_metadata(export_root, monkeypatch):
monkeypatch.setattr("finance.exports.build_export", synthetic_artifact)
output = StringIO()
call_command("export_finances", "--output", "synthetic.zip", stdout=output)
payload = json.loads(output.getvalue())
assert payload == {
"name": "synthetic.zip",
"version": 1,
"rows": {"accounts.csv": 2, "transactions.csv": 4},
}
assert str(export_root) not in output.getvalue()
assert "synthetic zip bytes" not in output.getvalue()
def test_export_command_converts_private_failure_to_safe_code(export_root, monkeypatch):
def failed_snapshot():
raise RuntimeError("SYNTHETIC PRIVATE DATABASE DETAIL")
monkeypatch.setattr("finance.exports.build_export", failed_snapshot)
with pytest.raises(CommandError, match="export_failed") as caught:
call_command("export_finances", "--output", "failed.zip")
assert "PRIVATE" not in str(caught.value)
assert list(export_root.iterdir()) == []