updates
This commit is contained in:
		
							parent
							
								
									aa61bfabc1
								
							
						
					
					
						commit
						86359ec850
					
				
							
								
								
									
										313
									
								
								.github/instructions/concept.instructions.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								.github/instructions/concept.instructions.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,313 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					applyTo: '**'
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					- # ForensicTrails - Technical Specification
 | 
				
			||||||
 | 
						- ## Forensic Investigation Documentation System
 | 
				
			||||||
 | 
							- **Version:** 1.0
 | 
				
			||||||
 | 
							- **Target:** Third-semester student project with AI assistance
 | 
				
			||||||
 | 
							- **Status:** Design Specification for Implementation
 | 
				
			||||||
 | 
						- ## 1. Project Overview
 | 
				
			||||||
 | 
							- ### 1.1 Purpose
 | 
				
			||||||
 | 
								- Desktop application for forensic investigators to document case work with:
 | 
				
			||||||
 | 
									- Immutable, timestamped note-taking
 | 
				
			||||||
 | 
									- Evidence tracking with chain of custody
 | 
				
			||||||
 | 
									- Configurable Investigation question framework (Standard: WHO/WHAT/WHEN/WHERE/HOW/WHY/WITH WHAT)
 | 
				
			||||||
 | 
									- Report generation
 | 
				
			||||||
 | 
									- Optional multi-user sync capability
 | 
				
			||||||
 | 
							- ### 1.2 Core Principles
 | 
				
			||||||
 | 
								- **Offline-first**: Must work without network
 | 
				
			||||||
 | 
								- **Simplicity**: Intuitive for solo investigators
 | 
				
			||||||
 | 
								- **Integrity**: Cryptographic Documentation of all data
 | 
				
			||||||
 | 
								- **Court-ready**: All documentation legally admissible
 | 
				
			||||||
 | 
								- **Case-agnostic**: No predefined templates, universal investigation framework
 | 
				
			||||||
 | 
							- ### 1.3 Success Criteria
 | 
				
			||||||
 | 
								- Solo investigator can document case from start to finish
 | 
				
			||||||
 | 
								- Generate PDF report with digital signatures
 | 
				
			||||||
 | 
								- Maintain complete chain of custody
 | 
				
			||||||
 | 
								- Evidence integrity verification via hashes
 | 
				
			||||||
 | 
								- All notes immutable with timestamps (can edit, but edits are documented)
 | 
				
			||||||
 | 
						- ## 2. Technical Architecture
 | 
				
			||||||
 | 
							- ### 2.1 Technology Stack
 | 
				
			||||||
 | 
								-
 | 
				
			||||||
 | 
								  ```
 | 
				
			||||||
 | 
								  Frontend/GUI:
 | 
				
			||||||
 | 
								  - Python 3.13+
 | 
				
			||||||
 | 
								  - PyQt6 (desktop GUI framework)
 | 
				
			||||||
 | 
								  - QtWebEngine (for rich text/markdown rendering)
 | 
				
			||||||
 | 
								  
 | 
				
			||||||
 | 
								  Database:
 | 
				
			||||||
 | 
								  - SQLite3 (local storage)
 | 
				
			||||||
 | 
								  - SQLCipher (optional encryption)
 | 
				
			||||||
 | 
								  - Connection pooling for optional remote PostgreSQL
 | 
				
			||||||
 | 
								  
 | 
				
			||||||
 | 
								  Utilities:
 | 
				
			||||||
 | 
								  - hashlib (MD5, SHA256 computation)
 | 
				
			||||||
 | 
								  - cryptography (digital signatures, encryption)
 | 
				
			||||||
 | 
								  - ReportLab (PDF generation)
 | 
				
			||||||
 | 
								  - python-docx (Word export)
 | 
				
			||||||
 | 
								  - Pillow (screenshot handling)
 | 
				
			||||||
 | 
								  
 | 
				
			||||||
 | 
								  Deployment:
 | 
				
			||||||
 | 
								  - PyInstaller (standalone executable)
 | 
				
			||||||
 | 
								  - One build per OS (Windows, Linux, macOS)
 | 
				
			||||||
 | 
								  ```
 | 
				
			||||||
 | 
							- ### 2.2 System Architecture
 | 
				
			||||||
 | 
								-
 | 
				
			||||||
 | 
								  ```
 | 
				
			||||||
 | 
								  ┌─────────────────────────────────────────────┐
 | 
				
			||||||
 | 
								  │         PyQt6 GUI Layer                     │
 | 
				
			||||||
 | 
								  │  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
 | 
				
			||||||
 | 
								  │  │ Note     │  │ Evidence │  │  Report  │   │
 | 
				
			||||||
 | 
								  │  │ Editor   │  │ Manager  │  │ Generator│   │
 | 
				
			||||||
 | 
								  │  └──────────┘  └──────────┘  └──────────┘   │
 | 
				
			||||||
 | 
								  ├─────────────────────────────────────────────┤
 | 
				
			||||||
 | 
								  │         Business Logic Layer                │
 | 
				
			||||||
 | 
								  │  - Note immutability enforcement            │
 | 
				
			||||||
 | 
								  │  - Chain of custody tracking                │
 | 
				
			||||||
 | 
								  │  - Investigation question tagging           │
 | 
				
			||||||
 | 
								  │  - Timeline generation                      │
 | 
				
			||||||
 | 
								  ├─────────────────────────────────────────────┤
 | 
				
			||||||
 | 
								  │         Data Access Layer                   │
 | 
				
			||||||
 | 
								  │  - SQLite manager (local)                   │
 | 
				
			||||||
 | 
								  │  - MariaDB connector (optional remote)      │
 | 
				
			||||||
 | 
								  │  - Encryption wrapper                       │
 | 
				
			||||||
 | 
								  │  - Conflict resolution (for sync)           │
 | 
				
			||||||
 | 
								  ├─────────────────────────────────────────────┤
 | 
				
			||||||
 | 
								  │         Storage Layer                       │
 | 
				
			||||||
 | 
								  │  Local: SQLite + File attachments           │
 | 
				
			||||||
 | 
								  │  Remote (optional): MariaDB                 │
 | 
				
			||||||
 | 
								  └─────────────────────────────────────────────┘
 | 
				
			||||||
 | 
								  ```
 | 
				
			||||||
 | 
							- ## 3. Database Schema
 | 
				
			||||||
 | 
								- ### 3.1 Core Tables
 | 
				
			||||||
 | 
									-
 | 
				
			||||||
 | 
									  ```sql
 | 
				
			||||||
 | 
									  -- Cases table
 | 
				
			||||||
 | 
									  CREATE TABLE cases (
 | 
				
			||||||
 | 
									    case_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    title TEXT NOT NULL,
 | 
				
			||||||
 | 
									    date_opened TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    lead_investigator TEXT NOT NULL,
 | 
				
			||||||
 | 
									    classification TEXT,
 | 
				
			||||||
 | 
									    summary TEXT,
 | 
				
			||||||
 | 
									    status TEXT DEFAULT 'Active',
 | 
				
			||||||
 | 
									    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- Notes table (append-only, immutable)
 | 
				
			||||||
 | 
									  CREATE TABLE notes (
 | 
				
			||||||
 | 
									    note_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    case_id TEXT NOT NULL,
 | 
				
			||||||
 | 
									    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    content TEXT NOT NULL,
 | 
				
			||||||
 | 
									    investigator TEXT NOT NULL,
 | 
				
			||||||
 | 
									    question_tags TEXT, -- JSON array: ["WHO", "WHAT", etc.]
 | 
				
			||||||
 | 
									    hash TEXT NOT NULL, -- SHA256 of content + timestamp
 | 
				
			||||||
 | 
									    FOREIGN KEY (case_id) REFERENCES cases(case_id)
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- Evidence table
 | 
				
			||||||
 | 
									  CREATE TABLE evidence (
 | 
				
			||||||
 | 
									    evidence_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    case_id TEXT,
 | 
				
			||||||
 | 
									    description TEXT NOT NULL,
 | 
				
			||||||
 | 
									    filename TEXT,
 | 
				
			||||||
 | 
									    file_size INTEGER,
 | 
				
			||||||
 | 
									    md5_hash TEXT,
 | 
				
			||||||
 | 
									    sha256_hash TEXT,
 | 
				
			||||||
 | 
									    source_origin TEXT,
 | 
				
			||||||
 | 
									    received_date DATE,
 | 
				
			||||||
 | 
									    received_by TEXT,
 | 
				
			||||||
 | 
									    physical_location TEXT,
 | 
				
			||||||
 | 
									    notes TEXT,
 | 
				
			||||||
 | 
									    status TEXT DEFAULT 'Active',
 | 
				
			||||||
 | 
									    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    FOREIGN KEY (case_id) REFERENCES cases(case_id)
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- Chain of Custody table
 | 
				
			||||||
 | 
									  CREATE TABLE chain_of_custody (
 | 
				
			||||||
 | 
									    coc_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    evidence_id TEXT NOT NULL,
 | 
				
			||||||
 | 
									    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    action TEXT NOT NULL, -- 'received', 'transferred', 'accessed', 'archived'
 | 
				
			||||||
 | 
									    from_person TEXT,
 | 
				
			||||||
 | 
									    to_person TEXT,
 | 
				
			||||||
 | 
									    location TEXT,
 | 
				
			||||||
 | 
									    purpose TEXT,
 | 
				
			||||||
 | 
									    signature_hash TEXT, -- Digital signature if needed
 | 
				
			||||||
 | 
									    FOREIGN KEY (evidence_id) REFERENCES evidence(evidence_id)
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- Attachments table (screenshots, documents)
 | 
				
			||||||
 | 
									  CREATE TABLE attachments (
 | 
				
			||||||
 | 
									    attachment_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    case_id TEXT NOT NULL,
 | 
				
			||||||
 | 
									    note_id TEXT, -- Optional link to specific note
 | 
				
			||||||
 | 
									    filename TEXT NOT NULL,
 | 
				
			||||||
 | 
									    file_path TEXT NOT NULL,
 | 
				
			||||||
 | 
									    file_hash TEXT NOT NULL,
 | 
				
			||||||
 | 
									    mime_type TEXT,
 | 
				
			||||||
 | 
									    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    FOREIGN KEY (case_id) REFERENCES cases(case_id),
 | 
				
			||||||
 | 
									    FOREIGN KEY (note_id) REFERENCES notes(note_id)
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- Investigation Questions tracking
 | 
				
			||||||
 | 
									  CREATE TABLE question_entries (
 | 
				
			||||||
 | 
									    entry_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    case_id TEXT NOT NULL,
 | 
				
			||||||
 | 
									    note_id TEXT NOT NULL,
 | 
				
			||||||
 | 
									    question_type TEXT NOT NULL, -- WHO/WHAT/WHEN/WHERE/HOW/WHY/WITH_WHAT
 | 
				
			||||||
 | 
									    entry_text TEXT NOT NULL,
 | 
				
			||||||
 | 
									    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    FOREIGN KEY (case_id) REFERENCES cases(case_id),
 | 
				
			||||||
 | 
									    FOREIGN KEY (note_id) REFERENCES notes(note_id)
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- User settings (for multi-user)
 | 
				
			||||||
 | 
									  CREATE TABLE users (
 | 
				
			||||||
 | 
									    user_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    username TEXT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
									    full_name TEXT NOT NULL,
 | 
				
			||||||
 | 
									    role TEXT DEFAULT 'Investigator', -- Investigator/Manager/Admin
 | 
				
			||||||
 | 
									    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  
 | 
				
			||||||
 | 
									  -- Optional: Task assignments (team mode)
 | 
				
			||||||
 | 
									  CREATE TABLE tasks (
 | 
				
			||||||
 | 
									    task_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
									    case_id TEXT NOT NULL,
 | 
				
			||||||
 | 
									    title TEXT NOT NULL,
 | 
				
			||||||
 | 
									    description TEXT,
 | 
				
			||||||
 | 
									    assigned_to TEXT,
 | 
				
			||||||
 | 
									    assigned_by TEXT,
 | 
				
			||||||
 | 
									    priority TEXT,
 | 
				
			||||||
 | 
									    due_date DATE,
 | 
				
			||||||
 | 
									    status TEXT DEFAULT 'Open',
 | 
				
			||||||
 | 
									    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
									    FOREIGN KEY (case_id) REFERENCES cases(case_id)
 | 
				
			||||||
 | 
									  );
 | 
				
			||||||
 | 
									  ```
 | 
				
			||||||
 | 
								- ### 3.2 Indexes for Performance
 | 
				
			||||||
 | 
									-
 | 
				
			||||||
 | 
									  ```sql
 | 
				
			||||||
 | 
									  CREATE INDEX idx_notes_case ON notes(case_id);
 | 
				
			||||||
 | 
									  CREATE INDEX idx_notes_timestamp ON notes(timestamp);
 | 
				
			||||||
 | 
									  CREATE INDEX idx_evidence_case ON evidence(case_id);
 | 
				
			||||||
 | 
									  CREATE INDEX idx_coc_evidence ON chain_of_custody(evidence_id);
 | 
				
			||||||
 | 
									  CREATE INDEX idx_question_case ON question_entries(case_id, question_type);
 | 
				
			||||||
 | 
									  ```
 | 
				
			||||||
 | 
								- ## 4. Core Features
 | 
				
			||||||
 | 
									- ### 4.1 Case Management
 | 
				
			||||||
 | 
										- Create new case with minimal metadata
 | 
				
			||||||
 | 
										- List all cases with search (& Filter)
 | 
				
			||||||
 | 
										- Open/close/archive cases
 | 
				
			||||||
 | 
										- Case status tracking
 | 
				
			||||||
 | 
									- ### 4.2 Note-Taking
 | 
				
			||||||
 | 
										- Rich text editor for notes
 | 
				
			||||||
 | 
										- Auto-timestamp on every entry (immutable)
 | 
				
			||||||
 | 
										- Notes can be edited, but each edit is documented (can restore old states)
 | 
				
			||||||
 | 
										- Tag notes with investigation questions
 | 
				
			||||||
 | 
										- Search across all notes
 | 
				
			||||||
 | 
										- Screenshot integration with auto-hash
 | 
				
			||||||
 | 
									- ### 4.3 Evidence Management
 | 
				
			||||||
 | 
										- Add evidence with ID, description, hashes
 | 
				
			||||||
 | 
										- Compute MD5/SHA256 automatically or paste
 | 
				
			||||||
 | 
										- Track physical location (text field)
 | 
				
			||||||
 | 
										- Evidence status (Active/Archived/Destroyed)
 | 
				
			||||||
 | 
										- Link evidence to notes
 | 
				
			||||||
 | 
									- ### 4.4 Chain of Custody
 | 
				
			||||||
 | 
										- Automatic entry on evidence creation
 | 
				
			||||||
 | 
										- Manual entries for transfers/access
 | 
				
			||||||
 | 
										- Immutable CoC log
 | 
				
			||||||
 | 
									- ### 4.5 Investigation Questions Framework
 | 
				
			||||||
 | 
										- Tag any note with: WHO/WHAT/WHEN/WHERE/HOW/WHY/WITH_WHAT
 | 
				
			||||||
 | 
											- configurable questions
 | 
				
			||||||
 | 
										- View organized by question type
 | 
				
			||||||
 | 
										- Timeline view (auto-generated from WHEN tags)
 | 
				
			||||||
 | 
										- Summary view per question
 | 
				
			||||||
 | 
									- ### 4.6 Report Generation
 | 
				
			||||||
 | 
										- PDF export with all case data
 | 
				
			||||||
 | 
										- Sections: Metadata, Notes, Evidence, CoC, Questions
 | 
				
			||||||
 | 
										- Digital signature of report
 | 
				
			||||||
 | 
										- Court-ready formatting
 | 
				
			||||||
 | 
										- Optional DOCX export
 | 
				
			||||||
 | 
									- ### 4.7 Optional: Remote Sync
 | 
				
			||||||
 | 
										- Configure MariaDB connection
 | 
				
			||||||
 | 
										- Push/pull case data
 | 
				
			||||||
 | 
										- Conflict resolution (timestamp-based)
 | 
				
			||||||
 | 
										- Offline-capable (queue sync)
 | 
				
			||||||
 | 
								- ## 5. User Interface Layout
 | 
				
			||||||
 | 
									- ### 5.1 Main Window Structure
 | 
				
			||||||
 | 
										-
 | 
				
			||||||
 | 
										  ```
 | 
				
			||||||
 | 
										  ┌─────────────────────────────────────────────────────┐
 | 
				
			||||||
 | 
										  │ Menu Bar: File | Case | Evidence | View | Tools     │
 | 
				
			||||||
 | 
										  ├──────────┬──────────────────────────────┬───────────┤
 | 
				
			||||||
 | 
										  │          │                              │           │
 | 
				
			||||||
 | 
										  │ Cases    │   Active View Area          │  Sidebar  │
 | 
				
			||||||
 | 
										  │ List     │   (Notes/Evidence/Timeline)  │  Panel    │
 | 
				
			||||||
 | 
										  │          │                              │           │
 | 
				
			||||||
 | 
										  │ - Case 1 │   [Content depends on       │  • Case   │
 | 
				
			||||||
 | 
										  │ - Case 2 │    selected tab below]       │    Info   │
 | 
				
			||||||
 | 
										  │ - Case 3 │                              │  • Ques-  │
 | 
				
			||||||
 | 
										  │          │                              │    tions  │
 | 
				
			||||||
 | 
										  │ [Search] │                              │  • Evid-  │
 | 
				
			||||||
 | 
										  │          │                              │    ence   │
 | 
				
			||||||
 | 
										  │          │                              │           │
 | 
				
			||||||
 | 
										  ├──────────┴──────────────────────────────┴───────────┤
 | 
				
			||||||
 | 
										  │ Tab Bar: Notes | Evidence | Questions | Timeline    │
 | 
				
			||||||
 | 
										  │          | Chain of Custody | Reports                │
 | 
				
			||||||
 | 
										  └─────────────────────────────────────────────────────┘
 | 
				
			||||||
 | 
										  ```
 | 
				
			||||||
 | 
									- ### 5.2 Key Views
 | 
				
			||||||
 | 
										- **Notes View:**
 | 
				
			||||||
 | 
											- Chronological log of all notes (immutable)
 | 
				
			||||||
 | 
											- New note entry at bottom
 | 
				
			||||||
 | 
											- Quick tag buttons (WHO/WHAT/WHEN/WHERE/HOW/WHY/WITH_WHAT)
 | 
				
			||||||
 | 
												- or whatever can be configured
 | 
				
			||||||
 | 
											- Screenshot button
 | 
				
			||||||
 | 
											- Evidence reference button
 | 
				
			||||||
 | 
										- **Evidence View:**
 | 
				
			||||||
 | 
											- Table of all evidence items
 | 
				
			||||||
 | 
											- Add/view evidence details
 | 
				
			||||||
 | 
											- CoC view per item
 | 
				
			||||||
 | 
										- **Questions View:**
 | 
				
			||||||
 | 
											- Accordion/expandable sections per question
 | 
				
			||||||
 | 
											- Shows all notes tagged with that question
 | 
				
			||||||
 | 
											- Quick navigation
 | 
				
			||||||
 | 
										- **Timeline View:**
 | 
				
			||||||
 | 
											- Visual timeline of events
 | 
				
			||||||
 | 
											- Generated from WHEN-tagged notes
 | 
				
			||||||
 | 
											- Zoomable, filterable
 | 
				
			||||||
 | 
										- **Chain of Custody View:**
 | 
				
			||||||
 | 
											- Per-evidence CoC log
 | 
				
			||||||
 | 
											- Transfer recording interface
 | 
				
			||||||
 | 
										- **Reports View:**
 | 
				
			||||||
 | 
											- Report templates
 | 
				
			||||||
 | 
											- Generate PDF/DOCX
 | 
				
			||||||
 | 
											- Preview before export
 | 
				
			||||||
 | 
								- ## 6. Implementation Priorities
 | 
				
			||||||
 | 
									- ### Phase 1: Minimum Viable Product (Core Solo Mode)
 | 
				
			||||||
 | 
										- 1. Case creation and listing
 | 
				
			||||||
 | 
										- 2. Note-taking with immutable timestamps
 | 
				
			||||||
 | 
										- 3. Evidence management with hashing
 | 
				
			||||||
 | 
										- 4. Basic Chain of Custody
 | 
				
			||||||
 | 
										- 5. Simple PDF export
 | 
				
			||||||
 | 
										- **Deliverable:** Functional solo investigator tool
 | 
				
			||||||
 | 
									- ### Phase 2: Enhanced Features
 | 
				
			||||||
 | 
										- 1. Investigation questions tagging
 | 
				
			||||||
 | 
										- 2. Questions-organized view
 | 
				
			||||||
 | 
										- 3. Timeline visualization
 | 
				
			||||||
 | 
										- 4. Screenshot integration
 | 
				
			||||||
 | 
										- 5. Advanced PDF report with formatting
 | 
				
			||||||
 | 
										- **Deliverable:** Full-featured documentation tool
 | 
				
			||||||
 | 
									- ### Phase 3: Team & Advanced
 | 
				
			||||||
 | 
										- 1. Multi-user support (local)
 | 
				
			||||||
 | 
										- 2. Task assignment
 | 
				
			||||||
 | 
										- 3. MariaDB remote sync
 | 
				
			||||||
 | 
										- 4. Digital signatures on reports
 | 
				
			||||||
 | 
										- 5. Advanced search and filtering
 | 
				
			||||||
 | 
										- **Deliverable:** Team-capable system
 | 
				
			||||||
@ -26,7 +26,6 @@ dependencies = [
 | 
				
			|||||||
    "Pillow>=10.0.0",
 | 
					    "Pillow>=10.0.0",
 | 
				
			||||||
    "cryptography>=41.0.0",
 | 
					    "cryptography>=41.0.0",
 | 
				
			||||||
    "pyinstaller>=6.0.0",
 | 
					    "pyinstaller>=6.0.0",
 | 
				
			||||||
    "logging>=0.4.9.6",
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[project.optional-dependencies]
 | 
					[project.optional-dependencies]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import utils.config as config
 | 
					from ..utils.config import config
 | 
				
			||||||
from db.database import get_db_connection
 | 
					from ..db.database import get_db_connection
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,24 +1,57 @@
 | 
				
			|||||||
import sqlite3
 | 
					import sqlite3
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..utils.config import config
 | 
					from ..utils.config import config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_db_connection(db_path=None):
 | 
					def get_db_connection(db_path=None):
 | 
				
			||||||
    if db_path is None:
 | 
					    if db_path is None:
 | 
				
			||||||
        db_path = create_db_if_not_exists()
 | 
					        db_path = config.database_path
 | 
				
			||||||
    conn = sqlite3.connect(db_path)
 | 
					    conn = sqlite3.connect(db_path)
 | 
				
			||||||
    conn.row_factory = sqlite3.Row
 | 
					    conn.row_factory = sqlite3.Row
 | 
				
			||||||
    return conn
 | 
					    return conn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_db_if_not_exists(db_path=None, schema_path=None):
 | 
					def validate_database_schema(db_path):
 | 
				
			||||||
 | 
					    """Check if the database has a valid schema matching the current version."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        conn = sqlite3.connect(db_path)
 | 
				
			||||||
 | 
					        cursor = conn.cursor()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check if required tables exist
 | 
				
			||||||
 | 
					        cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
 | 
				
			||||||
 | 
					        tables = {row[0] for row in cursor.fetchall()}
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        required_tables = {
 | 
				
			||||||
 | 
					            'cases', 'notes', 'evidence', 'chain_of_custody',
 | 
				
			||||||
 | 
					            'attachments', 'question_entries', 'users', 'tasks'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check if all required tables exist
 | 
				
			||||||
 | 
					        if not required_tables.issubset(tables):
 | 
				
			||||||
 | 
					            logging.warning(f"Database missing required tables. Expected: {required_tables}, Found: {tables}")
 | 
				
			||||||
 | 
					            conn.close()
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # TODO: Add version check when we implement a metadata/version table
 | 
				
			||||||
 | 
					        # For now, we just check if tables exist
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logging.error(f"Error validating database: {e}")
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_fresh_database(db_path=None, schema_path=None):
 | 
				
			||||||
 | 
					    """Create a fresh database with the current schema."""
 | 
				
			||||||
    if db_path is None:
 | 
					    if db_path is None:
 | 
				
			||||||
        db_path = config.database_path
 | 
					        db_path = config.database_path
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if schema_path is None:
 | 
					    if schema_path is None:
 | 
				
			||||||
        schema_path = Path(__file__).parent / config.database_template
 | 
					        schema_path = Path(__file__).parent / config.database_template
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    conn = get_db_connection(db_path)
 | 
					    # Create a direct connection without calling get_db_connection
 | 
				
			||||||
 | 
					    conn = sqlite3.connect(db_path)
 | 
				
			||||||
    cursor = conn.cursor()
 | 
					    cursor = conn.cursor()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    with open(schema_path, 'r') as f:
 | 
					    with open(schema_path, 'r') as f:
 | 
				
			||||||
@ -27,14 +60,37 @@ def create_db_if_not_exists(db_path=None, schema_path=None):
 | 
				
			|||||||
    cursor.executescript(schema)
 | 
					    cursor.executescript(schema)
 | 
				
			||||||
    conn.commit()
 | 
					    conn.commit()
 | 
				
			||||||
    conn.close()
 | 
					    conn.close()
 | 
				
			||||||
 | 
					    logging.info(f"Fresh database created at {db_path}")
 | 
				
			||||||
 | 
					    return db_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def initialize_database(db_path=None):
 | 
					def initialize_database(db_path=None):
 | 
				
			||||||
 | 
					    """Initialize the database, creating or validating it as needed."""
 | 
				
			||||||
    if db_path is None:
 | 
					    if db_path is None:
 | 
				
			||||||
        db_path = config.database_path
 | 
					        db_path = config.database_path
 | 
				
			||||||
    create_db_if_not_exists(db_path)
 | 
					    
 | 
				
			||||||
 | 
					    db_file = Path(db_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Case 1: Database doesn't exist - create fresh
 | 
				
			||||||
 | 
					    if not db_file.exists():
 | 
				
			||||||
 | 
					        logging.info(f"No database found at {db_path}, creating fresh database...")
 | 
				
			||||||
 | 
					        create_fresh_database(db_path)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        # Case 2: Database exists - validate it
 | 
				
			||||||
 | 
					        logging.info(f"Database found at {db_path}, validating schema...")
 | 
				
			||||||
 | 
					        if validate_database_schema(db_path):
 | 
				
			||||||
 | 
					            logging.info("Database schema is valid, using existing database")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Case 3: Database is invalid - delete and recreate
 | 
				
			||||||
 | 
					            logging.warning(f"Database schema is invalid or outdated. Deleting old database...")
 | 
				
			||||||
 | 
					            # TODO: Show GUI warning to user before deleting
 | 
				
			||||||
 | 
					            os.remove(db_path)
 | 
				
			||||||
 | 
					            logging.info("Creating fresh database with current schema...")
 | 
				
			||||||
 | 
					            create_fresh_database(db_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    if config.log_level == 'DEBUG':
 | 
					    if config.log_level == 'DEBUG':
 | 
				
			||||||
        show_db_schema(db_path)
 | 
					        show_db_schema(db_path)
 | 
				
			||||||
    logging.info(f"Database initialized at {db_path}")
 | 
					    
 | 
				
			||||||
 | 
					    logging.info(f"Database initialized successfully at {db_path}")
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
def show_db_schema(db_path):
 | 
					def show_db_schema(db_path):
 | 
				
			||||||
    conn = get_db_connection(db_path)
 | 
					    conn = get_db_connection(db_path)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										123
									
								
								tests/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								tests/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					# ForensicTrails Test Suite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Overview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This directory contains unit and integration tests for the ForensicTrails application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Running Tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run all tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run specific test file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run with verbose output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py -v
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run with coverage report
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py --cov=forensictrails.db.database --cov-report=term-missing
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run specific test class or method
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py::TestGetDbConnection
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py::TestGetDbConnection::test_get_db_connection_creates_file
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `test_database.py`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Comprehensive tests for the database module (`forensictrails.db.database`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Coverage: 94%**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Test Classes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestGetDbConnection**: Tests for `get_db_connection()` function
 | 
				
			||||||
 | 
					  - Connection creation and file creation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestValidateDatabaseSchema**: Tests for `validate_database_schema()` function
 | 
				
			||||||
 | 
					  - Empty database validation
 | 
				
			||||||
 | 
					  - Incomplete database validation
 | 
				
			||||||
 | 
					  - Complete database validation
 | 
				
			||||||
 | 
					  - Non-existent database validation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestCreateFreshDatabase**: Tests for `create_fresh_database()` function
 | 
				
			||||||
 | 
					  - Database file creation
 | 
				
			||||||
 | 
					  - All required tables creation
 | 
				
			||||||
 | 
					  - Return value verification
 | 
				
			||||||
 | 
					  - Clean path behavior
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestInitializeDatabase**: Tests for `initialize_database()` function
 | 
				
			||||||
 | 
					  - New database creation
 | 
				
			||||||
 | 
					  - Valid database preservation
 | 
				
			||||||
 | 
					  - Invalid database recreation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestShowDbSchema**: Tests for `show_db_schema()` function
 | 
				
			||||||
 | 
					  - Logging behavior
 | 
				
			||||||
 | 
					  - Exception handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestDatabaseIntegration**: Full lifecycle integration tests
 | 
				
			||||||
 | 
					  - Complete workflow: create → use → corrupt → recreate
 | 
				
			||||||
 | 
					  - Data persistence and loss scenarios
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Coverage Summary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Module | Statements | Missing | Coverage |
 | 
				
			||||||
 | 
					|--------|-----------|---------|----------|
 | 
				
			||||||
 | 
					| `forensictrails.db.database` | 65 | 4 | **94%** |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Uncovered Lines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Line 10: Config fallback in `get_db_connection()` (uses mocked config in tests)
 | 
				
			||||||
 | 
					- Line 48: Config fallback in `create_fresh_database()` (uses explicit paths in tests)
 | 
				
			||||||
 | 
					- Line 69: Config fallback in `initialize_database()` (uses explicit paths in tests)
 | 
				
			||||||
 | 
					- Line 91: Debug logging in `show_db_schema()` (covered but not counted due to mocking)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					These uncovered lines are primarily default parameter handling that relies on the global config object, which is mocked in tests.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Design Principles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **Isolation**: Each test uses temporary directories and cleans up after itself
 | 
				
			||||||
 | 
					2. **Independence**: Tests don't depend on each other and can run in any order
 | 
				
			||||||
 | 
					3. **Mocking**: External dependencies (config, logging) are mocked where appropriate
 | 
				
			||||||
 | 
					4. **Real Database**: Tests use actual SQLite databases to ensure realistic behavior
 | 
				
			||||||
 | 
					5. **Comprehensive**: Tests cover success paths, error paths, and edge cases
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Adding New Tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When adding new database functionality:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Add unit tests for individual functions
 | 
				
			||||||
 | 
					2. Add integration tests for complex workflows
 | 
				
			||||||
 | 
					3. Ensure cleanup in `tearDown()` methods
 | 
				
			||||||
 | 
					4. Use descriptive test names that explain what is being tested
 | 
				
			||||||
 | 
					5. Run coverage to ensure new code is tested
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tests require:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `pytest` - Test framework
 | 
				
			||||||
 | 
					- `pytest-cov` - Coverage reporting
 | 
				
			||||||
 | 
					- Standard library: `unittest`, `tempfile`, `sqlite3`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Install test dependencies:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pip install pytest pytest-cov
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										92
									
								
								tests/SUMMARY.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								tests/SUMMARY.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					ForensicTrails Test Suite Summary
 | 
				
			||||||
 | 
					==================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OVERVIEW
 | 
				
			||||||
 | 
					--------
 | 
				
			||||||
 | 
					✅ 54 tests across 4 test files
 | 
				
			||||||
 | 
					✅ 87% overall code coverage
 | 
				
			||||||
 | 
					✅ All tests passing
 | 
				
			||||||
 | 
					⚡ Fast execution (~0.11 seconds)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TEST FILES
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. test_config.py (8 tests) - 100% coverage
 | 
				
			||||||
 | 
					   - Config file loading
 | 
				
			||||||
 | 
					   - Default value handling
 | 
				
			||||||
 | 
					   - Partial configuration support
 | 
				
			||||||
 | 
					   - Data type validation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. test_logging.py (9 tests) - 100% coverage
 | 
				
			||||||
 | 
					   - Log file creation
 | 
				
			||||||
 | 
					   - Handler configuration
 | 
				
			||||||
 | 
					   - Log level management
 | 
				
			||||||
 | 
					   - Formatter verification
 | 
				
			||||||
 | 
					   - Integration tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. test_database.py (16 tests) - 94% coverage
 | 
				
			||||||
 | 
					   - Database initialization
 | 
				
			||||||
 | 
					   - Schema validation
 | 
				
			||||||
 | 
					   - Fresh database creation
 | 
				
			||||||
 | 
					   - Invalid database recovery
 | 
				
			||||||
 | 
					   - Full lifecycle integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. test_case_manager.py (21 tests) - 97% coverage
 | 
				
			||||||
 | 
					   - Case CRUD operations
 | 
				
			||||||
 | 
					   - Search and filtering
 | 
				
			||||||
 | 
					   - Status management
 | 
				
			||||||
 | 
					   - Update tracking
 | 
				
			||||||
 | 
					   - Full workflow integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COVERAGE BY MODULE
 | 
				
			||||||
 | 
					------------------
 | 
				
			||||||
 | 
					forensictrails.utils.config        100% ✓
 | 
				
			||||||
 | 
					forensictrails.utils.logging       100% ✓
 | 
				
			||||||
 | 
					forensictrails.core.case_manager    97% ✓
 | 
				
			||||||
 | 
					forensictrails.db.database          94% ✓
 | 
				
			||||||
 | 
					forensictrails.__main__              0% (GUI entry point)
 | 
				
			||||||
 | 
					----------------------------------------
 | 
				
			||||||
 | 
					TOTAL                                87% ✓
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUNNING TESTS
 | 
				
			||||||
 | 
					-------------
 | 
				
			||||||
 | 
					All tests:       python -m pytest tests/
 | 
				
			||||||
 | 
					Specific file:   python -m pytest tests/test_config.py
 | 
				
			||||||
 | 
					With coverage:   python -m pytest tests/ --cov=forensictrails
 | 
				
			||||||
 | 
					Verbose:         python -m pytest tests/ -v
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COVERAGE REPORT
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					HTML report:     python -m pytest tests/ --cov=forensictrails --cov-report=html
 | 
				
			||||||
 | 
					                 (Open htmlcov/index.html in browser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					UNCOVERED CODE
 | 
				
			||||||
 | 
					--------------
 | 
				
			||||||
 | 
					- database.py (4 lines): Default parameter fallbacks
 | 
				
			||||||
 | 
					- case_manager.py (2 lines): TODO export/import functions
 | 
				
			||||||
 | 
					- __main__.py (17 lines): GUI entry point (requires GUI testing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					KNOWN ISSUES
 | 
				
			||||||
 | 
					------------
 | 
				
			||||||
 | 
					⚠️  DeprecationWarning in case_manager.py:60
 | 
				
			||||||
 | 
					    Using datetime.utcnow() - should migrate to datetime.now(datetime.UTC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TEST PRINCIPLES
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					✓ Isolated - Each test uses temp directories
 | 
				
			||||||
 | 
					✓ Independent - Tests run in any order
 | 
				
			||||||
 | 
					✓ Mocked - External dependencies mocked appropriately
 | 
				
			||||||
 | 
					✓ Realistic - Uses real SQLite databases
 | 
				
			||||||
 | 
					✓ Comprehensive - Success, error, and edge cases
 | 
				
			||||||
 | 
					✓ Fast - All 54 tests in ~0.11 seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MAINTENANCE
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					- Keep coverage above 85%
 | 
				
			||||||
 | 
					- Update tests when refactoring
 | 
				
			||||||
 | 
					- Add tests for new features
 | 
				
			||||||
 | 
					- Run tests before committing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Last Updated: 2025-10-08
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
							
								
								
									
										243
									
								
								tests/TEST_SUMMARY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								tests/TEST_SUMMARY.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,243 @@
 | 
				
			|||||||
 | 
					# ForensicTrails Test Suite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Overview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This directory contains comprehensive unit and integration tests for the ForensicTrails application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Summary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **Total Tests:** 54
 | 
				
			||||||
 | 
					- **Test Files:** 4
 | 
				
			||||||
 | 
					- **Overall Coverage:** 87%
 | 
				
			||||||
 | 
					- **Execution Time:** ~0.17s
 | 
				
			||||||
 | 
					- **Status:** ✅ All tests passing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Running Tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run all tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run specific test file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py
 | 
				
			||||||
 | 
					python -m pytest tests/test_config.py
 | 
				
			||||||
 | 
					python -m pytest tests/test_logging.py
 | 
				
			||||||
 | 
					python -m pytest tests/test_case_manager.py
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run with verbose output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/ -v
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run with coverage report
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/ --cov=forensictrails --cov-report=term-missing
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Generate HTML coverage report
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/ --cov=forensictrails --cov-report=html
 | 
				
			||||||
 | 
					# Open htmlcov/index.html in your browser
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Run specific test class or method
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python -m pytest tests/test_database.py::TestGetDbConnection
 | 
				
			||||||
 | 
					python -m pytest tests/test_case_manager.py::TestCaseManager::test_create_case
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1. `test_config.py` (8 tests)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tests for the configuration module (`forensictrails.utils.config`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Coverage: 100%** ✓
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Test Classes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestConfig** (7 tests)
 | 
				
			||||||
 | 
					  - Loading from valid JSON file
 | 
				
			||||||
 | 
					  - Handling non-existent files
 | 
				
			||||||
 | 
					  - Partial configuration with defaults
 | 
				
			||||||
 | 
					  - Empty configuration files
 | 
				
			||||||
 | 
					  - Extra keys in configuration
 | 
				
			||||||
 | 
					  - Default constructor behavior
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestConfigDataTypes** (1 test)
 | 
				
			||||||
 | 
					  - String and integer value handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2. `test_logging.py` (9 tests)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tests for the logging setup module (`forensictrails.utils.logging`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Coverage: 100%** ✓
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Test Classes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestSetupLogging** (8 tests)
 | 
				
			||||||
 | 
					  - Log file creation
 | 
				
			||||||
 | 
					  - Nested directory creation
 | 
				
			||||||
 | 
					  - Handler addition (FileHandler, StreamHandler)
 | 
				
			||||||
 | 
					  - Log level configuration
 | 
				
			||||||
 | 
					  - Message logging
 | 
				
			||||||
 | 
					  - Formatter verification
 | 
				
			||||||
 | 
					  - Config fallback behavior
 | 
				
			||||||
 | 
					  - Multiple log level testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestLoggingIntegration** (1 test)
 | 
				
			||||||
 | 
					  - Multiple message logging integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 3. `test_database.py` (16 tests)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Comprehensive tests for the database module (`forensictrails.db.database`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Coverage: 94%** ✓
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Test Classes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestGetDbConnection** (2 tests)
 | 
				
			||||||
 | 
					  - Connection creation and file creation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestValidateDatabaseSchema** (4 tests)
 | 
				
			||||||
 | 
					  - Empty database validation
 | 
				
			||||||
 | 
					  - Incomplete database validation
 | 
				
			||||||
 | 
					  - Complete database validation
 | 
				
			||||||
 | 
					  - Non-existent database validation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestCreateFreshDatabase** (4 tests)
 | 
				
			||||||
 | 
					  - Database file creation
 | 
				
			||||||
 | 
					  - All required tables creation
 | 
				
			||||||
 | 
					  - Return value verification
 | 
				
			||||||
 | 
					  - Clean path behavior
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestInitializeDatabase** (3 tests)
 | 
				
			||||||
 | 
					  - New database creation
 | 
				
			||||||
 | 
					  - Valid database preservation
 | 
				
			||||||
 | 
					  - Invalid database recreation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestShowDbSchema** (2 tests)
 | 
				
			||||||
 | 
					  - Logging behavior
 | 
				
			||||||
 | 
					  - Exception handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestDatabaseIntegration** (1 test)
 | 
				
			||||||
 | 
					  - Complete workflow: create → use → corrupt → recreate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 4. `test_case_manager.py` (21 tests)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Comprehensive tests for the case manager module (`forensictrails.core.case_manager`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Coverage: 97%** ✓
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Test Classes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestCaseManager** (19 tests)
 | 
				
			||||||
 | 
					  - Initialization
 | 
				
			||||||
 | 
					  - Case creation (full and minimal)
 | 
				
			||||||
 | 
					  - Getting cases (existent and non-existent)
 | 
				
			||||||
 | 
					  - Listing cases (empty, multiple, filtered)
 | 
				
			||||||
 | 
					  - Search functionality
 | 
				
			||||||
 | 
					  - Combined filters
 | 
				
			||||||
 | 
					  - Case updates (valid, invalid, non-existent)
 | 
				
			||||||
 | 
					  - Status changes (close, archive)
 | 
				
			||||||
 | 
					  - Case deletion
 | 
				
			||||||
 | 
					  - Modified timestamp tracking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **TestCaseManagerIntegration** (2 tests)
 | 
				
			||||||
 | 
					  - Full case lifecycle integration
 | 
				
			||||||
 | 
					  - Multiple cases workflow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Coverage Summary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Module | Statements | Missing | Coverage |
 | 
				
			||||||
 | 
					|--------|-----------|---------|----------|
 | 
				
			||||||
 | 
					| `forensictrails.utils.config` | 16 | 0 | **100%** ✓ |
 | 
				
			||||||
 | 
					| `forensictrails.utils.logging` | 20 | 0 | **100%** ✓ |
 | 
				
			||||||
 | 
					| `forensictrails.core.case_manager` | 65 | 2 | **97%** ✓ |
 | 
				
			||||||
 | 
					| `forensictrails.db.database` | 65 | 4 | **94%** ✓ |
 | 
				
			||||||
 | 
					| `forensictrails.__main__` | 17 | 17 | **0%** (GUI entry) |
 | 
				
			||||||
 | 
					| **TOTAL** | **183** | **23** | **87%** |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Uncovered Lines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### database.py (4 lines)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Line 10: Config fallback in `get_db_connection()` (uses mocked config)
 | 
				
			||||||
 | 
					- Line 48: Config fallback in `create_fresh_database()` (uses explicit paths)
 | 
				
			||||||
 | 
					- Line 69: Config fallback in `initialize_database()` (uses explicit paths)
 | 
				
			||||||
 | 
					- Line 91: Debug logging (covered but mocked)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### case_manager.py (2 lines)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Lines 88, 93: Export/import functions (TODO - not implemented yet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### **main**.py (17 lines)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Entry point for GUI application (requires PyQt6 GUI testing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Design Principles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **Isolation**: Each test uses temporary directories and cleans up after itself
 | 
				
			||||||
 | 
					2. **Independence**: Tests don't depend on each other and can run in any order
 | 
				
			||||||
 | 
					3. **Mocking**: External dependencies (config, logging) are mocked where appropriate
 | 
				
			||||||
 | 
					4. **Real Database**: Tests use actual SQLite databases to ensure realistic behavior
 | 
				
			||||||
 | 
					5. **Comprehensive**: Tests cover success paths, error paths, and edge cases
 | 
				
			||||||
 | 
					6. **Fast**: All 54 tests run in ~0.17 seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Adding New Tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When adding new functionality:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Add unit tests for individual functions
 | 
				
			||||||
 | 
					2. Add integration tests for complex workflows
 | 
				
			||||||
 | 
					3. Ensure cleanup in `tearDown()` methods
 | 
				
			||||||
 | 
					4. Use descriptive test names that explain what is being tested
 | 
				
			||||||
 | 
					5. Mock external dependencies appropriately
 | 
				
			||||||
 | 
					6. Run coverage to ensure new code is tested
 | 
				
			||||||
 | 
					7. Aim for >90% coverage on new modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tests require:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `pytest` - Test framework
 | 
				
			||||||
 | 
					- `pytest-cov` - Coverage reporting
 | 
				
			||||||
 | 
					- Standard library: `unittest`, `tempfile`, `sqlite3`, `json`, `logging`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Install test dependencies:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pip install pytest pytest-cov
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Continuous Integration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					These tests are designed to run in CI/CD pipelines. Example usage:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					# Run tests with JUnit XML output for CI
 | 
				
			||||||
 | 
					python -m pytest tests/ --junitxml=test-results.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run with coverage and fail if below threshold
 | 
				
			||||||
 | 
					python -m pytest tests/ --cov=forensictrails --cov-fail-under=85
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Known Issues
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- **DeprecationWarning** in `case_manager.py:60`: Uses `datetime.utcnow()` which is deprecated. Should be updated to use `datetime.now(datetime.UTC)` in the future.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Test Maintenance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Tests are automatically run on code changes
 | 
				
			||||||
 | 
					- Coverage reports are generated in `htmlcov/` directory
 | 
				
			||||||
 | 
					- Keep test coverage above 85% for the project
 | 
				
			||||||
 | 
					- Review and update tests when refactoring code
 | 
				
			||||||
							
								
								
									
										378
									
								
								tests/test_case_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								tests/test_case_manager.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,378 @@
 | 
				
			|||||||
 | 
					"""Unit tests for the case_manager module."""
 | 
				
			||||||
 | 
					import unittest
 | 
				
			||||||
 | 
					import tempfile
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add the src directory to the path
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from forensictrails.core.case_manager import CaseManager
 | 
				
			||||||
 | 
					from forensictrails.db.database import create_fresh_database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestCaseManager(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for CaseManager 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 case manager instance
 | 
				
			||||||
 | 
					        self.case_manager = CaseManager(self.test_db_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        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_case_manager_initialization(self):
 | 
				
			||||||
 | 
					        """Test CaseManager initializes correctly."""
 | 
				
			||||||
 | 
					        self.assertEqual(self.case_manager.db_path, self.test_db_path)
 | 
				
			||||||
 | 
					        self.assertIsNotNone(self.case_manager.conn)
 | 
				
			||||||
 | 
					        self.assertIsNotNone(self.case_manager.cursor)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify case was created
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-002')
 | 
				
			||||||
 | 
					        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'])
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_get_case_nonexistent(self):
 | 
				
			||||||
 | 
					        """Test getting a non-existent case returns None."""
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('NONEXISTENT')
 | 
				
			||||||
 | 
					        self.assertIsNone(case)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_list_cases_empty(self):
 | 
				
			||||||
 | 
					        """Test listing cases when database is empty."""
 | 
				
			||||||
 | 
					        cases = self.case_manager.list_cases()
 | 
				
			||||||
 | 
					        self.assertEqual(cases, [])
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Close one case
 | 
				
			||||||
 | 
					        self.case_manager.close_case('CASE-001')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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 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')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Search by title
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.case_manager.close_case('CASE-003')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Search for "Murder" in active cases only
 | 
				
			||||||
 | 
					        results = self.case_manager.list_cases(status='active', search_term='Murder')
 | 
				
			||||||
 | 
					        self.assertEqual(len(results), 1)
 | 
				
			||||||
 | 
					        self.assertEqual(results[0]['case_id'], 'CASE-001')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_update_case(self):
 | 
				
			||||||
 | 
					        """Test updating a case."""
 | 
				
			||||||
 | 
					        self.case_manager.create_case('CASE-001', 'Original Title', 'Detective A')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = self.case_manager.update_case(
 | 
				
			||||||
 | 
					            'CASE-001',
 | 
				
			||||||
 | 
					            case_title='Updated Title',
 | 
				
			||||||
 | 
					            classification='Homicide',
 | 
				
			||||||
 | 
					            summary='Updated summary'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = self.case_manager.update_case(
 | 
				
			||||||
 | 
					            'CASE-001',
 | 
				
			||||||
 | 
					            case_title='Updated Title',
 | 
				
			||||||
 | 
					            invalid_field='Should be ignored'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should still work, just ignoring invalid field
 | 
				
			||||||
 | 
					        self.assertTrue(result)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertEqual(case['case_title'], 'Updated Title')
 | 
				
			||||||
 | 
					        self.assertNotIn('invalid_field', case)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = self.case_manager.update_case(
 | 
				
			||||||
 | 
					            'CASE-001',
 | 
				
			||||||
 | 
					            invalid_field='Should be ignored'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should return None since no valid fields
 | 
				
			||||||
 | 
					        self.assertIsNone(result)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        case_before = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        created_at = case_before['created_at']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Small delay to ensure different timestamp
 | 
				
			||||||
 | 
					        import time
 | 
				
			||||||
 | 
					        time.sleep(0.01)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.case_manager.update_case('CASE-001', case_title='Updated')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        case_after = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        modified_at = case_after['modified_at']
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # modified_at should be different from created_at
 | 
				
			||||||
 | 
					        self.assertNotEqual(created_at, modified_at)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertFalse(result)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_close_case(self):
 | 
				
			||||||
 | 
					        """Test closing a case."""
 | 
				
			||||||
 | 
					        self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = self.case_manager.close_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertTrue(result)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertEqual(case['status'], 'closed')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_archive_case(self):
 | 
				
			||||||
 | 
					        """Test archiving a case."""
 | 
				
			||||||
 | 
					        self.case_manager.create_case('CASE-001', 'Test Case', 'Detective A')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = self.case_manager.archive_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertTrue(result)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify case exists
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertIsNotNone(case)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Delete case
 | 
				
			||||||
 | 
					        result = self.case_manager.delete_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertTrue(result)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify case is gone
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertIsNone(case)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_delete_nonexistent_case(self):
 | 
				
			||||||
 | 
					        """Test deleting a non-existent case returns False."""
 | 
				
			||||||
 | 
					        result = self.case_manager.delete_case('NONEXISTENT')
 | 
				
			||||||
 | 
					        self.assertFalse(result)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_case_manager_with_default_db_path(self):
 | 
				
			||||||
 | 
					        """Test CaseManager uses config default when no path provided."""
 | 
				
			||||||
 | 
					        with patch('forensictrails.core.case_manager.config') as mock_config:
 | 
				
			||||||
 | 
					            mock_config.database_path = self.test_db_path
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            cm = CaseManager()
 | 
				
			||||||
 | 
					            self.assertEqual(cm.db_path, self.test_db_path)
 | 
				
			||||||
 | 
					            cm.conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestCaseManagerIntegration(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Integration tests for CaseManager."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        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_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'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify creation
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertEqual(case['status'], 'active')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Close case
 | 
				
			||||||
 | 
					        self.case_manager.close_case('CASE-001')
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Delete case
 | 
				
			||||||
 | 
					        self.case_manager.delete_case('CASE-001')
 | 
				
			||||||
 | 
					        case = self.case_manager.get_case('CASE-001')
 | 
				
			||||||
 | 
					        self.assertIsNone(case)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_multiple_cases_workflow(self):
 | 
				
			||||||
 | 
					        """Test working with multiple cases simultaneously."""
 | 
				
			||||||
 | 
					        # Create multiple cases
 | 
				
			||||||
 | 
					        for i in range(1, 6):
 | 
				
			||||||
 | 
					            self.case_manager.create_case(
 | 
				
			||||||
 | 
					                f'CASE-{i:03d}',
 | 
				
			||||||
 | 
					                f'Case {i}',
 | 
				
			||||||
 | 
					                f'Detective {chr(64+i)}'  # Detective A, B, C, etc.
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Archive one
 | 
				
			||||||
 | 
					        self.case_manager.archive_case('CASE-005')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertEqual(len(active_cases), 2)  # 002, 004
 | 
				
			||||||
 | 
					        self.assertEqual(len(closed_cases), 2)  # 001, 003
 | 
				
			||||||
 | 
					        self.assertEqual(len(archived_cases), 1)  # 005
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    unittest.main()
 | 
				
			||||||
							
								
								
									
										177
									
								
								tests/test_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								tests/test_config.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,177 @@
 | 
				
			|||||||
 | 
					"""Unit tests for the config module."""
 | 
				
			||||||
 | 
					import unittest
 | 
				
			||||||
 | 
					import tempfile
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add the src directory to the path
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from forensictrails.utils.config import Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestConfig(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for Config class."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        """Set up test fixtures."""
 | 
				
			||||||
 | 
					        self.temp_dir = tempfile.mkdtemp()
 | 
				
			||||||
 | 
					        self.test_config_path = os.path.join(self.temp_dir, 'test_config.json')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_config_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_config_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_with_valid_file(self):
 | 
				
			||||||
 | 
					        """Test Config loads from valid JSON file."""
 | 
				
			||||||
 | 
					        config_data = {
 | 
				
			||||||
 | 
					            'database_path': 'custom.db',
 | 
				
			||||||
 | 
					            'database_template': 'custom_schema.sql',
 | 
				
			||||||
 | 
					            'database_schema_version': 2,
 | 
				
			||||||
 | 
					            'log_path': 'custom.log',
 | 
				
			||||||
 | 
					            'log_level': 'INFO'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        with open(self.test_config_path, 'w') as f:
 | 
				
			||||||
 | 
					            json.dump(config_data, f)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        config = Config(self.test_config_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_path, 'custom.db')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_template, 'custom_schema.sql')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_schema_version, 2)
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_path, 'custom.log')
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_level, 'INFO')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_with_nonexistent_file(self):
 | 
				
			||||||
 | 
					        """Test Config uses defaults when file doesn't exist."""
 | 
				
			||||||
 | 
					        config = Config('/nonexistent/config.json')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should use default values
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_template, 'schema.sql')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_path, 'forensic_trails.db')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_schema_version, 1)
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_path, 'forensic_trails.log')
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_level, 'DEBUG')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_with_partial_data(self):
 | 
				
			||||||
 | 
					        """Test Config uses defaults for missing keys."""
 | 
				
			||||||
 | 
					        config_data = {
 | 
				
			||||||
 | 
					            'database_path': 'partial.db',
 | 
				
			||||||
 | 
					            'log_level': 'WARNING'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        with open(self.test_config_path, 'w') as f:
 | 
				
			||||||
 | 
					            json.dump(config_data, f)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        config = Config(self.test_config_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should use provided values
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_path, 'partial.db')
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_level, 'WARNING')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should use defaults for missing keys
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_template, 'schema.sql')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_schema_version, 1)
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_path, 'forensic_trails.log')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_with_empty_file(self):
 | 
				
			||||||
 | 
					        """Test Config handles empty JSON file."""
 | 
				
			||||||
 | 
					        with open(self.test_config_path, 'w') as f:
 | 
				
			||||||
 | 
					            json.dump({}, f)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        config = Config(self.test_config_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should use all defaults
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_template, 'schema.sql')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_path, 'forensic_trails.db')
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_schema_version, 1)
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_path, 'forensic_trails.log')
 | 
				
			||||||
 | 
					        self.assertEqual(config.log_level, 'DEBUG')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_with_extra_keys(self):
 | 
				
			||||||
 | 
					        """Test Config ignores extra keys in JSON file."""
 | 
				
			||||||
 | 
					        config_data = {
 | 
				
			||||||
 | 
					            'database_path': 'test.db',
 | 
				
			||||||
 | 
					            'extra_key': 'should_be_ignored',
 | 
				
			||||||
 | 
					            'another_key': 123
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        with open(self.test_config_path, 'w') as f:
 | 
				
			||||||
 | 
					            json.dump(config_data, f)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        config = Config(self.test_config_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should load valid keys
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_path, 'test.db')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should not have extra attributes
 | 
				
			||||||
 | 
					        self.assertFalse(hasattr(config, 'extra_key'))
 | 
				
			||||||
 | 
					        self.assertFalse(hasattr(config, 'another_key'))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_default_constructor(self):
 | 
				
			||||||
 | 
					        """Test Config uses 'config.json' as default filename."""
 | 
				
			||||||
 | 
					        # This just tests that it doesn't crash with default parameter
 | 
				
			||||||
 | 
					        # The actual config.json file may or may not exist in the project
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            config = Config()
 | 
				
			||||||
 | 
					            # Should have all required attributes
 | 
				
			||||||
 | 
					            self.assertTrue(hasattr(config, 'database_path'))
 | 
				
			||||||
 | 
					            self.assertTrue(hasattr(config, 'database_template'))
 | 
				
			||||||
 | 
					            self.assertTrue(hasattr(config, 'database_schema_version'))
 | 
				
			||||||
 | 
					            self.assertTrue(hasattr(config, 'log_path'))
 | 
				
			||||||
 | 
					            self.assertTrue(hasattr(config, 'log_level'))
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            self.fail(f"Config() with default parameter raised exception: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestConfigDataTypes(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for Config data type handling."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        """Set up test fixtures."""
 | 
				
			||||||
 | 
					        self.temp_dir = tempfile.mkdtemp()
 | 
				
			||||||
 | 
					        self.test_config_path = os.path.join(self.temp_dir, 'test_config.json')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_config_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_config_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_string_values(self):
 | 
				
			||||||
 | 
					        """Test Config handles string values correctly."""
 | 
				
			||||||
 | 
					        config_data = {
 | 
				
			||||||
 | 
					            'database_path': 'test.db',
 | 
				
			||||||
 | 
					            'log_level': 'ERROR'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        with open(self.test_config_path, 'w') as f:
 | 
				
			||||||
 | 
					            json.dump(config_data, f)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        config = Config(self.test_config_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertIsInstance(config.database_path, str)
 | 
				
			||||||
 | 
					        self.assertIsInstance(config.log_level, str)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_config_integer_values(self):
 | 
				
			||||||
 | 
					        """Test Config handles integer values correctly."""
 | 
				
			||||||
 | 
					        config_data = {
 | 
				
			||||||
 | 
					            'database_schema_version': 5
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        with open(self.test_config_path, 'w') as f:
 | 
				
			||||||
 | 
					            json.dump(config_data, f)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        config = Config(self.test_config_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertIsInstance(config.database_schema_version, int)
 | 
				
			||||||
 | 
					        self.assertEqual(config.database_schema_version, 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    unittest.main()
 | 
				
			||||||
							
								
								
									
										383
									
								
								tests/test_database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										383
									
								
								tests/test_database.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,383 @@
 | 
				
			|||||||
 | 
					"""Unit tests for the database module."""
 | 
				
			||||||
 | 
					import unittest
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					import tempfile
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from unittest.mock import patch, MagicMock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add the src directory to the path so we can import forensictrails
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from forensictrails.db.database import (
 | 
				
			||||||
 | 
					    get_db_connection,
 | 
				
			||||||
 | 
					    validate_database_schema,
 | 
				
			||||||
 | 
					    create_fresh_database,
 | 
				
			||||||
 | 
					    initialize_database,
 | 
				
			||||||
 | 
					    show_db_schema
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestGetDbConnection(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for get_db_connection function."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        """Set up test fixtures."""
 | 
				
			||||||
 | 
					        self.temp_dir = tempfile.mkdtemp()
 | 
				
			||||||
 | 
					        self.test_db_path = os.path.join(self.temp_dir, 'test.db')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_db_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_db_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_get_db_connection_creates_connection(self):
 | 
				
			||||||
 | 
					        """Test that get_db_connection creates a valid connection."""
 | 
				
			||||||
 | 
					        conn = get_db_connection(self.test_db_path)
 | 
				
			||||||
 | 
					        self.assertIsInstance(conn, sqlite3.Connection)
 | 
				
			||||||
 | 
					        self.assertEqual(conn.row_factory, sqlite3.Row)
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_get_db_connection_creates_file(self):
 | 
				
			||||||
 | 
					        """Test that get_db_connection creates database file if it doesn't exist."""
 | 
				
			||||||
 | 
					        self.assertFalse(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					        conn = get_db_connection(self.test_db_path)
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestValidateDatabaseSchema(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for validate_database_schema function."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_db_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_db_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_validate_empty_database_returns_false(self):
 | 
				
			||||||
 | 
					        """Test that an empty database is invalid."""
 | 
				
			||||||
 | 
					        conn = sqlite3.connect(self.test_db_path)
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = validate_database_schema(self.test_db_path)
 | 
				
			||||||
 | 
					        self.assertFalse(result)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_validate_incomplete_database_returns_false(self):
 | 
				
			||||||
 | 
					        """Test that a database with missing tables is invalid."""
 | 
				
			||||||
 | 
					        conn = sqlite3.connect(self.test_db_path)
 | 
				
			||||||
 | 
					        cursor = conn.cursor()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Create only some of the required tables
 | 
				
			||||||
 | 
					        cursor.execute("""
 | 
				
			||||||
 | 
					            CREATE TABLE cases (
 | 
				
			||||||
 | 
					                case_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					                case_title TEXT NOT NULL
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					        cursor.execute("""
 | 
				
			||||||
 | 
					            CREATE TABLE notes (
 | 
				
			||||||
 | 
					                note_id TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					                case_id TEXT NOT NULL
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					        conn.commit()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = validate_database_schema(self.test_db_path)
 | 
				
			||||||
 | 
					        self.assertFalse(result)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_validate_complete_database_returns_true(self):
 | 
				
			||||||
 | 
					        """Test that a database with all required tables is valid."""
 | 
				
			||||||
 | 
					        # Create database with full schema
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        result = validate_database_schema(self.test_db_path)
 | 
				
			||||||
 | 
					        self.assertTrue(result)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_validate_nonexistent_database_returns_false(self):
 | 
				
			||||||
 | 
					        """Test that validation of non-existent database returns False."""
 | 
				
			||||||
 | 
					        result = validate_database_schema('/nonexistent/path/test.db')
 | 
				
			||||||
 | 
					        self.assertFalse(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestCreateFreshDatabase(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for create_fresh_database function."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_db_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_db_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_create_fresh_database_creates_file(self):
 | 
				
			||||||
 | 
					        """Test that create_fresh_database creates a database file."""
 | 
				
			||||||
 | 
					        self.assertFalse(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_create_fresh_database_creates_all_tables(self):
 | 
				
			||||||
 | 
					        """Test that create_fresh_database creates all required tables."""
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        conn = sqlite3.connect(self.test_db_path)
 | 
				
			||||||
 | 
					        cursor = conn.cursor()
 | 
				
			||||||
 | 
					        cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
 | 
				
			||||||
 | 
					        tables = {row[0] for row in cursor.fetchall()}
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        required_tables = {
 | 
				
			||||||
 | 
					            'cases', 'notes', 'evidence', 'chain_of_custody',
 | 
				
			||||||
 | 
					            'attachments', 'question_entries', 'users', 'tasks'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertEqual(tables, required_tables)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_create_fresh_database_returns_path(self):
 | 
				
			||||||
 | 
					        """Test that create_fresh_database returns the database path."""
 | 
				
			||||||
 | 
					        result = create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        self.assertEqual(result, self.test_db_path)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_create_fresh_database_on_clean_path(self):
 | 
				
			||||||
 | 
					        """Test that create_fresh_database works correctly on a clean database path."""
 | 
				
			||||||
 | 
					        # Ensure no database exists
 | 
				
			||||||
 | 
					        self.assertFalse(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Create fresh database
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify all tables exist
 | 
				
			||||||
 | 
					        conn = sqlite3.connect(self.test_db_path)
 | 
				
			||||||
 | 
					        cursor = conn.cursor()
 | 
				
			||||||
 | 
					        cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
 | 
				
			||||||
 | 
					        tables = {row[0] for row in cursor.fetchall()}
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        required_tables = {
 | 
				
			||||||
 | 
					            'cases', 'notes', 'evidence', 'chain_of_custody',
 | 
				
			||||||
 | 
					            'attachments', 'question_entries', 'users', 'tasks'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self.assertEqual(tables, required_tables)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestInitializeDatabase(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for initialize_database function."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_db_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_db_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.db.database.config')
 | 
				
			||||||
 | 
					    def test_initialize_database_creates_new_database(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that initialize_database creates a new database if none exists."""
 | 
				
			||||||
 | 
					        mock_config.database_path = self.test_db_path
 | 
				
			||||||
 | 
					        mock_config.database_template = 'schema.sql'
 | 
				
			||||||
 | 
					        mock_config.log_level = 'INFO'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertFalse(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        initialize_database(self.test_db_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					        self.assertTrue(validate_database_schema(self.test_db_path))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.db.database.config')
 | 
				
			||||||
 | 
					    def test_initialize_database_keeps_valid_database(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that initialize_database keeps a valid existing database."""
 | 
				
			||||||
 | 
					        mock_config.database_path = self.test_db_path
 | 
				
			||||||
 | 
					        mock_config.database_template = 'schema.sql'
 | 
				
			||||||
 | 
					        mock_config.log_level = 'INFO'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Create a valid database
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Add some data
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					        conn.commit()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Initialize again
 | 
				
			||||||
 | 
					        initialize_database(self.test_db_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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'")
 | 
				
			||||||
 | 
					        result = cursor.fetchone()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertIsNotNone(result)
 | 
				
			||||||
 | 
					        self.assertEqual(result[0], 'TEST-001')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.db.database.config')
 | 
				
			||||||
 | 
					    def test_initialize_database_recreates_invalid_database(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that initialize_database recreates an invalid database."""
 | 
				
			||||||
 | 
					        mock_config.database_path = self.test_db_path
 | 
				
			||||||
 | 
					        mock_config.database_template = 'schema.sql'
 | 
				
			||||||
 | 
					        mock_config.log_level = 'INFO'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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')")
 | 
				
			||||||
 | 
					        conn.commit()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Initialize
 | 
				
			||||||
 | 
					        initialize_database(self.test_db_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify database is now valid and old data is gone
 | 
				
			||||||
 | 
					        self.assertTrue(validate_database_schema(self.test_db_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        conn = sqlite3.connect(self.test_db_path)
 | 
				
			||||||
 | 
					        cursor = conn.cursor()
 | 
				
			||||||
 | 
					        cursor.execute("SELECT case_id FROM cases WHERE case_id = 'TEST-001'")
 | 
				
			||||||
 | 
					        result = cursor.fetchone()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertIsNone(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestShowDbSchema(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for show_db_schema function."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_db_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_db_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.db.database.logging')
 | 
				
			||||||
 | 
					    def test_show_db_schema_logs_tables(self, mock_logging):
 | 
				
			||||||
 | 
					        """Test that show_db_schema logs all table information."""
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        show_db_schema(self.test_db_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def test_show_db_schema_doesnt_raise_exception(self):
 | 
				
			||||||
 | 
					        """Test that show_db_schema handles execution without raising exceptions."""
 | 
				
			||||||
 | 
					        create_fresh_database(self.test_db_path, self.schema_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            show_db_schema(self.test_db_path)
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            self.fail(f"show_db_schema raised an exception: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestDatabaseIntegration(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Integration tests for the database module."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_db_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_db_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.db.database.config')
 | 
				
			||||||
 | 
					    def test_full_database_lifecycle(self, mock_config):
 | 
				
			||||||
 | 
					        """Test complete database lifecycle: create, use, invalidate, recreate."""
 | 
				
			||||||
 | 
					        mock_config.database_path = self.test_db_path
 | 
				
			||||||
 | 
					        mock_config.database_template = 'schema.sql'
 | 
				
			||||||
 | 
					        mock_config.log_level = 'INFO'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Step 1: Initialize new database
 | 
				
			||||||
 | 
					        initialize_database(self.test_db_path)
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(self.test_db_path))
 | 
				
			||||||
 | 
					        self.assertTrue(validate_database_schema(self.test_db_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Step 2: Add some data
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					        cursor.execute("""
 | 
				
			||||||
 | 
					            INSERT INTO users (user_id, username, full_name)
 | 
				
			||||||
 | 
					            VALUES ('USER-001', 'dsmith', 'Detective Smith')
 | 
				
			||||||
 | 
					        """)
 | 
				
			||||||
 | 
					        conn.commit()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # 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'")
 | 
				
			||||||
 | 
					        result = cursor.fetchone()
 | 
				
			||||||
 | 
					        self.assertEqual(result['case_title'], '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")
 | 
				
			||||||
 | 
					        conn.commit()
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Step 5: Verify database is now invalid
 | 
				
			||||||
 | 
					        self.assertFalse(validate_database_schema(self.test_db_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Step 6: Re-initialize (should recreate)
 | 
				
			||||||
 | 
					        initialize_database(self.test_db_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Step 7: Verify database is valid again and old data is gone
 | 
				
			||||||
 | 
					        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'")
 | 
				
			||||||
 | 
					        result = cursor.fetchone()
 | 
				
			||||||
 | 
					        self.assertIsNone(result)
 | 
				
			||||||
 | 
					        conn.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    unittest.main()
 | 
				
			||||||
							
								
								
									
										249
									
								
								tests/test_logging.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								tests/test_logging.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,249 @@
 | 
				
			|||||||
 | 
					"""Unit tests for the logging module."""
 | 
				
			||||||
 | 
					import unittest
 | 
				
			||||||
 | 
					import tempfile
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from unittest.mock import patch, MagicMock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add the src directory to the path
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from forensictrails.utils.logging import setup_logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestSetupLogging(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Test cases for setup_logging function."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        """Set up test fixtures."""
 | 
				
			||||||
 | 
					        self.temp_dir = tempfile.mkdtemp()
 | 
				
			||||||
 | 
					        self.test_log_path = os.path.join(self.temp_dir, 'test.log')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Clear any existing handlers
 | 
				
			||||||
 | 
					        logger = logging.getLogger()
 | 
				
			||||||
 | 
					        for handler in logger.handlers[:]:
 | 
				
			||||||
 | 
					            logger.removeHandler(handler)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        # Clear handlers after test
 | 
				
			||||||
 | 
					        logger = logging.getLogger()
 | 
				
			||||||
 | 
					        for handler in logger.handlers[:]:
 | 
				
			||||||
 | 
					            handler.close()
 | 
				
			||||||
 | 
					            logger.removeHandler(handler)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_log_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Clean up any nested directories
 | 
				
			||||||
 | 
					        if os.path.exists(self.temp_dir):
 | 
				
			||||||
 | 
					            for root, dirs, files in os.walk(self.temp_dir, topdown=False):
 | 
				
			||||||
 | 
					                for name in files:
 | 
				
			||||||
 | 
					                    os.remove(os.path.join(root, name))
 | 
				
			||||||
 | 
					                for name in dirs:
 | 
				
			||||||
 | 
					                    os.rmdir(os.path.join(root, name))
 | 
				
			||||||
 | 
					            os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_creates_log_file(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging creates the log file."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'DEBUG'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertFalse(os.path.exists(self.test_log_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(self.test_log_path))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_creates_nested_directories(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging creates nested directories."""
 | 
				
			||||||
 | 
					        nested_log_path = os.path.join(self.temp_dir, 'logs', 'nested', 'test.log')
 | 
				
			||||||
 | 
					        mock_config.log_path = nested_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'INFO'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertFalse(os.path.exists(nested_log_path))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(nested_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(nested_log_path))
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.isfile(nested_log_path))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_adds_handlers(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging adds file and stream handlers."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'DEBUG'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger = logging.getLogger()
 | 
				
			||||||
 | 
					        initial_handler_count = len(logger.handlers)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should add 2 handlers: FileHandler and StreamHandler
 | 
				
			||||||
 | 
					        self.assertEqual(len(logger.handlers), initial_handler_count + 2)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check handler types
 | 
				
			||||||
 | 
					        handler_types = [type(h).__name__ for h in logger.handlers]
 | 
				
			||||||
 | 
					        self.assertIn('FileHandler', handler_types)
 | 
				
			||||||
 | 
					        self.assertIn('StreamHandler', handler_types)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_sets_correct_log_level(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging sets the correct log level."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'WARNING'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger = logging.getLogger()
 | 
				
			||||||
 | 
					        self.assertEqual(logger.level, logging.WARNING)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_logs_messages(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging enables logging messages."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'DEBUG'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Log a test message
 | 
				
			||||||
 | 
					        test_message = "Test logging message"
 | 
				
			||||||
 | 
					        logging.info(test_message)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Force flush
 | 
				
			||||||
 | 
					        for handler in logging.getLogger().handlers:
 | 
				
			||||||
 | 
					            handler.flush()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check that message was written to file
 | 
				
			||||||
 | 
					        with open(self.test_log_path, 'r') as f:
 | 
				
			||||||
 | 
					            log_content = f.read()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertIn(test_message, log_content)
 | 
				
			||||||
 | 
					        self.assertIn('INFO', log_content)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_formatter(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging uses correct formatter."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'DEBUG'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Log a message
 | 
				
			||||||
 | 
					        logging.info("Formatter test")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Force flush
 | 
				
			||||||
 | 
					        for handler in logging.getLogger().handlers:
 | 
				
			||||||
 | 
					            handler.flush()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check log format
 | 
				
			||||||
 | 
					        with open(self.test_log_path, 'r') as f:
 | 
				
			||||||
 | 
					            log_content = f.read()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should contain timestamp, level, and message
 | 
				
			||||||
 | 
					        self.assertIn('INFO', log_content)
 | 
				
			||||||
 | 
					        self.assertIn('Formatter test', log_content)
 | 
				
			||||||
 | 
					        # Check for timestamp pattern (YYYY-MM-DD HH:MM:SS)
 | 
				
			||||||
 | 
					        import re
 | 
				
			||||||
 | 
					        timestamp_pattern = r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
 | 
				
			||||||
 | 
					        self.assertTrue(re.search(timestamp_pattern, log_content))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_uses_config_when_no_path_provided(self, mock_config):
 | 
				
			||||||
 | 
					        """Test that setup_logging uses config.log_path when no path is provided."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'INFO'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging()  # No path provided
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Should create file at config.log_path
 | 
				
			||||||
 | 
					        self.assertTrue(os.path.exists(self.test_log_path))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_setup_logging_different_log_levels(self, mock_config):
 | 
				
			||||||
 | 
					        """Test setup_logging with different log levels."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for level_name, level_value in [
 | 
				
			||||||
 | 
					            ('DEBUG', logging.DEBUG),
 | 
				
			||||||
 | 
					            ('INFO', logging.INFO),
 | 
				
			||||||
 | 
					            ('WARNING', logging.WARNING),
 | 
				
			||||||
 | 
					            ('ERROR', logging.ERROR),
 | 
				
			||||||
 | 
					            ('CRITICAL', logging.CRITICAL)
 | 
				
			||||||
 | 
					        ]:
 | 
				
			||||||
 | 
					            with self.subTest(level=level_name):
 | 
				
			||||||
 | 
					                # Clear handlers
 | 
				
			||||||
 | 
					                logger = logging.getLogger()
 | 
				
			||||||
 | 
					                for handler in logger.handlers[:]:
 | 
				
			||||||
 | 
					                    handler.close()
 | 
				
			||||||
 | 
					                    logger.removeHandler(handler)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                mock_config.log_level = level_name
 | 
				
			||||||
 | 
					                setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                self.assertEqual(logger.level, level_value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestLoggingIntegration(unittest.TestCase):
 | 
				
			||||||
 | 
					    """Integration tests for logging functionality."""
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        """Set up test fixtures."""
 | 
				
			||||||
 | 
					        self.temp_dir = tempfile.mkdtemp()
 | 
				
			||||||
 | 
					        self.test_log_path = os.path.join(self.temp_dir, 'integration.log')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Clear any existing handlers
 | 
				
			||||||
 | 
					        logger = logging.getLogger()
 | 
				
			||||||
 | 
					        for handler in logger.handlers[:]:
 | 
				
			||||||
 | 
					            logger.removeHandler(handler)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def tearDown(self):
 | 
				
			||||||
 | 
					        """Clean up test fixtures."""
 | 
				
			||||||
 | 
					        # Clear handlers after test
 | 
				
			||||||
 | 
					        logger = logging.getLogger()
 | 
				
			||||||
 | 
					        for handler in logger.handlers[:]:
 | 
				
			||||||
 | 
					            handler.close()
 | 
				
			||||||
 | 
					            logger.removeHandler(handler)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if os.path.exists(self.test_log_path):
 | 
				
			||||||
 | 
					            os.remove(self.test_log_path)
 | 
				
			||||||
 | 
					        os.rmdir(self.temp_dir)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @patch('forensictrails.utils.logging.config')
 | 
				
			||||||
 | 
					    def test_logging_multiple_messages(self, mock_config):
 | 
				
			||||||
 | 
					        """Test logging multiple messages of different levels."""
 | 
				
			||||||
 | 
					        mock_config.log_path = self.test_log_path
 | 
				
			||||||
 | 
					        mock_config.log_level = 'DEBUG'
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setup_logging(self.test_log_path)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Log messages at different levels
 | 
				
			||||||
 | 
					        logging.debug("Debug message")
 | 
				
			||||||
 | 
					        logging.info("Info message")
 | 
				
			||||||
 | 
					        logging.warning("Warning message")
 | 
				
			||||||
 | 
					        logging.error("Error message")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Force flush
 | 
				
			||||||
 | 
					        for handler in logging.getLogger().handlers:
 | 
				
			||||||
 | 
					            handler.flush()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Verify all messages are in the log
 | 
				
			||||||
 | 
					        with open(self.test_log_path, 'r') as f:
 | 
				
			||||||
 | 
					            log_content = f.read()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.assertIn("Debug message", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("Info message", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("Warning message", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("Error message", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("DEBUG", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("INFO", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("WARNING", log_content)
 | 
				
			||||||
 | 
					        self.assertIn("ERROR", log_content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    unittest.main()
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user