Skip to content

feat(dataconnect): Implementation and Testing of the _DataConnectApiClient class#965

Open
mk2023 wants to merge 7 commits into
winefrom
champagne
Open

feat(dataconnect): Implementation and Testing of the _DataConnectApiClient class#965
mk2023 wants to merge 7 commits into
winefrom
champagne

Conversation

@mk2023

@mk2023 mk2023 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Implemented three helper functions in _DataConnectApiClient along with their corresponding implementations:

  • _validate_inputs: Validates the query structure, options, variables types, and custom impersonation claims.
  • _prepare_graphql_payload: Prepares the JSON payload for GraphQL calls, serializing variables (including nested dataclasses) and configuring optional extensions like impersonation.
  • _get_firebase_dataconnect_service_url: Formats the endpoint URL for execution, supporting both production host formats and local emulator host formats.
    Added comprehensive unit tests covering standard cases, invalid options, missing configurations, and emulator environments for each of the helper functions.

Added comprehensive unit tests to TestDataConnectApiClient covering client constructor, inputs validation, variables and impersonation serialization, and service URL construction.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive suite of unit tests for the _DataConnectApiClient class, covering its constructor, input validation, payload preparation, and service URL generation. The review feedback identifies critical issues in the tests, including a missing import for the dataclass decorator and references to undefined classes and attributes in the dataconnect module, both of which will cause runtime errors during test execution.

Comment thread tests/test_data_connect.py
Comment thread tests/test_data_connect.py
mk2023 added 2 commits July 9, 2026 14:54
Implemented _validate_inputs to validate queries, options, variables, and impersonation, and _prepare_graphql_payload to construct GraphQL JSON payloads. Implemented _get_firebase_dataconnect_service_url to build production and emulator service endpoint URLs. Added full unit test coverage for input validation, payload construction, and URL formatting.
…uilder

Implemented three helper functions in _DataConnectApiClient along with their corresponding implementations:
- _validate_inputs: Validates the query structure, options, variables types, and custom impersonation claims.
- _prepare_graphql_payload: Prepares the JSON payload for GraphQL calls, serializing variables (including nested dataclasses) and configuring optional extensions like impersonation.
- _get_firebase_dataconnect_service_url: Formats the endpoint URL for execution, supporting both production host formats and local emulator host formats.
Added comprehensive unit tests covering standard cases, invalid options, missing configurations, and emulator environments for each of the helper functions.
This completes the first half of milestone 3.
@mk2023 mk2023 changed the title feat(dataconnect): Added unit tests for DataConnect API Client helpers feat(dataconnect): Implementation and Testing of the _DataConnectApiClient class Jul 10, 2026
mk2023 added 2 commits July 10, 2026 10:22
Implemented `_get_headers` inside `_DataConnectApiClient` to build standard telemetry headers for outgoing HTTP requests. Specifically, it populates:
- X-Firebase-Client: The current Python SDK version header.
- x-goog-api-client: The Google telemetry metrics header.
Added the `TestDataConnectApiClientGetHeaders` unit test suite to verify the return type and values.
Fixed formatting and style issues highlighted by pylint:
- Removed trailing whitespace in TestDataConnectApiClientGetHeaders test class.
- Resolved missing final newline warning at the end of the test file.

@stephenarosaj stephenarosaj left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review - just dropping comments early

Comment thread firebase_admin/dataconnect.py Outdated
return {"unauthenticated": True}

@staticmethod
def authenticated(auth_claims: Dict[str, Any]) -> Dict[str, Any]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to more strongly type this? In node we use this type:

/**
 * Type representing the partial claims of a Firebase Auth token used to evaluate the
 * Data Connect auth policy.
 */
export type AuthClaims = Partial<DecodedIdToken>;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point! After taking a look at AuthClaims and DecodedIdToken, I used typing.TypedDict with total=False to mirror Partial<DecodedIdToken>, along with a nested FirebaseClaim type structure. Additionally, I annotated Impersonation.authenticated with Union[AuthClaims, Dict[str, Any]] so that type checkers provide full auto-completion for standard claims while still supporting arbitrary custom claim dictionaries:

class FirebaseClaim(TypedDict, total=False):
"""Provider-specific identity details and sign-in info within a decoded ID token."""
identities: Dict[str, Any]
sign_in_provider: str
sign_in_second_factor: str
second_factor_identifier: str
tenant: str

class AuthClaims(TypedDict, total=False):
"""Partial claims of a Firebase Auth token used for Data Connect policy evaluation."""
aud: str
auth_time: int
email: str
email_verified: bool
exp: int
firebase: FirebaseClaim
iat: int
iss: str
phone_number: str
picture: str
sub: str
uid: str

class Impersonation:
"""Represents impersonation configuration for DataConnect requests."""

@staticmethod
def unauthenticated() -> Dict[str, bool]:
    """Returns impersonation configuration for unauthenticated requests."""
    return {"unauthenticated": True}

@staticmethod
def authenticated(auth_claims: Union[AuthClaims, Dict[str, Any]]) -> Dict[str, Any]:
    """Returns impersonation configuration for authenticated requests."""
    return {"authClaims": auth_claims}`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the Node.js Admin SDK, which exports DecodedIdToken from auth/token-verifier.ts, the Python Admin SDK does not define or export any typed dictionary or class representation for verified ID tokens (the auth module returns raw dicts). Thus, for right now, we just left AuthClaimsas a raw dictionary.

Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated

@stephenarosaj stephenarosaj left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further partial review

Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated
# Validate Variables against expected variable_type
variables = graphql_options.variables
if variables is not None and variable_type is not None:
if not isinstance(variables, variable_type):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isinstsance() can only be used to check that an object is an instance of a class (documentation)

Is it possible that a user passes in a type that's not a class or that will otherwise cause a false / throw an error in isinstance() even it really is the desired type?? after doing some research, it seems so:

  • PEP 484 talks about runtime type erasure for generics
  • there's also reddit and stackoverflow posts about for example it not working with list[<some type>] or Dict[str, any]

There's a bunch of solutions floating around on the internet / with AI, but I propose we ask Lahiru and/or Denver for advice

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! You're completely right; passing something like list[User] or Dict[str, Any] will crash isinstance() with a TypeError because of type erasure in Python. I've been seeing a bunch of solutions as well, like utilizing typing.get_origin() to extract the base class from a generic type, and then using typing.get_args()to complete a deep-check of the inner types. I think it's a great idea to ask Lahiru and/or Denver for advice on how to go about this matter.

@stephenarosaj stephenarosaj Jul 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is a point of active discussion and there may be some API changes - for now, let's continue with the rest of the PR and project, and we'll make adjustments later as needed

@stephenarosaj stephenarosaj Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: our discussion at lunch today - let's update the code to the following:

  • the generic variables type should be limited to being either collections.abc.Mapping or a dataclass
  • _validate_variables_type() should validate that if the variables are not None, they must either be a collections.abc.Mapping or a dataclass
  • variables_type should be type-annotated as an optional dataclass since we can't type-check a TypedDict (we may end up removing variables_type after further design - for now let's keep it in to follow the API proposal, and because it's easier to remove than to add :P)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I updated _validate_variables_type() and _validate_graphql_options()!

Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py Outdated

@itsrakhil itsrakhil left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments.

@stephenarosaj stephenarosaj left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partial review of main file - now for tests

Comment thread firebase_admin/dataconnect.py Outdated
Comment thread firebase_admin/dataconnect.py
def _prepare_graphql_payload(
self,
graphql_query: str,
graphql_options: Optional[GraphqlOptions[Any]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it GraphqlOptions[Any]? shouldn't it be GraphqlOptions[_Variables]?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we actually need _Variables here. Since _prepare_graphql_payload just serializes the options to a standard Dict[str, Any] and doesn't propagate the type to the return type, the generic type variable doesn't have an effect. I'm totally happy to switch it back if you'd prefer, though!"

…ost utility

- Inherited Impersonation from dict to resolve the type-checking mismatch for static methods while maintaining dictionary behavior at runtime.

- Updated GraphqlOptions.impersonate type annotation to accept Union[Impersonation, Dict[str, Any]] to support both helper and raw dictionary inputs.

- Extracted generic variable and impersonate validation into separate helper methods _validate_variables_type and _validate_impersonation_options.

- Shared emulator host extraction and validation logic in _utils.get_emulator_host and reused it across dataconnect and functions.

- Removed AuthClaims and FirebaseClaim TypedDicts to keep them as raw dicts for now.

- Renamed generic TypeVars to private naming conventions (_Data and _Variables).

@stephenarosaj stephenarosaj left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some minor changes but i think this looks good!

Comment thread tests/test_data_connect.py Outdated
Comment thread tests/test_data_connect.py Outdated
Comment thread tests/test_data_connect.py Outdated
Comment thread tests/test_data_connect.py
Comment thread tests/test_data_connect.py Outdated
…te 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.
Comment thread firebase_admin/dataconnect.py Outdated
"""Validates variables against expected type."""
if variables is not None and variable_type is not None:
if not isinstance(variables, variable_type):
raise ValueError(f"variables must be of type {variable_type.__name__}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user passes something like a tuple of types (e.g., (list, dict)) or certain generics from the typing module, Python will crash with an AttributeError when trying to construct this string, hiding the actual ValueError.
To make the error reporting completely safe, we can use getattr with a fallback to str():
raise ValueError(f"variables must be of type {getattr(variable_type, '__name__', str(variable_type))}")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants