main
py 203 lines 7.91 KB
Raw
1 # Copyright 2026 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 """Tests for the Application Default Credentials (ADC) auth provider."""
16
17 from unittest.mock import MagicMock
18
19 import pytest
20
21 from colab_cli.auth import AuthProvider, get_credentials
22
23
24 def test_get_credentials_adc_success(mocker):
25 """`--auth=adc` should delegate to google.auth.default() and wrap the
26 resulting credentials in an AuthorizedSession."""
27 mock_creds = MagicMock()
28 # Default ADC user creds don't need (or support) re-scoping; the scopes
29 # are fixed at `gcloud auth application-default login` time.
30 mock_creds.requires_scopes = False
31 mock_default = mocker.patch(
32 "google.auth.default", return_value=(mock_creds, "some-project-id")
33 )
34 mock_session_cls = mocker.patch("colab_cli.auth.requests.AuthorizedSession")
35
36 res = get_credentials(provider=AuthProvider.ADC)
37
38 mock_default.assert_called_once()
39 mock_session_cls.assert_called_once_with(mock_creds)
40 assert res == mock_session_cls.return_value
41
42
43 def test_get_credentials_adc_default_error_propagates(mocker):
44 """If google.auth.default() raises (e.g. no ADC configured), the error
45 should propagate to the caller so they can run `gcloud auth
46 application-default login`."""
47 from google.auth.exceptions import DefaultCredentialsError
48
49 mocker.patch(
50 "google.auth.default",
51 side_effect=DefaultCredentialsError("No ADC found"),
52 )
53 mocker.patch("colab_cli.auth.requests.AuthorizedSession")
54
55 with pytest.raises(DefaultCredentialsError):
56 get_credentials(provider=AuthProvider.ADC)
57
58
59 def test_get_credentials_adc_does_not_invoke_other_providers(mocker):
60 """ADC path must not kick off the InstalledAppFlow."""
61 mock_creds = MagicMock()
62 mock_creds.requires_scopes = False
63 mocker.patch("google.auth.default", return_value=(mock_creds, None))
64 mocker.patch("colab_cli.auth.requests.AuthorizedSession")
65
66 mock_flow = mocker.patch("colab_cli.auth.InstalledAppFlow")
67
68 get_credentials(provider=AuthProvider.ADC)
69
70 mock_flow.from_client_config.assert_not_called()
71
72
73 def test_get_credentials_adc_requests_required_scopes(mocker):
74 """ADC must request required scopes via google.auth.default()."""
75 mock_creds = MagicMock()
76 # Pretend creds don't need re-scoping (e.g., user creds from gcloud).
77 mock_creds.requires_scopes = False
78 mock_default = mocker.patch(
79 "google.auth.default", return_value=(mock_creds, "proj")
80 )
81 mocker.patch("colab_cli.auth.requests.AuthorizedSession")
82
83 get_credentials(provider=AuthProvider.ADC)
84
85 scopes = mock_default.call_args.kwargs.get("scopes")
86 assert scopes is not None, "google.auth.default() must be called with scopes="
87 assert "https://www.googleapis.com/auth/colaboratory" in scopes
88 assert "https://www.googleapis.com/auth/cloud-platform" in scopes
89 assert "https://www.googleapis.com/auth/userinfo.email" in scopes
90
91
92 def test_get_credentials_adc_reapplies_scopes_for_scopable_creds(mocker):
93 """For credential subclasses that support `with_scopes` (service accounts,
94 GCE/GKE, etc.), we must call it so the scopes stick even if
95 google.auth.default() ignored the kwarg.
96 """
97 rescoped = MagicMock(name="rescoped_creds")
98 mock_creds = MagicMock()
99 mock_creds.requires_scopes = True
100 mock_creds.with_scopes.return_value = rescoped
101 mocker.patch("google.auth.default", return_value=(mock_creds, "proj"))
102 mock_session_cls = mocker.patch("colab_cli.auth.requests.AuthorizedSession")
103
104 get_credentials(provider=AuthProvider.ADC)
105
106 mock_creds.with_scopes.assert_called_once()
107 applied_scopes = mock_creds.with_scopes.call_args.args[0]
108 assert "https://www.googleapis.com/auth/cloud-platform" in applied_scopes
109 # The session is built from the *rescoped* creds, not the original.
110 mock_session_cls.assert_called_once_with(rescoped)
111
112
113 def test_get_credentials_adc_tolerates_with_scopes_failure(mocker):
114 """User creds (from `gcloud auth application-default login`) raise
115 NotImplementedError on with_scopes. We must fall back gracefully."""
116 mock_creds = MagicMock()
117 mock_creds.requires_scopes = True
118 mock_creds.with_scopes.side_effect = NotImplementedError("user creds")
119 mocker.patch("google.auth.default", return_value=(mock_creds, "proj"))
120 mock_session_cls = mocker.patch("colab_cli.auth.requests.AuthorizedSession")
121
122 # Should not raise.
123 get_credentials(provider=AuthProvider.ADC)
124
125 # Falls back to using the original (un-rescoped) creds.
126 mock_session_cls.assert_called_once_with(mock_creds)
127
128
129 def test_get_credentials_adc_suppresses_quota_project_warning(mocker, recwarn):
130 """`google-auth` emits a UserWarning ("Your application has authenticated
131 using end user credentials from Google Cloud SDK without a quota project.
132 You might receive a 'quota exceeded' or 'API not enabled' error.") whenever
133 ADC user credentials lack a quota project.
134
135 For this CLI the warning is strictly false: we send
136 `X-Goog-User-Project: 1014160490159` (Colab's project) ourselves
137 (AGENTS.md item 18), so google-auth's heuristic does not apply. Suppress
138 it locally around the `google.auth.default()` call so it never reaches
139 the user's terminal on every `colab` invocation.
140 """
141 import warnings
142
143 mock_creds = MagicMock()
144 mock_creds.requires_scopes = False
145
146 # Simulate google-auth emitting the noisy warning during default(), as
147 # google.auth._default does for `_CLOUD_SDK_CREDENTIALS_WARNING`.
148 def fake_default(*args, **kwargs):
149 warnings.warn(
150 "Your application has authenticated using end user credentials "
151 "from Google Cloud SDK without a quota project. You might receive "
152 'a "quota exceeded" or "API not enabled" error. See the following '
153 "page for troubleshooting: "
154 "https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. ",
155 UserWarning,
156 )
157 return mock_creds, "proj"
158
159 mocker.patch("google.auth.default", side_effect=fake_default)
160 mocker.patch("colab_cli.auth.requests.AuthorizedSession")
161
162 get_credentials(provider=AuthProvider.ADC)
163
164 # The quota-project warning must be filtered out; nothing else.
165 matching = [
166 w
167 for w in recwarn.list
168 if issubclass(w.category, UserWarning)
169 and "without a quota project" in str(w.message)
170 ]
171 assert matching == [], (
172 "Expected the quota-project UserWarning to be suppressed, but it "
173 f"was visible: {[str(w.message) for w in matching]}"
174 )
175
176
177 def test_get_credentials_adc_does_not_suppress_unrelated_warnings(mocker, recwarn):
178 """Suppression must be tightly scoped to the quota-project warning text
179 so that any future genuinely-relevant `google.auth` warning still
180 surfaces to the user."""
181 import warnings
182
183 mock_creds = MagicMock()
184 mock_creds.requires_scopes = False
185
186 def fake_default(*args, **kwargs):
187 warnings.warn("some genuinely concerning new warning", UserWarning)
188 return mock_creds, "proj"
189
190 mocker.patch("google.auth.default", side_effect=fake_default)
191 mocker.patch("colab_cli.auth.requests.AuthorizedSession")
192
193 get_credentials(provider=AuthProvider.ADC)
194
195 matching = [
196 w
197 for w in recwarn.list
198 if issubclass(w.category, UserWarning)
199 and "genuinely concerning" in str(w.message)
200 ]
201 assert len(matching) == 1, (
202 "Unrelated UserWarnings from google.auth must NOT be suppressed."
203 )