Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def extract_modified_date(self, metadata: dict[str, Any]) -> datetime | None:
return None

def test_credentials(self) -> bool:
"""Test Google Cloud Storage credentials by accessing the root path info.
"""Test Google Cloud Storage credentials by listing the project's buckets.

Raises:
ConnectorError: connector-error
Expand All @@ -264,7 +264,11 @@ def test_credentials(self) -> bool:
boolean: true if test-connection is successful
"""
try:
self.get_fsspec_fs().info("/")
# `ls("")` lists buckets with the configured project. Do not use
# `info("/")`: from gcsfs 2026.x it sends `GET b/` without the
# project, which GCS rejects with "Required parameter: project"
# for every connector (UN-4224).
self.get_fsspec_fs().ls("")
except Exception as e:
error_msg = (
"Error from Google Cloud Storage while testing connection. \n"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""GCS connector test-connection request shape (UN-4224).

From gcsfs 2026.x, ``info("/")`` sends ``GET b/`` without the project, which
GCS rejects with ``Required parameter: project`` (400) for every connector.
Test connection must list buckets with the configured project instead — the
same request gcsfs 2024.x sent for ``info("/")``.
"""

from typing import Any

import pytest
from gcsfs import core as gcsfs_core

# The core class explicitly: production runs it (storage_compat keeps gcsfs off
# the experimental ExtendedGcsFileSystem), and `gcsfs.GCSFileSystem` would
# depend on whether gcsfs was imported before storage_compat in this process.
from gcsfs.core import GCSFileSystem

from unstract.connectors.filesystems.google_cloud_storage.google_cloud_storage import (
GoogleCloudStorageFS,
)

_Calls = list[tuple[str, str, dict[str, Any]]]


@pytest.fixture
def gcs_requests(monkeypatch: pytest.MonkeyPatch) -> _Calls:
"""Record gcsfs API requests and answer the way GCS does."""
calls: _Calls = []

async def fake_call(
self: GCSFileSystem, method: str, path: str, *args: Any, **kwargs: Any
) -> dict[str, Any]:
calls.append((method, path, kwargs))
if path.rstrip("/") == "b" and "project" not in kwargs:
# What GCS answers for a bucket listing that carries no project.
raise gcsfs_core.HttpError(
{"code": 400, "message": "Required parameter: project"}
)
return {"kind": "storage#buckets", "items": [{"name": "bucket-a"}]}

monkeypatch.setattr(gcsfs_core.GCSFileSystem, "_call", fake_call)
return calls


def _connector(project: str) -> GoogleCloudStorageFS:
connector = GoogleCloudStorageFS({"project_id": project, "json_credentials": "{}"})
# Bypass the lazy credential-backed client; only the request shape matters.
connector._gcs_fs = GCSFileSystem(
token="anon", project=project, skip_instance_cache=True
)
return connector


def test_test_credentials_lists_buckets_with_the_configured_project(
gcs_requests: _Calls,
) -> None:
assert _connector("my-project").test_credentials() is True

assert gcs_requests, "test_credentials made no request"
method, path, kwargs = gcs_requests[0]
assert (method, path) == ("GET", "b")
assert kwargs.get("project") == "my-project"
assert not any(c[1] == "b/" for c in gcs_requests), "sent the project-less GET b/"


def test_uses_the_core_gcsfs_class() -> None:
assert type(_connector("my-project")._gcs_fs) is GCSFileSystem
assert GCSFileSystem.__module__ == "gcsfs.core"
Comment thread
muhammad-ali-e marked this conversation as resolved.
Loading