fix(dropbox): fix Invalid redirect_uri error by adding PUBLIC_BASE_URL config and URL-encoding
- Add PUBLIC_BASE_URL optional config to override auto-detected OAuth redirect URIs when behind a reverse proxy that doesn't forward X-Forwarded-Proto headers - Add _build_dropbox_redirect_uri() helper in app/api/dropbox.py - URL-encode redirect_uri in server-side Dropbox authorization URL - Add _get_dropbox_callback_url() helper in app/views/dropbox.py - Pass callback_url to both setup and callback templates - Update templates to use server-provided callback_url instead of window.location.origin - Update settings_service.py to register new setting - Update .env.demo, ConfigurationGuide.md, and DropboxSetup.md documentation - Add tests for new helper functions and global-authorize-url endpoint Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -416,3 +416,110 @@ class TestSaveDropboxSettings:
|
||||
# .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 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
|
||||
|
||||
@@ -144,3 +144,37 @@ class TestDropboxViews:
|
||||
assert response.status_code == 200
|
||||
assert b"/Documents/Uploads" in response.content
|
||||
assert b"Back to Integrations" in response.content
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestDropboxCallbackUrl:
|
||||
"""Tests that the callback_url is correctly passed to templates."""
|
||||
|
||||
def test_setup_page_includes_callback_url(self, client):
|
||||
"""Setup page should include the callback_url variable in its response."""
|
||||
response = client.get("/dropbox-setup")
|
||||
assert response.status_code == 200
|
||||
# callback_url is embedded in the JS as the dropboxCallbackUrl constant
|
||||
assert b"dropboxCallbackUrl" in response.content
|
||||
|
||||
def test_callback_page_includes_callback_url(self, client):
|
||||
"""Callback page should embed the server-side callback URL."""
|
||||
response = client.get("/dropbox-callback?code=testcode")
|
||||
assert response.status_code == 200
|
||||
# callback_url is used as the redirectUri
|
||||
assert b"redirectUri" in response.content
|
||||
|
||||
def test_setup_page_uses_public_base_url_when_set(self, client):
|
||||
"""When PUBLIC_BASE_URL is configured, it should appear in the redirect URI hint."""
|
||||
with patch("app.views.dropbox.settings") as mock_settings:
|
||||
mock_settings.public_base_url = "https://configured.example.com"
|
||||
mock_settings.dropbox_app_key = ""
|
||||
mock_settings.dropbox_app_secret = ""
|
||||
mock_settings.dropbox_refresh_token = ""
|
||||
mock_settings.dropbox_folder = ""
|
||||
mock_settings.dropbox_allow_global_credentials_for_integrations = False
|
||||
response = client.get("/dropbox-setup")
|
||||
assert response.status_code == 200
|
||||
# The configured public_base_url hostname must appear in the page (redirect URI display)
|
||||
page_text = response.text
|
||||
assert "configured.example.com/dropbox-callback" in page_text
|
||||
|
||||
Reference in New Issue
Block a user