c7d3ec57c3
Commitd2217531(google-labs-jules SSRF fix) catastrophically deleted 11,500+ lines across 100+ files while fixing an unrelated IMAP issue. Restored from d2217531^ (pre-bad-commit state): Deleted files (fully restored): - app/api/{automation,classification_rules,comments,sharing}.py - app/middleware/upload_rate_limit.py - app/tasks/{automation_tasks,classify_document}.py - app/utils/{automation_hooks,classification_rules}.py - docs/AppleAppStoreCompliance.md - frontend/input.css, package.json, package-lock.json, tailwind.config.js - frontend/static/js/{annotations,claim,comments,sharing}.js - frontend/templates/{admin_connections,file_annotations,file_summary}.html - tests/{test_api_files_comprehensive,test_auth_extended,test_sharing, test_comments,test_connections,test_imap_profiles,test_api_sessions, test_automation,test_classification_rules,test_api_advanced_filters, test_api_classification_rules,test_upload_rate_limit,test_api_dropbox, test_classify_document,test_comments_ui,test_upload_to_icloud, test_api_onedrive_comprehensive,test_frontend_build,test_sentry, test_diagnostic,test_database,test_views_dropbox,test_local_auth}.py Truncated files (content restored): - app/{auth,config,main,models,celery_worker,database}.py - app/api/{__init__,api_tokens,diagnostic,dropbox,files,google_drive, integrations,local_auth,mobile,onedrive,pipelines,qr_auth, settings,url_upload}.py - app/middleware/upload_rate_limit.py - app/tasks/upload_to_nextcloud.py - app/utils/{allowed_types,settings_service,settings_sync,user_scope,webhook}.py - app/views/{base,dropbox,files,google_drive,onedrive,settings}.py - docs/{API,AuthenticationSetup,ConfigurationGuide,DatabaseConfiguration, DeploymentGuide,DropboxSetup,GoogleDriveSetup,KubernetesDeployment, MobileApp,OneDriveSetup,ProductionReadiness,SentrySetup, SocialLoginSetup,UserGuide}.md - frontend/static/{js/upload.js,styles.css} - frontend/templates/{api_tokens,base,devices,dropbox,dropbox_callback, file_view,files,google_drive,onedrive,onedrive_callback, signup}.html - frontend/translations/en.json - migrations/env.py - tests/{conftest,test_api_integrations,test_api_mobile,test_api_settings, test_api_tokens,test_audit_logs,test_duplicates,test_imap_tasks, test_setup_wizard,test_views_files_comprehensive}.py Security fixes kept from post-d2217531 commits: - app/utils/network.py: DNS SSRF fail-secure fix (06b0fced) - app/utils/file_operations.py: path traversal fix (1018ea17) - tests/test_imap_tasks.py: re-applied 4 is_private_ip mock patches Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/51133dd8-9bec-41ab-aa10-3de753634187
669 lines
25 KiB
Python
669 lines
25 KiB
Python
"""
|
|
Tests for app/api/dropbox.py module.
|
|
|
|
Covers Dropbox OAuth endpoints, settings management, and token testing.
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestExchangeDropboxToken:
|
|
"""Tests for exchange_dropbox_token endpoint."""
|
|
|
|
@patch("app.api.dropbox.exchange_oauth_token")
|
|
def test_successful_exchange(self, mock_exchange, client):
|
|
"""Test successful OAuth token exchange."""
|
|
mock_exchange.return_value = {
|
|
"refresh_token": "test-refresh",
|
|
"access_token": "test-access",
|
|
"expires_in": 14400,
|
|
}
|
|
|
|
response = client.post(
|
|
"/api/dropbox/exchange-token",
|
|
data={
|
|
"client_id": "test-client-id",
|
|
"client_secret": "test-client-secret",
|
|
"redirect_uri": "http://localhost/callback",
|
|
"code": "test-auth-code",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["refresh_token"] == "test-refresh"
|
|
assert data["access_token"] == "test-access"
|
|
assert data["expires_in"] == 14400
|
|
|
|
@patch("app.api.dropbox.exchange_oauth_token")
|
|
def test_exchange_default_expiry(self, mock_exchange, client):
|
|
"""Test token exchange returns default expires_in when not provided."""
|
|
mock_exchange.return_value = {
|
|
"refresh_token": "test-refresh",
|
|
"access_token": "test-access",
|
|
}
|
|
|
|
response = client.post(
|
|
"/api/dropbox/exchange-token",
|
|
data={
|
|
"client_id": "test-client-id",
|
|
"client_secret": "test-client-secret",
|
|
"redirect_uri": "http://localhost/callback",
|
|
"code": "test-auth-code",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["expires_in"] == 14400
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestUpdateDropboxSettings:
|
|
"""Tests for update_dropbox_settings endpoint."""
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_update_all_settings(self, mock_settings, client):
|
|
"""Test updating all Dropbox settings in memory."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
mock_settings.dropbox_app_key = ""
|
|
mock_settings.dropbox_app_secret = ""
|
|
mock_settings.dropbox_folder = ""
|
|
|
|
response = client.post(
|
|
"/api/dropbox/update-settings",
|
|
data={
|
|
"refresh_token": "new-refresh-token",
|
|
"app_key": "new-app-key",
|
|
"app_secret": "new-app-secret",
|
|
"folder_path": "/Documents",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_update_refresh_token_only(self, mock_settings, client):
|
|
"""Test updating only refresh token."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
|
|
response = client.post(
|
|
"/api/dropbox/update-settings",
|
|
data={
|
|
"refresh_token": "new-refresh-token",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_update_settings_exception(self, mock_settings, client):
|
|
"""Test that exceptions return 500 error."""
|
|
# Make setting the attribute raise an exception
|
|
type(mock_settings).dropbox_refresh_token = property(
|
|
lambda self: "",
|
|
lambda self, v: (_ for _ in ()).throw(RuntimeError("forced")),
|
|
)
|
|
|
|
response = client.post(
|
|
"/api/dropbox/update-settings",
|
|
data={"refresh_token": "token"},
|
|
)
|
|
|
|
assert response.status_code == 500
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestTestDropboxToken:
|
|
"""Tests for test_dropbox_token endpoint."""
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_not_configured(self, mock_settings, client):
|
|
"""Test response when Dropbox is not configured."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
mock_settings.dropbox_app_key = ""
|
|
mock_settings.dropbox_app_secret = ""
|
|
|
|
response = client.get("/api/dropbox/test-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "error"
|
|
assert "not fully configured" in data["message"]
|
|
|
|
@patch("app.api.dropbox.httpx.AsyncClient.post")
|
|
@patch("app.api.dropbox.settings")
|
|
def test_valid_token(self, mock_settings, mock_post, client):
|
|
"""Test successful token validation."""
|
|
mock_settings.dropbox_refresh_token = "valid-refresh-token"
|
|
mock_settings.dropbox_app_key = "app-key"
|
|
mock_settings.dropbox_app_secret = "app-secret"
|
|
mock_settings.http_request_timeout = 30
|
|
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"email": "user@example.com",
|
|
"name": {"display_name": "Test User"},
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.get("/api/dropbox/test-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
assert data["account"] == "user@example.com"
|
|
assert data["account_name"] == "Test User"
|
|
|
|
@patch("app.api.dropbox.httpx.AsyncClient.post")
|
|
@patch("app.api.dropbox.settings")
|
|
def test_expired_token_refreshed(self, mock_settings, mock_post, client):
|
|
"""Test that expired token triggers refresh and retry."""
|
|
mock_settings.dropbox_refresh_token = "refresh-token"
|
|
mock_settings.dropbox_app_key = "app-key"
|
|
mock_settings.dropbox_app_secret = "app-secret"
|
|
mock_settings.http_request_timeout = 30
|
|
|
|
# First call returns 401, second call (refresh) returns 200, third call returns 200
|
|
mock_401 = Mock()
|
|
mock_401.status_code = 401
|
|
|
|
mock_refresh = Mock()
|
|
mock_refresh.status_code = 200
|
|
mock_refresh.json.return_value = {"access_token": "new-access-token"}
|
|
|
|
mock_success = Mock()
|
|
mock_success.status_code = 200
|
|
mock_success.json.return_value = {
|
|
"email": "user@example.com",
|
|
"name": {"display_name": "Test User"},
|
|
}
|
|
|
|
mock_post.side_effect = [mock_401, mock_refresh, mock_success]
|
|
|
|
response = client.get("/api/dropbox/test-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
|
|
@patch("app.api.dropbox.httpx.AsyncClient.post")
|
|
@patch("app.api.dropbox.settings")
|
|
def test_refresh_token_expired(self, mock_settings, mock_post, client):
|
|
"""Test handling when refresh token itself is expired."""
|
|
mock_settings.dropbox_refresh_token = "expired-refresh"
|
|
mock_settings.dropbox_app_key = "app-key"
|
|
mock_settings.dropbox_app_secret = "app-secret"
|
|
mock_settings.http_request_timeout = 30
|
|
|
|
# First call returns 401, refresh also fails
|
|
mock_401 = Mock()
|
|
mock_401.status_code = 401
|
|
|
|
mock_refresh_fail = Mock()
|
|
mock_refresh_fail.status_code = 400
|
|
mock_refresh_fail.text = "invalid_grant"
|
|
|
|
mock_post.side_effect = [mock_401, mock_refresh_fail]
|
|
|
|
response = client.get("/api/dropbox/test-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "error"
|
|
assert data["needs_reauth"] is True
|
|
|
|
@patch("app.api.dropbox.httpx.AsyncClient.post")
|
|
@patch("app.api.dropbox.settings")
|
|
def test_token_validation_failure(self, mock_settings, mock_post, client):
|
|
"""Test handling non-401, non-200 response."""
|
|
mock_settings.dropbox_refresh_token = "token"
|
|
mock_settings.dropbox_app_key = "app-key"
|
|
mock_settings.dropbox_app_secret = "app-secret"
|
|
mock_settings.http_request_timeout = 30
|
|
|
|
mock_response = Mock()
|
|
mock_response.status_code = 500
|
|
mock_response.text = "Internal Server Error"
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.get("/api/dropbox/test-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "error"
|
|
|
|
@patch("app.api.dropbox.httpx.AsyncClient.post")
|
|
@patch("app.api.dropbox.settings")
|
|
def test_connection_error(self, mock_settings, mock_post, client):
|
|
"""Test handling of connection exceptions."""
|
|
import httpx
|
|
|
|
mock_settings.dropbox_refresh_token = "token"
|
|
mock_settings.dropbox_app_key = "app-key"
|
|
mock_settings.dropbox_app_secret = "app-secret"
|
|
mock_settings.http_request_timeout = 30
|
|
|
|
mock_post.side_effect = httpx.RequestError(
|
|
"Connection refused",
|
|
request=httpx.Request("POST", "https://api.dropboxapi.com/2/users/get_current_account"),
|
|
)
|
|
|
|
response = client.get("/api/dropbox/test-token")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "error"
|
|
assert "Connection error" in data["message"]
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestSaveDropboxSettings:
|
|
"""Tests for save_dropbox_settings endpoint."""
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_save_settings_env_not_found(self, mock_settings, client):
|
|
"""Test that missing .env file is non-fatal — DB write still succeeds."""
|
|
with patch("os.path.exists", return_value=False):
|
|
response = client.post(
|
|
"/api/dropbox/save-settings",
|
|
data={"refresh_token": "test-token"},
|
|
)
|
|
|
|
# .env write is best-effort; endpoint should still succeed via DB write
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "success"
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_save_settings_success(self, mock_settings, client, tmp_path):
|
|
"""Test successful save of Dropbox settings to .env file."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
mock_settings.dropbox_app_key = ""
|
|
mock_settings.dropbox_app_secret = ""
|
|
mock_settings.dropbox_folder = ""
|
|
|
|
# Create a temporary .env file
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("DROPBOX_REFRESH_TOKEN=old_token\nOTHER_VAR=value\n")
|
|
|
|
with (
|
|
patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
|
|
patch("app.api.dropbox.os.path.exists", return_value=True),
|
|
patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
|
|
):
|
|
response = client.post(
|
|
"/api/dropbox/save-settings",
|
|
data={
|
|
"refresh_token": "new-token",
|
|
"app_key": "new-key",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
|
|
# Verify the .env file was updated
|
|
content = env_file.read_text()
|
|
assert "new-token" in content
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_save_settings_with_all_optional_fields(self, mock_settings, client, tmp_path):
|
|
"""Test saving all Dropbox settings including optional fields."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
mock_settings.dropbox_app_key = ""
|
|
mock_settings.dropbox_app_secret = ""
|
|
mock_settings.dropbox_folder = ""
|
|
|
|
# Create a temporary .env file with commented lines
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text(
|
|
"# DROPBOX_REFRESH_TOKEN=old_token\n"
|
|
"# DROPBOX_APP_KEY=old_key\n"
|
|
"# DROPBOX_APP_SECRET=old_secret\n"
|
|
"OTHER_VAR=value\n"
|
|
)
|
|
|
|
with (
|
|
patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
|
|
patch("app.api.dropbox.os.path.exists", return_value=True),
|
|
patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
|
|
):
|
|
response = client.post(
|
|
"/api/dropbox/save-settings",
|
|
data={
|
|
"refresh_token": "new-token",
|
|
"app_key": "new-key",
|
|
"app_secret": "new-secret",
|
|
"folder_path": "/Documents",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
|
|
# Verify all settings were updated in the file
|
|
content = env_file.read_text()
|
|
assert "DROPBOX_REFRESH_TOKEN=new-token" in content
|
|
assert "DROPBOX_APP_KEY=new-key" in content
|
|
assert "DROPBOX_APP_SECRET=new-secret" in content
|
|
assert "DROPBOX_FOLDER=/Documents" in content
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_save_settings_adds_missing_keys(self, mock_settings, client, tmp_path):
|
|
"""Test that missing keys are added to .env file."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
mock_settings.dropbox_app_key = ""
|
|
mock_settings.dropbox_app_secret = ""
|
|
mock_settings.dropbox_folder = ""
|
|
|
|
# Create a temporary .env file without Dropbox settings
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("OTHER_VAR=value\nANOTHER_VAR=test\n")
|
|
|
|
with (
|
|
patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
|
|
patch("app.api.dropbox.os.path.exists", return_value=True),
|
|
patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
|
|
):
|
|
response = client.post(
|
|
"/api/dropbox/save-settings",
|
|
data={
|
|
"refresh_token": "new-token",
|
|
"app_key": "new-key",
|
|
"folder_path": "/Shared",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify new keys were added
|
|
content = env_file.read_text()
|
|
assert "DROPBOX_REFRESH_TOKEN=new-token" in content
|
|
assert "DROPBOX_APP_KEY=new-key" in content
|
|
assert "DROPBOX_FOLDER=/Shared" in content
|
|
# Original lines should be preserved
|
|
assert "OTHER_VAR=value" in content
|
|
assert "ANOTHER_VAR=test" in content
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_save_settings_io_error(self, mock_settings, client, tmp_path):
|
|
"""Test that I/O errors on .env write are non-fatal — DB write still succeeds."""
|
|
mock_settings.dropbox_refresh_token = ""
|
|
|
|
# Create a temporary .env file
|
|
env_file = tmp_path / ".env"
|
|
env_file.write_text("DROPBOX_REFRESH_TOKEN=old_token\n")
|
|
|
|
with (
|
|
patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
|
|
patch("app.api.dropbox.os.path.exists", return_value=True),
|
|
patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
|
|
patch("builtins.open", side_effect=IOError("Permission denied")),
|
|
):
|
|
response = client.post(
|
|
"/api/dropbox/save-settings",
|
|
data={"refresh_token": "new-token"},
|
|
)
|
|
|
|
# .env write is best-effort; endpoint should still succeed via DB write
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "success"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestListDropboxFolders:
|
|
"""Tests for list_dropbox_folders endpoint."""
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_success(self, mock_post, client):
|
|
"""Test successful folder listing at root."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"entries": [
|
|
{".tag": "folder", "name": "Documents", "path_display": "/Documents", "id": "id:1"},
|
|
{".tag": "folder", "name": "Photos", "path_display": "/Photos", "id": "id:2"},
|
|
{".tag": "file", "name": "readme.txt", "path_display": "/readme.txt", "id": "id:3"},
|
|
],
|
|
"has_more": False,
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "test-token", "path": ""},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["folders"]) == 2
|
|
assert data["folders"][0]["name"] == "Documents"
|
|
assert data["folders"][1]["name"] == "Photos"
|
|
assert data["path"] == "/"
|
|
assert data["has_more"] is False
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_subfolder(self, mock_post, client):
|
|
"""Test listing folders in a subfolder."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"entries": [
|
|
{".tag": "folder", "name": "Invoices", "path_display": "/Documents/Invoices", "id": "id:4"},
|
|
],
|
|
"has_more": False,
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "test-token", "path": "/Documents"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["folders"]) == 1
|
|
assert data["folders"][0]["path"] == "/Documents/Invoices"
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_empty(self, mock_post, client):
|
|
"""Test listing folders in an empty directory."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"entries": [], "has_more": False}
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "test-token", "path": "/EmptyFolder"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert len(response.json()["folders"]) == 0
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_unauthorized(self, mock_post, client):
|
|
"""Test listing folders with invalid token returns 401."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 401
|
|
mock_response.text = "Invalid access token"
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "bad-token", "path": ""},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_api_error(self, mock_post, client):
|
|
"""Test listing folders when Dropbox API returns an error."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 500
|
|
mock_response.text = "Internal server error"
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "test-token", "path": ""},
|
|
)
|
|
|
|
assert response.status_code == 502
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_root_path_normalization(self, mock_post, client):
|
|
"""Test that '/' is normalized to empty string for Dropbox API."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {"entries": [], "has_more": False}
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "test-token", "path": "/"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
# Check the actual API call used empty string for root
|
|
call_args = mock_post.call_args
|
|
assert call_args[1]["json"]["path"] == ""
|
|
|
|
@patch("app.api.dropbox.requests.post")
|
|
def test_list_folders_sorted_alphabetically(self, mock_post, client):
|
|
"""Test that folders are returned in alphabetical order."""
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.json.return_value = {
|
|
"entries": [
|
|
{".tag": "folder", "name": "Zebra", "path_display": "/Zebra", "id": "id:1"},
|
|
{".tag": "folder", "name": "Alpha", "path_display": "/Alpha", "id": "id:2"},
|
|
{".tag": "folder", "name": "middle", "path_display": "/middle", "id": "id:3"},
|
|
],
|
|
"has_more": False,
|
|
}
|
|
mock_post.return_value = mock_response
|
|
|
|
response = client.post(
|
|
"/api/dropbox/list-folders",
|
|
data={"access_token": "test-token", "path": ""},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
names = [f["name"] for f in response.json()["folders"]]
|
|
assert names == ["Alpha", "middle", "Zebra"]
|
|
|
|
|
|
class TestBuildDropboxRedirectUri:
|
|
"""Tests for the _build_dropbox_redirect_uri helper."""
|
|
|
|
def test_uses_public_base_url_when_set(self):
|
|
"""When PUBLIC_BASE_URL is configured, redirect URI should use it."""
|
|
from unittest.mock import MagicMock
|
|
|
|
with patch("app.api.dropbox.settings") as mock_settings:
|
|
mock_settings.public_base_url = "https://myapp.example.com"
|
|
from app.api.dropbox import _build_dropbox_redirect_uri
|
|
|
|
mock_request = MagicMock()
|
|
result = _build_dropbox_redirect_uri(mock_request)
|
|
|
|
assert result == "https://myapp.example.com/dropbox-callback"
|
|
|
|
def test_uses_public_base_url_strips_trailing_slash(self):
|
|
"""PUBLIC_BASE_URL with trailing slash should be handled correctly."""
|
|
from unittest.mock import MagicMock
|
|
|
|
with patch("app.api.dropbox.settings") as mock_settings:
|
|
mock_settings.public_base_url = "https://myapp.example.com/"
|
|
from app.api.dropbox import _build_dropbox_redirect_uri
|
|
|
|
mock_request = MagicMock()
|
|
result = _build_dropbox_redirect_uri(mock_request)
|
|
|
|
assert result == "https://myapp.example.com/dropbox-callback"
|
|
|
|
def test_falls_back_to_request_when_public_base_url_not_set(self):
|
|
"""When PUBLIC_BASE_URL is not set, use request scheme and netloc."""
|
|
from unittest.mock import MagicMock
|
|
|
|
with patch("app.api.dropbox.settings") as mock_settings:
|
|
mock_settings.public_base_url = None
|
|
from app.api.dropbox import _build_dropbox_redirect_uri
|
|
|
|
mock_request = MagicMock()
|
|
mock_request.url.scheme = "https"
|
|
mock_request.url.netloc = "other.example.com"
|
|
result = _build_dropbox_redirect_uri(mock_request)
|
|
|
|
assert result == "https://other.example.com/dropbox-callback"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestGlobalAuthorizeUrl:
|
|
"""Tests for GET /api/dropbox/global-authorize-url endpoint."""
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_returns_authorize_url(self, mock_settings, client):
|
|
"""Test that a valid authorize URL is returned when global creds are configured."""
|
|
mock_settings.dropbox_allow_global_credentials_for_integrations = True
|
|
mock_settings.dropbox_app_key = "test-app-key"
|
|
mock_settings.dropbox_app_secret = "test-app-secret"
|
|
mock_settings.public_base_url = "https://example.com"
|
|
|
|
response = client.get("/api/dropbox/global-authorize-url")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "authorize_url" in data
|
|
assert "https://www.dropbox.com/oauth2/authorize" in data["authorize_url"]
|
|
assert "client_id=test-app-key" in data["authorize_url"]
|
|
# redirect_uri should be URL-encoded
|
|
assert "redirect_uri=" in data["authorize_url"]
|
|
assert "https%3A%2F%2Fexample.com%2Fdropbox-callback" in data["authorize_url"]
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_returns_403_when_global_creds_disabled(self, mock_settings, client):
|
|
"""Test 403 when global credentials for integrations are disabled."""
|
|
mock_settings.dropbox_allow_global_credentials_for_integrations = False
|
|
mock_settings.dropbox_app_key = "test-app-key"
|
|
mock_settings.dropbox_app_secret = "test-app-secret"
|
|
|
|
response = client.get("/api/dropbox/global-authorize-url")
|
|
|
|
assert response.status_code == 403
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_returns_503_when_creds_not_configured(self, mock_settings, client):
|
|
"""Test 503 when global Dropbox credentials are not configured."""
|
|
mock_settings.dropbox_allow_global_credentials_for_integrations = True
|
|
mock_settings.dropbox_app_key = None
|
|
mock_settings.dropbox_app_secret = None
|
|
|
|
response = client.get("/api/dropbox/global-authorize-url")
|
|
|
|
assert response.status_code == 503
|
|
|
|
@patch("app.api.dropbox.settings")
|
|
def test_redirect_uri_uses_public_base_url(self, mock_settings, client):
|
|
"""Redirect URI in authorize URL must use PUBLIC_BASE_URL when configured."""
|
|
mock_settings.dropbox_allow_global_credentials_for_integrations = True
|
|
mock_settings.dropbox_app_key = "my-key"
|
|
mock_settings.dropbox_app_secret = "my-secret"
|
|
mock_settings.public_base_url = "https://prod.example.com"
|
|
|
|
response = client.get("/api/dropbox/global-authorize-url")
|
|
|
|
assert response.status_code == 200
|
|
authorize_url = response.json()["authorize_url"]
|
|
# The redirect_uri must be URL-encoded and contain the public base URL
|
|
assert "https%3A%2F%2Fprod.example.com%2Fdropbox-callback" in authorize_url
|