PYTHONPython

test pwm

real world projects / password manager / tests

PYTHON
test_pwm.py🐍
"""
Comprehensive tests for password manager.
"""

import pytest
from pathlib import Path
from datetime import datetime

from pwm.crypto import (
    generate_salt, derive_key, encrypt, decrypt,
    generate_hmac, verify_hmac
)
from pwm.generator import (
    generate_password, generate_passphrase,
    calculate_password_strength, calculate_entropy
)
from pwm.models import PasswordEntry
from pwm.manager import PasswordManager
from pwm.storage import VaultStorage


class TestCrypto:
    """Tests for cryptographic functions."""
    
    def test_generate_salt(self):
        """Test salt generation."""
        salt1 = generate_salt()
        salt2 = generate_salt()
        
        assert len(salt1) == 16
        assert len(salt2) == 16
        assert salt1 != salt2
    
    def test_derive_key(self):
        """Test key derivation."""
        password = "test_password"
        salt = generate_salt()
        
        key1 = derive_key(password, salt)
        key2 = derive_key(password, salt)
        
        assert len(key1) == 32
        assert key1 == key2
    
    def test_derive_key_different_salts(self):
        """Test different salts produce different keys."""
        password = "test_password"
        salt1 = generate_salt()
        salt2 = generate_salt()
        
        key1 = derive_key(password, salt1)
        key2 = derive_key(password, salt2)
        
        assert key1 != key2
    
    def test_encrypt_decrypt(self, sample_key: bytes):
        """Test encryption and decryption."""
        plaintext = b"Hello, World! This is a test message."
        
        ciphertext = encrypt(plaintext, sample_key)
        decrypted = decrypt(ciphertext, sample_key)
        
        assert decrypted == plaintext
    
    def test_encrypt_produces_different_output(self, sample_key: bytes):
        """Test that encryption produces different ciphertext each time."""
        plaintext = b"Same message"
        
        ciphertext1 = encrypt(plaintext, sample_key)
        ciphertext2 = encrypt(plaintext, sample_key)
        
        # Different nonces should produce different ciphertext
        assert ciphertext1 != ciphertext2
    
    def test_decrypt_wrong_key(self, sample_key: bytes):
        """Test decryption fails with wrong key."""
        plaintext = b"Secret message"
        ciphertext = encrypt(plaintext, sample_key)
        
        wrong_key = b"0" * 32
        
        with pytest.raises(Exception):
            decrypt(ciphertext, wrong_key)
    
    def test_hmac_verification(self, sample_key: bytes):
        """Test HMAC generation and verification."""
        data = b"Data to authenticate"
        
        hmac = generate_hmac(data, sample_key)
        assert verify_hmac(data, hmac, sample_key)
    
    def test_hmac_tampered_data(self, sample_key: bytes):
        """Test HMAC fails with tampered data."""
        data = b"Original data"
        hmac = generate_hmac(data, sample_key)
        
        tampered = b"Tampered data"
        assert not verify_hmac(tampered, hmac, sample_key)


class TestPasswordGenerator:
    """Tests for password generation."""
    
    def test_generate_password_default(self):
        """Test default password generation."""
        password = generate_password()
        
        assert len(password) == 16
        assert any(c.islower() for c in password)
        assert any(c.isupper() for c in password)
        assert any(c.isdigit() for c in password)
    
    def test_generate_password_length(self):
        """Test password length option."""
        password = generate_password(length=32)
        assert len(password) == 32
    
    def test_generate_password_no_symbols(self):
        """Test password without symbols."""
        password = generate_password(symbols=False)
        assert all(c.isalnum() for c in password)
    
    def test_generate_password_only_digits(self):
        """Test digits-only password."""
        password = generate_password(
            length=8,
            lowercase=False,
            uppercase=False,
            digits=True,
            symbols=False
        )
        assert password.isdigit()
        assert len(password) == 8
    
    def test_generate_passphrase(self):
        """Test passphrase generation."""
        passphrase = generate_passphrase(words=4)
        words = passphrase.split("-")
        
        assert len(words) == 4
        assert all(word.isalpha() for word in words)
    
    def test_generate_passphrase_custom_separator(self):
        """Test passphrase with custom separator."""
        passphrase = generate_passphrase(words=3, separator=" ")
        words = passphrase.split(" ")
        
        assert len(words) == 3
    
    def test_password_strength_weak(self):
        """Test weak password detection."""
        strength = calculate_password_strength("password")
        
        assert strength["score"] < 40
        assert strength["level"] in ("Very Weak", "Weak")
    
    def test_password_strength_strong(self):
        """Test strong password detection."""
        strength = calculate_password_strength("MyStr0ng!P@ssw0rd#2024")
        
        assert strength["score"] >= 80
        assert strength["level"] in ("Strong", "Very Strong")
    
    def test_password_strength_properties(self):
        """Test strength checker returns correct properties."""
        password = "Test123!"
        strength = calculate_password_strength(password)
        
        assert strength["has_lowercase"] is True
        assert strength["has_uppercase"] is True
        assert strength["has_digits"] is True
        assert strength["has_symbols"] is True
        assert strength["length"] == 8
    
    def test_calculate_entropy(self):
        """Test entropy calculation."""
        # Simple password
        entropy1 = calculate_entropy("aaaa")  # 4 chars, 26 possibilities
        
        # Complex password
        entropy2 = calculate_entropy("Aa1!")  # 4 chars, 95 possibilities
        
        assert entropy2 > entropy1


class TestPasswordEntry:
    """Tests for password entry model."""
    
    def test_create_entry(self):
        """Test creating a password entry."""
        entry = PasswordEntry(
            name="GitHub",
            username="user@example.com",
            password="secret123"
        )
        
        assert entry.name == "GitHub"
        assert entry.username == "user@example.com"
        assert entry.password == "secret123"
        assert entry.category == "General"
    
    def test_entry_with_all_fields(self):
        """Test entry with all fields."""
        entry = PasswordEntry(
            name="AWS",
            username="admin",
            password="aws_pass",
            url="https://aws.amazon.com",
            notes="Production account",
            category="Cloud",
            tags=["work", "admin"]
        )
        
        assert entry.url == "https://aws.amazon.com"
        assert entry.notes == "Production account"
        assert "work" in entry.tags
    
    def test_entry_to_dict(self):
        """Test entry serialization."""
        entry = PasswordEntry(
            name="Test",
            username="user",
            password="pass"
        )
        
        data = entry.to_dict()
        
        assert data["name"] == "Test"
        assert data["username"] == "user"
        assert "id" in data
        assert "created_at" in data
    
    def test_entry_from_dict(self):
        """Test entry deserialization."""
        data = {
            "id": "123",
            "name": "Test",
            "username": "user",
            "password": "pass",
            "url": "",
            "notes": "",
            "category": "General",
            "tags": [],
            "created_at": "2024-01-01T00:00:00",
            "updated_at": "2024-01-01T00:00:00"
        }
        
        entry = PasswordEntry.from_dict(data)
        
        assert entry.id == "123"
        assert entry.name == "Test"


class TestPasswordManager:
    """Tests for password manager."""
    
    def test_initialize(self, manager: PasswordManager, master_password: str):
        """Test vault initialization."""
        assert not manager.is_initialized
        
        manager.initialize(master_password)
        
        assert manager.is_initialized
        assert manager.is_unlocked
    
    def test_unlock(self, vault_path: Path, master_password: str):
        """Test vault unlocking."""
        # Initialize vault
        manager1 = PasswordManager(vault_path)
        manager1.initialize(master_password)
        manager1.lock()
        
        # Create new instance and unlock
        manager2 = PasswordManager(vault_path)
        assert manager2.is_initialized
        assert not manager2.is_unlocked
        
        manager2.unlock(master_password)
        assert manager2.is_unlocked
    
    def test_unlock_wrong_password(self, initialized_manager: PasswordManager):
        """Test unlock with wrong password."""
        initialized_manager.lock()
        
        with pytest.raises(ValueError):
            initialized_manager.unlock("wrong_password")
    
    def test_add_entry(self, initialized_manager: PasswordManager):
        """Test adding entry."""
        entry = initialized_manager.add(
            name="GitHub",
            username="user",
            password="pass123"
        )
        
        assert entry.name == "GitHub"
        assert initialized_manager.get("GitHub") is not None
    
    def test_get_entry(self, populated_manager: PasswordManager):
        """Test getting entry."""
        entry = populated_manager.get("GitHub")
        
        assert entry is not None
        assert entry.username == "user@example.com"
        assert entry.password == "github_pass123!"
    
    def test_update_entry(self, populated_manager: PasswordManager):
        """Test updating entry."""
        populated_manager.update("GitHub", password="new_password")
        
        entry = populated_manager.get("GitHub")
        assert entry.password == "new_password"
    
    def test_delete_entry(self, populated_manager: PasswordManager):
        """Test deleting entry."""
        populated_manager.delete("GitHub")
        
        assert populated_manager.get("GitHub") is None
    
    def test_search_by_query(self, populated_manager: PasswordManager):
        """Test searching by query."""
        results = populated_manager.search(query="git")
        
        assert len(results) == 1
        assert results[0].name == "GitHub"
    
    def test_search_by_category(self, populated_manager: PasswordManager):
        """Test searching by category."""
        results = populated_manager.search(category="Development")
        
        assert len(results) == 1
        assert results[0].name == "GitHub"
    
    def test_search_by_tags(self, populated_manager: PasswordManager):
        """Test searching by tags."""
        results = populated_manager.search(tags=["code"])
        
        assert len(results) == 1
        assert results[0].name == "GitHub"
    
    def test_list_categories(self, populated_manager: PasswordManager):
        """Test listing categories."""
        categories = populated_manager.list_categories()
        
        assert "Development" in categories
        assert "Email" in categories
        assert "Cloud" in categories
    
    def test_generate_password(self, initialized_manager: PasswordManager):
        """Test password generation via manager."""
        password = initialized_manager.generate_password(length=20)
        
        assert len(password) == 20
    
    def test_generate_passphrase(self, initialized_manager: PasswordManager):
        """Test passphrase generation via manager."""
        passphrase = initialized_manager.generate_passphrase(words=5)
        
        words = passphrase.split("-")
        assert len(words) == 5
    
    def test_change_master_password(
        self, 
        vault_path: Path, 
        initialized_manager: PasswordManager
    ):
        """Test changing master password."""
        # Add entry
        initialized_manager.add(
            name="Test",
            username="user",
            password="secret"
        )
        
        # Change password
        new_password = "NewMasterPass456!"
        initialized_manager.change_master_password(new_password)
        
        # Verify old password fails
        initialized_manager.lock()
        with pytest.raises(ValueError):
            initialized_manager.unlock("TestPassword123!")
        
        # Verify new password works
        initialized_manager.unlock(new_password)
        entry = initialized_manager.get("Test")
        assert entry.password == "secret"
    
    def test_audit_weak_passwords(self, initialized_manager: PasswordManager):
        """Test auditing for weak passwords."""
        initialized_manager.add(
            name="Weak",
            username="user",
            password="password"
        )
        
        issues = initialized_manager.audit()
        
        assert len(issues["weak_passwords"]) >= 1
        assert any(
            item["name"] == "Weak" 
            for item in issues["weak_passwords"]
        )
    
    def test_audit_duplicate_passwords(self, initialized_manager: PasswordManager):
        """Test auditing for duplicate passwords."""
        initialized_manager.add(
            name="Site1",
            username="user1",
            password="same_password"
        )
        initialized_manager.add(
            name="Site2",
            username="user2",
            password="same_password"
        )
        
        issues = initialized_manager.audit()
        
        assert len(issues["duplicate_passwords"]) >= 1
    
    def test_persistence(self, vault_path: Path, master_password: str):
        """Test data persistence across sessions."""
        # Create and add entries
        manager1 = PasswordManager(vault_path)
        manager1.initialize(master_password)
        manager1.add(
            name="Persistent",
            username="user",
            password="pass"
        )
        manager1.lock()
        
        # Load in new session
        manager2 = PasswordManager(vault_path)
        manager2.unlock(master_password)
        
        entry = manager2.get("Persistent")
        assert entry is not None
        assert entry.password == "pass"


class TestVaultStorage:
    """Tests for vault storage."""
    
    def test_save_and_load(
        self, 
        storage: VaultStorage, 
        sample_key: bytes,
        master_password: str
    ):
        """Test saving and loading vault."""
        vault_data = {
            "entries": [
                {
                    "id": "1",
                    "name": "Test",
                    "username": "user",
                    "password": "pass",
                    "url": "",
                    "notes": "",
                    "category": "General",
                    "tags": [],
                    "created_at": "2024-01-01T00:00:00",
                    "updated_at": "2024-01-01T00:00:00"
                }
            ]
        }
        
        storage.save(vault_data, sample_key)
        loaded = storage.load(sample_key)
        
        assert loaded["entries"][0]["name"] == "Test"
    
    def test_vault_exists(self, storage: VaultStorage, sample_key: bytes):
        """Test checking vault existence."""
        assert not storage.exists()
        
        storage.save({"entries": []}, sample_key)
        
        assert storage.exists()
    
    def test_delete_vault(self, storage: VaultStorage, sample_key: bytes):
        """Test deleting vault."""
        storage.save({"entries": []}, sample_key)
        assert storage.exists()
        
        storage.delete()
        
        assert not storage.exists()


class TestIntegration:
    """Integration tests for complete workflows."""
    
    def test_complete_workflow(self, vault_path: Path):
        """Test complete password manager workflow."""
        master_password = "Integration#Test$123"
        
        # Initialize
        manager = PasswordManager(vault_path)
        manager.initialize(master_password)
        
        # Add entries
        manager.add(
            name="GitHub",
            username="dev@company.com",
            password="github_secure_pass!",
            url="https://github.com",
            category="Development"
        )
        
        manager.add(
            name="AWS Console",
            username="admin@company.com",
            password=manager.generate_password(length=24),
            url="https://aws.amazon.com",
            category="Cloud"
        )
        
        # Search and verify
        dev_entries = manager.search(category="Development")
        assert len(dev_entries) == 1
        
        # Update entry
        manager.update("GitHub", notes="Use personal access token")
        github = manager.get("GitHub")
        assert "personal access token" in github.notes
        
        # Delete entry
        manager.delete("AWS Console")
        assert manager.get("AWS Console") is None
        
        # Lock and unlock
        manager.lock()
        assert not manager.is_unlocked
        
        manager.unlock(master_password)
        assert manager.is_unlocked
        
        # Verify data persisted
        github = manager.get("GitHub")
        assert github is not None
PreviousNext