Skip to content

Commit f73e571

Browse files
committed
test(client): add comprehensive unit tests for 100% coverage
1 parent c87a69a commit f73e571

File tree

3 files changed

+221
-129
lines changed

3 files changed

+221
-129
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: adk
16+
on:
17+
pull_request:
18+
paths:
19+
- 'packages/toolbox-adk/**'
20+
- '!packages/toolbox-adk/**/*.md'
21+
pull_request_target:
22+
types: [labeled]
23+
24+
# Declare default permissions as read only.
25+
permissions: read-all
26+
27+
jobs:
28+
lint:
29+
if: "${{ github.event.action != 'labeled' || github.event.label.name == 'tests: run' }}"
30+
name: lint
31+
runs-on: ubuntu-latest
32+
concurrency:
33+
group: ${{ github.workflow }}-${{ github.ref }}
34+
cancel-in-progress: true
35+
defaults:
36+
run:
37+
working-directory: ./packages/toolbox-adk
38+
permissions:
39+
contents: 'read'
40+
issues: 'write'
41+
pull-requests: 'write'
42+
steps:
43+
- name: Remove PR Label
44+
if: "${{ github.event.action == 'labeled' && github.event.label.name == 'tests: run' }}"
45+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
46+
with:
47+
github-token: ${{ secrets.GITHUB_TOKEN }}
48+
script: |
49+
try {
50+
await github.rest.issues.removeLabel({
51+
name: 'tests: run',
52+
owner: context.repo.owner,
53+
repo: context.repo.repo,
54+
issue_number: context.payload.pull_request.number
55+
});
56+
} catch (e) {
57+
console.log('Failed to remove label. Another job may have already removed it!');
58+
}
59+
- name: Checkout code
60+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
61+
with:
62+
ref: ${{ github.event.pull_request.head.sha }}
63+
repository: ${{ github.event.pull_request.head.repo.full_name }}
64+
token: ${{ secrets.GITHUB_TOKEN }}
65+
- name: Setup Python
66+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
67+
with:
68+
python-version: "3.11"
69+
70+
- name: Install dependencies
71+
run: |
72+
pip install .[test]
73+
# Install toolbox-core from local path to ensure we test against current codebase
74+
pip install ../toolbox-core
75+
76+
- name: Run linters
77+
run: |
78+
# Check if we need to run linters or just rely on pre-commit locally?
79+
# The other files allow this.
80+
# For consistency:
81+
# black --check .
82+
# isort --check .
83+
# But let's check if the user wants strictly same contents or just 'tests'.
84+
# I will add basic linting to be safe/compliant.
85+
pip install black isort mypy
86+
black --check .
87+
isort --check .
88+
89+
- name: Run type-check
90+
env:
91+
MYPYPATH: './src'
92+
run: mypy --install-types --non-interactive --cache-dir=.mypy_cache/ -p toolbox_adk

packages/toolbox-adk/src/toolbox_adk/client.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
from .credentials import CredentialConfig, CredentialType
2424

25-
# Global ContextVar for User Identity (3LO) tokens to be injected per-request
2625
USER_TOKEN_CONTEXT_VAR: ContextVar[Optional[str]] = ContextVar(
2726
"toolbox_user_token", default=None
2827
)
@@ -53,11 +52,6 @@ def __init__(
5352
self._credentials = credentials
5453
self._additional_headers = additional_headers or {}
5554

56-
# Prepare auth_token_getters for toolbox-core
57-
# toolbox_core expects: dict[str, Callable[[], str | Awaitable[str]]]
58-
# However, for general headers (like Authorization), we can pass them in client_headers
59-
# if they are static or simpler. Toolbox-core supports `client_headers` which can be dynamic.
60-
6155
self._core_client_headers: Dict[
6256
str, Union[str, Callable[[], str], Callable[[], Awaitable[str]]]
6357
] = {}
@@ -85,8 +79,6 @@ def _configure_auth(self, creds: CredentialConfig) -> None:
8579
)
8680

8781
# Create an async capable token getter
88-
# We wrap it to match the signature expected by toolbox-core headers
89-
# (which accepts callables)
9082
self._core_client_headers["Authorization"] = self._create_adc_token_getter(
9183
creds.target_audience
9284
)
@@ -108,14 +100,10 @@ def _configure_auth(self, creds: CredentialConfig) -> None:
108100

109101
elif creds.type == CredentialType.USER_IDENTITY:
110102
# For USER_IDENTITY (3LO), the *Tool* handles the interactive flow at runtime.
111-
# We use a ContextVar to inject the token per-request.
112103

113104
def get_user_token() -> str:
114105
token = USER_TOKEN_CONTEXT_VAR.get()
115106
if not token:
116-
# If this is called but no token is set in context, it means
117-
# the tool wrapper failed to set it or we are in a context where
118-
# we expected it. We return empty string which might cause 401.
119107
return ""
120108
return f"Bearer {token}"
121109

@@ -125,28 +113,19 @@ def _create_adc_token_getter(self, audience: str) -> Callable[[], str]:
125113
"""Returns a callable that fetches a fresh ID token using ADC."""
126114

127115
def get_token() -> str:
128-
# Note: This is a synchronous call. Toolbox-core supports sync callables in headers.
129-
# Ideally we would use async but google-auth is primarily sync for these helpers.
130116
request = transport.requests.Request()
131-
# Try to get ID token directly (e.g. on Cloud Run)
132117
try:
133118
token = id_token.fetch_id_token(request, audience)
134119
return f"Bearer {token}"
135120
except Exception:
136-
# Fallback to default credentials (e.g. local gcloud)
121+
# Fallback to default credentials
137122
creds, _ = google.auth.default()
138123
if not creds.valid:
139124
creds.refresh(request)
140-
# If specific ID token credentials, use them, otherwise this might be Access Token (scoped)
141-
# For Toolbox we usually need ID Tokens.
142-
# If the user is locally auth'd via `gcloud auth login`, fetch_id_token is preferred.
143-
# If falling back to service account file:
125+
144126
if hasattr(creds, "id_token") and creds.id_token:
145127
return f"Bearer {creds.id_token}"
146128

147-
# If we are here, we might need to manually sign via IAM or similar if it's a generic SA.
148-
# For simplicity in this v1, we assume fetch_id_token works or standard creds work.
149-
# Re-attempt fetch_id_token on the credentials object if possible
150129
curr_token = getattr(creds, "token", None)
151130
return f"Bearer {curr_token}" if curr_token else ""
152131

0 commit comments

Comments
 (0)