"""
FERPA Compliance Security Tests
Tests for Family Educational Rights and Privacy Act compliance,
focusing on student data protection and PII handling.
Test Coverage:
- TC-1.1: PII Anonymization
- TC-1.2: Audit Logging
- TC-1.3: Data Access Controls
- TC-1.4: Data Retention
"""
import os
from unittest.mock import patch
import pytest
from canvas_mcp.core.anonymization import anonymize_response_data
from canvas_mcp.core.config import Config
class TestPIIAnonymization:
"""Test student PII anonymization functionality."""
def test_student_name_anonymization(self):
"""TC-1.1.1: Verify student names are anonymized when enabled."""
# Setup
sample_data = {
"user": {
"name": "John Doe",
"id": 12345,
"email": "john.doe@example.com"
}
}
# Test with anonymization enabled
with patch.dict(os.environ, {"ENABLE_DATA_ANONYMIZATION": "true"}):
result = anonymize_response_data(sample_data, "test_endpoint")
# Verify name is anonymized
assert result["user"]["name"] != "John Doe"
assert result["user"]["name"].startswith("Student_")
# Verify ID preserved for functionality
assert result["user"]["id"] == 12345
def test_student_email_anonymization(self):
"""TC-1.1.2: Verify student emails are anonymized."""
sample_data = {
"user": {
"name": "Jane Smith",
"id": 54321, # ID is required for anonymization
"email": "jane.smith@university.edu"
}
}
with patch.dict(os.environ, {"ENABLE_DATA_ANONYMIZATION": "true"}):
result = anonymize_response_data(sample_data, "test_endpoint")
# Verify email is anonymized (format: student_xxxx@example.edu)
assert result["user"]["email"] != "jane.smith@university.edu"
assert "@example.edu" in result["user"]["email"]
def test_anonymization_consistency(self):
"""TC-1.1.1: Verify same student gets same anonymous ID across calls."""
sample_data = {
"user": {"name": "Test Student", "id": 99999}
}
with patch.dict(os.environ, {"ENABLE_DATA_ANONYMIZATION": "true"}):
result1 = anonymize_response_data(sample_data.copy(), "test_endpoint")
result2 = anonymize_response_data(sample_data.copy(), "test_endpoint")
# Same student should get same anonymous name
assert result1["user"]["name"] == result2["user"]["name"]
def test_no_pii_in_error_messages(self):
"""TC-1.1.3: Verify PII not leaked in error messages."""
# This test would verify error handling doesn't expose PII
# Implementation depends on error handling structure
pass
def test_no_pii_in_logs(self):
"""TC-1.1.4: Verify PII is redacted in log context when redaction is enabled."""
from canvas_mcp.core.logging import _sanitize_context
context = {
"user_id": 12345,
"email": "student@university.edu",
"name": "John Doe",
}
with patch.dict(os.environ, {"LOG_REDACT_PII": "true"}):
result = _sanitize_context(context)
assert result["user_id"] == "[REDACTED]"
assert result["email"] == "[REDACTED]"
assert result["name"] == "[REDACTED]"
class TestAuditLogging:
"""Test audit logging for PII access."""
def test_pii_access_logged(self, capsys):
"""TC-1.2.1: Verify data access creates audit log entry when enabled."""
import tempfile
from canvas_mcp.core import config as cfg_mod
from canvas_mcp.core.audit import (
init_audit_logging,
log_data_access,
reset_audit_state,
)
reset_audit_state()
with patch.dict(os.environ, {
"LOG_ACCESS_EVENTS": "true",
"LOG_EXECUTION_EVENTS": "false",
"CANVAS_API_TOKEN": "test",
"AUDIT_LOG_DIR": tempfile.mkdtemp(),
}):
old = cfg_mod._config
cfg_mod._config = None
try:
init_audit_logging()
log_data_access("GET", "/courses/123/users/456", "success")
captured = capsys.readouterr()
assert "data_access" in captured.err
# Verify endpoint is sanitized (no raw IDs)
assert "123" not in captured.err
assert "456" not in captured.err
finally:
cfg_mod._config = old
reset_audit_state()
@pytest.mark.skip(reason="Audit logging not yet implemented")
def test_audit_log_integrity(self):
"""TC-1.2.2: Verify audit log integrity."""
# Test that audit logs cannot be tampered with
pass
@pytest.mark.skip(reason="Audit logging not yet implemented")
def test_audit_log_retention(self):
"""TC-1.2.3: Verify audit logs are retained per policy."""
pass
class TestDataAccessControls:
"""Test data access control mechanisms."""
def test_student_tools_self_endpoints_only(self):
"""TC-2.2.1: Verify student tools only access own data."""
# Test that student-specific tools use Canvas "self" endpoints
# Would require analyzing tool implementations
pass
def test_educator_permission_required(self):
"""TC-2.2.2: Verify educator tools require proper permissions."""
# Test that educator tools check for instructor/TA role
pass
class TestDataRetention:
"""Test data retention and cleanup."""
def test_temporary_files_cleanup(self):
"""TC-1.3.1: Verify temporary files containing PII are cleaned up."""
# Test that temporary files are deleted after use
pass
@pytest.mark.skip(reason="Data retention policy not yet implemented")
def test_data_retention_policy(self):
"""TC-1.3.2: Verify data retention policy is enforced."""
pass
class TestComplianceFeatures:
"""Test FERPA compliance features."""
def test_anonymization_config_option(self):
"""Verify anonymization can be enabled via configuration."""
with patch.dict(os.environ, {"ENABLE_DATA_ANONYMIZATION": "true"}):
Config()
# Verify config reflects anonymization setting
# Implementation depends on Config structure
def test_anonymization_disabled_by_default_for_students(self):
"""Verify students don't need anonymization (self-endpoints)."""
# Students access only their own data, no anonymization needed
pass
if __name__ == "__main__":
pytest.main([__file__, "-v"])