""" Pytest configuration and fixtures for SneakyScanner tests. """ import os import tempfile from datetime import datetime from pathlib import Path import pytest import yaml from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from web.app import create_app from web.models import Base, Scan from web.utils.settings import PasswordManager, SettingsManager @pytest.fixture(scope='function') def test_db(): """ Create a temporary test database. Yields a SQLAlchemy session for testing, then cleans up. """ # Create temporary database file db_fd, db_path = tempfile.mkstemp(suffix='.db') # Create engine and session engine = create_engine(f'sqlite:///{db_path}', echo=False) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() yield session # Cleanup session.close() os.close(db_fd) os.unlink(db_path) @pytest.fixture def sample_scan_report(): """ Sample scan report matching the structure from scanner.py. Returns a dictionary representing a typical scan output. """ return { 'title': 'Test Scan', 'scan_time': '2025-11-14T10:30:00Z', 'scan_duration': 125.5, 'config_file': '/app/configs/test.yaml', 'sites': [ { 'name': 'Test Site', 'ips': [ { 'address': '192.168.1.10', 'expected': { 'ping': True, 'tcp_ports': [22, 80, 443], 'udp_ports': [53], 'services': [] }, 'actual': { 'ping': True, 'tcp_ports': [22, 80, 443, 8080], 'udp_ports': [53], 'services': [ { 'port': 22, 'service': 'ssh', 'product': 'OpenSSH', 'version': '8.9p1', 'extrainfo': 'Ubuntu', 'ostype': 'Linux' }, { 'port': 443, 'service': 'https', 'product': 'nginx', 'version': '1.24.0', 'extrainfo': '', 'ostype': '', 'http_info': { 'protocol': 'https', 'screenshot': 'screenshots/192_168_1_10_443.png', 'certificate': { 'subject': 'CN=example.com', 'issuer': 'CN=Let\'s Encrypt Authority', 'serial_number': '123456789', 'not_valid_before': '2025-01-01T00:00:00Z', 'not_valid_after': '2025-12-31T23:59:59Z', 'days_until_expiry': 365, 'sans': ['example.com', 'www.example.com'], 'is_self_signed': False, 'tls_versions': { 'TLS 1.2': { 'supported': True, 'cipher_suites': [ 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' ] }, 'TLS 1.3': { 'supported': True, 'cipher_suites': [ 'TLS_AES_256_GCM_SHA384', 'TLS_AES_128_GCM_SHA256' ] } } } } }, { 'port': 80, 'service': 'http', 'product': 'nginx', 'version': '1.24.0', 'extrainfo': '', 'ostype': '', 'http_info': { 'protocol': 'http', 'screenshot': 'screenshots/192_168_1_10_80.png' } }, { 'port': 8080, 'service': 'http', 'product': 'Jetty', 'version': '9.4.48', 'extrainfo': '', 'ostype': '' } ] } } ] } ] } @pytest.fixture def sample_config_file(tmp_path): """ Create a sample YAML config file for testing. Args: tmp_path: pytest temporary directory fixture Returns: Path to created config file """ config_data = { 'title': 'Test Scan', 'sites': [ { 'name': 'Test Site', 'ips': [ { 'address': '192.168.1.10', 'expected': { 'ping': True, 'tcp_ports': [22, 80, 443], 'udp_ports': [53], 'services': ['ssh', 'http', 'https'] } } ] } ] } config_file = tmp_path / 'test_config.yaml' with open(config_file, 'w') as f: yaml.dump(config_data, f) return str(config_file) @pytest.fixture def sample_invalid_config_file(tmp_path): """ Create an invalid config file for testing validation. Returns: Path to invalid config file """ config_file = tmp_path / 'invalid_config.yaml' with open(config_file, 'w') as f: f.write("invalid: yaml: content: [missing closing bracket") return str(config_file) @pytest.fixture(scope='function') def app(): """ Create Flask application for testing. Returns: Configured Flask app instance with test database """ # Create temporary database db_fd, db_path = tempfile.mkstemp(suffix='.db') # Create app with test config test_config = { 'TESTING': True, 'SQLALCHEMY_DATABASE_URI': f'sqlite:///{db_path}', 'SECRET_KEY': 'test-secret-key' } app = create_app(test_config) yield app # Cleanup os.close(db_fd) os.unlink(db_path) @pytest.fixture(scope='function') def client(app): """ Create Flask test client. Args: app: Flask application fixture Returns: Flask test client for making API requests """ return app.test_client() @pytest.fixture(scope='function') def db(app): """ Alias for database session that works with Flask app context. Args: app: Flask application fixture Returns: SQLAlchemy session """ with app.app_context(): yield app.db_session @pytest.fixture def sample_scan(db): """ Create a sample scan in the database for testing. Args: db: Database session fixture Returns: Scan model instance """ scan = Scan( timestamp=datetime.utcnow(), status='completed', config_file='/app/configs/test.yaml', title='Test Scan', duration=125.5, triggered_by='test', json_path='/app/output/scan_report_20251114_103000.json', html_path='/app/output/scan_report_20251114_103000.html', zip_path='/app/output/scan_report_20251114_103000.zip', screenshot_dir='/app/output/scan_report_20251114_103000_screenshots' ) db.add(scan) db.commit() db.refresh(scan) return scan # Authentication Fixtures @pytest.fixture def app_password(): """ Test password for authentication tests. Returns: Test password string """ return 'testpassword123' @pytest.fixture def db_with_password(db, app_password): """ Database session with application password set. Args: db: Database session fixture app_password: Test password fixture Returns: Database session with password configured """ settings_manager = SettingsManager(db) PasswordManager.set_app_password(settings_manager, app_password) return db @pytest.fixture def db_no_password(app): """ Database session without application password set. Args: app: Flask application fixture Returns: Database session without password """ with app.app_context(): # Clear any password that might be set settings_manager = SettingsManager(app.db_session) settings_manager.delete('app_password') yield app.db_session @pytest.fixture def authenticated_client(client, db_with_password, app_password): """ Flask test client with authenticated session. Args: client: Flask test client fixture db_with_password: Database with password set app_password: Test password fixture Returns: Test client with active session """ # Log in client.post('/auth/login', data={ 'password': app_password }) return client @pytest.fixture def client_no_password(app): """ Flask test client with no password set (for setup testing). Args: app: Flask application fixture Returns: Test client for testing setup flow """ # Create temporary database without password db_fd, db_path = tempfile.mkstemp(suffix='.db') test_config = { 'TESTING': True, 'SQLALCHEMY_DATABASE_URI': f'sqlite:///{db_path}', 'SECRET_KEY': 'test-secret-key' } test_app = create_app(test_config) test_client = test_app.test_client() yield test_client # Cleanup os.close(db_fd) os.unlink(db_path)