Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/fastapi_cloud_cli/commands/login.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import logging
import time
from typing import Any
from typing import Any, Tuple, Union

import httpx
import typer
from pydantic import BaseModel

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, is_logged_in, write_auth_config
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -72,10 +72,31 @@ def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -
return response_data.access_token


def _verify_token(client: httpx.Client) -> Tuple[bool, Union[str, None]]:
response = client.get("/users/me")
if response.status_code in {401, 403}:
return False, None
response.raise_for_status()
data = response.json()
return True, data.get("email")


def login() -> Any:
"""
Login to FastAPI Cloud. πŸš€
"""
if is_logged_in():
with APIClient() as client:
is_valid, email = _verify_token(client)

if is_valid:
with get_rich_toolkit(minimal=True) as toolkit:
toolkit.print(f"Already logged in as [bold]{email}[/bold]")
toolkit.print(
"Run [bold]fastapi logout[/bold] first if you want to switch accounts."
)
return
Comment on lines +88 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a PR to avoid doing any HTTP call, we will validate that the token is still valid

#105

(it could be invalid for other reasons, but I think I prefer avoiding doing an HTTP call)

what do you think?


with get_rich_toolkit() as toolkit, APIClient() as client:
toolkit.print_title("Login to FastAPI Cloud", tag="FastAPI")

Expand Down
45 changes: 45 additions & 0 deletions tests/test_cli_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,48 @@ 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:
respx_mock.get("/users/me").mock(
return_value=Response(200, json={"email": "userme@example.com"})
)

result = runner.invoke(app, ["login"])

assert result.exit_code == 0
assert "Already logged in as userme@example.com" 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_verify_token_returns_false_on_unauthorized(
respx_mock: respx.MockRouter,
) -> None:
from fastapi_cloud_cli.commands.login import _verify_token
from fastapi_cloud_cli.utils.api import APIClient

respx_mock.get("/users/me").mock(return_value=Response(401))

with APIClient() as client:
is_valid, email = _verify_token(client)

assert is_valid is False
assert email is None


@pytest.mark.respx(base_url=settings.base_api_url)
def test_verify_token_returns_false_on_forbidden(respx_mock: respx.MockRouter) -> None:
from fastapi_cloud_cli.commands.login import _verify_token
from fastapi_cloud_cli.utils.api import APIClient

respx_mock.get("/users/me").mock(return_value=Response(403))

with APIClient() as client:
is_valid, email = _verify_token(client)

assert is_valid is False
assert email is None