Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- pytest framework with fixtures for auth (auth_client, admin_client) - Unit tests for SearchService - Integration tests for auth flow - Security tests (OWASP Top 10: SQL injection, XSS, CSRF) - Smoke tests for production health and backup monitoring - E2E tests with Playwright (basic structure) - DR tests for backup/restore procedures - GitHub Actions CI/CD workflow (.github/workflows/test.yml) - Coverage configuration (.coveragerc) with 80% minimum - DR documentation and restore script Staging environment: VM 248, staging.nordabiznes.pl Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
159 lines
5.2 KiB
Python
159 lines
5.2 KiB
Python
"""
|
|
Disaster Recovery tests
|
|
========================
|
|
|
|
Verify backup and restore procedures work correctly.
|
|
These tests may require SSH access to production/staging.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.dr
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|
STAGING_HOST = os.environ.get('STAGING_HOST', 'maciejpi@10.22.68.251')
|
|
PROD_HOST = os.environ.get('PROD_HOST', 'maciejpi@10.22.68.249')
|
|
|
|
|
|
def ssh_command(host: str, cmd: str, timeout: int = 60) -> tuple[int, str, str]:
|
|
"""Execute SSH command."""
|
|
result = subprocess.run(
|
|
['ssh', host, cmd],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout
|
|
)
|
|
return result.returncode, result.stdout, result.stderr
|
|
|
|
|
|
class TestDRScripts:
|
|
"""Tests for DR scripts availability."""
|
|
|
|
def test_dr_restore_script_exists(self):
|
|
"""DR restore script should exist locally."""
|
|
script = PROJECT_ROOT / 'scripts' / 'dr-restore.sh'
|
|
assert script.exists(), f"DR restore script not found: {script}"
|
|
|
|
def test_dr_restore_script_executable(self):
|
|
"""DR restore script should be executable."""
|
|
script = PROJECT_ROOT / 'scripts' / 'dr-restore.sh'
|
|
|
|
if not script.exists():
|
|
pytest.skip("DR restore script not found")
|
|
|
|
# Check if it has executable permission
|
|
assert os.access(script, os.X_OK), "DR restore script is not executable"
|
|
|
|
def test_dr_playbook_exists(self):
|
|
"""DR playbook documentation should exist."""
|
|
playbook = PROJECT_ROOT / 'docs' / 'DR-PLAYBOOK.md'
|
|
assert playbook.exists(), f"DR playbook not found: {playbook}"
|
|
|
|
|
|
class TestBackupAvailability:
|
|
"""Tests for backup availability on production."""
|
|
|
|
@pytest.mark.slow
|
|
def test_hourly_backup_available(self):
|
|
"""Hourly backup should be available on production."""
|
|
returncode, stdout, stderr = ssh_command(
|
|
PROD_HOST,
|
|
'ls -la /var/backups/nordabiz/hourly/ | tail -3'
|
|
)
|
|
|
|
assert returncode == 0, f"Failed to list hourly backups: {stderr}"
|
|
assert 'nordabiz_' in stdout, "No hourly backups found"
|
|
|
|
@pytest.mark.slow
|
|
def test_daily_backup_available(self):
|
|
"""Daily backup should be available on production."""
|
|
returncode, stdout, stderr = ssh_command(
|
|
PROD_HOST,
|
|
'ls -la /var/backups/nordabiz/daily/ | tail -3'
|
|
)
|
|
|
|
assert returncode == 0, f"Failed to list daily backups: {stderr}"
|
|
assert 'nordabiz_' in stdout, "No daily backups found"
|
|
|
|
|
|
class TestRestoreOnStaging:
|
|
"""Tests for restore procedure on staging."""
|
|
|
|
@pytest.mark.slow
|
|
def test_can_copy_backup_to_staging(self):
|
|
"""Should be able to copy backup from prod to staging."""
|
|
# Get latest backup filename
|
|
returncode, stdout, _ = ssh_command(
|
|
PROD_HOST,
|
|
'ls -t /var/backups/nordabiz/hourly/ | head -1'
|
|
)
|
|
|
|
if returncode != 0 or not stdout.strip():
|
|
pytest.skip("No backup available on production")
|
|
|
|
backup_file = stdout.strip()
|
|
|
|
# Try to copy to staging (dry run - just check connectivity)
|
|
returncode, stdout, stderr = ssh_command(
|
|
STAGING_HOST,
|
|
f'scp -o StrictHostKeyChecking=no {PROD_HOST}:/var/backups/nordabiz/hourly/{backup_file} /tmp/ && echo OK'
|
|
)
|
|
|
|
# This might fail due to SSH key issues - that's expected in CI
|
|
if returncode != 0:
|
|
pytest.skip(f"Cannot copy backup to staging: {stderr}")
|
|
|
|
assert 'OK' in stdout
|
|
|
|
@pytest.mark.slow
|
|
def test_restore_script_runs_on_staging(self):
|
|
"""Restore script should run on staging."""
|
|
# This is a destructive test - should only run on staging
|
|
returncode, stdout, stderr = ssh_command(
|
|
STAGING_HOST,
|
|
'test -x /var/www/nordabiznes/scripts/dr-restore.sh && echo OK'
|
|
)
|
|
|
|
if returncode != 0:
|
|
pytest.skip("DR restore script not available on staging")
|
|
|
|
assert 'OK' in stdout
|
|
|
|
|
|
class TestHealthAfterRestore:
|
|
"""Tests for application health after restore."""
|
|
|
|
@pytest.mark.slow
|
|
def test_staging_health_check(self):
|
|
"""Staging should respond to health check."""
|
|
import requests
|
|
|
|
staging_url = os.environ.get('STAGING_URL', 'https://staging.nordabiznes.pl')
|
|
|
|
try:
|
|
response = requests.get(f'{staging_url}/health', timeout=10)
|
|
assert response.status_code == 200
|
|
assert response.json().get('status') == 'ok'
|
|
except requests.exceptions.RequestException as e:
|
|
pytest.skip(f"Staging not accessible: {e}")
|
|
|
|
@pytest.mark.slow
|
|
def test_staging_database_has_data(self):
|
|
"""Staging database should have company data after restore."""
|
|
import requests
|
|
|
|
staging_url = os.environ.get('STAGING_URL', 'https://staging.nordabiznes.pl')
|
|
|
|
try:
|
|
response = requests.get(f'{staging_url}/api/companies', timeout=10)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert len(data) > 0, "Staging database appears empty"
|
|
except requests.exceptions.RequestException as e:
|
|
pytest.skip(f"Staging not accessible: {e}")
|