From 4032ddf58bbec5cfc3d56008b929364b072ec299 Mon Sep 17 00:00:00 2001 From: Stanley Phu Date: Wed, 30 Apr 2025 13:06:34 -0700 Subject: [PATCH 1/4] Convert context into query string for FGA queries --- tests/test_fga.py | 11 +++++++++++ workos/fga.py | 8 +++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_fga.py b/tests/test_fga.py index 6a2d5d35..92e2615f 100644 --- a/tests/test_fga.py +++ b/tests/test_fga.py @@ -534,3 +534,14 @@ def test_query(self, mock_query_response, mock_http_client_with_response): warrant_token="warrant_token", ) assert response.dict(exclude_none=True) == mock_query_response + + def test_query_with_context(self, mock_query_response, mock_http_client_with_response): + mock_http_client_with_response(self.http_client, mock_query_response, 200) + + response = self.fga.query( + q="select member of type user for permission:view-docs", + order="asc", + warrant_token="warrant_token", + context={"region": "us", "subscription": "pro"}, + ) + assert response.dict(exclude_none=True) == mock_query_response diff --git a/workos/fga.py b/workos/fga.py index ae52b9b0..5b143b28 100644 --- a/workos/fga.py +++ b/workos/fga.py @@ -1,3 +1,4 @@ +import json from typing import Any, Mapping, Optional, Protocol, Sequence from workos.types.fga import ( CheckOperation, @@ -621,11 +622,16 @@ def query( "after": after, "context": context, } + parsed_list_params = { + key: json.dumps(value) if key == "context" and value is not None else value + for key, value in list_params.items() + if value is not None + } response = self._http_client.request( "fga/v1/query", method=REQUEST_METHOD_GET, - params=list_params, + params=parsed_list_params, headers={"Warrant-Token": warrant_token} if warrant_token else None, ) From 1d5aadb67b91a0d96e436fb0eadf621eb8631fb5 Mon Sep 17 00:00:00 2001 From: Stanley Phu Date: Wed, 30 Apr 2025 14:29:13 -0700 Subject: [PATCH 2/4] Linter --- tests/test_fga.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_fga.py b/tests/test_fga.py index 92e2615f..4bca8974 100644 --- a/tests/test_fga.py +++ b/tests/test_fga.py @@ -535,7 +535,9 @@ def test_query(self, mock_query_response, mock_http_client_with_response): ) assert response.dict(exclude_none=True) == mock_query_response - def test_query_with_context(self, mock_query_response, mock_http_client_with_response): + def test_query_with_context( + self, mock_query_response, mock_http_client_with_response + ): mock_http_client_with_response(self.http_client, mock_query_response, 200) response = self.fga.query( From 365b6c7eea0221a05e9c0479750bb71d2e924ebd Mon Sep 17 00:00:00 2001 From: Stanley Phu Date: Wed, 30 Apr 2025 15:16:34 -0700 Subject: [PATCH 3/4] Include full url with query params from mock helper --- tests/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index c89fc788..76d422b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ cast, ) from unittest.mock import AsyncMock, MagicMock +import urllib.parse import httpx import pytest @@ -162,6 +163,14 @@ def inner( def capture_and_mock(*args, **kwargs): request_kwargs.update(kwargs) + # Capture full URL with encoded params while keeping original URL + if kwargs and "params" in kwargs and kwargs["params"]: + # Convert params to query string with proper URL encoding + query_string = urllib.parse.urlencode( + kwargs["params"], doseq=True, quote_via=urllib.parse.quote_plus + ) + request_kwargs.update({"full_url": f"{kwargs['url']}?{query_string}"}) + return httpx.Response( status_code=status_code, headers=headers, From 21e6f9d5228776698c2438e904c8d8b5aceb6bf0 Mon Sep 17 00:00:00 2001 From: Stanley Phu Date: Wed, 30 Apr 2025 15:16:52 -0700 Subject: [PATCH 4/4] Update query test to check full request url --- tests/test_fga.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_fga.py b/tests/test_fga.py index 4bca8974..5bf3e046 100644 --- a/tests/test_fga.py +++ b/tests/test_fga.py @@ -536,9 +536,11 @@ def test_query(self, mock_query_response, mock_http_client_with_response): assert response.dict(exclude_none=True) == mock_query_response def test_query_with_context( - self, mock_query_response, mock_http_client_with_response + self, mock_query_response, capture_and_mock_http_client_request ): - mock_http_client_with_response(self.http_client, mock_query_response, 200) + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_query_response, 200 + ) response = self.fga.query( q="select member of type user for permission:view-docs", @@ -546,4 +548,8 @@ def test_query_with_context( warrant_token="warrant_token", context={"region": "us", "subscription": "pro"}, ) + + assert request_kwargs["url"] == "https://api.workos.test/fga/v1/query" + expected_full_url = "https://api.workos.test/fga/v1/query?q=select+member+of+type+user+for+permission%3Aview-docs&limit=10&order=asc&context=%7B%22region%22%3A+%22us%22%2C+%22subscription%22%3A+%22pro%22%7D" + assert request_kwargs["full_url"] == expected_full_url assert response.dict(exclude_none=True) == mock_query_response