mirror of
https://github.com/SpecterOps/Nemesis
synced 2026-06-08 12:36:42 +00:00
198 lines
6.8 KiB
Python
198 lines
6.8 KiB
Python
"""Tests for validation helpers."""
|
|
|
|
import pytest
|
|
from Crypto.Hash import SHA1
|
|
from nemesis_dpapi.validation import (
|
|
check_write_once_conflicts,
|
|
validate_and_calculate_sha1,
|
|
validate_no_empty_string,
|
|
)
|
|
|
|
|
|
class TestValidateAndCalculateSha1:
|
|
"""Test SHA1 validation and calculation."""
|
|
|
|
def test_calculate_sha1_when_only_plaintext_provided(self):
|
|
"""Should calculate SHA1 when only plaintext_key is provided."""
|
|
plaintext_key = b"my_secret_key"
|
|
expected_sha1 = SHA1.new(plaintext_key).digest()
|
|
|
|
result = validate_and_calculate_sha1(plaintext_key, None)
|
|
|
|
assert result == expected_sha1
|
|
|
|
def test_validate_matching_sha1(self):
|
|
"""Should accept when provided SHA1 matches calculated SHA1."""
|
|
plaintext_key = b"my_secret_key"
|
|
correct_sha1 = SHA1.new(plaintext_key).digest()
|
|
|
|
result = validate_and_calculate_sha1(plaintext_key, correct_sha1)
|
|
|
|
assert result == correct_sha1
|
|
|
|
def test_reject_mismatched_sha1(self):
|
|
"""Should raise ValueError when provided SHA1 doesn't match calculated."""
|
|
plaintext_key = b"my_secret_key"
|
|
wrong_sha1 = b"0" * 20 # Incorrect SHA1
|
|
|
|
with pytest.raises(ValueError, match="does not match calculated SHA1"):
|
|
validate_and_calculate_sha1(plaintext_key, wrong_sha1)
|
|
|
|
def test_accept_sha1_only_update(self):
|
|
"""Should accept when only SHA1 is provided (valid scenario)."""
|
|
sha1_only = b"1" * 20
|
|
|
|
result = validate_and_calculate_sha1(None, sha1_only)
|
|
|
|
assert result == sha1_only
|
|
|
|
def test_return_none_when_both_none(self):
|
|
"""Should return None when both inputs are None."""
|
|
result = validate_and_calculate_sha1(None, None)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestValidateNoEmptyString:
|
|
"""Test empty string validation."""
|
|
|
|
def test_accept_none(self):
|
|
"""Should accept None (unset value)."""
|
|
validate_no_empty_string(None, "test_field") # Should not raise
|
|
|
|
def test_accept_non_empty_string(self):
|
|
"""Should accept non-empty strings."""
|
|
validate_no_empty_string("DC01.contoso.com", "domain_controller") # Should not raise
|
|
|
|
def test_reject_empty_string(self):
|
|
"""Should reject empty strings."""
|
|
with pytest.raises(ValueError, match="cannot be empty string"):
|
|
validate_no_empty_string("", "domain_controller")
|
|
|
|
def test_error_message_includes_field_name(self):
|
|
"""Should include field name in error message."""
|
|
with pytest.raises(ValueError, match="domain_controller"):
|
|
validate_no_empty_string("", "domain_controller")
|
|
|
|
|
|
class TestCheckWriteOnceConflicts:
|
|
"""Test write-once conflict detection."""
|
|
|
|
class MockRecord:
|
|
"""Mock record for testing."""
|
|
|
|
def __init__(self, **kwargs):
|
|
for key, value in kwargs.items():
|
|
setattr(self, key, value)
|
|
|
|
def test_no_conflicts_when_existing_is_null(self):
|
|
"""Should allow writing to NULL fields."""
|
|
existing = self.MockRecord(field1=None, field2=None)
|
|
new = self.MockRecord(field1=b"value1", field2=b"value2")
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1", "field2"])
|
|
|
|
assert conflicts == []
|
|
|
|
def test_no_conflicts_when_values_match(self):
|
|
"""Should allow idempotent updates (same values)."""
|
|
existing = self.MockRecord(field1=b"value1", field2=b"value2")
|
|
new = self.MockRecord(field1=b"value1", field2=b"value2")
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1", "field2"])
|
|
|
|
assert conflicts == []
|
|
|
|
def test_conflict_when_trying_to_change_value(self):
|
|
"""Should detect conflict when trying to change non-NULL value."""
|
|
existing = self.MockRecord(field1=b"old_value", field2=None)
|
|
new = self.MockRecord(field1=b"new_value", field2=b"value2")
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1", "field2"])
|
|
|
|
assert conflicts == ["field1"]
|
|
|
|
def test_conflict_when_trying_to_clear_value(self):
|
|
"""Should detect conflict when trying to set value to NULL."""
|
|
existing = self.MockRecord(field1=b"value1", field2=None)
|
|
new = self.MockRecord(field1=None, field2=b"value2")
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1", "field2"])
|
|
|
|
assert conflicts == ["field1"]
|
|
|
|
def test_multiple_conflicts(self):
|
|
"""Should detect all conflicting fields."""
|
|
existing = self.MockRecord(
|
|
field1=b"value1",
|
|
field2=b"value2",
|
|
field3=b"value3",
|
|
)
|
|
new = self.MockRecord(
|
|
field1=b"changed1",
|
|
field2=b"value2", # Same
|
|
field3=b"changed3",
|
|
)
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1", "field2", "field3"])
|
|
|
|
assert set(conflicts) == {"field1", "field3"}
|
|
|
|
def test_partial_update_allowed(self):
|
|
"""Should allow updating NULL fields while preserving non-NULL."""
|
|
existing = self.MockRecord(
|
|
field1=b"existing_value",
|
|
field2=None,
|
|
field3=None,
|
|
)
|
|
new = self.MockRecord(
|
|
field1=b"existing_value", # Same
|
|
field2=b"new_value", # NULL -> value (allowed)
|
|
field3=None, # NULL -> NULL (allowed)
|
|
)
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1", "field2", "field3"])
|
|
|
|
assert conflicts == []
|
|
|
|
def test_empty_bytes_distinct_from_none(self):
|
|
"""Should treat empty bytes as distinct from None."""
|
|
existing = self.MockRecord(field1=b"")
|
|
new = self.MockRecord(field1=None)
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1"])
|
|
|
|
assert conflicts == ["field1"]
|
|
|
|
def test_empty_bytes_equality(self):
|
|
"""Should treat empty bytes as equal to empty bytes."""
|
|
existing = self.MockRecord(field1=b"")
|
|
new = self.MockRecord(field1=b"")
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["field1"])
|
|
|
|
assert conflicts == []
|
|
|
|
def test_string_fields(self):
|
|
"""Should work with string fields too."""
|
|
existing = self.MockRecord(name="Alice", email=None)
|
|
new = self.MockRecord(name="Bob", email="test@example.com")
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["name", "email"])
|
|
|
|
assert conflicts == ["name"]
|
|
|
|
def test_uuid_fields(self):
|
|
"""Should work with UUID fields."""
|
|
from uuid import UUID
|
|
|
|
guid1 = UUID("12345678-1234-5678-1234-567812345678")
|
|
guid2 = UUID("87654321-4321-8765-4321-876543218765")
|
|
|
|
existing = self.MockRecord(backup_key_guid=guid1)
|
|
new = self.MockRecord(backup_key_guid=guid2)
|
|
|
|
conflicts = check_write_once_conflicts(existing, new, ["backup_key_guid"])
|
|
|
|
assert conflicts == ["backup_key_guid"]
|