From a8c7e45f1eb6fb5f7fb68090472a72664e203246 Mon Sep 17 00:00:00 2001 From: SyedShahmeerAli12 Date: Mon, 3 Aug 2026 18:25:51 +0500 Subject: [PATCH 1/6] fix --- .../microsoft_sharepoint/retriever.py | 9 ++ .../tests/test_retriever.py | 83 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py b/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py index 9d2b62df85..80e8e17ee1 100644 --- a/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py +++ b/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py @@ -76,6 +76,7 @@ def __init__( top_k: int = 10, fields: list[str] | None = None, query_template: str | None = None, + region: str | None = None, graph_url: str = DEFAULT_GRAPH_URL, timeout: float = 30.0, max_retries: int = 3, @@ -96,6 +97,10 @@ def __init__( `'{searchTerms} path:"https://contoso.sharepoint.com/sites/Team"'`. The literal `{searchTerms}` placeholder is replaced by the run-time query. The template uses [Keyword Query Language (KQL)](https://learn.microsoft.com/en-us/sharepoint/dev/general-development/keyword-query-language-kql-syntax-reference). + :param region: The region code for the Microsoft Search index, for example `"US"`, `"EU"`, or `"APAC"`. + Required when using application permissions (app-only / client-credentials auth); omit for delegated + (on-behalf-of) tokens. See the `region` property of the + [searchRequest resource](https://learn.microsoft.com/en-us/graph/api/resources/searchrequest). :param graph_url: The Microsoft Graph base URL. Defaults to `https://graph.microsoft.com/v1.0`. Override for sovereign clouds. :param timeout: The HTTP timeout in seconds for each request to Microsoft Graph. @@ -121,6 +126,7 @@ def __init__( self.top_k = top_k self.fields = fields self.query_template = query_template + self.region = region self.graph_url = graph_url.rstrip("/") self.timeout = timeout self.max_retries = max_retries @@ -215,6 +221,8 @@ def _build_request_body(self, query: str, offset: int, size: int) -> dict[str, A } if self.fields: request["fields"] = self.fields + if self.region: + request["region"] = self.region return {"requests": [request]} @@ -328,6 +336,7 @@ def to_dict(self) -> dict[str, Any]: top_k=self.top_k, fields=self.fields, query_template=self.query_template, + region=self.region, graph_url=self.graph_url, timeout=self.timeout, max_retries=self.max_retries, diff --git a/integrations/microsoft_sharepoint/tests/test_retriever.py b/integrations/microsoft_sharepoint/tests/test_retriever.py index bac208d650..e6fa1c5ead 100644 --- a/integrations/microsoft_sharepoint/tests/test_retriever.py +++ b/integrations/microsoft_sharepoint/tests/test_retriever.py @@ -7,6 +7,7 @@ import httpx import pytest +import requests from haystack import Document, Pipeline from haystack.utils import Secret @@ -100,6 +101,7 @@ def test_defaults(self): assert retriever.top_k == 10 assert retriever.fields is None assert retriever.query_template is None + assert retriever.region is None assert retriever.graph_url == "https://graph.microsoft.com/v1.0" assert retriever.timeout == 30.0 assert retriever.max_retries == 3 @@ -140,12 +142,18 @@ def test_to_dict(self): "top_k": 5, "fields": ["title"], "query_template": '{searchTerms} path:"https://x"', + "region": None, "graph_url": "https://graph.microsoft.us/v1.0", "timeout": 10.0, "max_retries": 1, }, } + def test_to_dict_with_region(self): + retriever = MSSharePointRetriever(region="US") + data = retriever.to_dict() + assert data["init_parameters"]["region"] == "US" + def test_from_dict_round_trip(self): retriever = MSSharePointRetriever(entity_types=["site"], top_k=7, query_template="{searchTerms}") restored = MSSharePointRetriever.from_dict(retriever.to_dict()) @@ -241,6 +249,19 @@ def test_no_query_template_omits_key(self): assert "queryTemplate" not in mock_post.call_args.kwargs["json"]["requests"][0]["query"] assert "fields" not in mock_post.call_args.kwargs["json"]["requests"][0] + def test_region_included_in_request_body_when_set(self): + retriever = MSSharePointRetriever(region="US") + with patch.object(httpx.Client, "post", return_value=_make_response(json_body=EMPTY_RESPONSE)) as mock_post: + retriever.run(query="q", access_token="tok") + request = mock_post.call_args.kwargs["json"]["requests"][0] + assert request["region"] == "US" + + def test_region_omitted_from_request_body_when_none(self): + retriever = MSSharePointRetriever() + with patch.object(httpx.Client, "post", return_value=_make_response(json_body=EMPTY_RESPONSE)) as mock_post: + retriever.run(query="q", access_token="tok") + assert "region" not in mock_post.call_args.kwargs["json"]["requests"][0] + def test_empty_results(self): retriever = MSSharePointRetriever() with patch.object(httpx.Client, "post", return_value=_make_response(json_body=EMPTY_RESPONSE)): @@ -410,3 +431,65 @@ def test_run_against_microsoft_graph(self): for doc in documents: assert isinstance(doc, Document) assert "web_url" in doc.meta + + +@pytest.mark.integration +@pytest.mark.skipif( + not all( + os.environ.get(v) for v in ("MS_SHAREPOINT_TENANT_ID", "MS_SHAREPOINT_CLIENT_ID", "MS_SHAREPOINT_CLIENT_SECRET") + ), + reason="MS_SHAREPOINT_TENANT_ID / MS_SHAREPOINT_CLIENT_ID / MS_SHAREPOINT_CLIENT_SECRET not set", +) +class TestLiveAppOnly: + """ + End-to-end tests using app-only (client credentials) authentication. + + Set these env vars before running: + MS_SHAREPOINT_TENANT_ID — Azure AD tenant ID + MS_SHAREPOINT_CLIENT_ID — App registration client ID + MS_SHAREPOINT_CLIENT_SECRET — App registration client secret + MS_SHAREPOINT_REGION — Search region, e.g. "US" (default: "US") + + Run with: + hatch run test:integration + """ + + @staticmethod + def _get_app_token() -> str: + tenant_id = os.environ["MS_SHAREPOINT_TENANT_ID"] + client_id = os.environ["MS_SHAREPOINT_CLIENT_ID"] + client_secret = os.environ["MS_SHAREPOINT_CLIENT_SECRET"] + response = requests.post( + f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", + data={ + "client_id": client_id, + "client_secret": client_secret, + "scope": "https://graph.microsoft.com/.default", + "grant_type": "client_credentials", + }, + timeout=30, + ) + response.raise_for_status() + return response.json()["access_token"] + + def test_app_only_search_requires_region(self): + token = self._get_app_token() + + # Without region, the Search API must return 400. + retriever_no_region = MSSharePointRetriever(top_k=3, max_retries=0) + with pytest.raises(SharePointRequestError) as exc_info: + retriever_no_region.run(query="test", access_token=token) + assert exc_info.value.status_code == 400 + assert "Region is required" in str(exc_info.value) + + def test_app_only_search_with_region_succeeds(self): + token = self._get_app_token() + region = os.environ.get("MS_SHAREPOINT_REGION", "US") + + # With region, the request should succeed. + retriever = MSSharePointRetriever(top_k=3, region=region, max_retries=0) + documents = retriever.run(query="test", access_token=token)["documents"] + assert isinstance(documents, list) + for doc in documents: + assert isinstance(doc, Document) + assert "web_url" in doc.meta From 64320e4c6bb25b39692ee430bc50f6c14f982fbe Mon Sep 17 00:00:00 2001 From: SyedShahmeerAli12 Date: Tue, 4 Aug 2026 16:18:58 +0500 Subject: [PATCH 2/6] fix --- .../microsoft_sharepoint/retriever.py | 14 ++++----- .../tests/test_retriever.py | 30 +++++++------------ 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py b/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py index 80e8e17ee1..fe321aad4f 100644 --- a/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py +++ b/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py @@ -97,7 +97,7 @@ def __init__( `'{searchTerms} path:"https://contoso.sharepoint.com/sites/Team"'`. The literal `{searchTerms}` placeholder is replaced by the run-time query. The template uses [Keyword Query Language (KQL)](https://learn.microsoft.com/en-us/sharepoint/dev/general-development/keyword-query-language-kql-syntax-reference). - :param region: The region code for the Microsoft Search index, for example `"US"`, `"EU"`, or `"APAC"`. + :param region: The region code for the Microsoft Search index, for example `"NAM"`, `"EUR"`, or `"APC"`. Required when using application permissions (app-only / client-credentials auth); omit for delegated (on-behalf-of) tokens. See the `region` property of the [searchRequest resource](https://learn.microsoft.com/en-us/graph/api/resources/searchrequest). @@ -140,9 +140,9 @@ def run(self, query: str, access_token: str | Secret, top_k: int | None = None) operators directly in the query, for example `filetype:docx`, `author:"Jane Doe"`, or `path:"https://contoso.sharepoint.com/sites/Team"`. See the [KQL syntax reference](https://learn.microsoft.com/en-us/sharepoint/dev/general-development/keyword-query-language-kql-syntax-reference). - :param access_token: A delegated Microsoft Graph bearer token for the user whose content is searched, - typically wired from an upstream `OAuthResolver` (which emits a plain `str`). A `Secret` is also - accepted and resolved internally. + :param access_token: A Microsoft Graph bearer token. For delegated auth, pass a per-user token wired from + an upstream `OAuthResolver`. For app-only (client-credentials) auth, pass an application token and set + `region` at init time. A `Secret` is also accepted and resolved internally. :param top_k: Overrides the `top_k` configured at initialization for this run. :returns: A dictionary with a `documents` key holding the list of retrieved `Document` objects. :raises SharePointConfigError: If `access_token` is a `Secret` that does not resolve to a string. @@ -177,9 +177,9 @@ async def run_async( operators directly in the query, for example `filetype:docx`, `author:"Jane Doe"`, or `path:"https://contoso.sharepoint.com/sites/Team"`. See the [KQL syntax reference](https://learn.microsoft.com/en-us/sharepoint/dev/general-development/keyword-query-language-kql-syntax-reference). - :param access_token: A delegated Microsoft Graph bearer token for the user whose content is searched, - typically wired from an upstream `OAuthResolver` (which emits a plain `str`). A `Secret` is also - accepted and resolved internally. + :param access_token: A Microsoft Graph bearer token. For delegated auth, pass a per-user token wired from + an upstream `OAuthResolver`. For app-only (client-credentials) auth, pass an application token and set + `region` at init time. A `Secret` is also accepted and resolved internally. :param top_k: Overrides the `top_k` configured at initialization for this run. :returns: A dictionary with a `documents` key holding the list of retrieved `Document` objects. :raises SharePointConfigError: If `access_token` is a `Secret` that does not resolve to a string. diff --git a/integrations/microsoft_sharepoint/tests/test_retriever.py b/integrations/microsoft_sharepoint/tests/test_retriever.py index e6fa1c5ead..ae485dc711 100644 --- a/integrations/microsoft_sharepoint/tests/test_retriever.py +++ b/integrations/microsoft_sharepoint/tests/test_retriever.py @@ -436,19 +436,19 @@ def test_run_against_microsoft_graph(self): @pytest.mark.integration @pytest.mark.skipif( not all( - os.environ.get(v) for v in ("MS_SHAREPOINT_TENANT_ID", "MS_SHAREPOINT_CLIENT_ID", "MS_SHAREPOINT_CLIENT_SECRET") + os.environ.get(v) for v in ("MS_GRAPH_TENANT_ID", "MS_GRAPH_CLIENT_ID", "MS_GRAPH_CLIENT_SECRET") ), - reason="MS_SHAREPOINT_TENANT_ID / MS_SHAREPOINT_CLIENT_ID / MS_SHAREPOINT_CLIENT_SECRET not set", + reason="MS_GRAPH_TENANT_ID / MS_GRAPH_CLIENT_ID / MS_GRAPH_CLIENT_SECRET not set", ) class TestLiveAppOnly: """ End-to-end tests using app-only (client credentials) authentication. Set these env vars before running: - MS_SHAREPOINT_TENANT_ID — Azure AD tenant ID - MS_SHAREPOINT_CLIENT_ID — App registration client ID - MS_SHAREPOINT_CLIENT_SECRET — App registration client secret - MS_SHAREPOINT_REGION — Search region, e.g. "US" (default: "US") + MS_GRAPH_TENANT_ID — Azure AD tenant ID + MS_GRAPH_CLIENT_ID — App registration client ID + MS_GRAPH_CLIENT_SECRET — App registration client secret + MS_GRAPH_REGION — Search region, e.g. "NAM" (default: "NAM") Run with: hatch run test:integration @@ -456,9 +456,9 @@ class TestLiveAppOnly: @staticmethod def _get_app_token() -> str: - tenant_id = os.environ["MS_SHAREPOINT_TENANT_ID"] - client_id = os.environ["MS_SHAREPOINT_CLIENT_ID"] - client_secret = os.environ["MS_SHAREPOINT_CLIENT_SECRET"] + tenant_id = os.environ["MS_GRAPH_TENANT_ID"] + client_id = os.environ["MS_GRAPH_CLIENT_ID"] + client_secret = os.environ["MS_GRAPH_CLIENT_SECRET"] response = requests.post( f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", data={ @@ -472,19 +472,9 @@ def _get_app_token() -> str: response.raise_for_status() return response.json()["access_token"] - def test_app_only_search_requires_region(self): - token = self._get_app_token() - - # Without region, the Search API must return 400. - retriever_no_region = MSSharePointRetriever(top_k=3, max_retries=0) - with pytest.raises(SharePointRequestError) as exc_info: - retriever_no_region.run(query="test", access_token=token) - assert exc_info.value.status_code == 400 - assert "Region is required" in str(exc_info.value) - def test_app_only_search_with_region_succeeds(self): token = self._get_app_token() - region = os.environ.get("MS_SHAREPOINT_REGION", "US") + region = os.environ.get("MS_GRAPH_REGION", "NAM") # With region, the request should succeed. retriever = MSSharePointRetriever(top_k=3, region=region, max_retries=0) From 4c12dd9fcd78c1353460319967d79b316aa3ecd6 Mon Sep 17 00:00:00 2001 From: SyedShahmeerAli12 Date: Tue, 4 Aug 2026 16:24:59 +0500 Subject: [PATCH 3/6] fix --- .../retrievers/microsoft_sharepoint/retriever.py | 8 ++++---- integrations/microsoft_sharepoint/tests/test_retriever.py | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py b/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py index fe321aad4f..12b37c3915 100644 --- a/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py +++ b/integrations/microsoft_sharepoint/src/haystack_integrations/components/retrievers/microsoft_sharepoint/retriever.py @@ -44,10 +44,10 @@ class MSSharePointRetriever: download or convert the underlying files. Compose a downstream fetcher/converter (such as `MSSharePointFetcher`) when full content is needed. - The retriever takes a per-user `access_token` as a run input, typically wired - from an upstream `OAuthResolver`. The token must carry delegated Microsoft Graph permissions - (for example `Files.Read.All` and, for site/list scoping, `Sites.Read.All`). The Search API supports - delegated permissions only. + The retriever takes an `access_token` as a run input. For delegated (on-behalf-of) auth, pass a + per-user token wired from an upstream `OAuthResolver`; the token must carry delegated Microsoft Graph + permissions (for example `Files.Read.All` and, for site/list scoping, `Sites.Read.All`). For app-only + (client-credentials) auth, pass an application token and set the `region` parameter at init time. ### Usage example ```python diff --git a/integrations/microsoft_sharepoint/tests/test_retriever.py b/integrations/microsoft_sharepoint/tests/test_retriever.py index ae485dc711..5c30ff7cc6 100644 --- a/integrations/microsoft_sharepoint/tests/test_retriever.py +++ b/integrations/microsoft_sharepoint/tests/test_retriever.py @@ -435,9 +435,7 @@ def test_run_against_microsoft_graph(self): @pytest.mark.integration @pytest.mark.skipif( - not all( - os.environ.get(v) for v in ("MS_GRAPH_TENANT_ID", "MS_GRAPH_CLIENT_ID", "MS_GRAPH_CLIENT_SECRET") - ), + not all(os.environ.get(v) for v in ("MS_GRAPH_TENANT_ID", "MS_GRAPH_CLIENT_ID", "MS_GRAPH_CLIENT_SECRET")), reason="MS_GRAPH_TENANT_ID / MS_GRAPH_CLIENT_ID / MS_GRAPH_CLIENT_SECRET not set", ) class TestLiveAppOnly: From eb5492980cdbb78445617500e831581b27dc2760 Mon Sep 17 00:00:00 2001 From: bogdankostic Date: Tue, 4 Aug 2026 14:26:00 +0200 Subject: [PATCH 4/6] Update default region in test case --- integrations/microsoft_sharepoint/tests/test_retriever.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/microsoft_sharepoint/tests/test_retriever.py b/integrations/microsoft_sharepoint/tests/test_retriever.py index 5c30ff7cc6..ab4f13877e 100644 --- a/integrations/microsoft_sharepoint/tests/test_retriever.py +++ b/integrations/microsoft_sharepoint/tests/test_retriever.py @@ -472,7 +472,7 @@ def _get_app_token() -> str: def test_app_only_search_with_region_succeeds(self): token = self._get_app_token() - region = os.environ.get("MS_GRAPH_REGION", "NAM") + region = os.environ.get("MS_GRAPH_REGION", "DEU") # With region, the request should succeed. retriever = MSSharePointRetriever(top_k=3, region=region, max_retries=0) From d8fafc62d8ea463c996dc1edf09c429b2f0e9774 Mon Sep 17 00:00:00 2001 From: bogdankostic Date: Tue, 4 Aug 2026 14:29:18 +0200 Subject: [PATCH 5/6] Update default region in integration test case --- integrations/microsoft_sharepoint/tests/test_retriever.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/microsoft_sharepoint/tests/test_retriever.py b/integrations/microsoft_sharepoint/tests/test_retriever.py index ab4f13877e..1e74b2d31a 100644 --- a/integrations/microsoft_sharepoint/tests/test_retriever.py +++ b/integrations/microsoft_sharepoint/tests/test_retriever.py @@ -446,7 +446,7 @@ class TestLiveAppOnly: MS_GRAPH_TENANT_ID — Azure AD tenant ID MS_GRAPH_CLIENT_ID — App registration client ID MS_GRAPH_CLIENT_SECRET — App registration client secret - MS_GRAPH_REGION — Search region, e.g. "NAM" (default: "NAM") + MS_GRAPH_REGION — Search region, e.g. "NAM" (default: "DEU") Run with: hatch run test:integration From 41d0782755af3158aa5719fc106feb7c3c5ad021 Mon Sep 17 00:00:00 2001 From: bogdankostic Date: Tue, 4 Aug 2026 14:31:29 +0200 Subject: [PATCH 6/6] Update regions in tests --- integrations/microsoft_sharepoint/tests/test_retriever.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integrations/microsoft_sharepoint/tests/test_retriever.py b/integrations/microsoft_sharepoint/tests/test_retriever.py index 1e74b2d31a..1bf7a610f9 100644 --- a/integrations/microsoft_sharepoint/tests/test_retriever.py +++ b/integrations/microsoft_sharepoint/tests/test_retriever.py @@ -150,9 +150,9 @@ def test_to_dict(self): } def test_to_dict_with_region(self): - retriever = MSSharePointRetriever(region="US") + retriever = MSSharePointRetriever(region="NAM") data = retriever.to_dict() - assert data["init_parameters"]["region"] == "US" + assert data["init_parameters"]["region"] == "NAM" def test_from_dict_round_trip(self): retriever = MSSharePointRetriever(entity_types=["site"], top_k=7, query_template="{searchTerms}") @@ -250,11 +250,11 @@ def test_no_query_template_omits_key(self): assert "fields" not in mock_post.call_args.kwargs["json"]["requests"][0] def test_region_included_in_request_body_when_set(self): - retriever = MSSharePointRetriever(region="US") + retriever = MSSharePointRetriever(region="NAM") with patch.object(httpx.Client, "post", return_value=_make_response(json_body=EMPTY_RESPONSE)) as mock_post: retriever.run(query="q", access_token="tok") request = mock_post.call_args.kwargs["json"]["requests"][0] - assert request["region"] == "US" + assert request["region"] == "NAM" def test_region_omitted_from_request_body_when_none(self): retriever = MSSharePointRetriever()