"""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 queue_edit
from gn3.db.case_attributes import (
CaseAttributeEdit,
EditStatus,
apply_change,
get_changes,
view_change
)
@pytest.mark.unit_test
def test_queue_edit(mocker: MockFixture) -> None:
"""Test queueing an edit."""
mock_conn = mocker.MagicMock()
with mock_conn.cursor() as cursor:
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(
"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_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_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_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_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"}
]
)
@pytest.mark.unit_test
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_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"
}
}
}
assert result == expected