| 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 developer-only `colab whoami` command. |
| 16 | |
| 17 | This is a debugging aid that resolves the active credentials, mints an access |
| 18 | token, and queries Google's tokeninfo endpoint to print the human-readable |
| 19 | identity (email), grant scopes, and expiry of whatever the CLI is about to |
| 20 | send to colab.research.google.com / colab.pa.googleapis.com. |
| 21 | """ |
| 22 | |
| 23 | import sys |
| 24 | from unittest.mock import MagicMock, patch |
| 25 | |
| 26 | import pytest |
| 27 | |
| 28 | from colab_cli.cli import main |
| 29 | from colab_cli.auth import AuthProvider |
| 30 | |
| 31 | |
| 32 | # Unambiguously-fake placeholders so credential-scanner pre-commit hooks |
| 33 | # don't false-positive on `ya29.*` strings. The whoami code only ever uses |
| 34 | # these as opaque payloads passed to a mocked urllib.request.urlopen. |
| 35 | _FAKE_TOKEN = "TEST-TOKEN-PLACEHOLDER" |
| 36 | |
| 37 | |
| 38 | def _fake_creds(token: str = _FAKE_TOKEN): |
| 39 | """Build a credentials-like mock: has .token and .refresh().""" |
| 40 | creds = MagicMock() |
| 41 | creds.token = token |
| 42 | creds.refresh = MagicMock() |
| 43 | return creds |
| 44 | |
| 45 | |
| 46 | def _fake_authed_session(token: str = _FAKE_TOKEN): |
| 47 | """Mimic google.auth.transport.requests.AuthorizedSession enough for whoami.""" |
| 48 | sess = MagicMock() |
| 49 | sess.credentials = _fake_creds(token) |
| 50 | return sess |
| 51 | |
| 52 | |
| 53 | def test_whoami_prints_human_readable_summary(mock_common_state, capsys): |
| 54 | """Default invocation should fetch the token, hit tokeninfo, and print |
| 55 | a labelled summary including email, the active auth provider, scopes |
| 56 | (one per line), and an Expires line.""" |
| 57 | mock_common_state.auth_provider = AuthProvider.ADC |
| 58 | |
| 59 | fake_response = MagicMock() |
| 60 | fake_response.status_code = 200 |
| 61 | fake_response.json.return_value = { |
| 62 | "email": "user@example.com", |
| 63 | "scope": ( |
| 64 | "https://www.googleapis.com/auth/userinfo.email " |
| 65 | "https://www.googleapis.com/auth/colaboratory" |
| 66 | ), |
| 67 | "expires_in": "2847", |
| 68 | "audience": "32555940559.apps.googleusercontent.com", |
| 69 | } |
| 70 | |
| 71 | with patch("colab_cli.auth.get_credentials", return_value=_fake_authed_session()): |
| 72 | with patch("urllib.request.urlopen") as mock_urlopen: |
| 73 | cm = MagicMock() |
| 74 | cm.__enter__ = MagicMock(return_value=cm) |
| 75 | cm.__exit__ = MagicMock(return_value=False) |
| 76 | cm.read.return_value = b'{"email":"user@example.com","scope":"https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/colaboratory","expires_in":"2847","audience":"32555940559.apps.googleusercontent.com"}' |
| 77 | cm.status = 200 |
| 78 | mock_urlopen.return_value = cm |
| 79 | |
| 80 | with patch.object(sys, "argv", ["colab", "--auth=adc", "whoami"]): |
| 81 | with pytest.raises(SystemExit) as error: |
| 82 | main() |
| 83 | assert error.value.code == 0 |
| 84 | |
| 85 | out = capsys.readouterr().out |
| 86 | assert "user@example.com" in out |
| 87 | assert "adc" in out.lower() |
| 88 | assert "userinfo.email" in out |
| 89 | assert "colaboratory" in out |
| 90 | # Expiry should be rendered in a human form (minutes), not raw seconds. |
| 91 | assert "47m" in out or "47 min" in out |
| 92 | |
| 93 | |
| 94 | def test_whoami_handles_tokeninfo_error_gracefully(mock_common_state, capsys): |
| 95 | """If tokeninfo returns 4xx (e.g. expired/revoked token), whoami should |
| 96 | print an error and exit non-zero rather than blowing up with a stack trace. |
| 97 | Developers reading the message should be able to tell what happened. |
| 98 | """ |
| 99 | mock_common_state.auth_provider = AuthProvider.ADC |
| 100 | |
| 101 | with patch("colab_cli.auth.get_credentials", return_value=_fake_authed_session()): |
| 102 | with patch("urllib.request.urlopen") as mock_urlopen: |
| 103 | import urllib.error |
| 104 | |
| 105 | mock_urlopen.side_effect = urllib.error.HTTPError( |
| 106 | url="https://oauth2.googleapis.com/tokeninfo", |
| 107 | code=400, |
| 108 | msg="Bad Request", |
| 109 | hdrs=None, |
| 110 | fp=None, |
| 111 | ) |
| 112 | |
| 113 | with patch.object(sys, "argv", ["colab", "--auth=adc", "whoami"]): |
| 114 | with pytest.raises(SystemExit) as error: |
| 115 | main() |
| 116 | assert error.value.code != 0 |
| 117 | |
| 118 | captured = capsys.readouterr() |
| 119 | assert "tokeninfo" in (captured.out + captured.err).lower() or "400" in ( |
| 120 | captured.out + captured.err |
| 121 | ) |
| 122 | |
| 123 | |
| 124 | def test_whoami_is_hidden_from_top_level_help(mock_common_state, capsys): |
| 125 | """`colab --help` should not list `whoami` (it's a developer tool). |
| 126 | Also asserts that `colab whoami --help` still works (the command is |
| 127 | hidden, not removed).""" |
| 128 | # `--help` exits 0 |
| 129 | with patch.object(sys, "argv", ["colab", "--help"]): |
| 130 | with pytest.raises(SystemExit) as error: |
| 131 | main() |
| 132 | assert error.value.code == 0 |
| 133 | out = capsys.readouterr().out |
| 134 | assert "whoami" not in out, ( |
| 135 | f"`whoami` should be hidden from `colab --help`, but appeared in:\n{out}" |
| 136 | ) |
| 137 | |
| 138 | # Confirm `colab whoami --help` is still reachable. |
| 139 | with patch.object(sys, "argv", ["colab", "whoami", "--help"]): |
| 140 | with pytest.raises(SystemExit) as error: |
| 141 | main() |
| 142 | assert error.value.code == 0 |
| 143 | out2 = capsys.readouterr().out |
| 144 | assert "whoami" in out2.lower(), ( |
| 145 | f"`colab whoami --help` should describe the command, got:\n{out2}" |
| 146 | ) |
| 147 | |
| 148 | |
| 149 | def test_whoami_refreshes_credentials_before_reading_token(mock_common_state): |
| 150 | """Some credentials (ADC service account, GCE) lazy-mint the token only |
| 151 | when refresh() is called. whoami must call refresh() before reading |
| 152 | creds.token, otherwise creds.token may be None even for valid creds. |
| 153 | """ |
| 154 | mock_common_state.auth_provider = AuthProvider.ADC |
| 155 | sess = _fake_authed_session(token="TEST-TOKEN-AFTER-REFRESH") |
| 156 | |
| 157 | with patch("colab_cli.auth.get_credentials", return_value=sess): |
| 158 | with patch("urllib.request.urlopen") as mock_urlopen: |
| 159 | cm = MagicMock() |
| 160 | cm.__enter__ = MagicMock(return_value=cm) |
| 161 | cm.__exit__ = MagicMock(return_value=False) |
| 162 | cm.read.return_value = ( |
| 163 | b'{"email":"x@y.com","scope":"a b","expires_in":"60"}' |
| 164 | ) |
| 165 | cm.status = 200 |
| 166 | mock_urlopen.return_value = cm |
| 167 | |
| 168 | with patch.object(sys, "argv", ["colab", "--auth=adc", "whoami"]): |
| 169 | with pytest.raises(SystemExit): |
| 170 | main() |
| 171 | |
| 172 | sess.credentials.refresh.assert_called_once() |