PYTHON
test_pwm.py🐍python
"""
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