From 1e1e6e62807a6d4b7bc84b2d8c967dceb97f1cb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:40:51 +0000 Subject: [PATCH] test: add tests for folder browser APIs and system credentials toggle Added tests for POST /api/dropbox/list-folders (7 tests) and POST /api/onedrive/list-folders (7 tests) covering success, subfolder navigation, empty directories, auth errors, API errors, path normalization, and alphabetical sorting. Added view tests for system credentials toggle visibility in Dropbox setup wizard. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/api/onedrive.py | 2 +- app/views/google_drive.py | 12 +- app/views/onedrive.py | 4 +- tests/test_api_dropbox.py | 144 ++++++++++++++++++ tests/test_api_onedrive_comprehensive.py | 179 +++++++++++++++++++++++ tests/test_views_dropbox.py | 58 ++++++++ 6 files changed, 386 insertions(+), 13 deletions(-) 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