ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
"""Unit tests for the credential encryption/decryption service.
|
|
|
|
Tests cover:
|
|
- Encryption/decryption round-trip with valid key
|
|
- Random nonce ensures different ciphertext per encryption
|
|
- Wrong key rejection (InvalidTag)
|
|
- Invalid key length rejection (ValueError)
|
|
- Unicode and JSON payload handling
|
|
- Tampered ciphertext detection
|
|
|
|
These are pure function tests -- no database or async required.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
from cryptography.exceptions import InvalidTag
|
|
|
|
from app.services.crypto import decrypt_credentials, encrypt_credentials
|
|
|
|
|
|
class TestEncryptDecryptRoundTrip:
|
|
"""Tests for successful encryption/decryption cycles."""
|
|
|
|
def test_basic_roundtrip(self):
|
|
key = os.urandom(32)
|
|
plaintext = "secret-password"
|
|
ciphertext = encrypt_credentials(plaintext, key)
|
|
result = decrypt_credentials(ciphertext, key)
|
|
assert result == plaintext
|
|
|
|
def test_json_credentials_roundtrip(self):
|
|
"""The actual use case: encrypting JSON credential objects."""
|
|
key = os.urandom(32)
|
|
creds = json.dumps({"username": "admin", "password": "RouterOS!123"})
|
|
ciphertext = encrypt_credentials(creds, key)
|
|
result = decrypt_credentials(ciphertext, key)
|
|
parsed = json.loads(result)
|
|
assert parsed["username"] == "admin"
|
|
assert parsed["password"] == "RouterOS!123"
|
|
|
|
def test_unicode_roundtrip(self):
|
|
key = os.urandom(32)
|
|
plaintext = "password-with-unicode-\u00e9\u00e8\u00ea"
|
|
ciphertext = encrypt_credentials(plaintext, key)
|
|
result = decrypt_credentials(ciphertext, key)
|
|
assert result == plaintext
|
|
|
|
def test_empty_string_roundtrip(self):
|
|
key = os.urandom(32)
|
|
ciphertext = encrypt_credentials("", key)
|
|
result = decrypt_credentials(ciphertext, key)
|
|
assert result == ""
|
|
|
|
def test_long_payload_roundtrip(self):
|
|
"""Ensure large payloads work (e.g., SSH keys in credentials)."""
|
|
key = os.urandom(32)
|
|
plaintext = "x" * 10000
|
|
ciphertext = encrypt_credentials(plaintext, key)
|
|
result = decrypt_credentials(ciphertext, key)
|
|
assert result == plaintext
|
|
|
|
|
|
class TestNonceRandomness:
|
|
"""Tests that encryption uses random nonces."""
|
|
|
|
def test_different_ciphertext_each_time(self):
|
|
"""Two encryptions of the same plaintext should produce different ciphertext
|
|
because a random 12-byte nonce is generated each time."""
|
|
key = os.urandom(32)
|
|
plaintext = "same-plaintext"
|
|
ct1 = encrypt_credentials(plaintext, key)
|
|
ct2 = encrypt_credentials(plaintext, key)
|
|
assert ct1 != ct2
|
|
|
|
def test_both_decrypt_correctly(self):
|
|
"""Both different ciphertexts should decrypt to the same plaintext."""
|
|
key = os.urandom(32)
|
|
plaintext = "same-plaintext"
|
|
ct1 = encrypt_credentials(plaintext, key)
|
|
ct2 = encrypt_credentials(plaintext, key)
|
|
assert decrypt_credentials(ct1, key) == plaintext
|
|
assert decrypt_credentials(ct2, key) == plaintext
|
|
|
|
|
|
class TestDecryptionFailures:
|
|
"""Tests for proper rejection of invalid inputs."""
|
|
|
|
def test_wrong_key_raises_invalid_tag(self):
|
|
key1 = os.urandom(32)
|
|
key2 = os.urandom(32)
|
|
ciphertext = encrypt_credentials("secret", key1)
|
|
with pytest.raises(InvalidTag):
|
|
decrypt_credentials(ciphertext, key2)
|
|
|
|
def test_tampered_ciphertext_raises_invalid_tag(self):
|
|
"""Flipping a byte in the ciphertext should cause authentication failure."""
|
|
key = os.urandom(32)
|
|
ciphertext = bytearray(encrypt_credentials("secret", key))
|
|
# Flip a byte in the encrypted portion (after the 12-byte nonce)
|
|
ciphertext[15] ^= 0xFF
|
|
with pytest.raises(InvalidTag):
|
|
decrypt_credentials(bytes(ciphertext), key)
|
|
|
|
|
|
class TestKeyValidation:
|
|
"""Tests for encryption key length validation."""
|
|
|
|
def test_short_key_encrypt_raises(self):
|
|
with pytest.raises(ValueError, match="32 bytes"):
|
|
encrypt_credentials("test", os.urandom(16))
|
|
|
|
def test_long_key_encrypt_raises(self):
|
|
with pytest.raises(ValueError, match="32 bytes"):
|
|
encrypt_credentials("test", os.urandom(64))
|
|
|
|
def test_short_key_decrypt_raises(self):
|
|
key = os.urandom(32)
|
|
ciphertext = encrypt_credentials("test", key)
|
|
with pytest.raises(ValueError, match="32 bytes"):
|
|
decrypt_credentials(ciphertext, os.urandom(16))
|
|
|
|
def test_empty_key_raises(self):
|
|
with pytest.raises(ValueError, match="32 bytes"):
|
|
encrypt_credentials("test", b"")
|