diff --git a/src/fastapi_cloud_cli/commands/login.py b/src/fastapi_cloud_cli/commands/login.py index ab0670d..a3378ee 100644 --- a/src/fastapi_cloud_cli/commands/login.py +++ b/src/fastapi_cloud_cli/commands/login.py @@ -8,7 +8,13 @@ from fastapi_cloud_cli.config import Settings from fastapi_cloud_cli.utils.api import APIClient -from fastapi_cloud_cli.utils.auth import AuthConfig, write_auth_config +from fastapi_cloud_cli.utils.auth import ( + AuthConfig, + get_auth_token, + is_logged_in, + is_token_expired, + write_auth_config, +) from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors logger = logging.getLogger(__name__) @@ -76,6 +82,20 @@ def login() -> Any: """ Login to FastAPI Cloud. 🚀 """ + token = get_auth_token() + if token is not None and is_token_expired(token): + with get_rich_toolkit(minimal=True) as toolkit: + toolkit.print("Your session has expired. Logging in again...") + toolkit.print_line() + + if is_logged_in(): + with get_rich_toolkit(minimal=True) as toolkit: + toolkit.print("You are already logged in.") + toolkit.print( + "Run [bold]fastapi logout[/bold] first if you want to switch accounts." + ) + return + with get_rich_toolkit() as toolkit, APIClient() as client: toolkit.print_title("Login to FastAPI Cloud", tag="FastAPI") diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index 072d3da..22d856a 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -1,3 +1,4 @@ +import time from pathlib import Path from unittest.mock import patch @@ -9,6 +10,7 @@ from fastapi_cloud_cli.cli import app from fastapi_cloud_cli.config import Settings +from tests.utils import create_jwt_token runner = CliRunner() settings = Settings.get() @@ -162,3 +164,54 @@ def test_fetch_access_token_handles_500_error(respx_mock: respx.MockRouter) -> N with APIClient() as client: with pytest.raises(httpx.HTTPStatusError): _fetch_access_token(client, "test_device_code", 5) + + +@pytest.mark.respx(base_url=settings.base_api_url) +def test_notify_already_logged_in_user( + respx_mock: respx.MockRouter, logged_in_cli: None +) -> None: + result = runner.invoke(app, ["login"]) + + assert result.exit_code == 0 + assert "You are already logged in." in result.output + assert "Run fastapi logout first if you want to switch accounts." in result.output + + +@pytest.mark.respx(base_url=settings.base_api_url) +def test_notify_expired_token_user( + respx_mock: respx.MockRouter, temp_auth_config: Path +) -> None: + past_exp = int(time.time()) - 3600 + expired_token = create_jwt_token({"sub": "test_user_12345", "exp": past_exp}) + + temp_auth_config.write_text(f'{{"access_token": "{expired_token}"}}') + + with patch("fastapi_cloud_cli.commands.login.typer.launch") as mock_open: + respx_mock.post( + "/login/device/authorization", data={"client_id": settings.client_id} + ).mock( + return_value=Response( + 200, + json={ + "verification_uri_complete": "http://test.com", + "verification_uri": "http://test.com", + "user_code": "1234", + "device_code": "5678", + }, + ) + ) + respx_mock.post( + "/login/device/token", + data={ + "device_code": "5678", + "client_id": settings.client_id, + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + }, + ).mock(return_value=Response(200, json={"access_token": "new_token_1234"})) + + result = runner.invoke(app, ["login"]) + + assert result.exit_code == 0 + assert "Your session has expired. Logging in again..." in result.output + assert "Now you are logged in!" in result.output + assert mock_open.called