from pathlib import Path import pytest from fastapi.testclient import TestClient from sqlmodel import SQLModel, create_engine @pytest.fixture def api_client(tmp_path, monkeypatch): db_path = tmp_path / "widefrog_test.db" monkeypatch.setenv("APPOPS_DB_PATH", str(db_path)) monkeypatch.setenv("APP_OPS_VOICE_TOKEN", "test-voice-token") from backend.app.main import app from backend.app.db import session from backend.app.db.init_db import init_db engine = create_engine(f"sqlite:///{Path(db_path)}", connect_args={"check_same_thread": False}) session.engine = engine SQLModel.metadata.drop_all(engine) SQLModel.metadata.create_all(engine) init_db() with TestClient(app) as client: yield client def _set_widefrog_config(url: str | None = "http://widefrog:1207", api_key: str | None = None): from backend.app.core.config import settings previous = dict(settings.apis) settings.apis['WIDEFROG_URL'] = url settings.apis['WIDEFROG_API_KEY'] = api_key settings.apis['WIDEFROG_VERIFY_TLS'] = False settings.apis['WIDEFROG_TIMEOUT_SECONDS'] = 10 return previous def _restore_config(previous): from backend.app.core.config import settings settings.apis.clear() settings.apis.update(previous) def test_widefrog_connector_missing_config_reports_required_env(api_client): response = api_client.post('/api/connectors/widefrog/test') assert response.status_code == 200 payload = response.json() assert payload['success'] is False assert payload['status'] == 'missing_config' assert payload['missing_config'] is True assert payload['required_env_vars'] == ['WIDEFROG_URL'] assert payload['secretRef'] == 'WIDEFROG_API_KEY' def test_widefrog_health_success_normalizes_response(monkeypatch): from backend.app.connectors.widefrog import WidefrogConnector previous = _set_widefrog_config(api_key='WIDEFROG_SECRET_TOKEN') def fake_request(self, method, path, **_kwargs): assert method == 'GET' assert path == '/api/health' return {'status': 'ok', 'version': '1.2.3', 'checks': [{'name': 'db', 'status': 'ok'}]} monkeypatch.setattr(WidefrogConnector, '_request_json', fake_request, raising=True) payload = WidefrogConnector().health() assert payload['success'] is True assert payload['status'] == 'connected' assert payload['version'] == '1.2.3' assert payload['port'] == 1207 assert payload['checks'] == [{'name': 'db', 'status': 'ok'}] assert 'WIDEFROG_SECRET_TOKEN' not in str(payload) _restore_config(previous) def test_widefrog_unreachable_redacts_secret(monkeypatch): from backend.app.connectors.widefrog import WidefrogConnector previous = _set_widefrog_config(api_key='WIDEFROG_SECRET_TOKEN') def fail_request(self, method, path, **_kwargs): raise RuntimeError('api_key=WIDEFROG_SECRET_TOKEN connection refused') monkeypatch.setattr(WidefrogConnector, '_request_json', fail_request, raising=True) payload = WidefrogConnector().health() assert payload['success'] is False assert payload['status'] in {'unreachable', 'error'} assert 'WIDEFROG_SECRET_TOKEN' not in str(payload) assert '[REDACTED]' in str(payload) _restore_config(previous) def test_widefrog_queue_unsupported_is_not_fake_success(monkeypatch): from backend.app.connectors.widefrog import WidefrogConnector previous = _set_widefrog_config() def fake_request(self, method, path, **_kwargs): raise RuntimeError('unsupported_endpoint') monkeypatch.setattr(WidefrogConnector, '_request_json', fake_request, raising=True) payload = WidefrogConnector().queue_status() assert payload['success'] is False assert payload['status'] in {'unsupported', 'unavailable'} assert payload['total'] == 0 assert payload['items'] == [] _restore_config(previous) def test_widefrog_queue_success_normalizes_summary(monkeypatch): from backend.app.connectors.widefrog import WidefrogConnector previous = _set_widefrog_config() def fake_request(self, method, path, **_kwargs): assert path == '/api/queue' return { 'items': [ {'id': 'a', 'title': 'active job', 'status': 'active'}, {'id': 'q', 'title': 'queued job', 'status': 'queued'}, {'id': 'c', 'title': 'completed job', 'status': 'completed'}, {'id': 'f', 'title': 'failed job', 'status': 'failed'}, ] } monkeypatch.setattr(WidefrogConnector, '_request_json', fake_request, raising=True) payload = WidefrogConnector().queue_status() assert payload['success'] is True assert payload['status'] == 'ok' assert payload['total'] == 4 assert payload['active'] == 1 assert payload['queued'] == 1 assert payload['completed'] == 1 assert payload['failed'] == 1 _restore_config(previous) def test_widefrog_failed_jobs_success_normalizes_items(monkeypatch): from backend.app.connectors.widefrog import WidefrogConnector previous = _set_widefrog_config() monkeypatch.setattr( WidefrogConnector, '_request_json', lambda self, method, path, **_kwargs: {'items': [{'id': 'f1', 'title': 'failed', 'status': 'failed'}]}, raising=True, ) payload = WidefrogConnector().failed_jobs() assert payload['success'] is True assert payload['failed'] == 1 assert payload['items'][0]['id'] == 'f1' _restore_config(previous) def test_widefrog_retry_failed_voice_requires_approval_without_connector_call(api_client, monkeypatch): def fail_retry(*_args, **_kwargs): raise AssertionError('connector should not run before approval') monkeypatch.setattr('backend.app.connectors.widefrog.WidefrogConnector.retry_failed', fail_retry, raising=True) response = api_client.post('/api/apps/widefrog/retry-failed', json={'source': 'voice'}) assert response.status_code == 200 payload = response.json() assert payload['success'] is False assert payload['decision'] == 'approval_required' assert payload['approvalId'].startswith('appr-') assert payload['auditId'].startswith('audit-') def test_widefrog_retry_failed_allowed_creates_job_and_audit(api_client, monkeypatch): monkeypatch.setattr( 'backend.app.connectors.widefrog.WidefrogConnector.retry_failed', lambda self, payload=None: {'success': True, 'status': 'ok', 'message': 'retry_started', 'data': {'retried': 2}}, raising=True, ) response = api_client.post('/api/apps/widefrog/retry-failed', json={'source': 'manual'}) assert response.status_code == 200 payload = response.json() assert payload['success'] is True assert payload['decision'] == 'allowed' assert payload['status'] == 'success' assert payload['jobId'].startswith('job-') assert payload['auditId'].startswith('audit-') assert payload['data']['retried'] == 2 def test_widefrog_retry_failed_unsupported_is_failed_not_fake_success(api_client, monkeypatch): monkeypatch.setattr( 'backend.app.connectors.widefrog.WidefrogConnector.retry_failed', lambda self, payload=None: {'success': False, 'status': 'unsupported', 'message': 'unsupported_endpoint'}, raising=True, ) response = api_client.post('/api/apps/widefrog/retry-failed', json={'source': 'manual'}) payload = response.json() assert payload['success'] is False assert payload['decision'] == 'failed' assert payload['status'] == 'failed' assert payload['message'] == 'unsupported_endpoint' assert payload['jobId'].startswith('job-') assert payload['auditId'].startswith('audit-') def test_widefrog_scan_library_allowed_and_unsupported_paths(api_client, monkeypatch): monkeypatch.setattr( 'backend.app.connectors.widefrog.WidefrogConnector.scan_library', lambda self, payload=None: {'success': True, 'status': 'ok', 'message': 'scan_started', 'data': {'accepted': True}}, raising=True, ) success = api_client.post('/api/apps/widefrog/scan-library', json={'source': 'manual'}).json() assert success['success'] is True assert success['decision'] == 'allowed' assert success['jobId'].startswith('job-') monkeypatch.setattr( 'backend.app.connectors.widefrog.WidefrogConnector.scan_library', lambda self, payload=None: {'success': False, 'status': 'unsupported', 'message': 'unsupported_endpoint'}, raising=True, ) failed = api_client.post('/api/apps/widefrog/scan-library', json={'source': 'manual'}).json() assert failed['success'] is False assert failed['decision'] == 'failed' assert failed['message'] == 'unsupported_endpoint' def test_widefrog_destructive_actions_blocked(api_client): for action in [ 'widefrog.delete_job', 'widefrog.delete_media', 'widefrog.clear_library', 'widefrog.clear_queue', 'widefrog.reset_database', 'widefrog.delete_downloads', 'widefrog.prune', ]: payload = api_client.post('/api/policies/evaluate', json={'action': action, 'target': 'widefrog', 'source': 'manual'}).json() assert payload['decision'] == 'blocked' def test_widefrog_inventory_classification_has_connector_and_port(): from backend.app.services.app_classifier import classify_container result = classify_container({'name': 'widefrog', 'image': 'widefrog/app:latest', 'ports': ['1207:1207']}) assert result['app_type'] == 'widefrog' assert result['category'] == 'media' assert result['connector'] == 'widefrog' assert result['policy_profile'] == 'media_custom_app' assert result['healthcheck_suggestion'] == {'port': 1207}