Coverage for tests/test_cli.py: 100%
282 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1import io 1abcdefg
2import json 1abcdefg
3import subprocess 1abcdefg
4import sys 1abcdefg
5from collections.abc import Callable, Iterator 1abcdefg
6from datetime import datetime, timezone 1abcdefg
7from inspect import signature 1abcdefg
8from types import FunctionType 1abcdefg
9from typing import Any 1abcdefg
11import pytest 1abcdefg
12import typer 1abcdefg
13from pydantic import BaseModel 1abcdefg
14from rich.console import Console, RenderableType 1abcdefg
15from rich.text import Text 1abcdefg
16from rich.theme import Theme 1abcdefg
17from rich_toolkit import RichToolkit 1abcdefg
18from rich_toolkit.container import Container 1abcdefg
19from rich_toolkit.element import Element 1abcdefg
20from rich_toolkit.input import Input 1abcdefg
21from rich_toolkit.progress import Progress 1abcdefg
22from typer.testing import CliRunner 1abcdefg
24from fastapi_cloud_cli.cli import app 1abcdefg
25from fastapi_cloud_cli.utils import cli as cli_utils 1abcdefg
26from fastapi_cloud_cli.utils.cli import ( 1abcdefg
27 ERROR_BULLET,
28 FastAPIRichToolkit,
29 FastAPIStyle,
30 IndentedBlock,
31 get_details_table,
32 get_rich_toolkit,
33)
34from fastapi_cloud_cli.utils.config import get_version_check_cache_path 1abcdefg
35from fastapi_cloud_cli.utils.errors import ErrorCode 1abcdefg
36from fastapi_cloud_cli.utils.execution import JsonOutputOption 1abcdefg
37from fastapi_cloud_cli.utils.version_check import write_latest_version_cache 1abcdefg
39runner = CliRunner() 1abcdefg
42def _get_command_name(callback: FunctionType, configured_name: str | None) -> str: 1abcdefg
43 if configured_name is not None: 1abcdefg
44 return configured_name 1abcdefg
46 return callback.__name__.replace("_", "-") 1abcdefg
49def _iter_cli_commands( 1abcdefg
50 typer_app: typer.Typer, path: tuple[str, ...] = ()
51) -> Iterator[tuple[str, FunctionType]]:
52 for command_info in typer_app.registered_commands: 1abcdefg
53 assert isinstance(command_info.callback, FunctionType) 1abcdefg
54 callback = command_info.callback 1abcdefg
55 command_path = ( 1abcdefg
56 *path,
57 _get_command_name(callback, command_info.name),
58 )
60 yield " ".join(command_path), callback 1abcdefg
62 for group_info in typer_app.registered_groups: 1abcdefg
63 group_name = group_info.name 1abcdefg
64 group_app = group_info.typer_instance 1abcdefg
66 assert isinstance(group_name, str) 1abcdefg
67 assert isinstance(group_app, typer.Typer) 1abcdefg
69 yield from _iter_cli_commands(group_app, (*path, group_name)) 1abcdefg
72def _has_json_output_option(callback: Callable[..., Any]) -> bool: 1abcdefg
73 return any( 2zcAcBcCcDcEcFc
74 parameter.annotation == JsonOutputOption
75 for parameter in signature(callback).parameters.values()
76 )
79class AuthStatus(BaseModel): 1abcdefg
80 authenticated: bool 1abcdefg
83class MetricsOutput(BaseModel): 1abcdefg
84 cpu: float 1abcdefg
87def render_plain(renderable: RenderableType, *, width: int = 80) -> str: 1abcdefg
88 console = Console(width=width, color_system=None) 2h o 6bEbFb-bi p 7bGbHb.bj q 8bIbJb/bk r 9bKbLb:bl s !bMbNb;bm t #bObPb=bn u $bQbRb?b
89 return "".join( 2h o 6bEbFb-bi p 7bGbHb.bj q 8bIbJb/bk r 9bKbLb:bl s !bMbNb;bm t #bObPb=bn u $bQbRb?b
90 segment.text for segment in console.render(renderable, console.options)
91 )
94def style_with_recording_console(*, force_terminal: bool) -> FastAPIStyle: 1abcdefg
95 style = FastAPIStyle() 2xbSbybTbzbUbAbVbBbWbCbXbDbYb
96 style.console = Console( 2xbSbybTbzbUbAbVbBbWbCbXbDbYb
97 force_terminal=force_terminal,
98 record=True,
99 width=20,
100 color_system=None,
101 theme=Theme({"tag.title": "#ffffff on #009485"}),
102 )
103 return style 2xbSbybTbzbUbAbVbBbWbCbXbDbYb
106def test_shows_help() -> None: 1abcdefg
107 result = runner.invoke(app, ["cloud", "--help"]) 2@b[b]b^b_b`b{b
109 assert result.exit_code == 0 2@b[b]b^b_b`b{b
110 assert "Usage:" in result.output 2@b[b]b^b_b`b{b
113def test_shows_help_without_args() -> None: 1abcdefg
114 result = runner.invoke(app, ["cloud"]) 2|b}b~bacbcccdc
116 assert result.exit_code == 2 2|b}b~bacbcccdc
117 assert "Usage:" in result.output 2|b}b~bacbcccdc
120def test_script() -> None: 1abcdefg
121 result = subprocess.run( 2GcHcIcJcKcLcMc
122 [sys.executable, "-m", "coverage", "run", "-m", "fastapi_cloud_cli", "--help"],
123 capture_output=True,
124 encoding="utf-8",
125 )
126 assert "Usage" in result.stdout 2GcHcIcJcKcLcMc
129def test_version() -> None: 1abcdefg
130 result = runner.invoke(app, ["cloud", "--version"]) 2ecfcgchcicjckc
131 assert result.exit_code == 0, result.output 2ecfcgchcicjckc
132 assert "FastAPI Cloud CLI version:" in result.output 2ecfcgchcicjckc
135@pytest.mark.parametrize( 1abcdefg
136 ("command_path", "callback"),
137 [
138 pytest.param(command_path, callback, id=command_path)
139 for command_path, callback in _iter_cli_commands(app)
140 ],
141)
142def test_cli_commands_include_json_output_option( 1abcdefg
143 command_path: str, callback: Callable[..., Any]
144) -> None:
145 assert _has_json_output_option(callback), ( 2zcAcBcCcDcEcFc
146 f"{command_path} is missing JsonOutputOption"
147 )
150def test_uses_header_style() -> None: 1abcdefg
151 toolkit = get_rich_toolkit() 2lcmcncocpcqcrc
152 assert isinstance(toolkit, FastAPIRichToolkit) 2lcmcncocpcqcrc
153 assert isinstance(toolkit.style, FastAPIStyle) 2lcmcncocpcqcrc
156def test_title_sweep_frames_keep_constant_chip_width() -> None: 1abcdefg
157 chip = " Deploy " 2Zb0b1b2b3b4b5b
158 frames = list(cli_utils._title_sweep_frames("Deploy")) 2Zb0b1b2b3b4b5b
160 assert frames[0] == ("", "", " " * len(chip)) 2Zb0b1b2b3b4b5b
161 assert frames[-1] == (chip, "", "") 2Zb0b1b2b3b4b5b
162 assert all(len("".join(frame)) == len(chip) for frame in frames) 2Zb0b1b2b3b4b5b
163 assert any(shades for _, shades, _ in frames) 2Zb0b1b2b3b4b5b
166def test_strip_rich_markup_preserves_empty_hint() -> None: 1abcdefg
167 assert cli_utils._strip_rich_markup(None) is None 2NcOcPcQcRcScTc
170def test_indented_block_hangs_prefix_and_preserves_final_blank_line() -> None: 1abcdefg
171 block = IndentedBlock( 2-b.b/b:b;b=b?b
172 Text("alpha\nbeta\n"),
173 first_prefix=Text("> "),
174 prefix=Text("| "),
175 )
177 assert render_plain(block, width=20) == "> alpha\n| beta\n\u200b\n" 2-b.b/b:b;b=b?b
180def test_fastapi_style_returns_parent_rendered_children_without_extra_indent() -> None: 1abcdefg
181 style = FastAPIStyle() 2sctcucvcwcxcyc
182 parent = Container(style=style) 2sctcucvcwcxcyc
184 assert style.render_element("child", parent=parent) == "child" 2sctcucvcwcxcyc
187def test_fastapi_style_uses_child_metadata_from_container() -> None: 1abcdefg
188 style = FastAPIStyle() 2EbGbIbKbMbObQb
189 container = Container(style=style) 2EbGbIbKbMbObQb
190 container.elements = [Input(value="abc", style=style, bullet=False)] 2EbGbIbKbMbObQb
192 assert ( 2EbGbIbKbMbObQb
193 render_plain(style.render_element(container), width=20) == " abc\n\n\u200b\n"
194 )
197def test_fastapi_style_renders_title_chip_and_tagged_title() -> None: 1abcdefg
198 style = FastAPIStyle(theme={"tag.title": "#ffffff on #009485"}) 26b7b8b9b!b#b$b
200 assert render_plain(style.render_element("Deploy", title=True)) == " Deploy \n" 26b7b8b9b!b#b$b
201 assert ( 26b7b8b9b!b#b$b
202 render_plain(
203 style.render_element("Deploy app", title=True, tag="cloud", emoji="🚀")
204 )
205 == " cloud \n\n 🚀 Deploy app\n"
206 )
209def test_fastapi_style_skips_title_animation_for_non_terminal_console() -> None: 1abcdefg
210 style = style_with_recording_console(force_terminal=False) 2SbTbUbVbWbXbYb
212 style.render_element("Deploy", title=True, animate=True) 2SbTbUbVbWbXbYb
214 assert style.console.export_text() == "" 2SbTbUbVbWbXbYb
217def test_fastapi_style_animates_title_sweep_in_terminal( 1abcdefg
218 monkeypatch: pytest.MonkeyPatch,
219) -> None:
220 monkeypatch.setattr(cli_utils, "TITLE_SWEEP_DELAY", 0) 2xbybzbAbBbCbDb
221 style = style_with_recording_console(force_terminal=True) 2xbybzbAbBbCbDb
223 style._animate_title_sweep("Go") 2xbybzbAbBbCbDb
225 assert " Go " in style.console.export_text() 2xbybzbAbBbCbDb
228def test_fastapi_style_renders_progress_and_bullet_prefixes() -> None: 1abcdefg
229 style = FastAPIStyle() 1opqrstu
230 progress = Progress("Working", style=style, console=style.console) 1opqrstu
232 assert render_plain(style.render_element("plain", bullet=False)) == " plain\n" 1opqrstu
233 assert render_plain(style.render_element("plain", emoji="[green]✓[/]")) == ( 1opqrstu
234 " ✓ plain\n"
235 )
236 assert render_plain( 1opqrstu
237 style.render_element(Input(value="abc", style=style, emoji="🚀"))
238 ) == (" 🚀 abc\n")
239 assert render_plain(style.render_element(progress)) == " 🐤 Working\n" 1opqrstu
240 assert style._get_bullet_prefix("").plain == " " 1opqrstu
241 assert style._get_bullet_prefix("🚀").plain == " 🚀 " 1opqrstu
244def test_fastapi_style_progress_status_emoji_states() -> None: 1abcdefg
245 style = FastAPIStyle() 1hijklmn
246 progress = Progress( 1hijklmn
247 "Working",
248 style=style,
249 console=style.console,
250 )
251 progress.metadata["done_emoji"] = "✅" 1hijklmn
252 style.animation_counter = 0 1hijklmn
254 assert style._get_progress_status_emoji(progress, done=False) == "🥚" 1hijklmn
255 assert style._get_progress_status_emoji(progress, done=True) == "✅" 1hijklmn
257 progress.metadata["emoji"] = "🤏" 1hijklmn
258 assert style._get_progress_status_emoji(progress, done=False) == "🤏" 1hijklmn
259 assert style._get_progress_status_emoji(progress, done=True) == "✅" 1hijklmn
261 progress._cancelled = True 1hijklmn
262 assert style._get_progress_status_emoji(progress, done=False) == "🟡" 1hijklmn
264 progress._cancelled = False 1hijklmn
265 progress.is_error = True 1hijklmn
266 assert style._get_progress_status_emoji(progress, done=False) == ERROR_BULLET 1hijklmn
267 assert render_plain(style.render_element(progress)) == " ✗ Working\n" 1hijklmn
270@pytest.mark.parametrize("encoding", ["ascii", "cp1252", "gbk", "utf-8"]) 1abcdefg
271@pytest.mark.parametrize("emoji", ["📁", "⚡", ERROR_BULLET, "✅", "🟡", "🐤"]) 1abcdefg
272def test_bullets_can_be_written_to_encoded_streams(encoding: str, emoji: str) -> None: 1abcdefg
273 style = FastAPIStyle() 1vwxyzAB
274 with io.TextIOWrapper( 1vwxyzAB
275 io.BytesIO(), encoding=encoding, errors="strict", newline="\n"
276 ) as stream:
277 style.console.file = stream 1vwxyzAB
278 style.console.print(style.render_element("Working\nSecond line", emoji=emoji)) 1vwxyzAB
279 stream.flush() 1vwxyzAB
281 expected_bullet = Text.from_markup(emoji).plain 1vwxyzAB
282 if encoding != "utf-8": 1vwxyzAB
283 expected_bullet = " x" if emoji == ERROR_BULLET else "*" 1vwxyzAB
284 padding = " " * (3 - Text(expected_bullet).cell_len) 1vwxyzAB
285 assert stream.buffer.getvalue().decode(encoding) == ( 1vwxyzAB
286 f" {expected_bullet}{padding}Working\n Second line\n"
287 )
290@pytest.mark.parametrize("encoding", ["cp1252", "gbk"]) 1abcdefg
291def test_bullet_fallback_preserves_supported_text_and_layout(encoding: str) -> None: 1abcdefg
292 style = FastAPIStyle() 1CDEFGHI
293 with io.TextIOWrapper( 1CDEFGHI
294 io.BytesIO(), encoding=encoding, errors="strict", newline="\n"
295 ) as stream:
296 style.console.file = stream 1CDEFGHI
297 text = "café" if encoding == "cp1252" else "项目" 1CDEFGHI
298 style.console.print(style.render_element(text, emoji="*")) 1CDEFGHI
299 style.console.print(style.render_element("alpha\nbeta", bullet=False)) 1CDEFGHI
300 style.console.print( 1CDEFGHI
301 style.render_element(get_details_table([("Name", text)]), bullet=False)
302 )
303 stream.flush() 1CDEFGHI
305 assert stream.buffer.getvalue().decode(encoding) == ( 1CDEFGHI
306 f" * {text}\n alpha\n beta\n Name {text}\n"
307 )
308 assert ( 1CDEFGHI
309 style.get_cursor_offset_for_element(
310 Input(label="Project name", style=style, emoji="📁")
311 ).left
312 == 4
313 )
316@pytest.mark.parametrize("encoding", ["cp1252", "gbk"]) 1abcdefg
317def test_error_bullet_fallback_keeps_error_style(encoding: str) -> None: 1abcdefg
318 style = FastAPIStyle() 1#$%'()*
319 with io.TextIOWrapper(io.BytesIO(), encoding=encoding) as stream: 1#$%'()*
320 style.console.file = stream 1#$%'()*
321 prefix = style._get_bullet_prefix(ERROR_BULLET) 1#$%'()*
323 assert prefix.plain == " x " 1#$%'()*
324 error_style = prefix.get_style_at_offset(style.console, 2) 1#$%'()*
325 assert error_style.bold 1#$%'()*
326 assert error_style.color == style.console.get_style("error").color 1#$%'()*
329def test_fastapi_style_gets_cursor_offset_with_and_without_bullet_column() -> None: 1abcdefg
330 style = FastAPIStyle() 1JKLMNOP
332 offset = style.get_cursor_offset_for_element(Element(metadata={"emoji": "🚀"})) 1JKLMNOP
333 assert offset.top == 0 1JKLMNOP
334 assert offset.left == 4 1JKLMNOP
336 offset = style.get_cursor_offset_for_element(Element(metadata={"bullet": False})) 1JKLMNOP
337 assert offset.top == 0 1JKLMNOP
338 assert offset.left == 2 1JKLMNOP
340 input_offset = style.get_cursor_offset_for_element( 1JKLMNOP
341 Input(label="Project name", style=style)
342 )
343 assert input_offset.top == 2 1JKLMNOP
344 assert input_offset.left == 4 1JKLMNOP
347def test_toolkit_success_delegates_human_output_renderer( 1abcdefg
348 monkeypatch: pytest.MonkeyPatch,
349) -> None:
350 toolkit = get_rich_toolkit(minimal=True) 1+,-./:;
351 calls: list[tuple[object, object | None]] = [] 1+,-./:;
353 def record_output(data: object, render_output: object | None = None) -> None: 1+,-./:;
354 calls.append((data, render_output)) 1+,-./:;
356 def render_output(data: AuthStatus, toolkit: RichToolkit) -> None: ... 1+,-./:;
358 monkeypatch.setattr(toolkit, "output", record_output) 1+,-./:;
360 toolkit.success(AuthStatus(authenticated=True), render_output=render_output) 1+,-./:;
362 assert calls == [(AuthStatus(authenticated=True), render_output)] 1+,-./:;
365def test_get_details_table_renders_label_value_rows() -> None: 1abcdefg
366 table = get_details_table( 2FbHbJbLbNbPbRb
367 [
368 ("Name", "example"),
369 ("Region", Text("iad")),
370 ]
371 )
373 assert table.row_count == 2 2FbHbJbLbNbPbRb
374 assert "Name" in render_plain(table) 2FbHbJbLbNbPbRb
375 assert "iad" in render_plain(table) 2FbHbJbLbNbPbRb
378def test_toolkit_success_prints_json_envelope() -> None: 1abcdefg
379 test_app = typer.Typer() 1=?@[]^_
381 @test_app.command() 1=?@[]^_
382 def command(json_output: JsonOutputOption = False) -> None: 1=?@[]^_
383 with get_rich_toolkit(minimal=True, json_output=json_output) as toolkit: 1=?@[]^_
384 toolkit.success(AuthStatus(authenticated=True), hint="next step") 1=?@[]^_
386 result = runner.invoke(test_app, env={"FASTAPI_CLOUD_JSON": "1"}) 1=?@[]^_
388 assert result.exit_code == 0 1=?@[]^_
389 assert json.loads(result.stdout) == { 1=?@[]^_
390 "data": {"authenticated": True},
391 "hint": "next step",
392 }
395def test_toolkit_success_omits_empty_json_envelope_fields() -> None: 1abcdefg
396 test_app = typer.Typer() 2` { | } ~ abbb
398 @test_app.command() 2` { | } ~ abbb
399 def command(json_output: JsonOutputOption = False) -> None: 2` { | } ~ abbb
400 with get_rich_toolkit(minimal=True, json_output=json_output) as toolkit: 2` { | } ~ abbb
401 toolkit.success(AuthStatus(authenticated=True)) 2` { | } ~ abbb
403 result = runner.invoke(test_app, env={"FASTAPI_CLOUD_JSON": "1"}) 2` { | } ~ abbb
405 assert result.exit_code == 0 2` { | } ~ abbb
406 assert json.loads(result.stdout) == { 2` { | } ~ abbb
407 "data": {"authenticated": True},
408 }
411def test_toolkit_success_includes_non_empty_warnings() -> None: 1abcdefg
412 test_app = typer.Typer() 2cbdbebfbgbhbib
414 @test_app.command() 2cbdbebfbgbhbib
415 def command(json_output: JsonOutputOption = False) -> None: 2cbdbebfbgbhbib
416 with get_rich_toolkit(minimal=True, json_output=json_output) as toolkit: 2cbdbebfbgbhbib
417 toolkit.success( 2cbdbebfbgbhbib
418 AuthStatus(authenticated=True),
419 warnings=[{"code": "not_validated", "message": "Token not validated."}],
420 )
422 result = runner.invoke(test_app, env={"FASTAPI_CLOUD_JSON": "1"}) 2cbdbebfbgbhbib
424 assert result.exit_code == 0 2cbdbebfbgbhbib
425 assert json.loads(result.stdout) == { 2cbdbebfbgbhbib
426 "data": {"authenticated": True},
427 "warnings": [{"code": "not_validated", "message": "Token not validated."}],
428 }
431def test_toolkit_success_uses_strict_json_output( 1abcdefg
432 monkeypatch: pytest.MonkeyPatch,
433) -> None:
434 monkeypatch.setenv("FASTAPI_CLOUD_JSON", "1") 2%b'b(b)b*b+b,b
435 monkeypatch.setenv("FASTAPI_CLOUD_DISABLE_VERSION_CHECK", "1") 2%b'b(b)b*b+b,b
437 with get_rich_toolkit(minimal=True, json_output=True) as toolkit: 2%b'b(b)b*b+b,b
438 with pytest.raises(ValueError, match="Out of range float values"): 2%b'b(b)b*b+b,b
439 toolkit.success(MetricsOutput(cpu=float("inf"))) 2%b'b(b)b*b+b,b
442def test_toolkit_json_mode_suppresses_update_message( 1abcdefg
443 monkeypatch: pytest.MonkeyPatch,
444) -> None:
445 test_app = typer.Typer() 1QRSTUVW
446 monkeypatch.setenv("FASTAPI_CLOUD_DISABLE_VERSION_CHECK", "") 1QRSTUVW
447 write_latest_version_cache( 1QRSTUVW
448 get_version_check_cache_path(),
449 latest_version="999.0.0",
450 now=datetime.now(timezone.utc),
451 )
453 @test_app.command() 1QRSTUVW
454 def command(json_output: JsonOutputOption = False) -> None: 1QRSTUVW
455 with get_rich_toolkit(minimal=True, json_output=json_output) as toolkit: 1QRSTUVW
456 toolkit.success(AuthStatus(authenticated=True)) 1QRSTUVW
458 result = runner.invoke(test_app, env={"FASTAPI_CLOUD_JSON": "1"}) 1QRSTUVW
460 assert result.exit_code == 0 1QRSTUVW
461 assert json.loads(result.stdout) == { 1QRSTUVW
462 "data": {"authenticated": True},
463 }
466def test_toolkit_fail_prints_json_error_and_exits() -> None: 1abcdefg
467 test_app = typer.Typer() 2jbkblbmbnbobpb
469 @test_app.command() 2jbkblbmbnbobpb
470 def command(json_output: JsonOutputOption = False) -> None: 2jbkblbmbnbobpb
471 with get_rich_toolkit(minimal=True, json_output=json_output) as toolkit: 2jbkblbmbnbobpb
472 toolkit.fail( 2jbkblbmbnbobpb
473 "api_error",
474 "[bold]A value is required.[/]",
475 hint="Pass [blue]--value[/].",
476 )
478 result = runner.invoke(test_app, env={"FASTAPI_CLOUD_JSON": "1"}) 2jbkblbmbnbobpb
480 assert result.exit_code == 1 2jbkblbmbnbobpb
481 assert json.loads(result.stdout) == { 2jbkblbmbnbobpb
482 "error": {
483 "code": "api_error",
484 "message": "A value is required.",
485 "hint": "Pass --value.",
486 }
487 }
490def test_toolkit_fail_uses_error_emoji( 1abcdefg
491 monkeypatch: pytest.MonkeyPatch,
492) -> None:
493 calls: list[dict[str, object]] = [] 2qbrbsbtbubvbwb
495 with get_rich_toolkit() as toolkit: 2qbrbsbtbubvbwb
497 def record_print( 2qbrbsbtbubvbwb
498 *renderables: object,
499 end: str = "\n",
500 **metadata: object,
501 ) -> None:
502 calls.append(metadata) 2qbrbsbtbubvbwb
504 monkeypatch.setattr(toolkit, "print", record_print) 2qbrbsbtbubvbwb
506 with pytest.raises(typer.Exit): 2qbrbsbtbubvbwb
507 toolkit.fail("api_error", "A value is required.") 2qbrbsbtbubvbwb
509 assert calls[0]["emoji"] == ERROR_BULLET 2qbrbsbtbubvbwb
512def test_toolkit_fail_uses_custom_human_output_renderer_and_exits() -> None: 1abcdefg
513 test_app = typer.Typer() 1XYZ0123
515 def render_output( 1XYZ0123
516 toolkit: RichToolkit,
517 *,
518 code: ErrorCode,
519 message: str,
520 hint: str,
521 ) -> None:
522 toolkit.print(f"{code}: {message} {hint}") 1XYZ0123
524 @test_app.command() 1XYZ0123
525 def command() -> None: 1XYZ0123
526 with get_rich_toolkit(minimal=True) as toolkit: 1XYZ0123
527 toolkit.fail( 1XYZ0123
528 "not_logged_in",
529 "No credentials found.",
530 render_output=render_output,
531 )
533 result = runner.invoke(test_app) 1XYZ0123
535 assert result.exit_code == 1 1XYZ0123
536 assert "not_logged_in: No credentials found." in result.output 1XYZ0123
539def test_embedded_fastapi_cli_prints_forced_update_message( 1abcdefg
540 monkeypatch: pytest.MonkeyPatch,
541) -> None:
542 parent_app = typer.Typer() 1456789!
543 parent_app.add_typer(app) 1456789!
544 monkeypatch.setattr(sys, "argv", ["fastapi", "cloud", "whoami"]) 1456789!
545 write_latest_version_cache( 1456789!
546 get_version_check_cache_path(),
547 latest_version="999.0.0",
548 now=datetime.now(timezone.utc),
549 )
551 result = runner.invoke( 1456789!
552 parent_app,
553 ["cloud", "whoami"],
554 env={"FASTAPI_CLOUD_DISABLE_VERSION_CHECK": ""},
555 )
557 assert result.exit_code == 1, result.output 1456789!
558 assert "No credentials found" in result.output 1456789!
559 assert "A newer FastAPI Cloud CLI version is available" in result.output 1456789!
560 assert "→ 999.0.0" in result.output 1456789!