275 lines
9.6 KiB
Python
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}
|