feat: implement comprehensive User Profile Management System (issue #107)

Complete user profile management system with CRUD operations and CLI integration:

## 🎯 Core Features Delivered
- **ProfileManager**: Complete CRUD operations with database integration
- **JSON Schema validation**: Comprehensive profile data validation
- **Multiple profile support**: Named profiles (personal, work, etc.)
- **Default profile system**: Set and manage default profiles
- **Profile inheritance**: Merge profiles with override capabilities
- **Template integration**: Extract flattened variables for template filling

## 📋 Profile Schema & Data Model
- **Structured data classes**: ProfileData, ContactInfo, Address, Organization
- **JSON Schema validation**: Full validation with field descriptions
- **Flexible structure**: Support for nested data and custom fields
- **Timestamp management**: Automatic created_at/updated_at tracking

## 🖥️ CLI Integration Complete
- **9 CLI Commands**: create, show, list, update, delete, set-default, export, import, variables
- **Multiple formats**: JSON, YAML, and table output formats
- **Interactive mode**: Guided profile creation and updates
- **Export/Import**: Full profile portability with validation
- **Template variables**: Extract flattened variables for template systems

## 📊 Implementation Stats
- **ProfileManager**: 500+ lines with comprehensive functionality
- **ProfileSchema**: 350+ lines with validation and data structures
- **CLI Commands**: 450+ lines of professional command interface
- **Test Coverage**: 66 tests (36 core + 30 CLI) with 100% pass rate

## 🚀 **Ready for Template Integration**
Foundation complete for Issue #99 (Auto Fill Templates) with:
- Template variable extraction from profiles
- Default profile system for seamless integration
- Profile merging for complex template scenarios
- Professional CLI for user profile management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 01:53:31 +02:00
parent 397b607442
commit b83dc14f7b
8 changed files with 2884 additions and 0 deletions

View File

@@ -0,0 +1,621 @@
"""
Tests for MarkiTect user profile CLI commands.
This module tests the command-line interface for user profile management
including creation, listing, updating, and template variable extraction.
"""
import pytest
import tempfile
import os
import json
from click.testing import CliRunner
from pathlib import Path
from markitect.profile.commands import profile_commands
from markitect.profile.manager import ProfileManager
from markitect.profile.schema import ProfileData, ContactInfo, Organization
class TestProfileCLICommands:
"""Test suite for profile management CLI commands."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
fd, path = tempfile.mkstemp(suffix='.db')
os.close(fd)
yield path
os.unlink(path)
@pytest.fixture
def setup_test_profile(self, temp_db):
"""Setup test database with a sample profile."""
profile_manager = ProfileManager(temp_db)
profile_data = ProfileData(
first_name="John",
last_name="Doe",
contact=ContactInfo(email="john@example.com", phone="555-0123"),
organization=Organization(name="Tech Corp", position="Developer")
)
profile_id = profile_manager.create_profile("test_profile", profile_data, "Test profile")
return temp_db, profile_id
@pytest.fixture
def runner(self):
"""Create Click test runner."""
return CliRunner()
def test_profile_create_basic(self, runner, temp_db):
"""Test basic profile creation."""
result = runner.invoke(profile_commands, [
'create', 'basic_profile',
'--first-name', 'Alice',
'--last-name', 'Smith',
'--email', 'alice@example.com',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Created profile 'basic_profile'" in result.output
# Verify profile was created
profile_manager = ProfileManager(temp_db)
profile = profile_manager.get_profile('basic_profile')
assert profile.first_name == "Alice"
assert profile.contact.email == "alice@example.com"
def test_profile_create_with_organization(self, runner, temp_db):
"""Test profile creation with organization info."""
result = runner.invoke(profile_commands, [
'create', 'work_profile',
'--first-name', 'Bob',
'--organization', 'ACME Corp',
'--position', 'Manager',
'--city', 'New York',
'--description', 'My work profile',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Created profile 'work_profile'" in result.output
# Verify organization details
profile_manager = ProfileManager(temp_db)
profile = profile_manager.get_profile('work_profile')
assert profile.organization.name == "ACME Corp"
assert profile.organization.position == "Manager"
assert profile.address.city == "New York"
def test_profile_create_set_default(self, runner, temp_db):
"""Test creating profile and setting as default."""
result = runner.invoke(profile_commands, [
'create', 'default_profile',
'--first-name', 'Default',
'--set-default',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Created profile 'default_profile'" in result.output
assert "🎯 Set 'default_profile' as default profile" in result.output
# Verify it's set as default
profile_manager = ProfileManager(temp_db)
default_profile = profile_manager.get_default_profile()
assert default_profile.first_name == "Default"
def test_profile_create_duplicate_name(self, runner, setup_test_profile):
"""Test creating profile with duplicate name fails."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'create', 'test_profile', # Same name as existing
'--first-name', 'Duplicate',
'--database', temp_db
])
assert result.exit_code == 1
assert "already exists" in result.output
def test_profile_show_table_format(self, runner, setup_test_profile):
"""Test showing profile in table format."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'show', 'test_profile',
'--database', temp_db
])
assert result.exit_code == 0
assert "👤 Profile: test_profile" in result.output
assert "First Name: John" in result.output
assert "📞 Contact Information" in result.output
assert "Email: john@example.com" in result.output
assert "🏢 Organization" in result.output
assert "Organization: Tech Corp" in result.output
def test_profile_show_json_format(self, runner, setup_test_profile):
"""Test showing profile in JSON format."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'show', 'test_profile',
'--format', 'json',
'--database', temp_db
])
assert result.exit_code == 0
# Parse JSON output
data = json.loads(result.output)
assert "profile_info" in data
assert "profile_data" in data
assert data["profile_info"]["name"] == "test_profile"
assert data["profile_data"]["first_name"] == "John"
def test_profile_show_nonexistent(self, runner, temp_db):
"""Test showing non-existent profile."""
result = runner.invoke(profile_commands, [
'show', 'nonexistent',
'--database', temp_db
])
assert result.exit_code == 1
assert "not found" in result.output
def test_profile_list_empty(self, runner, temp_db):
"""Test listing profiles when none exist."""
result = runner.invoke(profile_commands, [
'list',
'--database', temp_db
])
assert result.exit_code == 0
assert "No profiles found" in result.output
def test_profile_list_with_profiles(self, runner, temp_db):
"""Test listing profiles."""
# Create multiple profiles
profile_manager = ProfileManager(temp_db)
profile_manager.create_profile("profile1", ProfileData(first_name="User1"), "First profile")
profile_manager.create_profile("profile2", ProfileData(first_name="User2"), set_as_default=True)
result = runner.invoke(profile_commands, [
'list',
'--database', temp_db
])
assert result.exit_code == 0
assert "👤 User Profiles" in result.output
assert "profile1" in result.output
assert "profile2" in result.output
assert "Total: 2 profiles" in result.output
# Check default indicator
lines = result.output.split('\n')
profile2_line = [line for line in lines if 'profile2' in line][0]
assert "Yes" in profile2_line # Default column should show "Yes"
def test_profile_list_include_inactive(self, runner, temp_db):
"""Test listing profiles including inactive ones."""
profile_manager = ProfileManager(temp_db)
profile_manager.create_profile("active", ProfileData(first_name="Active"))
profile_manager.create_profile("inactive", ProfileData(first_name="Inactive"))
profile_manager.delete_profile("inactive", hard_delete=False) # Soft delete
# List without inactive
result = runner.invoke(profile_commands, [
'list',
'--database', temp_db
])
assert result.exit_code == 0
assert "active" in result.output
assert "inactive" not in result.output
# List with inactive
result = runner.invoke(profile_commands, [
'list',
'--include-inactive',
'--database', temp_db
])
assert result.exit_code == 0
assert "active" in result.output
assert "inactive" in result.output
def test_profile_list_json_format(self, runner, setup_test_profile):
"""Test listing profiles in JSON format."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'list',
'--format', 'json',
'--database', temp_db
])
assert result.exit_code == 0
# Parse JSON output
data = json.loads(result.output)
assert isinstance(data, list)
assert len(data) == 1
assert data[0]["name"] == "test_profile"
def test_profile_update_basic_fields(self, runner, setup_test_profile):
"""Test updating basic profile fields."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'update', 'test_profile',
'--first-name', 'Johnny',
'--email', 'johnny@example.com',
'--organization', 'New Corp',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Updated profile 'test_profile'" in result.output
# Verify updates
profile_manager = ProfileManager(temp_db)
profile = profile_manager.get_profile('test_profile')
assert profile.first_name == "Johnny"
assert profile.contact.email == "johnny@example.com"
assert profile.organization.name == "New Corp"
def test_profile_update_nonexistent(self, runner, temp_db):
"""Test updating non-existent profile."""
result = runner.invoke(profile_commands, [
'update', 'nonexistent',
'--first-name', 'New',
'--database', temp_db
])
assert result.exit_code == 1
assert "not found" in result.output
def test_profile_delete_soft(self, runner, setup_test_profile):
"""Test soft delete (deactivate) profile."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'delete', 'test_profile',
'--yes', # Skip confirmation
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Profile 'test_profile' deactivated" in result.output
# Verify profile is deactivated
profile_manager = ProfileManager(temp_db)
with pytest.raises(Exception): # ProfileNotFoundError
profile_manager.get_profile('test_profile')
def test_profile_delete_hard(self, runner, setup_test_profile):
"""Test hard delete (permanent) profile."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'delete', 'test_profile',
'--hard',
'--yes',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Profile 'test_profile' deleted permanently" in result.output
# Verify profile is completely gone
profile_manager = ProfileManager(temp_db)
all_profiles = profile_manager.list_profiles(include_inactive=True)
profile_names = [p["name"] for p in all_profiles]
assert "test_profile" not in profile_names
def test_profile_delete_with_confirmation(self, runner, setup_test_profile):
"""Test profile deletion with confirmation prompt."""
temp_db, _ = setup_test_profile
# Test declining confirmation
result = runner.invoke(profile_commands, [
'delete', 'test_profile',
'--database', temp_db
], input='n\n')
assert result.exit_code == 0
assert "Deletion cancelled" in result.output
# Profile should still exist
profile_manager = ProfileManager(temp_db)
profile = profile_manager.get_profile('test_profile')
assert profile.first_name == "John"
def test_profile_set_default(self, runner, temp_db):
"""Test setting profile as default."""
# Create multiple profiles
profile_manager = ProfileManager(temp_db)
profile_manager.create_profile("profile1", ProfileData(first_name="User1"))
profile_manager.create_profile("profile2", ProfileData(first_name="User2"))
result = runner.invoke(profile_commands, [
'set-default', 'profile2',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Set 'profile2' as default profile" in result.output
# Verify default was set
default_profile = profile_manager.get_default_profile()
assert default_profile.first_name == "User2"
def test_profile_set_default_nonexistent(self, runner, temp_db):
"""Test setting non-existent profile as default."""
result = runner.invoke(profile_commands, [
'set-default', 'nonexistent',
'--database', temp_db
])
assert result.exit_code == 1
assert "not found" in result.output
def test_profile_export_json(self, runner, setup_test_profile):
"""Test exporting profile to JSON."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'export', 'test_profile',
'--format', 'json',
'--database', temp_db
])
assert result.exit_code == 0
# Parse output as JSON
data = json.loads(result.output)
assert "profile_info" in data
assert "profile_data" in data
assert data["profile_data"]["first_name"] == "John"
def test_profile_export_to_file(self, runner, setup_test_profile):
"""Test exporting profile to file."""
temp_db, _ = setup_test_profile
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
output_file = f.name
try:
result = runner.invoke(profile_commands, [
'export', 'test_profile',
'--output', output_file,
'--database', temp_db
])
assert result.exit_code == 0
assert f"✅ Exported profile 'test_profile' to {output_file}" in result.output
# Verify file contents
data = json.loads(Path(output_file).read_text())
assert data["profile_data"]["first_name"] == "John"
finally:
os.unlink(output_file)
def test_profile_import_json(self, runner, temp_db):
"""Test importing profile from JSON file."""
import_data = {
"profile_info": {"description": "Imported profile"},
"profile_data": {
"first_name": "Imported",
"last_name": "User",
"contact": {"email": "imported@example.com"}
}
}
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
json.dump(import_data, f)
import_file = f.name
try:
result = runner.invoke(profile_commands, [
'import', 'imported_profile', import_file,
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Created profile 'imported_profile'" in result.output
# Verify imported data
profile_manager = ProfileManager(temp_db)
profile = profile_manager.get_profile('imported_profile')
assert profile.first_name == "Imported"
assert profile.contact.email == "imported@example.com"
finally:
os.unlink(import_file)
def test_profile_import_nonexistent_file(self, runner, temp_db):
"""Test importing from non-existent file."""
result = runner.invoke(profile_commands, [
'import', 'test', '/nonexistent/file.json',
'--database', temp_db
])
assert result.exit_code == 1
assert "not found" in result.output
def test_profile_import_overwrite(self, runner, setup_test_profile):
"""Test importing with overwrite flag."""
temp_db, _ = setup_test_profile
import_data = {
"profile_data": {
"first_name": "Overwritten",
"contact": {"email": "overwritten@example.com"}
}
}
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as f:
json.dump(import_data, f)
import_file = f.name
try:
result = runner.invoke(profile_commands, [
'import', 'test_profile', import_file,
'--overwrite',
'--database', temp_db
])
assert result.exit_code == 0
assert "✅ Updated profile 'test_profile'" in result.output
# Verify overwrite
profile_manager = ProfileManager(temp_db)
profile = profile_manager.get_profile('test_profile')
assert profile.first_name == "Overwritten"
finally:
os.unlink(import_file)
def test_profile_variables_table_format(self, runner, setup_test_profile):
"""Test showing template variables in table format."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'variables',
'--profile', 'test_profile',
'--database', temp_db
])
assert result.exit_code == 0
assert "📋 Template Variables - test_profile" in result.output
assert "first_name" in result.output
assert "John" in result.output
assert "contact.email" in result.output
assert "organization.name" in result.output
def test_profile_variables_json_format(self, runner, setup_test_profile):
"""Test showing template variables in JSON format."""
temp_db, _ = setup_test_profile
result = runner.invoke(profile_commands, [
'variables',
'--profile', 'test_profile',
'--format', 'json',
'--database', temp_db
])
assert result.exit_code == 0
# Parse JSON output
data = json.loads(result.output)
assert data["first_name"] == "John"
assert data["contact.email"] == "john@example.com"
assert data["organization.name"] == "Tech Corp"
def test_profile_variables_default_profile(self, runner, temp_db):
"""Test showing variables from default profile."""
# Create and set default profile
profile_manager = ProfileManager(temp_db)
profile_data = ProfileData(first_name="Default", last_name="User")
profile_manager.create_profile("default", profile_data, set_as_default=True)
result = runner.invoke(profile_commands, [
'variables', # No --profile specified, should use default
'--database', temp_db
])
assert result.exit_code == 0
assert "📋 Template Variables - (default)" in result.output
assert "first_name" in result.output
assert "Default" in result.output
def test_profile_variables_no_default(self, runner, temp_db):
"""Test showing variables when no default profile set."""
result = runner.invoke(profile_commands, [
'variables',
'--database', temp_db
])
assert result.exit_code == 0
assert "No default profile set" in result.output
def test_profile_help_commands(self, runner):
"""Test help output for profile commands."""
# Test main profile help
result = runner.invoke(profile_commands, ['--help'])
assert result.exit_code == 0
assert "User profile management commands" in result.output
# Test create help
result = runner.invoke(profile_commands, ['create', '--help'])
assert result.exit_code == 0
assert "Create a new user profile" in result.output
# Test show help
result = runner.invoke(profile_commands, ['show', '--help'])
assert result.exit_code == 0
assert "Show profile details" in result.output
def test_profile_commands_missing_database(self, runner):
"""Test profile commands without database specification."""
# These should use default config path
result = runner.invoke(profile_commands, [
'list'
])
# Should succeed with default database configuration
assert result.exit_code == 0
def test_complex_profile_workflow(self, runner, temp_db):
"""Test complex workflow with multiple operations."""
# Create profile
result = runner.invoke(profile_commands, [
'create', 'workflow_test',
'--first-name', 'Workflow',
'--last-name', 'Test',
'--email', 'workflow@example.com',
'--organization', 'Test Corp',
'--description', 'Workflow test profile',
'--database', temp_db
])
assert result.exit_code == 0
# Update profile
result = runner.invoke(profile_commands, [
'update', 'workflow_test',
'--first-name', 'Updated',
'--position', 'Manager',
'--database', temp_db
])
assert result.exit_code == 0
# Set as default
result = runner.invoke(profile_commands, [
'set-default', 'workflow_test',
'--database', temp_db
])
assert result.exit_code == 0
# Show variables
result = runner.invoke(profile_commands, [
'variables',
'--database', temp_db
])
assert result.exit_code == 0
assert "Updated" in result.output # Updated name
assert "Manager" in result.output # New position
# Export profile
result = runner.invoke(profile_commands, [
'export', 'workflow_test',
'--database', temp_db
])
assert result.exit_code == 0
# Verify export contains updates
data = json.loads(result.output)
assert data["profile_data"]["first_name"] == "Updated"
assert data["profile_data"]["organization"]["position"] == "Manager"

View File

@@ -0,0 +1,568 @@
"""
Tests for MarkiTect User Profile Management System.
This module tests the complete user profile management functionality including:
- CRUD operations for user profiles
- Profile validation and schema compliance
- Database integration and data persistence
- Profile inheritance and merging
- Template variable extraction
- Export/import functionality
"""
import pytest
import tempfile
import os
import json
from datetime import datetime
from pathlib import Path
from markitect.profile.manager import ProfileManager, ProfileNotFoundError, ProfileValidationError
from markitect.profile.schema import ProfileSchema, ProfileData, ContactInfo, Address, Organization
class TestProfileSchema:
"""Test suite for profile schema and validation."""
def test_profile_data_creation(self):
"""Test ProfileData dataclass creation."""
profile = ProfileData(
first_name="John",
last_name="Doe",
contact=ContactInfo(email="john@example.com"),
address=Address(city="Boston", country="USA")
)
assert profile.first_name == "John"
assert profile.last_name == "Doe"
assert profile.contact.email == "john@example.com"
assert profile.address.city == "Boston"
def test_profile_data_to_dict(self):
"""Test converting ProfileData to dictionary."""
profile = ProfileData(
first_name="Jane",
last_name="Smith",
contact=ContactInfo(email="jane@example.com", phone="123-456-7890")
)
profile_dict = profile.to_dict()
assert profile_dict["first_name"] == "Jane"
assert profile_dict["last_name"] == "Smith"
assert profile_dict["contact"]["email"] == "jane@example.com"
assert profile_dict["contact"]["phone"] == "123-456-7890"
def test_profile_data_from_dict(self):
"""Test creating ProfileData from dictionary."""
data = {
"first_name": "Bob",
"last_name": "Johnson",
"contact": {
"email": "bob@example.com",
"phone": "098-765-4321"
},
"organization": {
"name": "ACME Corp",
"position": "Developer"
}
}
profile = ProfileData.from_dict(data)
assert profile.first_name == "Bob"
assert profile.contact.email == "bob@example.com"
assert profile.organization.name == "ACME Corp"
def test_profile_schema_validation_success(self):
"""Test successful profile schema validation."""
valid_data = {
"first_name": "Alice",
"last_name": "Wilson",
"contact": {
"email": "alice@example.com"
},
"address": {
"city": "New York",
"country": "USA"
}
}
# Should not raise exception
ProfileSchema.validate(valid_data)
assert ProfileSchema.is_valid(valid_data) is True
def test_profile_schema_validation_failure(self):
"""Test profile schema validation with invalid data."""
invalid_data = {
"first_name": "A" * 150, # Too long
"contact": {
"email": "invalid-email" # Invalid email format
}
}
with pytest.raises(Exception): # ValidationError
ProfileSchema.validate(invalid_data)
assert ProfileSchema.is_valid(invalid_data) is False
def test_profile_schema_get_field_description(self):
"""Test getting field descriptions from schema."""
email_desc = ProfileSchema.get_field_description("contact.email")
assert "Email address" in email_desc
name_desc = ProfileSchema.get_field_description("first_name")
assert "first name" in name_desc.lower()
invalid_desc = ProfileSchema.get_field_description("nonexistent.field")
assert invalid_desc is None
def test_profile_schema_get_all_fields(self):
"""Test getting all available field paths."""
fields = ProfileSchema.get_all_fields()
assert "first_name" in fields
assert "contact.email" in fields
assert "organization.name" in fields
assert "address.city" in fields
assert len(fields) > 10 # Should have many fields
def test_create_empty_profile(self):
"""Test creating empty profile with timestamps."""
profile = ProfileSchema.create_empty_profile()
assert profile.first_name is None
assert profile.contact is not None
assert profile.created_at is not None
assert profile.updated_at is not None
class TestProfileManager:
"""Test suite for profile manager functionality."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
fd, path = tempfile.mkstemp(suffix='.db')
os.close(fd)
yield path
os.unlink(path)
@pytest.fixture
def profile_manager(self, temp_db):
"""Create profile manager with temporary database."""
return ProfileManager(temp_db)
@pytest.fixture
def sample_profile_data(self):
"""Sample profile data for testing."""
return ProfileData(
first_name="John",
last_name="Doe",
full_name="John Doe",
contact=ContactInfo(
email="john.doe@example.com",
phone="555-0123"
),
organization=Organization(
name="Tech Corp",
position="Senior Developer"
),
address=Address(
city="San Francisco",
state="CA",
country="USA"
)
)
def test_create_profile_success(self, profile_manager, sample_profile_data):
"""Test successful profile creation."""
profile_id = profile_manager.create_profile(
name="personal",
data=sample_profile_data,
description="My personal profile"
)
assert profile_id is not None
assert isinstance(profile_id, int)
# Verify profile was created
retrieved_profile = profile_manager.get_profile("personal")
assert retrieved_profile.first_name == "John"
assert retrieved_profile.last_name == "Doe"
assert retrieved_profile.contact.email == "john.doe@example.com"
def test_create_profile_with_dict(self, profile_manager):
"""Test profile creation with dictionary data."""
profile_data = {
"first_name": "Jane",
"last_name": "Smith",
"contact": {
"email": "jane@example.com"
}
}
profile_id = profile_manager.create_profile("work", profile_data)
assert profile_id is not None
retrieved_profile = profile_manager.get_profile("work")
assert retrieved_profile.first_name == "Jane"
def test_create_profile_duplicate_name(self, profile_manager, sample_profile_data):
"""Test creating profile with duplicate name fails."""
profile_manager.create_profile("test", sample_profile_data)
with pytest.raises(ValueError, match="already exists"):
profile_manager.create_profile("test", sample_profile_data)
def test_create_profile_invalid_data(self, profile_manager):
"""Test creating profile with invalid data fails."""
invalid_data = {
"first_name": "A" * 150, # Too long
"contact": {
"email": "invalid-email"
}
}
with pytest.raises(ProfileValidationError):
profile_manager.create_profile("invalid", invalid_data)
def test_create_profile_set_default(self, profile_manager, sample_profile_data):
"""Test creating profile and setting as default."""
profile_id = profile_manager.create_profile(
"default_test",
sample_profile_data,
set_as_default=True
)
# Verify it's set as default
default_profile = profile_manager.get_default_profile()
assert default_profile is not None
assert default_profile.first_name == "John"
def test_get_profile_not_found(self, profile_manager):
"""Test getting non-existent profile raises error."""
with pytest.raises(ProfileNotFoundError):
profile_manager.get_profile("nonexistent")
def test_get_profile_info(self, profile_manager, sample_profile_data):
"""Test getting profile metadata."""
profile_manager.create_profile("info_test", sample_profile_data, description="Test profile")
profile_info = profile_manager.get_profile_info("info_test")
assert profile_info["name"] == "info_test"
assert profile_info["description"] == "Test profile"
assert profile_info["is_active"] is True
assert profile_info["is_default"] is False
assert "created_at" in profile_info
assert "updated_at" in profile_info
def test_update_profile_success(self, profile_manager, sample_profile_data):
"""Test successful profile update."""
profile_manager.create_profile("update_test", sample_profile_data)
# Update some fields
updated_data = ProfileData(
first_name="Johnny",
last_name="Doe",
contact=ContactInfo(email="johnny@example.com")
)
success = profile_manager.update_profile("update_test", updated_data, "Updated description")
assert success is True
# Verify updates
updated_profile = profile_manager.get_profile("update_test")
assert updated_profile.first_name == "Johnny"
assert updated_profile.contact.email == "johnny@example.com"
profile_info = profile_manager.get_profile_info("update_test")
assert profile_info["description"] == "Updated description"
def test_update_profile_not_found(self, profile_manager):
"""Test updating non-existent profile fails."""
with pytest.raises(ProfileNotFoundError):
profile_manager.update_profile("nonexistent", ProfileData())
def test_delete_profile_soft_delete(self, profile_manager, sample_profile_data):
"""Test soft delete (deactivate) profile."""
profile_manager.create_profile("delete_test", sample_profile_data)
success = profile_manager.delete_profile("delete_test", hard_delete=False)
assert success is True
# Profile should not be found in active profiles
with pytest.raises(ProfileNotFoundError):
profile_manager.get_profile("delete_test")
# But should appear in list with inactive profiles
all_profiles = profile_manager.list_profiles(include_inactive=True)
inactive_names = [p["name"] for p in all_profiles if not p["is_active"]]
assert "delete_test" in inactive_names
def test_delete_profile_hard_delete(self, profile_manager, sample_profile_data):
"""Test hard delete (permanent) profile."""
profile_manager.create_profile("hard_delete_test", sample_profile_data)
success = profile_manager.delete_profile("hard_delete_test", hard_delete=True)
assert success is True
# Profile should not appear anywhere
all_profiles = profile_manager.list_profiles(include_inactive=True)
all_names = [p["name"] for p in all_profiles]
assert "hard_delete_test" not in all_names
def test_list_profiles_active_only(self, profile_manager, sample_profile_data):
"""Test listing active profiles only."""
# Create multiple profiles
profile_manager.create_profile("active1", sample_profile_data)
profile_manager.create_profile("active2", sample_profile_data)
profile_manager.create_profile("to_deactivate", sample_profile_data)
# Deactivate one
profile_manager.delete_profile("to_deactivate", hard_delete=False)
profiles = profile_manager.list_profiles(include_inactive=False)
active_names = [p["name"] for p in profiles]
assert "active1" in active_names
assert "active2" in active_names
assert "to_deactivate" not in active_names
def test_list_profiles_include_inactive(self, profile_manager, sample_profile_data):
"""Test listing all profiles including inactive."""
profile_manager.create_profile("active", sample_profile_data)
profile_manager.create_profile("inactive", sample_profile_data)
profile_manager.delete_profile("inactive", hard_delete=False)
profiles = profile_manager.list_profiles(include_inactive=True)
all_names = [p["name"] for p in profiles]
assert "active" in all_names
assert "inactive" in all_names
assert len(profiles) == 2
def test_set_default_profile(self, profile_manager, sample_profile_data):
"""Test setting default profile."""
# Create multiple profiles
profile_manager.create_profile("profile1", sample_profile_data)
profile_manager.create_profile("profile2", sample_profile_data)
# Set profile2 as default
success = profile_manager.set_default_profile("profile2")
assert success is True
# Verify default
default_profile = profile_manager.get_default_profile()
assert default_profile is not None
assert default_profile.first_name == "John" # From sample data
# Check that only profile2 is marked as default
profiles = profile_manager.list_profiles()
default_profiles = [p for p in profiles if p["is_default"]]
assert len(default_profiles) == 1
assert default_profiles[0]["name"] == "profile2"
def test_get_default_profile_none_set(self, profile_manager):
"""Test getting default profile when none is set."""
default_profile = profile_manager.get_default_profile()
assert default_profile is None
def test_export_profile_json(self, profile_manager, sample_profile_data):
"""Test exporting profile to JSON format."""
profile_manager.create_profile("export_test", sample_profile_data, "Test for export")
exported = profile_manager.export_profile("export_test", format="json")
# Parse and verify
data = json.loads(exported)
assert "profile_info" in data
assert "profile_data" in data
assert data["profile_info"]["name"] == "export_test"
assert data["profile_data"]["first_name"] == "John"
def test_export_profile_yaml(self, profile_manager, sample_profile_data):
"""Test exporting profile to YAML format."""
profile_manager.create_profile("yaml_test", sample_profile_data)
try:
exported = profile_manager.export_profile("yaml_test", format="yaml")
assert "first_name: John" in exported
assert "profile_info:" in exported
except ValueError as e:
if "PyYAML" in str(e):
pytest.skip("PyYAML not available")
raise
def test_export_profile_unsupported_format(self, profile_manager, sample_profile_data):
"""Test exporting profile with unsupported format."""
profile_manager.create_profile("format_test", sample_profile_data)
with pytest.raises(ValueError, match="Unsupported export format"):
profile_manager.export_profile("format_test", format="xml")
def test_import_profile_json(self, profile_manager):
"""Test importing profile from JSON."""
import_data = {
"profile_info": {
"description": "Imported profile"
},
"profile_data": {
"first_name": "Imported",
"last_name": "User",
"contact": {
"email": "imported@example.com"
}
}
}
json_data = json.dumps(import_data)
profile_id = profile_manager.import_profile("imported", json_data, format="json")
assert profile_id is not None
# Verify imported data
imported_profile = profile_manager.get_profile("imported")
assert imported_profile.first_name == "Imported"
assert imported_profile.contact.email == "imported@example.com"
profile_info = profile_manager.get_profile_info("imported")
assert profile_info["description"] == "Imported profile"
def test_import_profile_overwrite(self, profile_manager, sample_profile_data):
"""Test importing profile with overwrite."""
# Create existing profile
profile_manager.create_profile("overwrite_test", sample_profile_data)
# Import new data
import_data = {
"profile_data": {
"first_name": "Overwritten",
"contact": {"email": "new@example.com"}
}
}
json_data = json.dumps(import_data)
profile_id = profile_manager.import_profile("overwrite_test", json_data, overwrite=True)
# Verify overwrite
profile = profile_manager.get_profile("overwrite_test")
assert profile.first_name == "Overwritten"
def test_import_profile_no_overwrite_fails(self, profile_manager, sample_profile_data):
"""Test importing existing profile without overwrite fails."""
profile_manager.create_profile("existing", sample_profile_data)
import_data = {"profile_data": {"first_name": "New"}}
json_data = json.dumps(import_data)
with pytest.raises(ValueError, match="already exists"):
profile_manager.import_profile("existing", json_data, overwrite=False)
def test_merge_profiles(self, profile_manager):
"""Test merging two profiles."""
# Create base profile
base_data = ProfileData(
first_name="Base",
last_name="User",
contact=ContactInfo(email="base@example.com", phone="123-456-7890"),
address=Address(city="BaseCity")
)
profile_manager.create_profile("base", base_data)
# Create override profile
override_data = ProfileData(
first_name="Override",
contact=ContactInfo(email="override@example.com"),
organization=Organization(name="Override Corp")
)
profile_manager.create_profile("override", override_data)
# Merge profiles
merged_profile = profile_manager.merge_profiles("base", "override")
# Verify merge results
assert merged_profile.first_name == "Override" # Overridden
assert merged_profile.last_name == "User" # From base
assert merged_profile.contact.email == "override@example.com" # Overridden
assert merged_profile.contact.phone == "123-456-7890" # From base
assert merged_profile.address.city == "BaseCity" # From base
assert merged_profile.organization.name == "Override Corp" # From override
def test_get_template_variables(self, profile_manager, sample_profile_data):
"""Test extracting template variables from profile."""
profile_manager.create_profile("template_test", sample_profile_data, set_as_default=True)
variables = profile_manager.get_template_variables("template_test")
# Check flattened variables
assert variables["first_name"] == "John"
assert variables["last_name"] == "Doe"
assert variables["contact.email"] == "john.doe@example.com"
assert variables["organization.name"] == "Tech Corp"
assert variables["address.city"] == "San Francisco"
# Check computed variable
assert variables["full_name"] == "John Doe"
def test_get_template_variables_default_profile(self, profile_manager, sample_profile_data):
"""Test getting template variables from default profile."""
profile_manager.create_profile("default_vars", sample_profile_data, set_as_default=True)
# Get variables without specifying profile name
variables = profile_manager.get_template_variables()
assert variables["first_name"] == "John"
assert "contact.email" in variables
def test_get_template_variables_no_default(self, profile_manager):
"""Test getting template variables when no default profile."""
variables = profile_manager.get_template_variables()
assert variables == {}
def test_database_integration(self, profile_manager, sample_profile_data):
"""Test database persistence and retrieval."""
# Create profile
profile_id = profile_manager.create_profile("db_test", sample_profile_data)
# Create new manager instance with same database
new_manager = ProfileManager(profile_manager.db_path)
# Verify data persists
retrieved_profile = new_manager.get_profile("db_test")
assert retrieved_profile.first_name == "John"
assert retrieved_profile.contact.email == "john.doe@example.com"
def test_profile_timestamps(self, profile_manager, sample_profile_data):
"""Test profile creation and update timestamps."""
before_create = datetime.now().isoformat()
profile_manager.create_profile("timestamp_test", sample_profile_data)
after_create = datetime.now().isoformat()
profile_info = profile_manager.get_profile_info("timestamp_test")
assert before_create <= profile_info["created_at"] <= after_create
assert before_create <= profile_info["updated_at"] <= after_create
# Update profile
before_update = datetime.now().isoformat()
profile_manager.update_profile("timestamp_test", ProfileData(first_name="Updated"))
after_update = datetime.now().isoformat()
updated_info = profile_manager.get_profile_info("timestamp_test")
assert updated_info["created_at"] == profile_info["created_at"] # Unchanged
assert before_update <= updated_info["updated_at"] <= after_update
def test_edge_cases(self, profile_manager):
"""Test edge cases and boundary conditions."""
# Empty profile
empty_profile = ProfileData()
profile_manager.create_profile("empty", empty_profile)
retrieved = profile_manager.get_profile("empty")
assert retrieved.first_name is None
# Profile with only custom fields
custom_profile = ProfileData(custom_fields={"hobby": "coding", "level": "expert"})
profile_manager.create_profile("custom", custom_profile)
retrieved_custom = profile_manager.get_profile("custom")
assert retrieved_custom.custom_fields["hobby"] == "coding"