about summary refs log tree commit diff
path: root/tests/unit/db/test_case_attributes.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/db/test_case_attributes.py')
-rw-r--r--tests/unit/db/test_case_attributes.py471
1 files changed, 296 insertions, 175 deletions
diff --git a/tests/unit/db/test_case_attributes.py b/tests/unit/db/test_case_attributes.py
index 97a0703..998b58d 100644
--- a/tests/unit/db/test_case_attributes.py
+++ b/tests/unit/db/test_case_attributes.py
@@ -1,205 +1,326 @@
 """Test cases for gn3.db.case_attributes.py"""
 
+import pickle
+import tempfile
+import os
+import json
+from pathlib import Path
 import pytest
 from pytest_mock import MockFixture
-from gn3.db.case_attributes import get_unreviewed_diffs
-from gn3.db.case_attributes import get_case_attributes
-from gn3.db.case_attributes import insert_case_attribute_audit
-from gn3.db.case_attributes import approve_case_attribute
-from gn3.db.case_attributes import reject_case_attribute
+from gn3.db.case_attributes import queue_edit
+from gn3.db.case_attributes import (
+    CaseAttributeEdit,
+    EditStatus,
+    apply_change,
+    get_changes,
+    view_change
+)
 
 
 @pytest.mark.unit_test
-def test_get_case_attributes(mocker: MockFixture) -> None:
-    """Test that all the case attributes are fetched correctly"""
+def test_queue_edit(mocker: MockFixture) -> None:
+    """Test queueing an edit."""
     mock_conn = mocker.MagicMock()
     with mock_conn.cursor() as cursor:
-        cursor.fetchall.return_value = (
-            (1, "Condition", None),
-            (2, "Tissue", None),
-            (3, "Age", "Cum sociis natoque penatibus et magnis dis"),
-            (4, "Condition", "Description A"),
-            (5, "Condition", "Description B"),
-        )
-        results = get_case_attributes(mock_conn)
+        type(cursor).lastrowid = 28
+        tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
+        caseattr_id = queue_edit(
+            cursor,
+            directory=tmpdir,
+            edit=CaseAttributeEdit(
+                inbredset_id=1, status=EditStatus.review,
+                user_id="xxxx", changes={"a": 1, "b": 2}
+            ))
         cursor.execute.assert_called_once_with(
-            "SELECT Id, Name, Description FROM CaseAttribute"
-        )
-        assert results == (
-            (1, "Condition", None),
-            (2, "Tissue", None),
-            (3, "Age", "Cum sociis natoque penatibus et magnis dis"),
-            (4, "Condition", "Description A"),
-            (5, "Condition", "Description B"),
-        )
+            "INSERT INTO "
+            "caseattributes_audit(status, editor, json_diff_data) "
+            "VALUES (%s, %s, %s) "
+            "ON DUPLICATE KEY UPDATE status=%s",
+            ('review', 'xxxx', '{"a": 1, "b": 2}', 'review'))
+        assert 28 == caseattr_id
 
 
 @pytest.mark.unit_test
-def test_get_unreviewed_diffs(mocker: MockFixture) -> None:
-    """Test that the correct query is called when fetching unreviewed
-    case-attributes diff"""
-    mock_conn = mocker.MagicMock()
-    with mock_conn.cursor() as cursor:
-        _ = get_unreviewed_diffs(mock_conn)
-        cursor.fetchall.return_value = ((1, "editor", "diff_data_1"),)
-        cursor.execute.assert_called_once_with(
-            "SELECT id, editor, json_diff_data FROM "
-            "caseattributes_audit WHERE status = 'review'"
-        )
+def test_view_change(mocker: MockFixture) -> None:
+    """Test view_change function."""
+    sample_json_diff = {
+        "inbredset_id": 1,
+        "Modifications": {
+            "Original": {
+                "B6D2F1": {"Epoch": "10au"},
+                "BXD100": {"Epoch": "3b"},
+                "BXD101": {"SeqCvge": "29"},
+                "BXD102": {"Epoch": "3b"},
+                "BXD108": {"SeqCvge": ""}
+            },
+            "Current": {
+                "B6D2F1": {"Epoch": "10"},
+                "BXD100": {"Epoch": "3"},
+                "BXD101": {"SeqCvge": "2"},
+                "BXD102": {"Epoch": "3"},
+                "BXD108": {"SeqCvge": "oo"}
+            }
+        }
+    }
+    change_id = 28
+    mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock()
+    mock_conn.cursor.return_value = mock_cursor
+    mock_cursor.fetchone.return_value = (json.dumps(sample_json_diff), None)
+    assert view_change(mock_cursor, change_id) == sample_json_diff
+    mock_cursor.execute.assert_called_once_with(
+        "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s",
+        (change_id,))
+    mock_cursor.fetchone.assert_called_once()
 
 
 @pytest.mark.unit_test
-def test_insert_case_attribute_audit(mocker: MockFixture) -> None:
-    """Test that the updating case attributes uses the correct query"""
-    mock_conn = mocker.MagicMock()
-    with mock_conn.cursor() as cursor:
-        _ = insert_case_attribute_audit(
-            mock_conn, status="review", author="Author", data="diff_data"
-        )
-        cursor.execute.assert_called_once_with(
-            "INSERT INTO caseattributes_audit "
-            "(status, editor, json_diff_data) "
-            "VALUES (%s, %s, %s)",
-            ("review", "Author", "diff_data"),
-        )
+def test_view_change_invalid_json(mocker: MockFixture) -> None:
+    """Test invalid json when view_change is called"""
+    change_id = 28
+    mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock()
+    mock_conn.cursor.return_value = mock_cursor
+    mock_cursor.fetchone.return_value = ("invalid_json_string", None)
+    with pytest.raises(json.JSONDecodeError):
+        view_change(mock_cursor, change_id)
+    mock_cursor.execute.assert_called_once_with(
+        "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s",
+        (change_id,))
 
 
 @pytest.mark.unit_test
-def test_reject_case_attribute(mocker: MockFixture) -> None:
-    """Test rejecting a case-attribute"""
-    mock_conn = mocker.MagicMock()
-    with mock_conn.cursor() as cursor:
-        _ = reject_case_attribute(
-            mock_conn,
-            case_attr_audit_id=1,
-        )
-        cursor.execute.assert_called_once_with(
-            "UPDATE caseattributes_audit SET "
-            "status = 'rejected' WHERE id = %s",
-            (1,),
-        )
+def test_view_change_no_data(mocker: MockFixture) -> None:
+    "Test no result when view_change is called"
+    change_id = 28
+    mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock()
+    mock_conn.cursor.return_value = mock_cursor
+    mock_cursor.fetchone.return_value = (None, None)
+    assert view_change(mock_cursor, change_id) == {}
+    mock_cursor.execute.assert_called_once_with(
+        "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s",
+        (change_id,))
 
 
 @pytest.mark.unit_test
-def test_approve_inserting_case_attribute(mocker: MockFixture) -> None:
-    """Test approving inserting a case-attribute"""
-    mock_conn = mocker.MagicMock()
-    with mock_conn.cursor() as cursor:
-        type(cursor).rowcount = 1
-        cursor.fetchone.return_value = (
-            """
-        {"Insert": {"name": "test", "description": "Random Description"}}
-        """,
-        )
-        _ = approve_case_attribute(
-            mock_conn,
-            case_attr_audit_id=3,
-        )
-        calls = [
-            mocker.call(
-                "SELECT json_diff_data FROM caseattributes_audit "
-                "WHERE id = %s",
-                (3,),
-            ),
-            mocker.call(
-                "INSERT INTO CaseAttribute "
-                "(Name, Description) VALUES "
-                "(%s, %s)",
-                (
-                    "test",
-                    "Random Description",
-                ),
-            ),
-            mocker.call(
-                "UPDATE caseattributes_audit SET "
-                "status = 'approved' WHERE id = %s",
-                (3,),
-            ),
+def test_apply_change_approved(mocker: MockFixture) -> None:
+    """Test approving a change"""
+    mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock()
+    mock_conn.cursor.return_value = mock_cursor
+    mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb")
+    mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock()
+    mock_lmdb.open.return_value = mock_env
+    mock_env.begin.return_value.__enter__.return_value = mock_txn
+    change_id, review_ids = 1, {1, 2, 3}
+    mock_txn.get.side_effect = (
+        pickle.dumps(review_ids),  # b"review" key
+        None,                      # b"approved" key
+    )
+    tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
+    mock_cursor.fetchone.return_value = (json.dumps({
+        "inbredset_id": 1,
+        "Modifications": {
+            "Current": {
+                "B6D2F1": {"Epoch": "10"},
+                "BXD100": {"Epoch": "3"},
+                "BXD101": {"SeqCvge": "2"},
+                "BXD102": {"Epoch": "3"},
+                "BXD108": {"SeqCvge": "oo"}
+            }
+        }
+    }), None)
+    mock_cursor.fetchall.side_effect = [
+        [  # Strain query
+            ("B6D2F1", 1), ("BXD100", 2),
+            ("BXD101", 3), ("BXD102", 4),
+            ("BXD108", 5)],
+        [  # CaseAttribute query
+            ("Epoch", 101), ("SeqCvge", 102)]
+    ]
+    assert apply_change(mock_cursor, EditStatus.approved,
+                        change_id, tmpdir) is True
+    assert mock_cursor.execute.call_count == 4
+    mock_cursor.execute.assert_has_calls([
+        mocker.call(
+            "SELECT json_diff_data FROM caseattributes_audit WHERE id = %s",
+            (change_id,)),
+        mocker.call(
+            "SELECT Name, Id FROM Strain WHERE Name IN (%s, %s, %s, %s, %s)",
+            ("B6D2F1", "BXD100", "BXD101", "BXD102", "BXD108")),
+        mocker.call(
+            "SELECT Name, CaseAttributeId FROM CaseAttribute "
+            "WHERE InbredSetId = %s AND Name IN (%s, %s)",
+            (1, "Epoch", "SeqCvge")),
+        mocker.call(
+            "UPDATE caseattributes_audit SET status = %s WHERE id = %s",
+            ("approved", change_id))
+    ])
+    mock_cursor.executemany.assert_called_once_with(
+        "INSERT INTO CaseAttributeXRefNew (InbredSetId, StrainId, CaseAttributeId, Value) "
+        "VALUES (%(inbredset_id)s, %(strain_id)s, %(caseattr_id)s, %(value)s) "
+        "ON DUPLICATE KEY UPDATE Value = VALUES(Value)",
+        [
+            {"inbredset_id": 1, "strain_id": 1, "caseattr_id": 101, "value": "10"},
+            {"inbredset_id": 1, "strain_id": 2, "caseattr_id": 101, "value": "3"},
+            {"inbredset_id": 1, "strain_id": 3, "caseattr_id": 102, "value": "2"},
+            {"inbredset_id": 1, "strain_id": 4, "caseattr_id": 101, "value": "3"},
+            {"inbredset_id": 1, "strain_id": 5, "caseattr_id": 102, "value": "oo"}
         ]
-        cursor.execute.assert_has_calls(calls, any_order=False)
+    )
 
 
 @pytest.mark.unit_test
-def test_approve_deleting_case_attribute(mocker: MockFixture) -> None:
-    """Test deleting a case-attribute"""
-    mock_conn = mocker.MagicMock()
-    with mock_conn.cursor() as cursor:
-        type(cursor).rowcount = 1
-        cursor.fetchone.return_value = (
-            """
-        {"Deletion": {"id": "12", "name": "test", "description": ""}}
-        """,
-        )
-        _ = approve_case_attribute(
-            mock_conn,
-            case_attr_audit_id=3,
-        )
-        calls = [
-            mocker.call(
-                "SELECT json_diff_data FROM caseattributes_audit "
-                "WHERE id = %s",
-                (3,),
-            ),
-            mocker.call("DELETE FROM CaseAttribute WHERE Id = %s", ("12",)),
-            mocker.call(
-                "UPDATE caseattributes_audit SET "
-                "status = 'approved' WHERE id = %s",
-                (3,),
-            ),
-        ]
-        cursor.execute.assert_has_calls(calls, any_order=False)
+def test_apply_change_rejected(mocker: MockFixture) -> None:
+    """Test rejecting a change"""
+    mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock()
+    mock_conn.cursor.return_value = mock_cursor
+    mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb")
+    mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock()
+    mock_lmdb.open.return_value = mock_env
+    mock_env.begin.return_value.__enter__.return_value = mock_txn
+    tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
+    change_id, review_ids = 3, {1, 2, 3}
+    mock_txn.get.side_effect = [
+        pickle.dumps(review_ids),  # review_ids
+        None  # rejected_ids (initially empty)
+    ]
+
+    assert apply_change(mock_cursor, EditStatus.rejected,
+                        change_id, tmpdir) is True
+
+    # Verify SQL query call sequence
+    mock_cursor.execute.assert_called_once_with(
+        "UPDATE caseattributes_audit SET status = %s WHERE id = %s",
+        (str(EditStatus.rejected), change_id))
+    mock_cursor.executemany.assert_not_called()
+
+    # Verify LMDB operations
+    mock_env.begin.assert_called_once_with(write=True)
+    expected_txn_calls = [
+        mocker.call(b"review", pickle.dumps({1, 2})),
+        mocker.call(b"rejected", pickle.dumps({3}))
+    ]
+    mock_txn.put.assert_has_calls(expected_txn_calls, any_order=False)
 
 
 @pytest.mark.unit_test
-def test_approve_modifying_case_attribute(mocker: MockFixture) -> None:
-    """Test modifying a case-attribute"""
-    mock_conn = mocker.MagicMock()
-    with mock_conn.cursor() as cursor:
-        type(cursor).rowcount = 1
-        cursor.fetchone.return_value = (
-            """
-{
-  "id": "12",
-  "Modification": {
-    "description": {
-      "Current": "Test",
-      "Original": "A"
-    },
-    "name": {
-      "Current": "Height (A)",
-      "Original": "Height"
+def test_apply_change_non_existent_change_id(mocker: MockFixture) -> None:
+    """Test that there's a missing change_id from the returned LMDB rejected set."""
+    mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock()
+    mock_cursor, mock_conn = mocker.MagicMock(), mocker.MagicMock()
+    mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb")
+    mock_lmdb.open.return_value = mock_env
+    mock_conn.cursor.return_value = mock_cursor
+    mock_env.begin.return_value.__enter__.return_value = mock_txn
+    change_id, review_ids = 28, {1, 2, 3}
+    mock_txn.get.side_effect = [
+        pickle.dumps(review_ids),  # b"review" key
+        None,                      # b"approved" key
+    ]
+    tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir()))
+    assert apply_change(mock_cursor, EditStatus.approved,
+                        change_id, tmpdir) is False
+
+
+@pytest.mark.unit_test
+def test_get_changes(mocker: MockFixture) -> None:
+    """Test that reviews are correctly fetched"""
+    mock_fetch_case_attrs_changes = mocker.patch(
+        "gn3.db.case_attributes.__fetch_case_attrs_changes__"
+    )
+    mock_fetch_case_attrs_changes.return_value = [
+        {
+            "editor": "user1",
+            "json_diff_data": {
+                "inbredset_id": 1,
+                "Modifications": {
+                    "Original": {
+                        "B6D2F1": {"Epoch": "10au"},
+                        "BXD100": {"Epoch": "3b"},
+                        "BXD101": {"SeqCvge": "29"},
+                        "BXD102": {"Epoch": "3b"},
+                        "BXD108": {"SeqCvge": ""}
+                    },
+                    "Current": {
+                        "B6D2F1": {"Epoch": "10"},
+                        "BXD100": {"Epoch": "3"},
+                        "BXD101": {"SeqCvge": "2"},
+                        "BXD102": {"Epoch": "3"},
+                        "BXD108": {"SeqCvge": "oo"}
+                    }
+                }
+            },
+            "time_stamp": "2025-07-01 12:00:00"
+        },
+        {
+            "editor": "user2",
+            "json_diff_data": {
+                "inbredset_id": 1,
+                "Modifications": {
+                    "Original": {"BXD200": {"Epoch": "5a"}},
+                    "Current": {"BXD200": {"Epoch": "5"}}
+                }
+            },
+            "time_stamp": "2025-07-01 12:01:00"
+        }
+    ]
+    mock_lmdb = mocker.patch("gn3.db.case_attributes.lmdb")
+    mock_env, mock_txn = mocker.MagicMock(), mocker.MagicMock()
+    mock_lmdb.open.return_value = mock_env
+    mock_env.begin.return_value.__enter__.return_value = mock_txn
+    review_ids, approved_ids, rejected_ids = {1, 4}, {2, 3}, {5, 6, 7, 10}
+    mock_txn.get.side_effect = (
+        pickle.dumps(review_ids),    # b"review" key
+        pickle.dumps(approved_ids),  # b"approved" key
+        pickle.dumps(rejected_ids)   # b"rejected" key
+    )
+    result = get_changes(cursor=mocker.MagicMock(),
+                         change_type=EditStatus.review,
+                         directory=Path("/tmp"))
+    expected = {
+        "change-type": "review",
+        "count": {
+            "reviews": 2,
+            "approvals": 2,
+            "rejections": 4
+        },
+        "data": {
+            1: {
+                "editor": "user1",
+                "json_diff_data": {
+                    "inbredset_id": 1,
+                    "Modifications": {
+                        "Original": {
+                            "B6D2F1": {"Epoch": "10au"},
+                            "BXD100": {"Epoch": "3b"},
+                            "BXD101": {"SeqCvge": "29"},
+                            "BXD102": {"Epoch": "3b"},
+                            "BXD108": {"SeqCvge": ""}
+                        },
+                        "Current": {
+                            "B6D2F1": {"Epoch": "10"},
+                            "BXD100": {"Epoch": "3"},
+                            "BXD101": {"SeqCvge": "2"},
+                            "BXD102": {"Epoch": "3"},
+                            "BXD108": {"SeqCvge": "oo"}
+                        }
+                    }
+                },
+                "time_stamp": "2025-07-01 12:00:00"
+            },
+            4: {
+                'editor': 'user2',
+                'json_diff_data': {
+                    'inbredset_id': 1,
+                    'Modifications': {
+                        'Original': {
+                            'BXD200': {'Epoch': '5a'}
+                        },
+                        'Current': {
+                            'BXD200': {'Epoch': '5'}
+                        }
+                    }
+                },
+                "time_stamp": "2025-07-01 12:01:00"
+            }
+        }
     }
-  }
-}""",
-        )
-        _ = approve_case_attribute(
-            mock_conn,
-            case_attr_audit_id=3,
-        )
-        calls = [
-            mocker.call(
-                "SELECT json_diff_data FROM caseattributes_audit "
-                "WHERE id = %s",
-                (3,),
-            ),
-            mocker.call(
-                "UPDATE CaseAttribute SET Description = %s WHERE Id = %s",
-                (
-                    "Test",
-                    "12",
-                ),
-            ),
-            mocker.call(
-                "UPDATE CaseAttribute SET Name = %s WHERE Id = %s",
-                (
-                    "Height (A)",
-                    "12",
-                ),
-            ),
-            mocker.call(
-                "UPDATE caseattributes_audit SET "
-                "status = 'approved' WHERE id = %s",
-                (3,),
-            ),
-        ]
-        cursor.execute.assert_has_calls(calls, any_order=False)
+    assert result == expected