Skip to content

Commit 361336b

Browse files
committed
feat(fdc): Enforce Mapping or dataclass for variables and type-annotate variable_type
- Updated _validate_variables_type to validate that if variables are provided, they must be either a collections.abc.Mapping or a dataclass. - Type-annotated the variable_type parameter as Optional[Type[Any]] = None in both validation helper signatures. - Refactored variable unit tests to use a realistic CreateUserVariables (nesting a UserProfile dataclass) instead of response-like shapes. - Added a test case confirming that standard dictionaries (Mapping) are accepted as valid variables.
1 parent 8b29c87 commit 361336b

2 files changed

Lines changed: 80 additions & 53 deletions

File tree

firebase_admin/dataconnect.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
Firebase apps.
1919
"""
2020

21+
from collections.abc import Mapping
2122
from dataclasses import dataclass, asdict, is_dataclass
22-
from typing import Any, Dict, Generic, Optional, TypeVar, Union
23+
from typing import Any, Dict, Generic, Optional, Type, TypeVar, Union
2324
import firebase_admin
2425
from firebase_admin import _utils, _http_client, App
2526

@@ -214,11 +215,18 @@ def __init__(self, connector_config: ConnectorConfig, app: App) -> None:
214215

215216
self._http_client = _http_client.JsonHttpClient(credential=self._credential)
216217

217-
def _validate_variables_type(self, variables: Any, variable_type: Any) -> None:
218+
def _validate_variables_type(
219+
self,
220+
variables: Any,
221+
variable_type: Optional[Type[Any]] = None
222+
) -> None:
218223
"""Validates variables against expected type."""
219-
if variables is not None and variable_type is not None:
220-
if not isinstance(variables, variable_type):
221-
raise ValueError(f"variables must be of type {variable_type.__name__}")
224+
if variables is not None:
225+
if not (isinstance(variables, Mapping) or is_dataclass(variables)):
226+
raise ValueError("variables must be a collections.abc.Mapping or a dataclass")
227+
if variable_type is not None:
228+
if not isinstance(variables, variable_type):
229+
raise ValueError(f"variables must be of type {variable_type.__name__}")
222230

223231
def _validate_impersonation_options(self, impersonate: Any) -> None:
224232
"""Validates impersonation dictionary options."""
@@ -245,7 +253,7 @@ def _validate_impersonation_options(self, impersonate: Any) -> None:
245253
def _validate_graphql_options(
246254
self,
247255
graphql_options: Optional[GraphqlOptions[Any]],
248-
variable_type: Any = None
256+
variable_type: Optional[Type[Any]] = None
249257
) -> None:
250258
"""Validates GraphqlOptions inputs at runtime."""
251259
if graphql_options is not None:

tests/test_data_connect.py

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ def test_validate_graphql_options_valid(self):
399399
# Valid with no options
400400
self.api_client._validate_graphql_options(None)
401401

402-
# Valid with options
403-
options = dataconnect.GraphqlOptions(variables={"foo": "bar"})
402+
# Valid with default options (no arguments)
403+
options = dataconnect.GraphqlOptions()
404404
self.api_client._validate_graphql_options(options)
405405

406406
def test_validate_graphql_options_valid_impersonate(self):
@@ -416,21 +416,28 @@ def test_validate_graphql_options_valid_impersonate(self):
416416
options = dataconnect.GraphqlOptions(impersonate=imp_auth)
417417
self.api_client._validate_graphql_options(options)
418418

419-
def test_validate_graphql_options_valid_variables(self):
419+
def test_validate_graphql_options_valid_dataclass_variables(self):
420420
@dataclass
421-
class User:
422-
user_id: str
423-
name: str
421+
class UserProfile:
424422
address: str
423+
phone: str
425424

426425
@dataclass
427-
class UsersResponse:
428-
users: list[User]
426+
class CreateUserVariables:
427+
user_id: str
428+
name: str
429+
profile: UserProfile
429430

430-
users_val = [User(user_id="1", name="Fred", address="123 Road")]
431-
valid_variables = UsersResponse(users=users_val)
431+
profile_val = UserProfile(address="123 Road", phone="332-3233-0199")
432+
valid_variables = CreateUserVariables(
433+
user_id="1", name="Fred", profile=profile_val
434+
)
432435
options = dataconnect.GraphqlOptions(variables=valid_variables)
433-
self.api_client._validate_graphql_options(options, UsersResponse)
436+
self.api_client._validate_graphql_options(options, CreateUserVariables)
437+
438+
def test_validate_graphql_options_valid_mapping_variables(self):
439+
options = dataconnect.GraphqlOptions(variables={"user_id": "1", "name": "Fred"})
440+
self.api_client._validate_graphql_options(options)
434441

435442
def test_validate_graphql_options_invalid_options(self):
436443
with pytest.raises(ValueError, match="options must be a GraphqlOptions instance"):
@@ -490,18 +497,26 @@ def test_validate_graphql_options_invalid_operation_name(self):
490497

491498
def test_validate_graphql_options_invalid_variables(self):
492499
@dataclass
493-
class User:
494-
user_id: str
495-
name: str
500+
class UserProfile:
496501
address: str
502+
phone: str
497503

498504
@dataclass
499-
class UsersResponse:
500-
users: list[User]
505+
class CreateUserVariables:
506+
user_id: str
507+
name: str
508+
profile: UserProfile
509+
510+
# Test invalid variable format (not Mapping or dataclass)
511+
options = dataconnect.GraphqlOptions(variables="invalid-string-format")
512+
msg = "variables must be a collections.abc.Mapping or a dataclass"
513+
with pytest.raises(ValueError, match=msg):
514+
self.api_client._validate_graphql_options(options)
501515

502-
options = dataconnect.GraphqlOptions(variables="not-users-response")
503-
with pytest.raises(ValueError, match="variables must be of type UsersResponse"):
504-
self.api_client._validate_graphql_options(options, UsersResponse)
516+
# Test valid Mapping format but type mismatch against expected dataclass type
517+
options = dataconnect.GraphqlOptions(variables={"foo": "bar"})
518+
with pytest.raises(ValueError, match="variables must be of type CreateUserVariables"):
519+
self.api_client._validate_graphql_options(options, CreateUserVariables)
505520

506521

507522
class TestDataConnectApiClientPrepareGraphqlPayload:
@@ -529,29 +544,31 @@ def test_prepare_graphql_payload_with_variables(self):
529544

530545
def test_prepare_graphql_payload_with_dataclass_variables(self):
531546
@dataclass
532-
class User:
533-
user_id: str
534-
name: str
547+
class UserProfile:
535548
address: str
549+
phone: str
536550

537551
@dataclass
538-
class UsersResponse:
539-
users: list[User]
552+
class CreateUserVariables:
553+
user_id: str
554+
name: str
555+
profile: UserProfile
540556

541-
users_val = [User(user_id="1", name="Fred", address="123 Road")]
542-
valid_variables = UsersResponse(users=users_val)
557+
profile_val = UserProfile(address="123 Road", phone="332-3233-0199")
558+
valid_variables = CreateUserVariables(
559+
user_id="1", name="Fred", profile=profile_val
560+
)
543561
options = dataconnect.GraphqlOptions(variables=valid_variables)
544562
payload = self.api_client._prepare_graphql_payload("query { hello }", options)
545563
assert payload == {
546564
"query": "query { hello }",
547565
"variables": {
548-
"users": [
549-
{
550-
"user_id": "1",
551-
"name": "Fred",
552-
"address": "123 Road"
553-
}
554-
]
566+
"user_id": "1",
567+
"name": "Fred",
568+
"profile": {
569+
"address": "123 Road",
570+
"phone": "332-3233-0199"
571+
}
555572
}
556573
}
557574

@@ -589,17 +606,20 @@ def test_prepare_graphql_payload_with_impersonate_authenticated(self):
589606

590607
def test_prepare_graphql_payload_with_all_fields(self):
591608
@dataclass
592-
class User:
593-
user_id: str
594-
name: str
609+
class UserProfile:
595610
address: str
611+
phone: str
596612

597613
@dataclass
598-
class UsersResponse:
599-
users: list[User]
614+
class CreateUserVariables:
615+
user_id: str
616+
name: str
617+
profile: UserProfile
600618

601-
users_val = [User(user_id="1", name="Fred", address="123 Road")]
602-
valid_variables = UsersResponse(users=users_val)
619+
profile_val = UserProfile(address="123 Road", phone="332-3233-0199")
620+
valid_variables = CreateUserVariables(
621+
user_id="1", name="Fred", profile=profile_val
622+
)
603623
imp_auth = dataconnect.Impersonation.authenticated(
604624
{"sub": "authenticated-UUID"}
605625
)
@@ -613,13 +633,12 @@ class UsersResponse:
613633
"query": "query { hello }",
614634
"operationName": "getUsers",
615635
"variables": {
616-
"users": [
617-
{
618-
"user_id": "1",
619-
"name": "Fred",
620-
"address": "123 Road"
621-
}
622-
]
636+
"user_id": "1",
637+
"name": "Fred",
638+
"profile": {
639+
"address": "123 Road",
640+
"phone": "332-3233-0199"
641+
}
623642
},
624643
"extensions": {
625644
"impersonate": {"authClaims": {"sub": "authenticated-UUID"}}

0 commit comments

Comments
 (0)