Files
Unraid-AppOps-Gateway-Public/backend/tests/test_widefrog_connector.py
T

275 lines
9.6 KiB
Python

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}