diff --git a/app/api/onedrive.py b/app/api/onedrive.py index ca13c3bf..48b043ce 100644 --- a/app/api/onedrive.py +++ b/app/api/onedrive.py @@ -270,7 +270,7 @@ async def list_onedrive_folders( raw_parent = item["parentReference"]["path"] prefix = "/drive/root:" if raw_parent.startswith(prefix): - parent_path = raw_parent[len(prefix):] + parent_path = raw_parent[len(prefix) :] elif raw_parent == "/drive/root": parent_path = "" diff --git a/app/views/google_drive.py b/app/views/google_drive.py index 20b38366..ec70f92c 100644 --- a/app/views/google_drive.py +++ b/app/views/google_drive.py @@ -47,9 +47,7 @@ async def google_drive_setup_page( folder_id = cfg.get("folder_id", "") # Provide system-wide OAuth credentials when available so users can # authorize without registering their own Google Cloud app. - has_system_credentials = bool( - settings.google_drive_client_id and settings.google_drive_client_secret - ) + has_system_credentials = bool(settings.google_drive_client_id and settings.google_drive_client_secret) return templates.TemplateResponse( "google_drive.html", { @@ -65,9 +63,7 @@ async def google_drive_setup_page( "sa_configured": False, "has_system_credentials": has_system_credentials, "client_id": bool(settings.google_drive_client_id) if has_system_credentials else False, - "client_id_value": ( - settings.google_drive_client_id or "" if has_system_credentials else "" - ), + "client_id_value": (settings.google_drive_client_id or "" if has_system_credentials else ""), "client_secret": bool(settings.google_drive_client_secret) if has_system_credentials else False, "client_secret_value": ( settings.google_drive_client_secret or "" if has_system_credentials else "" @@ -100,9 +96,7 @@ async def google_drive_setup_page( "use_oauth": use_oauth, "oauth_configured": oauth_configured, "sa_configured": sa_configured, - "has_system_credentials": bool( - settings.google_drive_client_id and settings.google_drive_client_secret - ), + "has_system_credentials": bool(settings.google_drive_client_id and settings.google_drive_client_secret), "client_id": bool(settings.google_drive_client_id), "client_id_value": settings.google_drive_client_id or "", "client_secret": bool(settings.google_drive_client_secret), diff --git a/app/views/onedrive.py b/app/views/onedrive.py index 18e823f5..d9fd5e6e 100644 --- a/app/views/onedrive.py +++ b/app/views/onedrive.py @@ -61,9 +61,7 @@ async def onedrive_setup_page( "client_id": bool(settings.onedrive_client_id) if has_system_credentials else False, "client_id_value": settings.onedrive_client_id or "" if has_system_credentials else "", "client_secret": bool(settings.onedrive_client_secret) if has_system_credentials else False, - "client_secret_value": ( - settings.onedrive_client_secret or "" if has_system_credentials else "" - ), + "client_secret_value": (settings.onedrive_client_secret or "" if has_system_credentials else ""), "tenant_id": settings.onedrive_tenant_id or "common", "refresh_token": False, "refresh_token_value": "", diff --git a/tests/test_api_dropbox.py b/tests/test_api_dropbox.py index f3d3a7a1..ce6bc2ad 100644 --- a/tests/test_api_dropbox.py +++ b/tests/test_api_dropbox.py @@ -412,3 +412,147 @@ 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 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"] diff --git a/tests/test_api_onedrive_comprehensive.py b/tests/test_api_onedrive_comprehensive.py index 1019031b..f46fb397 100644 --- a/tests/test_api_onedrive_comprehensive.py +++ b/tests/test_api_onedrive_comprehensive.py @@ -695,3 +695,182 @@ class TestOneDriveIntegration: # Verify env format is present (exact values may vary) assert "env_format" in config_data + + +@pytest.mark.unit +class TestListOneDriveFolders: + """Tests for list_onedrive_folders endpoint.""" + + @patch("app.api.onedrive.requests.get") + def test_list_folders_success(self, mock_get, client): + """Test successful folder listing at root.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "value": [ + { + "name": "Documents", + "id": "id:1", + "folder": {"childCount": 3}, + "parentReference": {"path": "/drive/root:"}, + }, + { + "name": "Pictures", + "id": "id:2", + "folder": {"childCount": 10}, + "parentReference": {"path": "/drive/root:"}, + }, + ], + } + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/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"][0]["path"] == "/Documents" + assert data["folders"][1]["name"] == "Pictures" + assert data["path"] == "/" + + @patch("app.api.onedrive.requests.get") + def test_list_folders_subfolder(self, mock_get, client): + """Test listing folders in a subfolder.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "value": [ + { + "name": "Invoices", + "id": "id:3", + "folder": {"childCount": 0}, + "parentReference": {"path": "/drive/root:/Documents"}, + }, + ], + } + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/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" + assert data["path"] == "/Documents" + + @patch("app.api.onedrive.requests.get") + def test_list_folders_empty(self, mock_get, client): + """Test listing folders in an empty directory.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"value": []} + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/list-folders", + data={"access_token": "test-token", "path": "EmptyFolder"}, + ) + + assert response.status_code == 200 + assert len(response.json()["folders"]) == 0 + + @patch("app.api.onedrive.requests.get") + def test_list_folders_unauthorized(self, mock_get, client): + """Test listing folders with invalid token returns 401.""" + mock_response = Mock() + mock_response.status_code = 401 + mock_response.text = "Invalid access token" + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/list-folders", + data={"access_token": "bad-token", "path": ""}, + ) + + assert response.status_code == 401 + + @patch("app.api.onedrive.requests.get") + def test_list_folders_api_error(self, mock_get, client): + """Test listing folders when Graph API returns an error.""" + mock_response = Mock() + mock_response.status_code = 500 + mock_response.text = "Internal server error" + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/list-folders", + data={"access_token": "test-token", "path": ""}, + ) + + assert response.status_code == 502 + + @patch("app.api.onedrive.requests.get") + def test_list_folders_sorted_alphabetically(self, mock_get, client): + """Test that folders are returned in alphabetical order.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "value": [ + { + "name": "Zebra", + "id": "id:1", + "folder": {"childCount": 0}, + "parentReference": {"path": "/drive/root:"}, + }, + { + "name": "Alpha", + "id": "id:2", + "folder": {"childCount": 0}, + "parentReference": {"path": "/drive/root:"}, + }, + { + "name": "middle", + "id": "id:3", + "folder": {"childCount": 0}, + "parentReference": {"path": "/drive/root:"}, + }, + ], + } + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/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"] + + @patch("app.api.onedrive.requests.get") + def test_list_folders_root_drive_parent(self, mock_get, client): + """Test folder path construction when parentReference.path is /drive/root.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "value": [ + { + "name": "TopLevel", + "id": "id:1", + "folder": {"childCount": 0}, + "parentReference": {"path": "/drive/root"}, + }, + ], + } + mock_get.return_value = mock_response + + response = client.post( + "/api/onedrive/list-folders", + data={"access_token": "test-token", "path": ""}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["folders"][0]["path"] == "/TopLevel" diff --git a/tests/test_views_dropbox.py b/tests/test_views_dropbox.py index d42b9304..5975fc4c 100644 --- a/tests/test_views_dropbox.py +++ b/tests/test_views_dropbox.py @@ -144,3 +144,61 @@ class TestDropboxViews: assert response.status_code == 200 assert b"/Documents/Uploads" in response.content assert b"Back to Integrations" in response.content + + def test_dropbox_setup_user_mode_system_credentials_shown(self, client, db_session): + """Test that system credentials toggle appears when system creds are configured.""" + owner_id = "user_sys_creds@example.com" + integration = UserIntegration( + owner_id=owner_id, + direction="DESTINATION", + integration_type="DROPBOX", + name="My Dropbox", + config=json.dumps({"folder": "/test"}), + is_active=True, + ) + db_session.add(integration) + db_session.commit() + db_session.refresh(integration) + + with ( + patch("app.views.dropbox.get_current_owner_id", return_value=owner_id), + patch("app.views.dropbox.settings") as mock_settings, + ): + mock_settings.dropbox_app_key = "system-key" + mock_settings.dropbox_app_secret = "system-secret" + mock_settings.dropbox_refresh_token = None + mock_settings.dropbox_folder = None + response = client.get(f"/dropbox-setup?integration_id={integration.id}") + + assert response.status_code == 200 + assert b"use-system-creds" in response.content + assert b"DocuElevate" in response.content + + def test_dropbox_setup_user_mode_no_system_credentials(self, client, db_session): + """Test that system credentials toggle is hidden when no system creds configured.""" + owner_id = "user_no_sys@example.com" + integration = UserIntegration( + owner_id=owner_id, + direction="DESTINATION", + integration_type="DROPBOX", + name="My Dropbox", + config=json.dumps({"folder": "/test"}), + is_active=True, + ) + db_session.add(integration) + db_session.commit() + db_session.refresh(integration) + + with ( + patch("app.views.dropbox.get_current_owner_id", return_value=owner_id), + patch("app.views.dropbox.settings") as mock_settings, + ): + mock_settings.dropbox_app_key = None + mock_settings.dropbox_app_secret = None + mock_settings.dropbox_refresh_token = None + mock_settings.dropbox_folder = None + response = client.get(f"/dropbox-setup?integration_id={integration.id}") + + assert response.status_code == 200 + # When no system credentials, hasSystemCredentials JS var should be false + assert b"hasSystemCredentials = false" in response.content