new database scheme
This commit is contained in:
@@ -6,6 +6,7 @@ import sqlite3
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
# Add the src directory to the path
|
||||
import sys
|
||||
@@ -47,43 +48,53 @@ class TestCaseManager(unittest.TestCase):
|
||||
|
||||
def test_create_case(self):
|
||||
"""Test creating a new case."""
|
||||
self.case_manager.create_case(
|
||||
case_id='CASE-001',
|
||||
case_title='Test Case',
|
||||
investigator='Detective Smith',
|
||||
classification='Homicide',
|
||||
summary='Test summary'
|
||||
case_id = self.case_manager.create_case(
|
||||
description='Test homicide investigation',
|
||||
investigator_name='Detective Smith',
|
||||
investigator_role='Lead Detective'
|
||||
)
|
||||
|
||||
self.assertIsNotNone(case_id)
|
||||
self.assertIsInstance(case_id, int)
|
||||
|
||||
# Verify case was created
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
self.assertIsNotNone(case)
|
||||
self.assertEqual(case['case_id'], 'CASE-001')
|
||||
self.assertEqual(case['case_title'], 'Test Case')
|
||||
self.assertEqual(case['investigator'], 'Detective Smith')
|
||||
self.assertEqual(case['classification'], 'Homicide')
|
||||
self.assertEqual(case['summary'], 'Test summary')
|
||||
self.assertEqual(case['status'], 'active')
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['case_id'], case_id)
|
||||
self.assertEqual(case['description'], 'Test homicide investigation')
|
||||
self.assertEqual(case['investigators'], 'Detective Smith')
|
||||
self.assertEqual(case['status'], 'Open')
|
||||
|
||||
def test_create_case_minimal_fields(self):
|
||||
"""Test creating a case with only required fields."""
|
||||
self.case_manager.create_case(
|
||||
case_id='CASE-002',
|
||||
case_title='Minimal Case',
|
||||
investigator='Detective Jones'
|
||||
def test_create_case_minimal(self):
|
||||
"""Test creating a case with minimal fields."""
|
||||
case_id = self.case_manager.create_case(
|
||||
description='Minimal case',
|
||||
investigator_name='Detective Jones'
|
||||
)
|
||||
|
||||
case = self.case_manager.get_case('CASE-002')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
self.assertIsNotNone(case)
|
||||
self.assertEqual(case['case_id'], 'CASE-002')
|
||||
self.assertEqual(case['case_title'], 'Minimal Case')
|
||||
self.assertEqual(case['investigator'], 'Detective Jones')
|
||||
self.assertIsNone(case['classification'])
|
||||
self.assertIsNone(case['summary'])
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['description'], 'Minimal case')
|
||||
self.assertEqual(case['investigators'], 'Detective Jones')
|
||||
self.assertEqual(case['status'], 'Open')
|
||||
|
||||
def test_create_case_with_status(self):
|
||||
"""Test creating a case with specific status."""
|
||||
case_id = self.case_manager.create_case(
|
||||
description='Archived case',
|
||||
investigator_name='Detective Brown',
|
||||
status='Archived'
|
||||
)
|
||||
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Archived')
|
||||
|
||||
def test_get_case_nonexistent(self):
|
||||
"""Test getting a non-existent case returns None."""
|
||||
case = self.case_manager.get_case('NONEXISTENT')
|
||||
case = self.case_manager.get_case(99999)
|
||||
self.assertIsNone(case)
|
||||
|
||||
def test_list_cases_empty(self):
|
||||
@@ -94,184 +105,178 @@ class TestCaseManager(unittest.TestCase):
|
||||
def test_list_cases_multiple(self):
|
||||
"""Test listing multiple cases."""
|
||||
# Create multiple cases
|
||||
self.case_manager.create_case('CASE-001', 'Case One', 'Detective A')
|
||||
self.case_manager.create_case('CASE-002', 'Case Two', 'Detective B')
|
||||
self.case_manager.create_case('CASE-003', 'Case Three', 'Detective C')
|
||||
case_id1 = self.case_manager.create_case('Case One', 'Detective A')
|
||||
case_id2 = self.case_manager.create_case('Case Two', 'Detective B')
|
||||
case_id3 = self.case_manager.create_case('Case Three', 'Detective C')
|
||||
|
||||
cases = self.case_manager.list_cases()
|
||||
|
||||
self.assertEqual(len(cases), 3)
|
||||
case_ids = [c['case_id'] for c in cases]
|
||||
self.assertIn('CASE-001', case_ids)
|
||||
self.assertIn('CASE-002', case_ids)
|
||||
self.assertIn('CASE-003', case_ids)
|
||||
self.assertIn(case_id1, case_ids)
|
||||
self.assertIn(case_id2, case_ids)
|
||||
self.assertIn(case_id3, case_ids)
|
||||
|
||||
def test_list_cases_filter_by_status(self):
|
||||
"""Test listing cases filtered by status."""
|
||||
self.case_manager.create_case('CASE-001', 'Active Case', 'Detective A')
|
||||
self.case_manager.create_case('CASE-002', 'Another Active', 'Detective B')
|
||||
case_id1 = self.case_manager.create_case('Active Case', 'Detective A')
|
||||
case_id2 = self.case_manager.create_case('Another Active', 'Detective B')
|
||||
|
||||
# Close one case
|
||||
self.case_manager.close_case('CASE-001')
|
||||
self.case_manager.close_case(case_id1)
|
||||
|
||||
# List only active cases
|
||||
active_cases = self.case_manager.list_cases(status='active')
|
||||
self.assertEqual(len(active_cases), 1)
|
||||
self.assertEqual(active_cases[0]['case_id'], 'CASE-002')
|
||||
# List only Open cases
|
||||
open_cases = self.case_manager.list_cases(status='Open')
|
||||
self.assertEqual(len(open_cases), 1)
|
||||
self.assertEqual(open_cases[0]['case_id'], case_id2)
|
||||
|
||||
# List only closed cases
|
||||
closed_cases = self.case_manager.list_cases(status='closed')
|
||||
# List only Closed cases
|
||||
closed_cases = self.case_manager.list_cases(status='Closed')
|
||||
self.assertEqual(len(closed_cases), 1)
|
||||
self.assertEqual(closed_cases[0]['case_id'], 'CASE-001')
|
||||
self.assertEqual(closed_cases[0]['case_id'], case_id1)
|
||||
|
||||
def test_list_cases_search_term(self):
|
||||
"""Test listing cases with search term."""
|
||||
self.case_manager.create_case('CASE-001', 'Murder Investigation', 'Detective Smith')
|
||||
self.case_manager.create_case('CASE-002', 'Theft Case', 'Detective Jones')
|
||||
self.case_manager.create_case('CASE-003', 'Murder Trial', 'Detective Brown')
|
||||
self.case_manager.create_case('Murder investigation', 'Detective Smith')
|
||||
self.case_manager.create_case('Theft case', 'Detective Jones')
|
||||
self.case_manager.create_case('Murder trial', 'Detective Brown')
|
||||
|
||||
# Search by title
|
||||
# Search by description
|
||||
results = self.case_manager.list_cases(search_term='Murder')
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
# Search by investigator
|
||||
results = self.case_manager.list_cases(search_term='Smith')
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]['case_id'], 'CASE-001')
|
||||
|
||||
# Search by case ID
|
||||
results = self.case_manager.list_cases(search_term='002')
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]['case_id'], 'CASE-002')
|
||||
|
||||
def test_list_cases_combined_filters(self):
|
||||
"""Test listing cases with combined status and search filters."""
|
||||
self.case_manager.create_case('CASE-001', 'Active Murder', 'Detective A')
|
||||
self.case_manager.create_case('CASE-002', 'Active Theft', 'Detective B')
|
||||
self.case_manager.create_case('CASE-003', 'Closed Murder', 'Detective C')
|
||||
case_id1 = self.case_manager.create_case('Active Murder', 'Detective A')
|
||||
case_id2 = self.case_manager.create_case('Active Theft', 'Detective B')
|
||||
case_id3 = self.case_manager.create_case('Closed Murder', 'Detective C')
|
||||
|
||||
self.case_manager.close_case('CASE-003')
|
||||
self.case_manager.close_case(case_id3)
|
||||
|
||||
# Search for "Murder" in active cases only
|
||||
results = self.case_manager.list_cases(status='active', search_term='Murder')
|
||||
# Search for "Murder" in Open cases only
|
||||
results = self.case_manager.list_cases(status='Open', search_term='Murder')
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]['case_id'], 'CASE-001')
|
||||
self.assertEqual(results[0]['case_id'], case_id1)
|
||||
|
||||
def test_update_case(self):
|
||||
"""Test updating a case."""
|
||||
self.case_manager.create_case('CASE-001', 'Original Title', 'Detective A')
|
||||
def test_update_case_description(self):
|
||||
"""Test updating a case description."""
|
||||
case_id = self.case_manager.create_case('Original description', 'Detective A')
|
||||
|
||||
result = self.case_manager.update_case(
|
||||
'CASE-001',
|
||||
case_title='Updated Title',
|
||||
classification='Homicide',
|
||||
summary='Updated summary'
|
||||
case_id,
|
||||
description='Updated description'
|
||||
)
|
||||
|
||||
self.assertTrue(result)
|
||||
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['case_title'], 'Updated Title')
|
||||
self.assertEqual(case['classification'], 'Homicide')
|
||||
self.assertEqual(case['summary'], 'Updated summary')
|
||||
self.assertEqual(case['investigator'], 'Detective A') # Unchanged
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['description'], 'Updated description')
|
||||
|
||||
def test_update_case_invalid_fields(self):
|
||||
"""Test updating case with invalid fields ignores them."""
|
||||
self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
|
||||
def test_update_case_status(self):
|
||||
"""Test updating a case status."""
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
result = self.case_manager.update_case(
|
||||
'CASE-001',
|
||||
case_title='Updated Title',
|
||||
invalid_field='Should be ignored'
|
||||
)
|
||||
|
||||
# Should still work, just ignoring invalid field
|
||||
result = self.case_manager.update_case(case_id, status='Archived')
|
||||
self.assertTrue(result)
|
||||
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['case_title'], 'Updated Title')
|
||||
self.assertNotIn('invalid_field', case)
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Archived')
|
||||
|
||||
def test_update_case_no_valid_fields(self):
|
||||
"""Test updating case with no valid fields."""
|
||||
self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
|
||||
def test_update_case_both_fields(self):
|
||||
"""Test updating both description and status."""
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
result = self.case_manager.update_case(
|
||||
'CASE-001',
|
||||
invalid_field='Should be ignored'
|
||||
case_id,
|
||||
description='Updated description',
|
||||
status='Closed'
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Should return None since no valid fields
|
||||
self.assertIsNone(result)
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['description'], 'Updated description')
|
||||
self.assertEqual(case['status'], 'Closed')
|
||||
|
||||
def test_update_case_sets_modified_at(self):
|
||||
"""Test that updating a case sets modified_at timestamp."""
|
||||
self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
|
||||
def test_update_case_no_fields(self):
|
||||
"""Test updating case with no fields returns False."""
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
case_before = self.case_manager.get_case('CASE-001')
|
||||
created_at = case_before['created_at']
|
||||
result = self.case_manager.update_case(case_id)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_update_case_sets_modified_timestamp(self):
|
||||
"""Test that updating a case sets modified timestamp."""
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
# Small delay to ensure different timestamp
|
||||
import time
|
||||
time.sleep(0.01)
|
||||
case_before = self.case_manager.get_case(case_id)
|
||||
assert case_before is not None # Type narrowing for Pylance
|
||||
created = case_before['created']
|
||||
|
||||
self.case_manager.update_case('CASE-001', case_title='Updated')
|
||||
time.sleep(1) # Ensure timestamp difference
|
||||
|
||||
case_after = self.case_manager.get_case('CASE-001')
|
||||
modified_at = case_after['modified_at']
|
||||
self.case_manager.update_case(case_id, description='Updated')
|
||||
|
||||
# modified_at should be different from created_at
|
||||
self.assertNotEqual(created_at, modified_at)
|
||||
case_after = self.case_manager.get_case(case_id)
|
||||
assert case_after is not None # Type narrowing for Pylance
|
||||
modified = case_after['modified']
|
||||
|
||||
# Modified should be different from created
|
||||
self.assertNotEqual(created, modified)
|
||||
|
||||
def test_update_nonexistent_case(self):
|
||||
"""Test updating a non-existent case returns False."""
|
||||
result = self.case_manager.update_case(
|
||||
'NONEXISTENT',
|
||||
case_title='Updated Title'
|
||||
)
|
||||
|
||||
result = self.case_manager.update_case(99999, description='Updated')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_close_case(self):
|
||||
"""Test closing a case."""
|
||||
self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
result = self.case_manager.close_case('CASE-001')
|
||||
result = self.case_manager.close_case(case_id)
|
||||
self.assertTrue(result)
|
||||
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['status'], 'closed')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Closed')
|
||||
self.assertIsNotNone(case['closed'])
|
||||
|
||||
def test_archive_case(self):
|
||||
"""Test archiving a case."""
|
||||
self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
result = self.case_manager.archive_case('CASE-001')
|
||||
result = self.case_manager.archive_case(case_id)
|
||||
self.assertTrue(result)
|
||||
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['status'], 'archived')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Archived')
|
||||
|
||||
def test_delete_case(self):
|
||||
"""Test deleting a case."""
|
||||
self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
|
||||
case_id = self.case_manager.create_case('Test case', 'Detective A')
|
||||
|
||||
# Verify case exists
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
self.assertIsNotNone(case)
|
||||
|
||||
# Delete case
|
||||
result = self.case_manager.delete_case('CASE-001')
|
||||
result = self.case_manager.delete_case(case_id)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify case is gone
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
self.assertIsNone(case)
|
||||
|
||||
def test_delete_nonexistent_case(self):
|
||||
"""Test deleting a non-existent case returns False."""
|
||||
result = self.case_manager.delete_case('NONEXISTENT')
|
||||
result = self.case_manager.delete_case(99999)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_case_manager_with_default_db_path(self):
|
||||
@@ -282,6 +287,19 @@ class TestCaseManager(unittest.TestCase):
|
||||
cm = CaseManager()
|
||||
self.assertEqual(cm.db_path, self.test_db_path)
|
||||
cm.conn.close()
|
||||
|
||||
def test_multiple_investigators_on_case(self):
|
||||
"""Test adding multiple investigators to a case."""
|
||||
case_id1 = self.case_manager.create_case('Case 1', 'Detective Smith')
|
||||
case_id2 = self.case_manager.create_case('Case 2', 'Detective Smith')
|
||||
|
||||
# Verify investigator is reused
|
||||
case1 = self.case_manager.get_case(case_id1)
|
||||
case2 = self.case_manager.get_case(case_id2)
|
||||
assert case1 is not None and case2 is not None # Type narrowing for Pylance
|
||||
|
||||
self.assertEqual(case1['investigators'], 'Detective Smith')
|
||||
self.assertEqual(case2['investigators'], 'Detective Smith')
|
||||
|
||||
|
||||
class TestCaseManagerIntegration(unittest.TestCase):
|
||||
@@ -308,70 +326,69 @@ class TestCaseManagerIntegration(unittest.TestCase):
|
||||
def test_full_case_lifecycle(self):
|
||||
"""Test complete case lifecycle: create, update, close, archive, delete."""
|
||||
# Create case
|
||||
self.case_manager.create_case(
|
||||
'CASE-001',
|
||||
'Investigation Case',
|
||||
'Detective Smith',
|
||||
'Robbery',
|
||||
'Initial summary'
|
||||
case_id = self.case_manager.create_case(
|
||||
'Investigation case',
|
||||
'Detective Smith'
|
||||
)
|
||||
|
||||
# Verify creation
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['status'], 'active')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Open')
|
||||
|
||||
# Update case
|
||||
self.case_manager.update_case(
|
||||
'CASE-001',
|
||||
summary='Updated with new findings'
|
||||
)
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['summary'], 'Updated with new findings')
|
||||
self.case_manager.update_case(case_id, description='Updated findings')
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['description'], 'Updated findings')
|
||||
|
||||
# Close case
|
||||
self.case_manager.close_case('CASE-001')
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['status'], 'closed')
|
||||
self.case_manager.close_case(case_id)
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Closed')
|
||||
|
||||
# Archive case
|
||||
self.case_manager.archive_case('CASE-001')
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.assertEqual(case['status'], 'archived')
|
||||
self.case_manager.archive_case(case_id)
|
||||
case = self.case_manager.get_case(case_id)
|
||||
assert case is not None # Type narrowing for Pylance
|
||||
self.assertEqual(case['status'], 'Archived')
|
||||
|
||||
# Delete case
|
||||
self.case_manager.delete_case('CASE-001')
|
||||
case = self.case_manager.get_case('CASE-001')
|
||||
self.case_manager.delete_case(case_id)
|
||||
case = self.case_manager.get_case(case_id)
|
||||
self.assertIsNone(case)
|
||||
|
||||
def test_multiple_cases_workflow(self):
|
||||
"""Test working with multiple cases simultaneously."""
|
||||
# Create multiple cases
|
||||
case_ids = []
|
||||
for i in range(1, 6):
|
||||
self.case_manager.create_case(
|
||||
f'CASE-{i:03d}',
|
||||
case_id = self.case_manager.create_case(
|
||||
f'Case {i}',
|
||||
f'Detective {chr(64+i)}' # Detective A, B, C, etc.
|
||||
)
|
||||
case_ids.append(case_id)
|
||||
|
||||
# Verify all created
|
||||
all_cases = self.case_manager.list_cases()
|
||||
self.assertEqual(len(all_cases), 5)
|
||||
|
||||
# Close some cases
|
||||
self.case_manager.close_case('CASE-001')
|
||||
self.case_manager.close_case('CASE-003')
|
||||
self.case_manager.close_case(case_ids[0])
|
||||
self.case_manager.close_case(case_ids[2])
|
||||
|
||||
# Archive one
|
||||
self.case_manager.archive_case('CASE-005')
|
||||
self.case_manager.archive_case(case_ids[4])
|
||||
|
||||
# Check status distribution
|
||||
active_cases = self.case_manager.list_cases(status='active')
|
||||
closed_cases = self.case_manager.list_cases(status='closed')
|
||||
archived_cases = self.case_manager.list_cases(status='archived')
|
||||
open_cases = self.case_manager.list_cases(status='Open')
|
||||
closed_cases = self.case_manager.list_cases(status='Closed')
|
||||
archived_cases = self.case_manager.list_cases(status='Archived')
|
||||
|
||||
self.assertEqual(len(active_cases), 2) # 002, 004
|
||||
self.assertEqual(len(closed_cases), 2) # 001, 003
|
||||
self.assertEqual(len(archived_cases), 1) # 005
|
||||
self.assertEqual(len(open_cases), 2) # case_ids[1], case_ids[3]
|
||||
self.assertEqual(len(closed_cases), 2) # case_ids[0], case_ids[2]
|
||||
self.assertEqual(len(archived_cases), 1) # case_ids[4]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -78,15 +78,15 @@ class TestValidateDatabaseSchema(unittest.TestCase):
|
||||
|
||||
# Create only some of the required tables
|
||||
cursor.execute("""
|
||||
CREATE TABLE cases (
|
||||
case_id TEXT PRIMARY KEY,
|
||||
case_title TEXT NOT NULL
|
||||
CREATE TABLE "case" (
|
||||
case_id INTEGER PRIMARY KEY,
|
||||
description TEXT
|
||||
)
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE notes (
|
||||
note_id TEXT PRIMARY KEY,
|
||||
case_id TEXT NOT NULL
|
||||
CREATE TABLE note (
|
||||
note_id INTEGER PRIMARY KEY,
|
||||
case_id INTEGER NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
@@ -143,11 +143,13 @@ class TestCreateFreshDatabase(unittest.TestCase):
|
||||
conn.close()
|
||||
|
||||
required_tables = {
|
||||
'cases', 'notes', 'evidence', 'chain_of_custody',
|
||||
'attachments', 'question_entries', 'users', 'tasks'
|
||||
'case', 'note', 'note_revision', 'investigator',
|
||||
'evidence', 'chain_of_custody', 'question_definition',
|
||||
'note_question_tag', 'investigator_case'
|
||||
}
|
||||
|
||||
self.assertEqual(tables, required_tables)
|
||||
# sqlite_sequence is automatically created by SQLite for AUTOINCREMENT
|
||||
self.assertTrue(required_tables.issubset(tables))
|
||||
|
||||
def test_create_fresh_database_returns_path(self):
|
||||
"""Test that create_fresh_database returns the database path."""
|
||||
@@ -170,10 +172,12 @@ class TestCreateFreshDatabase(unittest.TestCase):
|
||||
conn.close()
|
||||
|
||||
required_tables = {
|
||||
'cases', 'notes', 'evidence', 'chain_of_custody',
|
||||
'attachments', 'question_entries', 'users', 'tasks'
|
||||
'case', 'note', 'note_revision', 'investigator',
|
||||
'evidence', 'chain_of_custody', 'question_definition',
|
||||
'note_question_tag', 'investigator_case'
|
||||
}
|
||||
self.assertEqual(tables, required_tables)
|
||||
# sqlite_sequence is automatically created by SQLite for AUTOINCREMENT
|
||||
self.assertTrue(required_tables.issubset(tables))
|
||||
|
||||
|
||||
class TestInitializeDatabase(unittest.TestCase):
|
||||
@@ -219,10 +223,11 @@ class TestInitializeDatabase(unittest.TestCase):
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO cases (case_id, case_title, investigator)
|
||||
VALUES ('TEST-001', 'Test Case', 'Test Investigator')
|
||||
INSERT INTO "case" (description, status)
|
||||
VALUES ('Test Case', 'Open')
|
||||
""")
|
||||
conn.commit()
|
||||
case_id = cursor.lastrowid
|
||||
conn.close()
|
||||
|
||||
# Initialize again
|
||||
@@ -231,12 +236,12 @@ class TestInitializeDatabase(unittest.TestCase):
|
||||
# Verify data still exists
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT case_id FROM cases WHERE case_id = 'TEST-001'")
|
||||
cursor.execute("SELECT case_id FROM \"case\" WHERE case_id = ?", (case_id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result[0], 'TEST-001')
|
||||
self.assertEqual(result[0], case_id)
|
||||
|
||||
@patch('forensictrails.db.database.config')
|
||||
def test_initialize_database_recreates_invalid_database(self, mock_config):
|
||||
@@ -248,8 +253,8 @@ class TestInitializeDatabase(unittest.TestCase):
|
||||
# Create an incomplete database
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("CREATE TABLE cases (case_id TEXT PRIMARY KEY)")
|
||||
cursor.execute("INSERT INTO cases VALUES ('TEST-001')")
|
||||
cursor.execute("CREATE TABLE \"case\" (case_id INTEGER PRIMARY KEY)")
|
||||
cursor.execute("INSERT INTO \"case\" (case_id) VALUES (999)")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -261,7 +266,7 @@ class TestInitializeDatabase(unittest.TestCase):
|
||||
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT case_id FROM cases WHERE case_id = 'TEST-001'")
|
||||
cursor.execute("SELECT case_id FROM \"case\" WHERE case_id = 999")
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
@@ -293,9 +298,9 @@ class TestShowDbSchema(unittest.TestCase):
|
||||
# Verify that logging.debug was called
|
||||
self.assertTrue(mock_logging.debug.called)
|
||||
|
||||
# Check that it was called for each table (8 tables + 1 header message)
|
||||
# Should be at least 9 calls (header + 8 tables)
|
||||
self.assertGreaterEqual(mock_logging.debug.call_count, 9)
|
||||
# Check that it was called for each table (9 tables + 1 header message)
|
||||
# Should be at least 10 calls (header + 9 tables)
|
||||
self.assertGreaterEqual(mock_logging.debug.call_count, 10)
|
||||
|
||||
def test_show_db_schema_doesnt_raise_exception(self):
|
||||
"""Test that show_db_schema handles execution without raising exceptions."""
|
||||
@@ -338,12 +343,13 @@ class TestDatabaseIntegration(unittest.TestCase):
|
||||
conn = get_db_connection(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO cases (case_id, case_title, investigator)
|
||||
VALUES ('CASE-001', 'Murder Investigation', 'Detective Smith')
|
||||
INSERT INTO "case" (description, status)
|
||||
VALUES ('Murder Investigation', 'Open')
|
||||
""")
|
||||
case_id = cursor.lastrowid
|
||||
cursor.execute("""
|
||||
INSERT INTO users (user_id, username, full_name)
|
||||
VALUES ('USER-001', 'dsmith', 'Detective Smith')
|
||||
INSERT INTO investigator (name, role)
|
||||
VALUES ('Detective Smith', 'Lead Investigator')
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -351,15 +357,15 @@ class TestDatabaseIntegration(unittest.TestCase):
|
||||
# Step 3: Verify data exists
|
||||
conn = get_db_connection(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT case_title FROM cases WHERE case_id = 'CASE-001'")
|
||||
cursor.execute("SELECT description FROM \"case\" WHERE case_id = ?", (case_id,))
|
||||
result = cursor.fetchone()
|
||||
self.assertEqual(result['case_title'], 'Murder Investigation')
|
||||
self.assertEqual(result['description'], 'Murder Investigation')
|
||||
conn.close()
|
||||
|
||||
# Step 4: Corrupt database (remove a required table)
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DROP TABLE users")
|
||||
cursor.execute("DROP TABLE investigator")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -373,7 +379,7 @@ class TestDatabaseIntegration(unittest.TestCase):
|
||||
self.assertTrue(validate_database_schema(self.test_db_path))
|
||||
conn = get_db_connection(self.test_db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT case_id FROM cases WHERE case_id = 'CASE-001'")
|
||||
cursor.execute("SELECT case_id FROM \"case\" WHERE case_id = ?", (case_id,))
|
||||
result = cursor.fetchone()
|
||||
self.assertIsNone(result)
|
||||
conn.close()
|
||||
|
||||
524
tests/test_note_manager.py
Normal file
524
tests/test_note_manager.py
Normal file
@@ -0,0 +1,524 @@
|
||||
"""Unit tests for the note_manager module."""
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
# Add the src directory to the path
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
from forensictrails.core.note_manager import NoteManager
|
||||
from forensictrails.core.case_manager import CaseManager
|
||||
from forensictrails.db.database import create_fresh_database
|
||||
|
||||
|
||||
class TestNoteManager(unittest.TestCase):
|
||||
"""Test cases for NoteManager class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.test_db_path = os.path.join(self.temp_dir, 'test.db')
|
||||
self.schema_path = Path(__file__).parent.parent / 'src' / 'forensictrails' / 'db' / 'schema.sql'
|
||||
|
||||
# Create a fresh database for testing
|
||||
create_fresh_database(self.test_db_path, self.schema_path)
|
||||
|
||||
# Create manager instances
|
||||
self.case_manager = CaseManager(self.test_db_path)
|
||||
self.note_manager = NoteManager(self.test_db_path)
|
||||
|
||||
# Create a test case for notes
|
||||
self.case_id = self.case_manager.create_case(
|
||||
description='Test Case for Notes',
|
||||
investigator_name='Detective Smith'
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures."""
|
||||
if hasattr(self, 'note_manager') and hasattr(self.note_manager, 'conn'):
|
||||
self.note_manager.conn.close()
|
||||
if hasattr(self, 'case_manager') and hasattr(self.case_manager, 'conn'):
|
||||
self.case_manager.conn.close()
|
||||
|
||||
if os.path.exists(self.test_db_path):
|
||||
os.remove(self.test_db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def test_note_manager_initialization(self):
|
||||
"""Test NoteManager initializes correctly."""
|
||||
self.assertEqual(self.note_manager.db_path, self.test_db_path)
|
||||
self.assertIsNotNone(self.note_manager.conn)
|
||||
self.assertIsNotNone(self.note_manager.cursor)
|
||||
|
||||
def test_create_note_basic(self):
|
||||
"""Test creating a basic note."""
|
||||
note_id = self.note_manager.create_note(
|
||||
case_id=self.case_id,
|
||||
content='This is a test note.',
|
||||
investigator='Detective Smith'
|
||||
)
|
||||
|
||||
self.assertIsNotNone(note_id)
|
||||
self.assertIsInstance(note_id, int)
|
||||
|
||||
# Verify note was created
|
||||
note = self.note_manager.get_note(note_id)
|
||||
self.assertIsNotNone(note)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['note_id'], note_id)
|
||||
self.assertEqual(note['case_id'], self.case_id)
|
||||
self.assertEqual(note['content'], 'This is a test note.')
|
||||
self.assertEqual(note['investigator'], 'Detective Smith')
|
||||
self.assertEqual(note['question_tags'], [])
|
||||
|
||||
def test_create_note_with_tags(self):
|
||||
"""Test creating a note with question tags."""
|
||||
note_id = self.note_manager.create_note(
|
||||
case_id=self.case_id,
|
||||
content='Who was at the scene?',
|
||||
investigator='Detective Jones',
|
||||
question_tags=['WHO', 'WHEN']
|
||||
)
|
||||
|
||||
note = self.note_manager.get_note(note_id)
|
||||
self.assertIsNotNone(note)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertIn('WHO', note['question_tags'])
|
||||
self.assertIn('WHEN', note['question_tags'])
|
||||
|
||||
def test_get_note_nonexistent(self):
|
||||
"""Test getting a non-existent note returns None."""
|
||||
note = self.note_manager.get_note(99999)
|
||||
self.assertIsNone(note)
|
||||
|
||||
def test_get_notes_empty(self):
|
||||
"""Test getting notes for a case with no notes."""
|
||||
notes = self.note_manager.get_notes(self.case_id)
|
||||
self.assertEqual(notes, [])
|
||||
|
||||
def test_get_notes_multiple(self):
|
||||
"""Test getting multiple notes for a case."""
|
||||
# Create multiple notes
|
||||
note_id1 = self.note_manager.create_note(
|
||||
self.case_id, 'First note', 'Detective A'
|
||||
)
|
||||
time.sleep(0.01) # Ensure different timestamps
|
||||
note_id2 = self.note_manager.create_note(
|
||||
self.case_id, 'Second note', 'Detective B'
|
||||
)
|
||||
time.sleep(0.01)
|
||||
note_id3 = self.note_manager.create_note(
|
||||
self.case_id, 'Third note', 'Detective C'
|
||||
)
|
||||
|
||||
notes = self.note_manager.get_notes(self.case_id)
|
||||
|
||||
self.assertEqual(len(notes), 3)
|
||||
note_ids = [n['note_id'] for n in notes]
|
||||
self.assertIn(note_id1, note_ids)
|
||||
self.assertIn(note_id2, note_ids)
|
||||
self.assertIn(note_id3, note_ids)
|
||||
|
||||
# Verify they're ordered by creation time (DESC)
|
||||
# Check that timestamps are in descending order
|
||||
timestamps = [n['timestamp'] for n in notes]
|
||||
self.assertEqual(timestamps, sorted(timestamps, reverse=True))
|
||||
|
||||
def test_update_note_creates_revision(self):
|
||||
"""Test that updating a note creates a new revision."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Original content', 'Detective Smith'
|
||||
)
|
||||
|
||||
# Update the note
|
||||
result = self.note_manager.update_note(note_id, 'Updated content')
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify the note now shows updated content
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['content'], 'Updated content')
|
||||
|
||||
# Verify history exists
|
||||
history = self.note_manager.get_note_history(note_id)
|
||||
self.assertEqual(len(history), 2)
|
||||
self.assertEqual(history[0]['content'], 'Updated content') # Newest first
|
||||
self.assertEqual(history[0]['revision_number'], 2)
|
||||
self.assertEqual(history[1]['content'], 'Original content')
|
||||
self.assertEqual(history[1]['revision_number'], 1)
|
||||
|
||||
def test_update_note_multiple_times(self):
|
||||
"""Test updating a note multiple times."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Version 1', 'Detective Smith'
|
||||
)
|
||||
|
||||
self.note_manager.update_note(note_id, 'Version 2')
|
||||
self.note_manager.update_note(note_id, 'Version 3')
|
||||
self.note_manager.update_note(note_id, 'Version 4')
|
||||
|
||||
# Verify latest version
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['content'], 'Version 4')
|
||||
|
||||
# Verify all revisions exist
|
||||
history = self.note_manager.get_note_history(note_id)
|
||||
self.assertEqual(len(history), 4)
|
||||
for i, rev in enumerate(history):
|
||||
self.assertEqual(rev['revision_number'], 4 - i)
|
||||
|
||||
def test_get_note_history_empty(self):
|
||||
"""Test getting history for non-existent note."""
|
||||
history = self.note_manager.get_note_history(99999)
|
||||
self.assertEqual(history, [])
|
||||
|
||||
def test_update_note_tags(self):
|
||||
"""Test updating question tags for a note."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Test note', 'Detective Smith',
|
||||
question_tags=['WHO']
|
||||
)
|
||||
|
||||
# Verify initial tags
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['question_tags'], ['WHO'])
|
||||
|
||||
# Update tags
|
||||
result = self.note_manager.update_note_tags(note_id, ['WHAT', 'WHEN', 'WHERE'])
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify updated tags
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertIn('WHAT', note['question_tags'])
|
||||
self.assertIn('WHEN', note['question_tags'])
|
||||
self.assertIn('WHERE', note['question_tags'])
|
||||
self.assertNotIn('WHO', note['question_tags'])
|
||||
|
||||
def test_update_note_tags_remove_all(self):
|
||||
"""Test removing all tags from a note."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Test note', 'Detective Smith',
|
||||
question_tags=['WHO', 'WHAT']
|
||||
)
|
||||
|
||||
# Remove all tags
|
||||
result = self.note_manager.update_note_tags(note_id, [])
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify no tags
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['question_tags'], [])
|
||||
|
||||
def test_update_note_tags_nonexistent_note(self):
|
||||
"""Test updating tags for non-existent note."""
|
||||
result = self.note_manager.update_note_tags(99999, ['WHO'])
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_delete_note(self):
|
||||
"""Test deleting a note."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Test note', 'Detective Smith'
|
||||
)
|
||||
|
||||
# Verify note exists
|
||||
note = self.note_manager.get_note(note_id)
|
||||
self.assertIsNotNone(note)
|
||||
|
||||
# Delete note
|
||||
result = self.note_manager.delete_note(note_id)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify note is gone
|
||||
note = self.note_manager.get_note(note_id)
|
||||
self.assertIsNone(note)
|
||||
|
||||
def test_delete_note_deletes_revisions(self):
|
||||
"""Test that deleting a note also deletes all revisions."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Original', 'Detective Smith'
|
||||
)
|
||||
self.note_manager.update_note(note_id, 'Updated')
|
||||
self.note_manager.update_note(note_id, 'Updated again')
|
||||
|
||||
# Delete note
|
||||
self.note_manager.delete_note(note_id)
|
||||
|
||||
# Verify revisions are gone
|
||||
history = self.note_manager.get_note_history(note_id)
|
||||
self.assertEqual(history, [])
|
||||
|
||||
def test_delete_nonexistent_note(self):
|
||||
"""Test deleting a non-existent note returns False."""
|
||||
result = self.note_manager.delete_note(99999)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_search_notes_by_case(self):
|
||||
"""Test searching notes by case ID."""
|
||||
# Create second case
|
||||
case_id2 = self.case_manager.create_case(
|
||||
description='Second Case',
|
||||
investigator_name='Detective Jones'
|
||||
)
|
||||
|
||||
# Create notes in both cases
|
||||
self.note_manager.create_note(self.case_id, 'Case 1 Note 1', 'Detective A')
|
||||
self.note_manager.create_note(self.case_id, 'Case 1 Note 2', 'Detective B')
|
||||
self.note_manager.create_note(case_id2, 'Case 2 Note 1', 'Detective C')
|
||||
|
||||
# Search by case
|
||||
case1_notes = self.note_manager.search_notes(case_id=self.case_id)
|
||||
case2_notes = self.note_manager.search_notes(case_id=case_id2)
|
||||
|
||||
self.assertEqual(len(case1_notes), 2)
|
||||
self.assertEqual(len(case2_notes), 1)
|
||||
|
||||
def test_search_notes_by_content(self):
|
||||
"""Test searching notes by content text."""
|
||||
self.note_manager.create_note(self.case_id, 'This is about murder', 'Detective A')
|
||||
self.note_manager.create_note(self.case_id, 'This is about theft', 'Detective B')
|
||||
self.note_manager.create_note(self.case_id, 'Another murder note', 'Detective C')
|
||||
|
||||
# Search for "murder"
|
||||
results = self.note_manager.search_notes(case_id=self.case_id, search_term='murder')
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
# Search for "theft"
|
||||
results = self.note_manager.search_notes(case_id=self.case_id, search_term='theft')
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
def test_search_notes_by_question_tags(self):
|
||||
"""Test searching notes by question tags."""
|
||||
self.note_manager.create_note(
|
||||
self.case_id, 'Note 1', 'Detective A', question_tags=['WHO', 'WHAT']
|
||||
)
|
||||
self.note_manager.create_note(
|
||||
self.case_id, 'Note 2', 'Detective B', question_tags=['WHEN', 'WHERE']
|
||||
)
|
||||
self.note_manager.create_note(
|
||||
self.case_id, 'Note 3', 'Detective C', question_tags=['WHO', 'WHEN']
|
||||
)
|
||||
|
||||
# Search for WHO tags
|
||||
results = self.note_manager.search_notes(case_id=self.case_id, question_tags=['WHO'])
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
# Search for WHERE tags
|
||||
results = self.note_manager.search_notes(case_id=self.case_id, question_tags=['WHERE'])
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
def test_search_notes_combined_filters(self):
|
||||
"""Test searching notes with multiple filters."""
|
||||
self.note_manager.create_note(
|
||||
self.case_id, 'Murder investigation note', 'Detective A', question_tags=['WHO']
|
||||
)
|
||||
self.note_manager.create_note(
|
||||
self.case_id, 'Theft investigation note', 'Detective B', question_tags=['WHO']
|
||||
)
|
||||
self.note_manager.create_note(
|
||||
self.case_id, 'Murder witness statement', 'Detective C', question_tags=['WHAT']
|
||||
)
|
||||
|
||||
# Search for "murder" with WHO tag
|
||||
results = self.note_manager.search_notes(
|
||||
case_id=self.case_id,
|
||||
search_term='murder',
|
||||
question_tags=['WHO']
|
||||
)
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertIn('Murder investigation', results[0]['content'])
|
||||
|
||||
def test_search_notes_all_cases(self):
|
||||
"""Test searching notes across all cases."""
|
||||
case_id2 = self.case_manager.create_case(
|
||||
description='Second Case',
|
||||
investigator_name='Detective Jones'
|
||||
)
|
||||
|
||||
self.note_manager.create_note(self.case_id, 'Case 1 evidence', 'Detective A')
|
||||
self.note_manager.create_note(case_id2, 'Case 2 evidence', 'Detective B')
|
||||
|
||||
# Search without case_id filter
|
||||
results = self.note_manager.search_notes(search_term='evidence')
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
def test_investigator_reuse(self):
|
||||
"""Test that investigators are reused across notes."""
|
||||
note_id1 = self.note_manager.create_note(
|
||||
self.case_id, 'Note 1', 'Detective Smith'
|
||||
)
|
||||
note_id2 = self.note_manager.create_note(
|
||||
self.case_id, 'Note 2', 'Detective Smith'
|
||||
)
|
||||
|
||||
# Both notes should reference the same investigator
|
||||
note1 = self.note_manager.get_note(note_id1)
|
||||
note2 = self.note_manager.get_note(note_id2)
|
||||
assert note1 is not None and note2 is not None # Type narrowing for Pylance
|
||||
|
||||
self.assertEqual(note1['investigator'], note2['investigator'])
|
||||
|
||||
def test_question_definition_reuse(self):
|
||||
"""Test that question definitions are reused within a case."""
|
||||
note_id1 = self.note_manager.create_note(
|
||||
self.case_id, 'Note 1', 'Detective A', question_tags=['WHO']
|
||||
)
|
||||
note_id2 = self.note_manager.create_note(
|
||||
self.case_id, 'Note 2', 'Detective B', question_tags=['WHO']
|
||||
)
|
||||
|
||||
# Both notes should use the same question definition
|
||||
# Verify by checking the database directly
|
||||
cursor = self.note_manager.cursor
|
||||
cursor.execute("""
|
||||
SELECT COUNT(DISTINCT question_id) as count
|
||||
FROM question_definition
|
||||
WHERE case_id = ? AND question_text = 'WHO'
|
||||
""", (self.case_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
self.assertEqual(result['count'], 1)
|
||||
|
||||
def test_note_manager_with_default_db_path(self):
|
||||
"""Test NoteManager uses config default when no path provided."""
|
||||
with patch('forensictrails.core.note_manager.config') as mock_config:
|
||||
mock_config.database_path = self.test_db_path
|
||||
|
||||
nm = NoteManager()
|
||||
self.assertEqual(nm.db_path, self.test_db_path)
|
||||
nm.conn.close()
|
||||
|
||||
|
||||
class TestNoteManagerIntegration(unittest.TestCase):
|
||||
"""Integration tests for NoteManager."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.test_db_path = os.path.join(self.temp_dir, 'integration.db')
|
||||
self.schema_path = Path(__file__).parent.parent / 'src' / 'forensictrails' / 'db' / 'schema.sql'
|
||||
|
||||
create_fresh_database(self.test_db_path, self.schema_path)
|
||||
|
||||
self.case_manager = CaseManager(self.test_db_path)
|
||||
self.note_manager = NoteManager(self.test_db_path)
|
||||
|
||||
self.case_id = self.case_manager.create_case(
|
||||
description='Integration Test Case',
|
||||
investigator_name='Detective Smith'
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures."""
|
||||
if hasattr(self, 'note_manager') and hasattr(self.note_manager, 'conn'):
|
||||
self.note_manager.conn.close()
|
||||
if hasattr(self, 'case_manager') and hasattr(self.case_manager, 'conn'):
|
||||
self.case_manager.conn.close()
|
||||
|
||||
if os.path.exists(self.test_db_path):
|
||||
os.remove(self.test_db_path)
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def test_full_note_lifecycle(self):
|
||||
"""Test complete note lifecycle: create, update, tag, delete."""
|
||||
# Create note
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id,
|
||||
'Initial observation',
|
||||
'Detective Smith',
|
||||
question_tags=['WHAT']
|
||||
)
|
||||
|
||||
# Verify creation
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['content'], 'Initial observation')
|
||||
self.assertEqual(note['question_tags'], ['WHAT'])
|
||||
|
||||
# Update content
|
||||
self.note_manager.update_note(note_id, 'Updated observation with more details')
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(note['content'], 'Updated observation with more details')
|
||||
|
||||
# Update tags
|
||||
self.note_manager.update_note_tags(note_id, ['WHAT', 'WHERE', 'WHEN'])
|
||||
note = self.note_manager.get_note(note_id)
|
||||
assert note is not None # Type narrowing for Pylance
|
||||
self.assertEqual(len(note['question_tags']), 3)
|
||||
|
||||
# Verify history
|
||||
history = self.note_manager.get_note_history(note_id)
|
||||
self.assertEqual(len(history), 2)
|
||||
|
||||
# Delete note
|
||||
self.note_manager.delete_note(note_id)
|
||||
note = self.note_manager.get_note(note_id)
|
||||
self.assertIsNone(note)
|
||||
|
||||
def test_case_with_multiple_notes_and_tags(self):
|
||||
"""Test a case with multiple notes and complex tagging."""
|
||||
# Create various notes with different tags
|
||||
notes_data = [
|
||||
('Suspect identified as John Doe', ['WHO']),
|
||||
('Crime occurred at 123 Main St', ['WHERE']),
|
||||
('Incident happened on Jan 15, 2025', ['WHEN']),
|
||||
('Weapon used was a knife', ['WITH_WHAT', 'WHAT']),
|
||||
('Motive appears to be robbery', ['WHY', 'WHAT']),
|
||||
]
|
||||
|
||||
note_ids = []
|
||||
for content, tags in notes_data:
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, content, 'Detective Smith', question_tags=tags
|
||||
)
|
||||
note_ids.append(note_id)
|
||||
|
||||
# Verify all notes created
|
||||
all_notes = self.note_manager.get_notes(self.case_id)
|
||||
self.assertEqual(len(all_notes), 5)
|
||||
|
||||
# Search by specific tags
|
||||
who_notes = self.note_manager.search_notes(case_id=self.case_id, question_tags=['WHO'])
|
||||
self.assertEqual(len(who_notes), 1)
|
||||
|
||||
what_notes = self.note_manager.search_notes(case_id=self.case_id, question_tags=['WHAT'])
|
||||
# Should find notes with WHAT tag: 'Weapon used was a knife' and 'Motive appears to be robbery'
|
||||
self.assertEqual(len(what_notes), 2)
|
||||
|
||||
def test_note_revision_immutability(self):
|
||||
"""Test that note revisions are truly immutable."""
|
||||
note_id = self.note_manager.create_note(
|
||||
self.case_id, 'Version 1', 'Detective Smith'
|
||||
)
|
||||
|
||||
# Create multiple revisions
|
||||
for i in range(2, 6):
|
||||
self.note_manager.update_note(note_id, f'Version {i}')
|
||||
|
||||
# Get full history
|
||||
history = self.note_manager.get_note_history(note_id)
|
||||
|
||||
# Verify all revisions are preserved
|
||||
self.assertEqual(len(history), 5)
|
||||
|
||||
# Verify each revision has correct content
|
||||
for i, revision in enumerate(reversed(history)):
|
||||
self.assertEqual(revision['revision_number'], i + 1)
|
||||
self.assertEqual(revision['content'], f'Version {i + 1}')
|
||||
|
||||
# Verify timestamp exists
|
||||
self.assertIsNotNone(revision['timestamp'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user