annator-command-center / tests /property_tests /integrations /test_integration_resilience.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 9)
81e3673 verified
Raw
History Blame Contribute Delete
24.8 kB
"""
Property-Based Tests for Integration Resilience Invariants - CRITICAL SYSTEM STABILITY
Tests critical integration resilience invariants:
- OAuth flow resilience (state uniqueness, token refresh, callback validation)
- Integration error handling (retry logic, backoff, circuit breaker)
- Webhook resilience (signature verification, idempotency, replay protection)
- Integration timeout handling
- Rate limiting enforcement
These tests protect against:
- OAuth state collisions and CSRF attacks
- Integration cascading failures
- Webhook replay attacks
- Duplicate webhook processing
- API abuse through rate limiting
"""
import pytest
from hypothesis import given, strategies as st, settings, HealthCheck, assume
from typing import List, Dict, Any
import sys
import os
from datetime import datetime, timedelta
# Add backend to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
class TestOAuthFlowInvariants:
"""Tests for OAuth flow resilience invariants"""
@given(
state_length=st.integers(min_value=32, max_value=128),
state_count=st.integers(min_value=1, max_value=100)
)
@settings(max_examples=50)
def test_oauth_state_uniqueness(self, state_length, state_count):
"""Test that OAuth states are unique"""
# Simulate state generation
import string
import random
states = set()
for _ in range(state_count):
# Generate random state
state = ''.join(random.choices(string.ascii_letters + string.digits, k=state_length))
states.add(state)
# Verify uniqueness
assert len(states) == state_count, \
f"All {state_count} OAuth states should be unique"
# Verify state length
for state in states:
assert len(state) == state_length, \
f"OAuth state should be {state_length} characters"
@given(
token_expiry_seconds=st.integers(min_value=300, max_value=3600), # 5 min to 1 hour
refresh_attempts=st.integers(min_value=1, max_value=5)
)
@settings(max_examples=50)
def test_oauth_token_refresh_retry(self, token_expiry_seconds, refresh_attempts):
"""Test that OAuth token refresh retries on failure"""
# Simulate token lifecycle
issued_at = datetime.now()
expires_at = issued_at + timedelta(seconds=token_expiry_seconds)
# Simulate refresh attempts
successful_refresh = False
attempts_made = 0
for attempt in range(refresh_attempts):
attempts_made = attempt + 1
# Simulate refresh success (70% chance per attempt)
import random
if random.random() < 0.7:
successful_refresh = True
break
# Verify retry logic
assert attempts_made <= refresh_attempts, \
f"Should not exceed {refresh_attempts} refresh attempts"
# Verify at least one attempt was made
assert attempts_made >= 1, \
"Should attempt at least one refresh"
# If successful, verify new token
if successful_refresh:
new_issued_at = datetime.now()
assert new_issued_at > issued_at, \
"New token should be issued after old token"
@given(
state_count=st.integers(min_value=1, max_value=50),
callback_count=st.integers(min_value=1, max_value=50)
)
@settings(max_examples=50)
def test_oauth_callback_validation(self, state_count, callback_count):
"""Test that OAuth callbacks are validated"""
# Simulate OAuth states
states = {f"state_{i}" for i in range(state_count)}
# Simulate callbacks
valid_callbacks = 0
invalid_callbacks = 0
for i in range(callback_count):
# 80% of callbacks use valid states
import random
if random.random() < 0.8:
# Use valid state
state = f"state_{i % state_count}"
if state in states:
valid_callbacks += 1
else:
# Use invalid state
invalid_callbacks += 1
# Verify validation counts
total_callbacks = valid_callbacks + invalid_callbacks
assert total_callbacks == callback_count, \
f"Should process {callback_count} callbacks"
# At least some callbacks should be valid
if callback_count > 1:
assert valid_callbacks >= 0 or invalid_callbacks >= 0, \
"Should have valid or invalid callbacks"
@given(
error_codes=st.lists(
st.sampled_from([
"invalid_request",
"unauthorized_client",
"access_denied",
"unsupported_response_type",
"invalid_scope",
"server_error",
"temporarily_unavailable"
]),
min_size=1,
max_size=10
)
)
@settings(max_examples=50)
def test_oauth_error_handling(self, error_codes):
"""Test that OAuth errors are handled correctly"""
# Define recoverable errors
recoverable_errors = {
"server_error",
"temporarily_unavailable"
}
# Define non-recoverable errors
non_recoverable_errors = {
"invalid_request",
"unauthorized_client",
"access_denied",
"unsupported_response_type",
"invalid_scope"
}
for error_code in error_codes:
# Categorize error
is_recoverable = error_code in recoverable_errors
is_non_recoverable = error_code in non_recoverable_errors
# Verify error is categorized
assert is_recoverable or is_non_recoverable, \
f"Error code '{error_code}' should be categorized"
# Verify mutual exclusivity
assert not (is_recoverable and is_non_recoverable), \
f"Error code '{error_code}' should be either recoverable or non-recoverable"
@given(
operation_timeout_seconds=st.integers(min_value=5, max_value=60),
execution_time_seconds=st.integers(min_value=1, max_value=120)
)
@settings(max_examples=50)
def test_oauth_timeout_handling(self, operation_timeout_seconds, execution_time_seconds):
"""Test that OAuth operations handle timeouts correctly"""
# Simulate operation execution
operation_completed = execution_time_seconds <= operation_timeout_seconds
operation_timed_out = execution_time_seconds > operation_timeout_seconds
# Verify timeout logic
if operation_completed:
assert execution_time_seconds <= operation_timeout_seconds, \
"Operation should complete within timeout"
else:
assert operation_timed_out, \
"Operation should timeout"
# Verify timeout is positive
assert operation_timeout_seconds > 0, \
"Timeout should be positive"
class TestIntegrationRetryInvariants:
"""Tests for integration retry logic invariants"""
@given(
failure_count=st.integers(min_value=1, max_value=10),
max_retries=st.integers(min_value=1, max_value=5)
)
@settings(max_examples=50)
def test_integration_retry_logic(self, failure_count, max_retries):
"""Test that integrations retry on transient failures"""
# Simulate retry attempts
actual_retries = min(failure_count, max_retries)
successful = failure_count <= max_retries
# Verify retry logic
assert actual_retries <= max_retries, \
f"Should not exceed {max_retries} retries"
# Verify retry count is non-negative
assert actual_retries >= 0, \
"Retry count should be non-negative"
# Verify success condition
if successful:
assert failure_count <= max_retries, \
"Should succeed within retry limit"
else:
assert failure_count > max_retries, \
"Should fail after exhausting retries"
@given(
retry_count=st.integers(min_value=1, max_value=10),
base_delay_seconds=st.floats(min_value=0.1, max_value=5.0, allow_nan=False, allow_infinity=False)
)
@settings(max_examples=50)
def test_rate_limiting_backoff(self, retry_count, base_delay_seconds):
"""Test that rate limiting uses exponential backoff"""
# Calculate backoff delays
backoff_delays = []
for attempt in range(retry_count):
# Exponential backoff: base_delay * 2^attempt
delay = base_delay_seconds * (2 ** attempt)
backoff_delays.append(delay)
# Verify backoff progression
for i in range(1, len(backoff_delays)):
assert backoff_delays[i] > backoff_delays[i-1], \
f"Backoff delay should increase (attempt {i}: {backoff_delays[i]} > attempt {i-1}: {backoff_delays[i-1]})"
# Verify all delays are positive
for delay in backoff_delays:
assert delay > 0, \
"Backoff delay should be positive"
@given(
failure_threshold=st.integers(min_value=3, max_value=10),
failure_count=st.integers(min_value=0, max_value=20)
)
@settings(max_examples=50)
def test_integration_circuit_breaker(self, failure_threshold, failure_count):
"""Test that circuit breaker opens after threshold failures"""
# Simulate circuit breaker state
circuit_open = failure_count >= failure_threshold
circuit_closed = failure_count < failure_threshold
# Verify circuit breaker logic
if circuit_open:
assert failure_count >= failure_threshold, \
"Circuit should open when failure threshold reached"
else:
assert failure_count < failure_threshold, \
"Circuit should remain closed below threshold"
# Verify threshold is positive
assert failure_threshold > 0, \
"Failure threshold should be positive"
# Verify failure count is non-negative
assert failure_count >= 0, \
"Failure count should be non-negative"
@given(
operation_count=st.integers(min_value=1, max_value=100),
timeout_seconds=st.integers(min_value=5, max_value=60)
)
@settings(max_examples=50)
def test_integration_timeout_handling(self, operation_count, timeout_seconds):
"""Test that integrations handle timeouts correctly"""
# Simulate operations with random execution times
import random
timed_out_operations = 0
completed_operations = 0
for _ in range(operation_count):
execution_time = random.uniform(1, 120) # 1 to 120 seconds
if execution_time > timeout_seconds:
timed_out_operations += 1
else:
completed_operations += 1
# Verify operation counts
total_operations = timed_out_operations + completed_operations
assert total_operations == operation_count, \
f"Should account for all {operation_count} operations"
# Verify counts are non-negative
assert timed_out_operations >= 0, \
"Timed out operations count should be non-negative"
assert completed_operations >= 0, \
"Completed operations count should be non-negative"
@given(
success_count=st.integers(min_value=1, max_value=20),
failure_count=st.integers(min_value=0, max_value=10)
)
@settings(max_examples=50)
def test_integration_error_recovery(self, success_count, failure_count):
"""Test that integrations recover from errors"""
# Simulate error recovery
total_attempts = success_count + failure_count
success_rate = success_count / total_attempts if total_attempts > 0 else 0
# Verify success rate is valid
assert 0.0 <= success_rate <= 1.0, \
f"Success rate {success_rate} should be in [0.0, 1.0]"
# Verify counts are non-negative
assert success_count >= 0, \
"Success count should be non-negative"
assert failure_count >= 0, \
"Failure count should be non-negative"
# Verify total attempts are positive
assert total_attempts > 0, \
"Total attempts should be positive"
class TestWebhookInvariants:
"""Tests for webhook resilience invariants"""
@given(
payload_count=st.integers(min_value=1, max_value=50),
secret_key_length=st.integers(min_value=32, max_value=64)
)
@settings(max_examples=50)
def test_webhook_signature_verification(self, payload_count, secret_key_length):
"""Test that webhook signatures are verified correctly"""
import hmac
import hashlib
import json
# Generate secret key
import string
import random
secret_key = ''.join(random.choices(string.ascii_letters + string.digits, k=secret_key_length))
# Simulate webhook payloads and signatures
valid_signatures = 0
for i in range(payload_count):
# Generate payload
payload = {"event_id": f"event_{i}", "timestamp": datetime.now().isoformat()}
payload_json = json.dumps(payload, sort_keys=True)
# Generate signature
signature = hmac.new(
secret_key.encode(),
payload_json.encode(),
hashlib.sha256
).hexdigest()
# Verify signature
expected_signature = hmac.new(
secret_key.encode(),
payload_json.encode(),
hashlib.sha256
).hexdigest()
if signature == expected_signature:
valid_signatures += 1
# Verify all signatures are valid
assert valid_signatures == payload_count, \
f"All {payload_count} webhook signatures should be valid"
# Verify secret key length
assert len(secret_key) == secret_key_length, \
f"Secret key should be {secret_key_length} characters"
@given(
webhook_count=st.integers(min_value=1, max_value=50),
duplicate_rate=st.floats(min_value=0.0, max_value=0.5, allow_nan=False, allow_infinity=False)
)
@settings(max_examples=50)
def test_webhook_idempotency(self, webhook_count, duplicate_rate):
"""Test that webhook processing is idempotent"""
# Simulate webhook processing
processed_webhooks = set()
duplicate_count = 0
# Calculate expected duplicates
import random
expected_duplicates = int(webhook_count * duplicate_rate)
# Generate webhook IDs
webhook_ids = []
for i in range(webhook_count):
# Some webhooks are duplicates
if i < expected_duplicates and webhook_ids:
# Duplicate of existing webhook
webhook_ids.append(webhook_ids[random.randint(0, len(webhook_ids) - 1)])
else:
webhook_ids.append(f"webhook_{i}")
# Process webhooks
for webhook_id in webhook_ids:
if webhook_id in processed_webhooks:
duplicate_count += 1
else:
processed_webhooks.add(webhook_id)
# Verify idempotency
assert len(processed_webhooks) <= webhook_count, \
"Unique webhooks should not exceed total webhooks"
# Verify duplicate count is reasonable
assert duplicate_count >= 0, \
"Duplicate count should be non-negative"
# Verify no duplicates in processed set
assert len(processed_webhooks) == len(set(processed_webhooks)), \
"Processed webhooks should be unique"
@given(
webhook_count=st.integers(min_value=2, max_value=50), # Changed min from 1 to 2
replay_window_seconds=st.integers(min_value=300, max_value=3600) # 5 min to 1 hour
)
@settings(max_examples=50)
def test_webhook_replay_protection(self, webhook_count, replay_window_seconds):
"""Test that webhook replay attacks are prevented"""
# Simulate webhook timestamps
now = datetime.now()
webhook_timestamps = []
for i in range(webhook_count):
# Generate timestamp within replay window
offset_seconds = (i * replay_window_seconds) // webhook_count
timestamp = now - timedelta(seconds=offset_seconds)
webhook_timestamps.append(timestamp)
# Check for replays within window
from collections import defaultdict
webhook_events = defaultdict(list)
# Use half the webhooks as duplicates
unique_webhook_count = webhook_count // 2
for i, timestamp in enumerate(webhook_timestamps):
webhook_id = f"webhook_{i % unique_webhook_count}" # Some duplicates
webhook_events[webhook_id].append(timestamp)
# Verify replay protection
replay_detected = False
for webhook_id, timestamps in webhook_events.items():
if len(timestamps) > 1:
# Check if timestamps are within replay window
time_diff = (max(timestamps) - min(timestamps)).total_seconds()
if time_diff < replay_window_seconds:
replay_detected = True
break
# If replay detected, verify it's within window
if replay_detected:
# Replay detection is working
assert True, "Replay attack should be detected"
# Verify replay window is positive
assert replay_window_seconds > 0, \
"Replay window should be positive"
@given(
payload_sizes=st.lists(
st.integers(min_value=100, max_value=100000), # 100 bytes to 100 KB
min_size=1,
max_size=50
)
)
@settings(max_examples=50)
def test_webhook_payload_validation(self, payload_sizes):
"""Test that webhook payloads are validated"""
# Define size limits
max_payload_size = 100000 # 100 KB
min_payload_size = 100 # 100 bytes
for payload_size in payload_sizes:
# Check if payload is within limits
is_valid = min_payload_size <= payload_size <= max_payload_size
if is_valid:
assert payload_size >= min_payload_size, \
f"Payload size {payload_size} should be >= minimum"
assert payload_size <= max_payload_size, \
f"Payload size {payload_size} should be <= maximum"
else:
# Invalid payload size
assert True, "Invalid payload size should be rejected"
@given(
webhook_count=st.integers(min_value=1, max_value=50),
error_rate=st.floats(min_value=0.0, max_value=0.3, allow_nan=False, allow_infinity=False)
)
@settings(max_examples=50)
def test_webhook_error_handling(self, webhook_count, error_rate):
"""Test that webhook errors are handled gracefully"""
# Calculate expected errors
expected_errors = int(webhook_count * error_rate)
successful_count = webhook_count - expected_errors
# Verify counts are valid
assert expected_errors >= 0, \
"Error count should be non-negative"
assert successful_count >= 0, \
"Success count should be non-negative"
# Verify total counts match
total_count = expected_errors + successful_count
assert total_count == webhook_count, \
f"Total count should match webhook count"
# Verify error rate is within bounds
assert 0.0 <= error_rate <= 0.3, \
f"Error rate {error_rate} should be in [0.0, 0.3]"
class TestIntegrationResilienceInvariants:
"""Tests for general integration resilience invariants"""
@given(
request_count=st.integers(min_value=1, max_value=100),
rate_limit_per_minute=st.integers(min_value=10, max_value=1000)
)
@settings(max_examples=50)
def test_rate_limiting_enforcement(self, request_count, rate_limit_per_minute):
"""Test that rate limiting is enforced correctly"""
# Calculate allowed requests
allowed_requests = min(request_count, rate_limit_per_minute)
throttled_requests = max(0, request_count - rate_limit_per_minute)
# Verify rate limiting logic
assert allowed_requests + throttled_requests == request_count, \
f"Total requests should equal {request_count}"
# Verify counts are non-negative
assert allowed_requests >= 0, \
"Allowed requests should be non-negative"
assert throttled_requests >= 0, \
"Throttled requests should be non-negative"
# Verify allowed requests don't exceed limit
assert allowed_requests <= rate_limit_per_minute, \
"Allowed requests should not exceed rate limit"
@given(
integration_count=st.integers(min_value=1, max_value=20),
health_check_interval_seconds=st.integers(min_value=30, max_value=300),
duration_seconds=st.integers(min_value=60, max_value=300)
)
@settings(max_examples=50, deadline=None)
def test_integration_health_check(self, integration_count, health_check_interval_seconds, duration_seconds):
"""Test that integration health checks are performed"""
# Simulate health checks without actual sleep
check_count = 0
# Calculate expected number of health check cycles
expected_cycles = duration_seconds // health_check_interval_seconds
# Simulate health checks
for cycle in range(expected_cycles):
# Perform health check for each integration
for _ in range(integration_count):
check_count += 1
# Verify health checks were performed
expected_checks = expected_cycles * integration_count
assert check_count == expected_checks, \
f"Should perform {expected_checks} health checks"
# Verify interval is positive
assert health_check_interval_seconds > 0, \
"Health check interval should be positive"
@given(
concurrent_requests=st.integers(min_value=1, max_value=50),
max_concurrent_requests=st.integers(min_value=5, max_value=20)
)
@settings(max_examples=50)
def test_concurrent_request_handling(self, concurrent_requests, max_concurrent_requests):
"""Test that concurrent requests are handled correctly"""
# Calculate requests processed immediately vs queued
processed_immediately = min(concurrent_requests, max_concurrent_requests)
queued_requests = max(0, concurrent_requests - max_concurrent_requests)
# Verify request handling
assert processed_immediately + queued_requests == concurrent_requests, \
f"Total requests should equal {concurrent_requests}"
# Verify counts are non-negative
assert processed_immediately >= 0, \
"Processed requests should be non-negative"
assert queued_requests >= 0, \
"Queued requests should be non-negative"
# Verify processed requests don't exceed max
assert processed_immediately <= max_concurrent_requests, \
"Processed requests should not exceed max concurrent"
@given(
cache_size=st.integers(min_value=10, max_value=1000),
access_count=st.integers(min_value=1, max_value=10000)
)
@settings(max_examples=50)
def test_cache_invalidation(self, cache_size, access_count):
"""Test that integration caches are invalidated correctly"""
# Simulate cache operations
cache_hit_count = 0
cache_miss_count = 0
for i in range(access_count):
# Simulate cache hit/miss (50% hit rate)
import random
if random.random() < 0.5:
cache_hit_count += 1
else:
cache_miss_count += 1
# Verify cache operations
total_operations = cache_hit_count + cache_miss_count
assert total_operations == access_count, \
f"Total operations should equal {access_count}"
# Verify cache size is positive
assert cache_size > 0, \
"Cache size should be positive"
# Verify hit rate is valid
hit_rate = cache_hit_count / access_count if access_count > 0 else 0
assert 0.0 <= hit_rate <= 1.0, \
f"Cache hit rate {hit_rate} should be in [0.0, 1.0]"