fix: merge main, address code review feedback for security fix PR #816
- Merge origin/main into branch (resolve conflict in integrations_dashboard.html) - Add defensive JSON parsing with try/except for integration.config - Wrap tester() call in try/except to prevent 500 errors from bad config - Add i18n key integrations.connection_test_failed_fallback in en.json - Reference i18n key in template JS fallback message - Update SECURITY_AUDIT.md: add fix date (2026-03-23), update doc date - Remove accidental revert.sh file - Fix missing MagicMock/patch imports in test file - Add tests for invalid JSON config and tester exception error paths Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/daebb70e-059a-4601-8864-88eef49f99cf
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
"""Tests for the per-user integrations API (app/api/integrations.py)."""
|
||||
|
||||
import unittest.mock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
@@ -443,6 +446,44 @@ class TestTestSavedIntegrationConnection:
|
||||
resp = int_client.post(f"/api/integrations/{other_integration.id}/test")
|
||||
assert resp.status_code == 404
|
||||
|
||||
@patch("app.api.integrations._CONNECTION_TESTERS")
|
||||
def test_invalid_json_config_returns_failure(self, mock_testers, int_client, int_session):
|
||||
"""Invalid JSON in config returns a controlled failure response."""
|
||||
mock_tester = MagicMock()
|
||||
mock_testers.get.return_value = mock_tester
|
||||
|
||||
bad_integration = UserIntegration(
|
||||
owner_id=_OWNER,
|
||||
direction="SOURCE",
|
||||
integration_type="IMAP",
|
||||
name="Bad Config",
|
||||
config="not-valid-json{{{",
|
||||
credentials="{}",
|
||||
is_active=True,
|
||||
)
|
||||
int_session.add(bad_integration)
|
||||
int_session.commit()
|
||||
|
||||
resp = int_client.post(f"/api/integrations/{bad_integration.id}/test")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "invalid" in data["message"].lower()
|
||||
mock_tester.assert_not_called()
|
||||
|
||||
@patch("app.api.integrations._CONNECTION_TESTERS")
|
||||
def test_tester_raises_exception_returns_failure(self, mock_testers, int_client):
|
||||
"""Tester that raises an exception returns a controlled failure response."""
|
||||
mock_testers.get.return_value = MagicMock(side_effect=ValueError("bad port"))
|
||||
|
||||
created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
|
||||
resp = int_client.post(f"/api/integrations/{created['id']}/test")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "unexpected error" in data["message"].lower()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIntegrationModel:
|
||||
@@ -888,9 +929,9 @@ class TestConnectionTestEndpoint:
|
||||
def test_test_unsupported_type(self, int_client):
|
||||
"""Unsupported integration types return a helpful non-error message."""
|
||||
payload = {
|
||||
"integration_type": "DROPBOX",
|
||||
"integration_type": "FTP",
|
||||
"config": {},
|
||||
"credentials": {"token": "abc"},
|
||||
"credentials": {"username": "user", "password": "pass"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
@@ -898,6 +939,83 @@ class TestConnectionTestEndpoint:
|
||||
assert data["success"] is False
|
||||
assert "not yet supported" in data["message"]
|
||||
|
||||
def test_test_dropbox_missing_refresh_token(self, int_client):
|
||||
"""Dropbox test with missing refresh_token returns failure."""
|
||||
payload = {
|
||||
"integration_type": "DROPBOX",
|
||||
"config": {},
|
||||
"credentials": {"app_key": "key", "app_secret": "secret"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "refresh_token" in data["message"].lower()
|
||||
|
||||
def test_test_dropbox_missing_app_key(self, int_client):
|
||||
"""Dropbox test with missing app_key/app_secret returns failure."""
|
||||
payload = {
|
||||
"integration_type": "DROPBOX",
|
||||
"config": {},
|
||||
"credentials": {"refresh_token": "rtoken"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "app_key" in data["message"].lower()
|
||||
|
||||
def test_test_dropbox_invalid_credentials(self, int_client):
|
||||
"""Dropbox test with bad credentials returns an auth failure."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import dropbox.exceptions as dbx_exc
|
||||
|
||||
with patch("app.api.integrations.dbx_lib") as mock_dbx:
|
||||
mock_instance = MagicMock()
|
||||
mock_dbx.Dropbox.return_value = mock_instance
|
||||
mock_instance.users_get_current_account.side_effect = dbx_exc.AuthError("req_id", MagicMock())
|
||||
payload = {
|
||||
"integration_type": "DROPBOX",
|
||||
"config": {},
|
||||
"credentials": {
|
||||
"app_key": "bad_key",
|
||||
"app_secret": "bad_secret",
|
||||
"refresh_token": "bad_token",
|
||||
},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "authentication failed" in data["message"].lower()
|
||||
|
||||
def test_test_dropbox_success(self, int_client):
|
||||
"""Dropbox test with valid (mocked) credentials returns success."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
with patch("app.api.integrations.dbx_lib") as mock_dbx:
|
||||
mock_instance = MagicMock()
|
||||
mock_dbx.Dropbox.return_value = mock_instance
|
||||
mock_account = MagicMock()
|
||||
mock_account.name.display_name = "Test User"
|
||||
mock_instance.users_get_current_account.return_value = mock_account
|
||||
|
||||
payload = {
|
||||
"integration_type": "DROPBOX",
|
||||
"config": {},
|
||||
"credentials": {
|
||||
"app_key": "valid_key",
|
||||
"app_secret": "valid_secret",
|
||||
"refresh_token": "valid_token",
|
||||
},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is True
|
||||
assert "dropbox connection successful" in data["message"].lower()
|
||||
|
||||
def test_test_invalid_type_returns_400(self, int_client):
|
||||
"""Invalid integration_type returns 400."""
|
||||
payload = {
|
||||
@@ -985,6 +1103,69 @@ class TestConnectionTestEndpoint:
|
||||
assert data["success"] is False
|
||||
assert "scheme" in data["message"].lower()
|
||||
|
||||
@unittest.mock.patch("httpx.request")
|
||||
def test_test_webdav_success(self, mock_request, int_client):
|
||||
"""WebDAV test succeeds with valid credentials and a valid status code."""
|
||||
mock_response = unittest.mock.MagicMock()
|
||||
mock_response.status_code = 207 # Typical WebDAV success for PROPFIND
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
payload = {
|
||||
"integration_type": "WEBDAV",
|
||||
"config": {"url": "https://example.com/webdav"},
|
||||
"credentials": {"username": "user1", "password": "password123"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is True
|
||||
|
||||
mock_request.assert_called_once_with(
|
||||
"PROPFIND",
|
||||
"https://example.com/webdav",
|
||||
auth=("user1", "password123"),
|
||||
headers={"Depth": "0"},
|
||||
timeout=10.0,
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
@unittest.mock.patch("httpx.request")
|
||||
def test_test_webdav_failure_status(self, mock_request, int_client):
|
||||
"""WebDAV test fails if the server returns a 4xx or 5xx status code."""
|
||||
mock_response = unittest.mock.MagicMock()
|
||||
mock_response.status_code = 401
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
payload = {
|
||||
"integration_type": "WEBDAV",
|
||||
"config": {"url": "https://example.com/webdav"},
|
||||
"credentials": {"username": "user1", "password": "wrong"},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "401" in data["message"]
|
||||
|
||||
@unittest.mock.patch("httpx.request")
|
||||
def test_test_webdav_exception(self, mock_request, int_client):
|
||||
"""WebDAV test fails gracefully if an exception occurs during the request."""
|
||||
mock_request.side_effect = Exception("Connection error")
|
||||
|
||||
payload = {
|
||||
"integration_type": "WEBDAV",
|
||||
"config": {"url": "https://example.com/webdav"},
|
||||
"credentials": {},
|
||||
}
|
||||
resp = int_client.post("/api/integrations/test", json=payload)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is False
|
||||
assert "failed" in data["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Quota endpoint tests
|
||||
|
||||
Reference in New Issue
Block a user