EcoSpecies-Atlas/apps/api/tests/test_repository.py

308 lines
12 KiB
Python

from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from ecospecies_api import repository
SAMPLE_PAYLOAD = [
{
"slug": "test-shad",
"source_file": "Test Shad.txt",
"title": "Test Shad (Alosa testus)",
"common_name": "Test Shad",
"scientific_name": "Alosa testus",
"flelmr_code": "9999",
"summary": "",
"section_count": 2,
"diagnostics": [
{
"level": "warning",
"code": "missing_summary",
"message": "Summary/Abstract section is missing.",
},
{
"level": "warning",
"code": "missing_citations",
"message": "References section not found.",
},
],
"sections": [
{"heading": "HEADER", "content": "Header content"},
{"heading": "HABITAT", "content": "Habitat content"},
],
}
]
UPDATED_PAYLOAD = [
{
"slug": "test-shad",
"source_file": "Test Shad v2.txt",
"title": "Test Shad Revised (Alosa testus)",
"common_name": "Test Shad",
"scientific_name": "Alosa testus revised",
"flelmr_code": "1000",
"summary": "Imported replacement summary.",
"section_count": 2,
"diagnostics": [
{
"level": "warning",
"code": "missing_flelmr_code",
"message": "Replacement diagnostic.",
}
],
"sections": [
{"heading": "HEADER", "content": "Replacement header content"},
{"heading": "HABITAT", "content": "Replacement habitat content"},
],
}
]
DIFFERENT_PAYLOAD = [
{
"slug": "other-fish",
"source_file": "Other Fish.txt",
"title": "Other Fish (Pisces otherus)",
"common_name": "Other Fish",
"scientific_name": "Pisces otherus",
"flelmr_code": "2000",
"summary": "Other fish summary.",
"section_count": 1,
"diagnostics": [],
"sections": [
{"heading": "HEADER", "content": "Other fish header"},
],
}
]
class RepositoryWorkflowTests(unittest.TestCase):
def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
db_path = Path(self.tempdir.name) / "test.db"
self.engine = create_engine(f"sqlite:///{db_path}", future=True)
self.session_local = sessionmaker(
bind=self.engine,
autoflush=False,
autocommit=False,
future=True,
)
self.engine_patch = patch.object(repository, "create_db_engine", return_value=self.engine)
self.session_patch = patch.object(repository, "SessionLocal", self.session_local)
self.engine_patch.start()
self.session_patch.start()
repository.import_species_payload(SAMPLE_PAYLOAD)
def tearDown(self) -> None:
self.session_patch.stop()
self.engine_patch.stop()
self.engine.dispose()
self.tempdir.cleanup()
def test_import_filters_missing_summary_diagnostic_from_accepted_dataset(self) -> None:
detail = repository.get_species_by_slug("test-shad")
self.assertIsNotNone(detail)
self.assertEqual(detail["section_count"], 2)
self.assertEqual([section["position"] for section in detail["sections"]], [1, 2])
self.assertEqual([item["code"] for item in detail["diagnostics"]], ["missing_citations"])
def test_editorial_update_changes_publication_visibility_and_creates_audit(self) -> None:
result = repository.update_species_editorial(
slug="test-shad",
publication_status="draft",
summary="Editor-authored summary.",
editor_notes="Needs another review pass.",
is_archived=None,
username="bob",
)
self.assertIsNotNone(result)
self.assertEqual(result["publication_status"], "draft")
self.assertEqual(result["summary"], "Editor-authored summary.")
self.assertEqual(result["last_modified_by"], "bob")
self.assertEqual(repository.get_species_by_slug("test-shad"), None)
editor_detail = repository.get_editor_species_detail("test-shad")
audit = repository.list_species_audit("test-shad")
self.assertIsNotNone(editor_detail)
self.assertEqual(editor_detail["publication_status"], "draft")
self.assertEqual(editor_detail["summary"], "Editor-authored summary.")
self.assertEqual(editor_detail["editor_notes"], "Needs another review pass.")
self.assertIsNotNone(audit)
self.assertEqual(audit[0]["action"], "editorial_update")
self.assertEqual(audit[0]["changed_by"], "bob")
self.assertIn("summary", audit[0]["details"])
self.assertIn("publication_status", audit[0]["details"])
def test_section_update_records_section_audit_metadata(self) -> None:
result = repository.update_species_section(
slug="test-shad",
section_position=2,
content="Updated habitat content.",
username="carol",
)
self.assertIsNotNone(result)
self.assertEqual(result["section"]["position"], 2)
self.assertEqual(result["section"]["content"], "Updated habitat content.")
self.assertEqual(result["last_modified_by"], "carol")
self.assertEqual(sorted(result["changed_fields"].keys()), ["section_content"])
editor_detail = repository.get_editor_species_detail("test-shad")
audit = repository.list_species_audit("test-shad")
self.assertIsNotNone(editor_detail)
self.assertEqual(editor_detail["sections"][1]["content"], "Updated habitat content.")
self.assertIsNotNone(audit)
self.assertEqual(audit[0]["action"], "section_update")
self.assertEqual(audit[0]["changed_by"], "carol")
self.assertEqual(audit[0]["details"]["section_position"], 2)
self.assertEqual(audit[0]["details"]["section_heading"], "HABITAT")
self.assertEqual(
audit[0]["details"]["section_content"],
{"from": "Habitat content", "to": "Updated habitat content."},
)
def test_reimport_preserves_editorial_state_and_audit_history(self) -> None:
repository.update_species_editorial(
slug="test-shad",
publication_status="draft",
summary="Editor-authored summary.",
editor_notes="Needs another review pass.",
is_archived=None,
username="bob",
)
repository.update_species_section(
slug="test-shad",
section_position=2,
content="Updated habitat content.",
username="carol",
)
repository.import_species_payload(UPDATED_PAYLOAD)
editor_detail = repository.get_editor_species_detail("test-shad")
audit = repository.list_species_audit("test-shad")
self.assertIsNotNone(editor_detail)
self.assertEqual(editor_detail["source_file"], "Test Shad v2.txt")
self.assertEqual(editor_detail["title"], "Test Shad Revised (Alosa testus)")
self.assertEqual(editor_detail["scientific_name"], "Alosa testus revised")
self.assertEqual(editor_detail["flelmr_code"], "1000")
self.assertEqual(editor_detail["publication_status"], "draft")
self.assertEqual(editor_detail["summary"], "Editor-authored summary.")
self.assertEqual(editor_detail["editor_notes"], "Needs another review pass.")
self.assertEqual(editor_detail["sections"][0]["content"], "Replacement header content")
self.assertEqual(editor_detail["sections"][1]["content"], "Updated habitat content.")
self.assertEqual([item["code"] for item in editor_detail["diagnostics"]], ["missing_flelmr_code"])
self.assertIsNotNone(audit)
self.assertEqual(len(audit), 2)
self.assertEqual([entry["action"] for entry in audit], ["section_update", "editorial_update"])
def test_reimport_updates_summary_when_no_editorial_override_exists(self) -> None:
repository.import_species_payload(UPDATED_PAYLOAD)
detail = repository.get_species_by_slug("test-shad")
self.assertIsNotNone(detail)
self.assertEqual(detail["summary"], "Imported replacement summary.")
self.assertEqual(detail["sections"][0]["content"], "Replacement header content")
def test_editor_can_archive_species_explicitly(self) -> None:
result = repository.update_species_editorial(
slug="test-shad",
publication_status=None,
summary=None,
editor_notes=None,
is_archived=True,
username="dana",
)
public_detail = repository.get_species_by_slug("test-shad")
editor_detail = repository.get_editor_species_detail("test-shad")
audit = repository.list_species_audit("test-shad")
self.assertIsNotNone(result)
self.assertTrue(result["is_archived"])
self.assertEqual(result["last_modified_by"], "dana")
self.assertIsNone(public_detail)
self.assertIsNotNone(editor_detail)
self.assertTrue(editor_detail["is_archived"])
self.assertIsNotNone(audit)
self.assertEqual(audit[0]["action"], "editorial_update")
self.assertEqual(audit[0]["details"]["is_archived"], {"from": False, "to": True})
def test_editor_can_unarchive_species_explicitly(self) -> None:
repository.update_species_editorial(
slug="test-shad",
publication_status=None,
summary=None,
editor_notes=None,
is_archived=True,
username="dana",
)
result = repository.update_species_editorial(
slug="test-shad",
publication_status=None,
summary=None,
editor_notes=None,
is_archived=False,
username="erin",
)
public_detail = repository.get_species_by_slug("test-shad")
audit = repository.list_species_audit("test-shad")
self.assertIsNotNone(result)
self.assertFalse(result["is_archived"])
self.assertEqual(result["last_modified_by"], "erin")
self.assertIsNotNone(public_detail)
self.assertIsNotNone(audit)
self.assertEqual(audit[0]["details"]["is_archived"], {"from": True, "to": False})
def test_missing_species_is_archived_instead_of_deleted(self) -> None:
repository.import_species_payload(DIFFERENT_PAYLOAD)
public_detail = repository.get_species_by_slug("test-shad")
editor_detail = repository.get_editor_species_detail("test-shad")
editor_items = repository.get_editor_species_list()
audit = repository.list_species_audit("test-shad")
self.assertIsNone(public_detail)
self.assertIsNotNone(editor_detail)
self.assertTrue(editor_detail["is_archived"])
self.assertEqual([item["slug"] for item in repository.list_species()], ["other-fish"])
self.assertEqual([item["slug"] for item in editor_items], ["other-fish", "test-shad"])
self.assertIsNotNone(audit)
self.assertEqual(audit[0]["action"], "import_archive")
self.assertEqual(audit[0]["details"]["is_archived"], {"from": False, "to": True})
def test_archived_species_is_restored_when_it_reappears(self) -> None:
repository.import_species_payload(DIFFERENT_PAYLOAD)
repository.import_species_payload(UPDATED_PAYLOAD)
public_detail = repository.get_species_by_slug("test-shad")
editor_detail = repository.get_editor_species_detail("test-shad")
audit = repository.list_species_audit("test-shad")
self.assertIsNotNone(public_detail)
self.assertIsNotNone(editor_detail)
self.assertFalse(editor_detail["is_archived"])
self.assertEqual(public_detail["summary"], "Imported replacement summary.")
self.assertIsNotNone(audit)
self.assertEqual(audit[0]["action"], "import_restore")
self.assertEqual(audit[0]["details"]["is_archived"], {"from": True, "to": False})
if __name__ == "__main__":
unittest.main()