wrolpi/modules/videos/test/test_cookies.py

405 lines
13 KiB
Python

"""Tests for the encrypted cookies module."""
import os
import pathlib
import tempfile
import pytest
from modules.videos import cookies
from modules.videos.cookies import (
encrypt_cookies,
decrypt_cookies,
validate_cookies_format,
save_encrypted_cookies,
delete_cookies,
cookies_exist,
unlock_cookies,
lock_cookies,
cookies_unlocked,
cookies_for_download,
get_cookies_status,
get_cookies_file_path,
MIN_PASSWORD_LENGTH,
)
# Sample valid Netscape format cookies
VALID_COOKIES_CONTENT = """# Netscape HTTP Cookie File
# This is a generated file! Do not edit.
.youtube.com\tTRUE\t/\tTRUE\t1735689600\tLOGIN_INFO\tABC123
.youtube.com\tTRUE\t/\tFALSE\t0\tSID\txyz789
.google.com\tTRUE\t/\tTRUE\t1735689600\tNID\tdef456
"""
VALID_COOKIES_SIMPLE = """.example.com\tTRUE\t/\tFALSE\t0\tsession_id\tabc123"""
class TestEncryptDecrypt:
"""Tests for basic encryption and decryption."""
def test_encrypt_decrypt_roundtrip(self):
"""Content can be encrypted and decrypted back to original."""
password = 'testpassword123'
content = VALID_COOKIES_CONTENT
encrypted = encrypt_cookies(content, password)
decrypted = decrypt_cookies(encrypted, password)
assert decrypted == content
def test_encrypt_produces_different_output_each_time(self):
"""Each encryption uses a unique salt, producing different ciphertext."""
password = 'testpassword123'
content = VALID_COOKIES_CONTENT
encrypted1 = encrypt_cookies(content, password)
encrypted2 = encrypt_cookies(content, password)
assert encrypted1 != encrypted2
def test_password_too_short(self):
"""Password must be at least MIN_PASSWORD_LENGTH characters."""
short_password = 'a' * (MIN_PASSWORD_LENGTH - 1)
content = VALID_COOKIES_CONTENT
with pytest.raises(ValueError, match='at least'):
encrypt_cookies(content, short_password)
def test_wrong_password_fails(self):
"""Decryption with wrong password raises ValueError."""
password = 'testpassword123'
wrong_password = 'wrongpassword456'
content = VALID_COOKIES_CONTENT
encrypted = encrypt_cookies(content, password)
with pytest.raises(ValueError, match='Invalid password'):
decrypt_cookies(encrypted, wrong_password)
def test_corrupted_data_fails(self):
"""Decryption of corrupted data raises ValueError."""
password = 'testpassword123'
# Random bytes that aren't valid encrypted data
corrupted = os.urandom(100)
with pytest.raises(ValueError):
decrypt_cookies(corrupted, password)
def test_truncated_data_fails(self):
"""Decryption of truncated data raises ValueError."""
password = 'testpassword123'
# Data too short to contain salt
truncated = b'short'
with pytest.raises(ValueError, match='too short'):
decrypt_cookies(truncated, password)
class TestValidateCookiesFormat:
"""Tests for cookies format validation."""
def test_valid_netscape_format(self):
"""Valid Netscape format cookies pass validation."""
is_valid, error = validate_cookies_format(VALID_COOKIES_CONTENT)
assert is_valid is True
assert error is None
def test_valid_simple_format(self):
"""Simple single-line cookies pass validation."""
is_valid, error = validate_cookies_format(VALID_COOKIES_SIMPLE)
assert is_valid is True
assert error is None
def test_empty_content_fails(self):
"""Empty content fails validation."""
is_valid, error = validate_cookies_format('')
assert is_valid is False
assert 'empty' in error.lower()
def test_whitespace_only_fails(self):
"""Whitespace-only content fails validation."""
is_valid, error = validate_cookies_format(' \n\t \n ')
assert is_valid is False
assert 'empty' in error.lower()
def test_comments_only_fails(self):
"""Content with only comments fails validation."""
content = """# This is a comment
# Another comment
# Netscape HTTP Cookie File
"""
is_valid, error = validate_cookies_format(content)
assert is_valid is False
assert 'No valid cookie lines' in error
def test_invalid_format_fails(self):
"""Content without proper tab-separated format fails."""
content = """This is not a valid cookies file
Just some random text
"""
is_valid, error = validate_cookies_format(content)
assert is_valid is False
class TestSaveAndDelete:
"""Tests for saving and deleting encrypted cookies."""
def test_save_and_exists(self, test_directory):
"""Saving cookies creates the encrypted file."""
password = 'testpassword123'
# Initially no cookies exist
assert not cookies_exist()
# Save cookies
path = save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
assert cookies_exist()
assert path.is_file()
assert path.name == 'cookies.txt.enc'
def test_save_sets_restrictive_permissions(self, test_directory):
"""Saved cookies file has restrictive permissions (owner only)."""
password = 'testpassword123'
path = save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
# Check file permissions (should be 0o600 - owner read/write only)
mode = path.stat().st_mode & 0o777
assert mode == 0o600
def test_save_invalid_format_fails(self, test_directory):
"""Saving invalid cookies format raises ValueError."""
password = 'testpassword123'
invalid_content = 'not valid cookies format'
with pytest.raises(ValueError):
save_encrypted_cookies(invalid_content, password)
def test_delete_removes_file(self, test_directory):
"""Delete removes the encrypted cookies file."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
assert cookies_exist()
result = delete_cookies()
assert result is True
assert not cookies_exist()
def test_delete_nonexistent_returns_false(self, test_directory):
"""Delete returns False when no file exists."""
assert not cookies_exist()
result = delete_cookies()
assert result is False
def test_delete_clears_session(self, test_directory):
"""Deleting cookies also clears the session."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
# save_encrypted_cookies auto-unlocks, but call unlock_cookies for clarity
unlock_cookies(password)
assert cookies_unlocked()
delete_cookies()
assert not cookies_unlocked()
def test_save_auto_unlocks(self, test_directory):
"""Saving cookies automatically unlocks them (user just provided password)."""
password = 'testpassword123'
assert not cookies_unlocked()
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
# Cookies should be auto-unlocked after save
assert cookies_unlocked()
class TestUnlockLock:
"""Tests for session unlock/lock functionality."""
def test_unlock_success(self, test_directory):
"""Unlock decrypts cookies to memory."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
# save_encrypted_cookies auto-unlocks, so lock first to test unlock
lock_cookies()
assert not cookies_unlocked()
unlock_cookies(password)
assert cookies_unlocked()
def test_unlock_no_file_fails(self, test_directory):
"""Unlock fails when no encrypted file exists."""
with pytest.raises(FileNotFoundError):
unlock_cookies('anypassword1')
def test_unlock_wrong_password_fails(self, test_directory):
"""Unlock with wrong password raises ValueError."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
with pytest.raises(ValueError, match='Invalid password'):
unlock_cookies('wrongpassword')
def test_lock_clears_memory(self, test_directory):
"""Lock clears decrypted cookies from memory."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
assert cookies_unlocked()
lock_cookies()
assert not cookies_unlocked()
def test_lock_idempotent(self, test_directory):
"""Lock can be called multiple times without error."""
lock_cookies()
lock_cookies()
lock_cookies()
assert not cookies_unlocked()
class TestCookiesForDownload:
"""Tests for the per-download temp file context manager."""
def test_provides_temp_file(self, test_directory):
"""Context manager provides a readable temp file."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
with cookies_for_download() as cookies_path:
assert cookies_path.is_file()
content = cookies_path.read_text()
assert content == VALID_COOKIES_CONTENT
def test_temp_file_deleted_after(self, test_directory):
"""Temp file is deleted after context manager exits."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
with cookies_for_download() as cookies_path:
temp_path = cookies_path # Save reference
assert not temp_path.exists()
def test_temp_file_deleted_on_exception(self, test_directory):
"""Temp file is deleted even if exception occurs."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
temp_path = None
try:
with cookies_for_download() as cookies_path:
temp_path = cookies_path
raise RuntimeError('Test exception')
except RuntimeError:
pass
assert temp_path is not None
assert not temp_path.exists()
def test_fails_when_not_unlocked(self, test_directory):
"""Raises RuntimeError when cookies not unlocked."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
# save_encrypted_cookies auto-unlocks, so lock first
lock_cookies()
with pytest.raises(RuntimeError, match='not unlocked'):
with cookies_for_download():
pass
def test_unique_files_for_concurrent_downloads(self, test_directory):
"""Each call creates a unique temp file."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
paths = []
# Start multiple contexts
with cookies_for_download() as path1:
paths.append(str(path1))
with cookies_for_download() as path2:
paths.append(str(path2))
with cookies_for_download() as path3:
paths.append(str(path3))
# All paths should be unique
assert len(paths) == len(set(paths))
def test_temp_file_has_restrictive_permissions(self, test_directory):
"""Temp file has restrictive permissions."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
with cookies_for_download() as cookies_path:
mode = cookies_path.stat().st_mode & 0o777
assert mode == 0o600
class TestGetCookiesStatus:
"""Tests for status reporting."""
def test_status_no_cookies(self, test_directory):
"""Status shows no cookies when file doesn't exist."""
status = get_cookies_status()
assert status['cookies_exist'] is False
assert status['cookies_unlocked'] is False
def test_status_locked(self, test_directory):
"""Status shows locked when cookies exist but not unlocked."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
# save_encrypted_cookies auto-unlocks, so lock first
lock_cookies()
status = get_cookies_status()
assert status['cookies_exist'] is True
assert status['cookies_unlocked'] is False
def test_status_unlocked(self, test_directory):
"""Status shows unlocked when cookies are in memory."""
password = 'testpassword123'
save_encrypted_cookies(VALID_COOKIES_CONTENT, password)
unlock_cookies(password)
status = get_cookies_status()
assert status['cookies_exist'] is True
assert status['cookies_unlocked'] is True
# Fixture to reset module state between tests
@pytest.fixture(autouse=True)
def reset_cookies_state(async_client):
"""Reset cookies module state before and after each test.
Depends on async_client to ensure shared_ctx is initialized.
"""
lock_cookies()
yield
lock_cookies()