Skip to content

Commit 657155d

Browse files
committed
refactor(fdc): Pythonic impersonation representation and test suite cleanup
- Impersonation API: Updated Impersonation to use auth_claims (snake_case) in Python land, while translating it to authClaims (camelCase) in _prepare_graphql_payload during JSON wire serialization. - Impersonation Validation: Updated _validate_impersonation_options to validate auth_claims in Python land. - Integration Tests (integration/test_data_connect.py): Added UPDATED_FRED_EMAIL mutation test fixtures with real state changes, restored initial state via UPSERT_FRED_EMAIL cleanup at the end of mutation tests, reordered query tests before mutations, and removed redundant read impersonation test cases.
1 parent b738f3a commit 657155d

3 files changed

Lines changed: 80 additions & 57 deletions

File tree

firebase_admin/dataconnect.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def __init__(
131131
else:
132132
if not isinstance(auth_claims, dict):
133133
raise ValueError("'auth_claims' must be a dictionary.")
134-
super().__init__(authClaims=auth_claims)
134+
super().__init__(auth_claims=auth_claims)
135135

136136
@staticmethod
137137
def unauthenticated() -> 'Impersonation':
@@ -364,22 +364,22 @@ def _validate_impersonation_options(self, impersonate: Any) -> None:
364364
if impersonate is not None:
365365
if not isinstance(impersonate, dict):
366366
raise ValueError('impersonate option must be a dictionary')
367-
if 'unauthenticated' not in impersonate and 'authClaims' not in impersonate:
367+
if 'unauthenticated' not in impersonate and 'auth_claims' not in impersonate:
368368
raise ValueError(
369369
"impersonate option must contain either "
370-
"'unauthenticated' or 'authClaims'"
370+
"'unauthenticated' or 'auth_claims'"
371371
)
372-
if 'unauthenticated' in impersonate and 'authClaims' in impersonate:
372+
if 'unauthenticated' in impersonate and 'auth_claims' in impersonate:
373373
raise ValueError(
374374
"impersonate option cannot contain both "
375-
"'unauthenticated' and 'authClaims'"
375+
"'unauthenticated' and 'auth_claims'"
376376
)
377377
if 'unauthenticated' in impersonate:
378378
if not isinstance(impersonate['unauthenticated'], bool):
379379
raise ValueError("'unauthenticated' claim must be a boolean")
380-
if 'authClaims' in impersonate:
381-
if not isinstance(impersonate['authClaims'], dict):
382-
raise ValueError("'authClaims' claim must be a dictionary")
380+
if 'auth_claims' in impersonate:
381+
if not isinstance(impersonate['auth_claims'], dict):
382+
raise ValueError("'auth_claims' claim must be a dictionary")
383383

384384
def _validate_graphql_options(
385385
self,
@@ -426,8 +426,11 @@ def _prepare_graphql_payload(
426426
payload["operationName"] = graphql_options.operation_name.strip()
427427

428428
if graphql_options.impersonate is not None:
429+
impersonate_payload = dict(graphql_options.impersonate)
430+
if "auth_claims" in impersonate_payload:
431+
impersonate_payload["authClaims"] = impersonate_payload.pop("auth_claims")
429432
payload["extensions"] = {
430-
"impersonate": graphql_options.impersonate
433+
"impersonate": impersonate_payload
431434
}
432435

433436
return payload

integration/test_data_connect.py

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
'from': {'id': FRED_USER['id']}
4141
}
4242

43+
UPDATED_FRED_EMAIL = {
44+
'id': FRED_EMAIL['id'],
45+
'subject': 'updated subject',
46+
'date': '2026-07-31',
47+
'text': 'updated body text!',
48+
'from': {'id': FRED_USER['id']}
49+
}
50+
4351
INITIAL_STATE = {
4452
'users': [FRED_USER, JEFF_USER],
4553
'emails': [FRED_EMAIL]
@@ -97,6 +105,17 @@
97105
}})
98106
}}"""
99107

108+
UPSERT_UPDATED_FRED_EMAIL = f"""
109+
mutation email {{
110+
email_upsert(data: {{
111+
id:"{UPDATED_FRED_EMAIL['id']}",
112+
subject: "{UPDATED_FRED_EMAIL['subject']}",
113+
date: "{UPDATED_FRED_EMAIL['date']}",
114+
text: "{UPDATED_FRED_EMAIL['text']}",
115+
fromId: "{UPDATED_FRED_EMAIL['from']['id']}"
116+
}})
117+
}}"""
118+
100119
DELETE_ALL = """
101120
mutation delete {
102121
email_deleteMany(all: true)
@@ -125,33 +144,22 @@
125144
class TestExecuteGraphql:
126145
"""Integration tests for execute_graphql method."""
127146

128-
def test_execute_graphql_mutation(self):
129-
"""Tests executing mutations via execute_graphql."""
130-
dc_client = dataconnect.client(CONNECTOR_CONFIG)
131-
fred_resp = dc_client.execute_graphql(UPSERT_FRED_USER)
132-
assert fred_resp.data['user_upsert']['id'] == FRED_USER['id']
133-
134-
jeff_resp = dc_client.execute_graphql(UPSERT_JEFF_USER)
135-
assert jeff_resp.data['user_upsert']['id'] == JEFF_USER['id']
136-
137-
upsert_email_resp = dc_client.execute_graphql(UPSERT_FRED_EMAIL)
138-
email_id = upsert_email_resp.data['email_upsert']['id']
139-
assert email_id
140-
141-
get_email_options = dataconnect.GraphqlOptions(variables={'id': email_id})
142-
query_email_resp = dc_client.execute_graphql(
143-
QUERY_GET_EMAIL, options=get_email_options
144-
)
145-
assert query_email_resp.data['email'] == FRED_EMAIL
146-
147147
def test_execute_graphql_query(self):
148148
"""Tests executing a query via execute_graphql."""
149149
dc_client = dataconnect.client(CONNECTOR_CONFIG)
150150
resp = dc_client.execute_graphql(QUERY_LIST_USERS)
151-
assert sorted(resp.data['users'], key=lambda x: x['id']) == sorted(
152-
INITIAL_STATE['users'], key=lambda x: x['id']
151+
assert sorted(resp.data['users'], key=lambda user: user['id']) == sorted(
152+
INITIAL_STATE['users'], key=lambda user: user['id']
153153
)
154154

155+
def test_execute_graphql_query_with_variables(self):
156+
"""Tests query execution with variables."""
157+
dc_client = dataconnect.client(CONNECTOR_CONFIG)
158+
user_id = INITIAL_STATE['users'][0]['id']
159+
options = dataconnect.GraphqlOptions(variables={'id': {'id': user_id}})
160+
resp = dc_client.execute_graphql(QUERY_GET_USER_BY_ID, options=options)
161+
assert resp.data['user'] == INITIAL_STATE['users'][0]
162+
155163
def test_execute_graphql_operation_name_multiple_queries(self):
156164
"""Tests operation_name with multi-query document."""
157165
dc_client = dataconnect.client(CONNECTOR_CONFIG)
@@ -166,13 +174,25 @@ def test_execute_graphql_query_error_missing_variables(self):
166174
dc_client.execute_graphql(QUERY_GET_USER_BY_ID)
167175
assert excinfo.value.code == 'query-error'
168176

169-
def test_execute_graphql_query_with_variables(self):
170-
"""Tests query execution with variables."""
177+
def test_execute_graphql_mutation(self):
178+
"""Tests executing mutations via execute_graphql."""
171179
dc_client = dataconnect.client(CONNECTOR_CONFIG)
172-
user_id = INITIAL_STATE['users'][0]['id']
173-
options = dataconnect.GraphqlOptions(variables={'id': {'id': user_id}})
174-
resp = dc_client.execute_graphql(QUERY_GET_USER_BY_ID, options=options)
175-
assert resp.data['user'] == INITIAL_STATE['users'][0]
180+
fred_resp = dc_client.execute_graphql(UPSERT_FRED_USER)
181+
assert fred_resp.data['user_upsert']['id'] == FRED_USER['id']
182+
183+
jeff_resp = dc_client.execute_graphql(UPSERT_JEFF_USER)
184+
assert jeff_resp.data['user_upsert']['id'] == JEFF_USER['id']
185+
186+
upsert_email_resp = dc_client.execute_graphql(UPSERT_UPDATED_FRED_EMAIL)
187+
email_id = upsert_email_resp.data['email_upsert']['id']
188+
assert email_id == UPDATED_FRED_EMAIL['id']
189+
190+
get_email_options = dataconnect.GraphqlOptions(variables={'id': email_id})
191+
query_email_resp = dc_client.execute_graphql(
192+
QUERY_GET_EMAIL, options=get_email_options
193+
)
194+
assert query_email_resp.data['email'] == UPDATED_FRED_EMAIL
195+
dc_client.execute_graphql(UPSERT_FRED_EMAIL)
176196

177197

178198
class TestExecuteGraphqlRead:
@@ -182,8 +202,8 @@ def test_execute_graphql_read_query(self):
182202
"""Tests read-only query execution."""
183203
dc_client = dataconnect.client(CONNECTOR_CONFIG)
184204
resp = dc_client.execute_graphql_read(QUERY_LIST_USERS)
185-
assert sorted(resp.data['users'], key=lambda x: x['id']) == sorted(
186-
INITIAL_STATE['users'], key=lambda x: x['id']
205+
assert sorted(resp.data['users'], key=lambda user: user['id']) == sorted(
206+
INITIAL_STATE['users'], key=lambda user: user['id']
187207
)
188208

189209
def test_execute_graphql_read_mutation_fails(self):
@@ -273,8 +293,8 @@ def test_impersonated_authenticated(self):
273293
resp = dc_client.execute_graphql(
274294
QUERY_LIST_USERS, options=OPTS_AUTHORIZED_FRED_CLAIMS
275295
)
276-
assert sorted(resp.data['users'], key=lambda x: x['id']) == sorted(
277-
INITIAL_STATE['users'], key=lambda x: x['id']
296+
assert sorted(resp.data['users'], key=lambda user: user['id']) == sorted(
297+
INITIAL_STATE['users'], key=lambda user: user['id']
278298
)
279299

280300
def test_impersonated_unauthenticated(self):
@@ -283,8 +303,8 @@ def test_impersonated_unauthenticated(self):
283303
resp = dc_client.execute_graphql(
284304
QUERY_LIST_USERS, options=OPTS_UNAUTHORIZED_CLAIMS
285305
)
286-
assert sorted(resp.data['users'], key=lambda x: x['id']) == sorted(
287-
INITIAL_STATE['users'], key=lambda x: x['id']
306+
assert sorted(resp.data['users'], key=lambda user: user['id']) == sorted(
307+
INITIAL_STATE['users'], key=lambda user: user['id']
288308
)
289309

290310
def test_impersonated_non_existing_claims(self):
@@ -293,8 +313,8 @@ def test_impersonated_non_existing_claims(self):
293313
resp = dc_client.execute_graphql(
294314
QUERY_LIST_USERS, options=OPTS_NON_EXISTING_CLAIMS
295315
)
296-
assert sorted(resp.data['users'], key=lambda x: x['id']) == sorted(
297-
INITIAL_STATE['users'], key=lambda x: x['id']
316+
assert sorted(resp.data['users'], key=lambda user: user['id']) == sorted(
317+
INITIAL_STATE['users'], key=lambda user: user['id']
298318
)
299319

300320

tests/test_data_connect.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
TEST_HEADERS = {"key": "val"}
4040
TEST_PAYLOAD = {"query": TEST_QUERY}
4141
TEST_AUTH_CLAIMS = {"sub": "user_123"}
42-
TEST_VARIABLES = {"foo": "bar"}
42+
TEST_VARIABLES = {"var_key": "var_val"}
4343

4444
@dataclass
4545
class UserProfile:
@@ -481,11 +481,11 @@ def test_validate_graphql_options_invalid_impersonate(self):
481481
with pytest.raises(ValueError, match="impersonate option must be a dictionary"):
482482
self.api_client._validate_graphql_options(options)
483483

484-
# impersonate must have either unauthenticated or authClaims
484+
# impersonate must have either unauthenticated or auth_claims
485485
options = dataconnect.GraphqlOptions(impersonate={"invalid_key": True})
486486
msg = (
487487
"impersonate option must contain either "
488-
"'unauthenticated' or 'authClaims'"
488+
"'unauthenticated' or 'auth_claims'"
489489
)
490490
with pytest.raises(ValueError, match=msg):
491491
self.api_client._validate_graphql_options(options)
@@ -495,18 +495,18 @@ def test_validate_graphql_options_invalid_impersonate(self):
495495
with pytest.raises(ValueError, match="'unauthenticated' claim must be a boolean"):
496496
self.api_client._validate_graphql_options(options)
497497

498-
# authClaims must be a dict
499-
options = dataconnect.GraphqlOptions(impersonate={"authClaims": "not-dict"})
500-
with pytest.raises(ValueError, match="'authClaims' claim must be a dictionary"):
498+
# auth_claims must be a dict
499+
options = dataconnect.GraphqlOptions(impersonate={"auth_claims": "not-dict"})
500+
with pytest.raises(ValueError, match="'auth_claims' claim must be a dictionary"):
501501
self.api_client._validate_graphql_options(options)
502502

503-
# impersonate cannot contain both unauthenticated and authClaims
503+
# impersonate cannot contain both unauthenticated and auth_claims
504504
options = dataconnect.GraphqlOptions(
505-
impersonate={"unauthenticated": True, "authClaims": {"uid": "123"}}
505+
impersonate={"unauthenticated": True, "auth_claims": {"uid": "123"}}
506506
)
507507
msg = (
508508
"impersonate option cannot contain both "
509-
"'unauthenticated' and 'authClaims'"
509+
"'unauthenticated' and 'auth_claims'"
510510
)
511511
with pytest.raises(ValueError, match=msg):
512512
self.api_client._validate_graphql_options(options)
@@ -564,7 +564,7 @@ def teardown_method(self, method):
564564

565565
def test_prepare_graphql_payload_only_query(self):
566566
payload = self.api_client._prepare_graphql_payload(TEST_QUERY, None)
567-
assert payload == {"query": TEST_QUERY}
567+
assert payload == TEST_PAYLOAD
568568

569569
def test_prepare_graphql_payload_with_variables(self):
570570
options = dataconnect.GraphqlOptions(variables=TEST_VARIABLES)
@@ -837,7 +837,7 @@ def test_unauthenticated_factory(self):
837837
def test_authenticated_factory(self):
838838
"""Tests factory method for authenticated impersonation."""
839839
imp = dataconnect.Impersonation.authenticated(TEST_AUTH_CLAIMS)
840-
assert imp == {"authClaims": TEST_AUTH_CLAIMS}
840+
assert imp == {"auth_claims": TEST_AUTH_CLAIMS}
841841

842842
def test_constructor_unauthenticated(self):
843843
"""Tests direct constructor with unauthenticated=True."""
@@ -847,7 +847,7 @@ def test_constructor_unauthenticated(self):
847847
def test_constructor_auth_claims(self):
848848
"""Tests direct constructor with auth_claims dict."""
849849
imp = dataconnect.Impersonation(auth_claims=TEST_AUTH_CLAIMS)
850-
assert imp == {"authClaims": TEST_AUTH_CLAIMS}
850+
assert imp == {"auth_claims": TEST_AUTH_CLAIMS}
851851

852852
def test_constructor_neither_unauth_nor_claims_fails(self):
853853
"""Tests specifying neither unauthenticated nor claims raises ValueError."""
@@ -971,7 +971,7 @@ def test_execute_graphql_payload_omits_empty_fields(self, mock_make_gql_request)
971971
mock_make_gql_request.assert_called_once_with(
972972
url=mock.ANY,
973973
headers=mock.ANY,
974-
payload={"query": TEST_QUERY}
974+
payload=TEST_PAYLOAD
975975
)
976976

977977
@mock.patch.object(_http_client.JsonHttpClient, "body_and_response")

0 commit comments

Comments
 (0)