updates
This commit is contained in:
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()
|
||||||
Reference in New Issue
Block a user