Skip to content

Commit 7d28991

Browse files
author
zhansheng.lzs
committed
test(cli): pin dashscope auth whoami and login behaviour
Eleven tests over the two commands a user runs first, against a throwaway ~/.dashscope and a stubbed Models.list, so no real key file is touched and no request is sent. whoami: the login guidance and exit 1 when nothing is configured; the masked key with its source on success; an environment key reported as its own source rather than the file; a short key masked as *** instead of half-revealed; exit 2 on a rejected key without leaking it; exit 2 with the reason when the validation call itself fails. login: prompting and saving, --key skipping the prompt, surrounding whitespace stripped, a blank key refused without writing anything, and a round trip proving whoami accepts what login wrote. The key_store fixture has to move both bindings of the api-key path constant: save_api_key/get_default_api_key read it from dashscope.common.api_key while auth reads its own imported copy, so patching one leaves login and whoami silently disagreeing about where the key lives. Messages are compared through a newline-stripping helper because rich hard-wraps at 80 columns under CliRunner and force-breaks long tokens without inserting a space. Wrapping Console.width instead would pin _width on the module-level consoles past teardown. Checked against three deliberate breaks of auth.py: 7 of the 11 failed, then all 11 passed again once it was restored.
1 parent cd085a0 commit 7d28991

1 file changed

Lines changed: 238 additions & 0 deletions

File tree

‎tests/unit/test_cli_auth.py‎

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
"""Behaviour of ``dashscope auth whoami`` and ``dashscope auth login``.
4+
5+
Pins what a user sees on the terminal: the guidance printed when no key is
6+
configured, the masked key and its source on success, and the file ``login``
7+
writes. Every test runs against a throwaway ``~/.dashscope`` and a stubbed
8+
``Models.list``, so no real key file is touched and no request is sent.
9+
"""
10+
from http import HTTPStatus
11+
from types import SimpleNamespace
12+
13+
import pytest
14+
from typer.testing import CliRunner
15+
16+
import dashscope
17+
from dashscope.cli import auth as auth_cli
18+
from dashscope.common import api_key as api_key_mod
19+
from dashscope.common.constants import (
20+
DASHSCOPE_API_KEY_ENV,
21+
DASHSCOPE_API_KEY_FILE_PATH_ENV,
22+
)
23+
24+
KEY = "sk-unittest-0123456789"
25+
MASKED = "sk-uni...6789"
26+
27+
28+
def _flat(text):
29+
"""Undo rich's 80-column hard wrapping so messages compare whole."""
30+
return "".join(text.splitlines())
31+
32+
33+
def _response(status_code, code=None, message=None):
34+
return SimpleNamespace(
35+
status_code=status_code,
36+
code=code,
37+
message=message,
38+
)
39+
40+
41+
def _stub_models_list(monkeypatch, rsp=None, exc=None):
42+
"""Replace the single validation call ``whoami`` makes; record its args."""
43+
calls = []
44+
45+
def fake_list(**kwargs):
46+
calls.append(kwargs)
47+
if exc is not None:
48+
raise exc
49+
return rsp
50+
51+
monkeypatch.setattr(dashscope.Models, "list", fake_list)
52+
return calls
53+
54+
55+
@pytest.fixture
56+
def key_store(monkeypatch, tmp_path):
57+
"""Redirect every api-key lookup to an empty throwaway home."""
58+
monkeypatch.delenv(DASHSCOPE_API_KEY_ENV, raising=False)
59+
monkeypatch.delenv(DASHSCOPE_API_KEY_FILE_PATH_ENV, raising=False)
60+
monkeypatch.setattr(dashscope, "api_key", None)
61+
monkeypatch.setattr(dashscope, "api_key_file_path", None)
62+
63+
cache_dir = tmp_path / ".dashscope"
64+
key_file = cache_dir / "api_key"
65+
cache_dir.mkdir(parents=True, exist_ok=True)
66+
# ``save_api_key``/``get_default_api_key`` read the constant from
67+
# dashscope.common.api_key, while the printed path and the source check in
68+
# auth read its own imported copy. Both bindings have to move, or login and
69+
# whoami silently disagree about where the key lives.
70+
monkeypatch.setattr(
71+
api_key_mod,
72+
"DEFAULT_DASHSCOPE_CACHE_PATH",
73+
cache_dir,
74+
)
75+
monkeypatch.setattr(
76+
api_key_mod,
77+
"DEFAULT_DASHSCOPE_API_KEY_FILE_PATH",
78+
key_file,
79+
)
80+
monkeypatch.setattr(
81+
auth_cli,
82+
"DEFAULT_DASHSCOPE_API_KEY_FILE_PATH",
83+
key_file,
84+
)
85+
return SimpleNamespace(dir=cache_dir, file=key_file)
86+
87+
88+
class TestAuthWhoami:
89+
def test_no_key_prints_login_guidance(self, monkeypatch, key_store):
90+
calls = _stub_models_list(monkeypatch, rsp=_response(HTTPStatus.OK))
91+
92+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
93+
94+
assert result.exit_code == 1
95+
text = _flat(result.output)
96+
assert "Error: No API key configured." in text
97+
guidance = (
98+
"Run dashscope auth login or set "
99+
"DASHSCOPE_API_KEY to configure one."
100+
)
101+
assert guidance in text
102+
assert calls == [], "must not reach the API without a key"
103+
104+
def test_masks_key_and_reports_the_file_source(
105+
self,
106+
monkeypatch,
107+
key_store,
108+
):
109+
key_store.file.write_text(KEY, encoding="utf-8")
110+
calls = _stub_models_list(monkeypatch, rsp=_response(HTTPStatus.OK))
111+
112+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
113+
114+
assert result.exit_code == 0
115+
text = _flat(result.output)
116+
assert f"Authenticated key={MASKED}" in text
117+
assert f"source=file ({key_store.file})" in text
118+
assert KEY not in text
119+
assert calls == [{"page": 1, "page_size": 1}]
120+
121+
def test_reports_an_environment_key_as_its_own_source(
122+
self,
123+
monkeypatch,
124+
key_store,
125+
):
126+
monkeypatch.setattr(dashscope, "api_key", KEY)
127+
_stub_models_list(monkeypatch, rsp=_response(HTTPStatus.OK))
128+
129+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
130+
131+
assert result.exit_code == 0
132+
text = _flat(result.output)
133+
assert f"Authenticated key={MASKED}" in text
134+
assert "source=environment / --api-key flag" in text
135+
136+
def test_short_key_is_not_partially_revealed(
137+
self,
138+
monkeypatch,
139+
key_store,
140+
):
141+
short = "sk-abc123"
142+
key_store.file.write_text(short, encoding="utf-8")
143+
_stub_models_list(monkeypatch, rsp=_response(HTTPStatus.OK))
144+
145+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
146+
147+
assert result.exit_code == 0
148+
text = _flat(result.output)
149+
assert "key=***" in text
150+
assert short not in text
151+
152+
def test_rejected_key_exits_2(self, monkeypatch, key_store):
153+
key_store.file.write_text(KEY, encoding="utf-8")
154+
_stub_models_list(
155+
monkeypatch,
156+
rsp=_response(
157+
HTTPStatus.UNAUTHORIZED,
158+
"InvalidApiKey",
159+
"Invalid API-key provided.",
160+
),
161+
)
162+
163+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
164+
165+
assert result.exit_code == 2
166+
text = _flat(result.output)
167+
assert "Invalid API key" in text
168+
assert f"source=file ({key_store.file})" in text
169+
assert "code=InvalidApiKey" in text
170+
assert KEY not in text
171+
assert MASKED not in text
172+
173+
def test_api_failure_exits_2_with_the_reason(
174+
self,
175+
monkeypatch,
176+
key_store,
177+
):
178+
key_store.file.write_text(KEY, encoding="utf-8")
179+
_stub_models_list(monkeypatch, exc=RuntimeError("no route to host"))
180+
181+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
182+
183+
assert result.exit_code == 2
184+
assert "Error: API call failed: no route to host" in _flat(
185+
result.output,
186+
)
187+
assert KEY not in result.output
188+
189+
190+
class TestAuthLogin:
191+
def test_prompts_for_the_key_and_saves_it(self, key_store):
192+
result = CliRunner().invoke(auth_cli.app, ["login"], input=f"{KEY}\n")
193+
194+
assert result.exit_code == 0
195+
text = _flat(result.output)
196+
assert "Enter your DashScope API key: " in text
197+
assert f"Saved API key to {key_store.file}" in text
198+
assert key_store.file.read_text(encoding="utf-8") == KEY
199+
assert KEY not in text, "hide_input must keep the key off screen"
200+
201+
def test_key_flag_skips_the_prompt(self, key_store):
202+
result = CliRunner().invoke(auth_cli.app, ["login", "-k", KEY])
203+
204+
assert result.exit_code == 0
205+
assert "Enter your DashScope API key" not in result.output
206+
assert f"Saved API key to {key_store.file}" in _flat(result.output)
207+
assert key_store.file.read_text(encoding="utf-8") == KEY
208+
209+
def test_surrounding_whitespace_is_stripped(self, key_store):
210+
result = CliRunner().invoke(
211+
auth_cli.app,
212+
["login"],
213+
input=f" {KEY} \n",
214+
)
215+
216+
assert result.exit_code == 0
217+
assert key_store.file.read_text(encoding="utf-8") == KEY
218+
219+
def test_blank_key_is_refused_and_nothing_is_written(self, key_store):
220+
result = CliRunner().invoke(auth_cli.app, ["login", "-k", " "])
221+
222+
assert result.exit_code == 1
223+
assert "Error: API key cannot be empty." in _flat(result.output)
224+
assert not key_store.file.exists()
225+
226+
def test_whoami_accepts_what_login_wrote(self, monkeypatch, key_store):
227+
"""The two commands must agree on the key file location."""
228+
saved = CliRunner().invoke(auth_cli.app, ["login", "-k", KEY])
229+
assert saved.exit_code == 0
230+
calls = _stub_models_list(monkeypatch, rsp=_response(HTTPStatus.OK))
231+
232+
result = CliRunner().invoke(auth_cli.app, ["whoami"])
233+
234+
assert result.exit_code == 0
235+
text = _flat(result.output)
236+
assert f"Authenticated key={MASKED}" in text
237+
assert f"source=file ({key_store.file})" in text
238+
assert calls == [{"page": 1, "page_size": 1}]

0 commit comments

Comments
 (0)