diff --git a/src/fastapi_cloud_cli/utils/config.py b/src/fastapi_cloud_cli/utils/config.py index 3da45cf..8de0957 100644 --- a/src/fastapi_cloud_cli/utils/config.py +++ b/src/fastapi_cloud_cli/utils/config.py @@ -1,9 +1,15 @@ +import os from pathlib import Path import typer def get_config_folder() -> Path: + config_dir = os.getenv("FASTAPI_CLOUD_CLI_CONFIG_DIR") + + if config_dir: + return Path(config_dir).expanduser() + return Path(typer.get_app_dir("fastapi-cli")) diff --git a/tests/conftest.py b/tests/conftest.py index 4571ea4..852c90a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,34 @@ +import os import sys +import tempfile from collections.abc import Generator from dataclasses import dataclass from pathlib import Path -from unittest.mock import patch import pytest +import respx from typer import rich_utils +from fastapi_cloud_cli.config import Settings + from .utils import create_jwt_token +@pytest.fixture(autouse=True) +def isolated_config_path() -> Generator[Path, None, None]: + with tempfile.TemporaryDirectory() as tmpdir: + os.environ["FASTAPI_CLOUD_CLI_CONFIG_DIR"] = tmpdir + + yield Path(tmpdir) + + +@pytest.fixture +def temp_auth_config( + isolated_config_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Generator[Path, None, None]: + yield isolated_config_path / "auth.json" + + @pytest.fixture(autouse=True) def reset_syspath() -> Generator[None, None, None]: initial_python_path = sys.path.copy() @@ -26,6 +45,17 @@ def setup_terminal() -> None: return +@pytest.fixture +def settings() -> Settings: + return Settings.get() + + +@pytest.fixture +def respx_mock(settings: Settings) -> Generator[respx.MockRouter, None, None]: + with respx.mock(base_url=settings.base_api_url) as mock_router: + yield mock_router + + @pytest.fixture def logged_in_cli(temp_auth_config: Path) -> Generator[None, None, None]: valid_token = create_jwt_token({"sub": "test_user_12345"}) @@ -60,13 +90,3 @@ def configured_app(tmp_path: Path) -> ConfiguredApp: config_path.write_text(f'{{"app_id": "{app_id}", "team_id": "{team_id}"}}') return ConfiguredApp(app_id=app_id, team_id=team_id, path=tmp_path) - - -@pytest.fixture -def temp_auth_config(tmp_path: Path) -> Generator[Path, None, None]: - """Provides a temporary auth config setup for testing file operations.""" - - with patch( - "fastapi_cloud_cli.utils.config.get_config_folder", return_value=tmp_path - ): - yield tmp_path / "auth.json" diff --git a/tests/test_api_client.py b/tests/test_api_client.py index c45f9c1..9a84d66 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -7,7 +7,6 @@ from httpx import Response from time_machine import TimeMachineFixture -from fastapi_cloud_cli.config import Settings from fastapi_cloud_cli.utils.api import ( STREAM_LOGS_MAX_RETRIES, APIClient, @@ -17,8 +16,6 @@ ) from tests.utils import build_logs_response -settings = Settings.get() - @pytest.fixture def client() -> httpx.Client: @@ -31,15 +28,11 @@ def deployment_id() -> str: return "test-deployment-123" -api_mock = respx.mock(base_url=settings.base_api_url) - - @pytest.fixture -def logs_route(deployment_id: str) -> respx.Route: - return api_mock.get(f"/deployments/{deployment_id}/build-logs") +def logs_route(respx_mock: respx.MockRouter, deployment_id: str) -> respx.Route: + return respx_mock.get(f"/deployments/{deployment_id}/build-logs") -@api_mock def test_stream_build_logs_successful( logs_route: respx.Route, client: APIClient, @@ -69,7 +62,6 @@ def test_stream_build_logs_successful( assert logs[2].type == "complete" -@api_mock def test_stream_build_logs_failed( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -91,7 +83,6 @@ def test_stream_build_logs_failed( @pytest.mark.parametrize("terminal_type", ["complete", "failed"]) -@api_mock def test_stream_build_logs_stop_after_terminal_state( logs_route: respx.Route, client: APIClient, @@ -116,7 +107,6 @@ def test_stream_build_logs_stop_after_terminal_state( assert logs[1].type == terminal_type -@api_mock def test_stream_build_logs_internal_messages_are_skipped( logs_route: respx.Route, client: APIClient, @@ -140,7 +130,6 @@ def test_stream_build_logs_internal_messages_are_skipped( assert logs[1].type == "complete" -@api_mock def test_stream_build_logs_malformed_json_is_skipped( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -161,7 +150,6 @@ def test_stream_build_logs_malformed_json_is_skipped( assert logs[1].type == "complete" -@api_mock def test_stream_build_logs_unknown_log_type_is_skipped( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -188,7 +176,6 @@ def test_stream_build_logs_unknown_log_type_is_skipped( "network_error", [httpx.NetworkError, httpx.TimeoutException, httpx.RemoteProtocolError], ) -@api_mock def test_stream_build_logs_network_error_retry( logs_route: respx.Route, client: APIClient, @@ -216,7 +203,6 @@ def test_stream_build_logs_network_error_retry( assert logs[0].message == "Success after retry" -@api_mock def test_stream_build_logs_server_error_retry( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -237,7 +223,6 @@ def test_stream_build_logs_server_error_retry( assert logs[0].type == "complete" -@api_mock def test_stream_build_logs_client_error_raises_immediately( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -247,7 +232,6 @@ def test_stream_build_logs_client_error_raises_immediately( list(client.stream_build_logs(deployment_id)) -@api_mock def test_stream_build_logs_max_retries_exceeded( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -261,7 +245,6 @@ def test_stream_build_logs_max_retries_exceeded( list(client.stream_build_logs(deployment_id)) -@api_mock def test_stream_build_logs_empty_lines_are_skipped( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -284,7 +267,6 @@ def test_stream_build_logs_empty_lines_are_skipped( assert logs[1].type == "complete" -@respx.mock(base_url=settings.base_api_url) def test_stream_build_logs_continue_after_timeout( respx_mock: respx.MockRouter, client: APIClient, @@ -328,7 +310,6 @@ def test_stream_build_logs_continue_after_timeout( assert next(logs).type == "complete" -@api_mock def test_stream_build_logs_connection_closed_without_complete_failed_or_timeout( logs_route: respx.Route, client: APIClient, deployment_id: str ) -> None: @@ -348,7 +329,6 @@ def test_stream_build_logs_connection_closed_without_complete_failed_or_timeout( next(logs) -@api_mock def test_stream_build_logs_retry_timeout( logs_route: respx.Route, client: APIClient, diff --git a/tests/test_cli_deploy.py b/tests/test_cli_deploy.py index 5c20ba5..2a76822 100644 --- a/tests/test_cli_deploy.py +++ b/tests/test_cli_deploy.py @@ -20,7 +20,6 @@ from tests.utils import Keys, build_logs_response, changing_dir runner = CliRunner() -settings = Settings.get() assets_path = Path(__file__).parent / "assets" @@ -63,9 +62,12 @@ def _get_random_deployment( } -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_chooses_login_option_when_not_logged_in( - logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter + logged_out_cli: None, + tmp_path: Path, + respx_mock: respx.MockRouter, + settings: Settings, ) -> None: steps = [Keys.ENTER] @@ -108,7 +110,7 @@ def test_chooses_login_option_when_not_logged_in( assert mock_launch.called -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_chooses_waitlist_option_when_not_logged_in( logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -153,7 +155,7 @@ def test_chooses_waitlist_option_when_not_logged_in( assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_waitlist_form_when_not_logged_in_longer_flow( logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -214,7 +216,7 @@ def test_shows_waitlist_form_when_not_logged_in_longer_flow( assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_when_trying_to_get_teams( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -235,7 +237,7 @@ def test_shows_error_when_trying_to_get_teams( assert "Error fetching teams. Please try again later" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_handles_invalid_auth( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -256,7 +258,7 @@ def test_handles_invalid_auth( assert "The specified token is not valid" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_teams( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -286,7 +288,7 @@ def test_shows_teams( assert team_2["name"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_asks_for_app_name_after_team( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -312,7 +314,7 @@ def test_asks_for_app_name_after_team( assert "What's your app name?" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_creates_app_on_backend( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -344,7 +346,7 @@ def test_creates_app_on_backend( assert "App created successfully" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_cancels_deployment_when_user_selects_no( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -378,7 +380,7 @@ def test_cancels_deployment_when_user_selects_no( assert "Deployment cancelled." in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_uses_existing_app( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -408,7 +410,7 @@ def test_uses_existing_app( assert app_data["slug"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_exits_successfully_when_deployment_is_done( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -482,7 +484,7 @@ def test_exits_successfully_when_deployment_is_done( # TODO: show a message when the deployment is done (based on the status) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_exits_successfully_when_deployment_is_done_when_app_is_configured( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -545,7 +547,7 @@ def test_exits_successfully_when_deployment_is_done_when_app_is_configured( assert deployment_data["url"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_exits_with_error_when_deployment_fails_to_build( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -597,7 +599,7 @@ def test_exits_with_error_when_deployment_fails_to_build( assert deployment_data["dashboard_url"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_when_deployment_build_fails( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -648,7 +650,7 @@ def test_shows_error_when_deployment_build_fails( assert result.exit_code == 1 -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_when_app_does_not_exist( logged_in_cli: None, configured_app: ConfiguredApp, respx_mock: respx.MockRouter ) -> None: @@ -722,7 +724,7 @@ def _deploy_without_waiting(respx_mock: respx.MockRouter, tmp_path: Path) -> Res return runner.invoke(app, ["deploy", "--no-wait"]) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_can_skip_waiting( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -733,7 +735,7 @@ def test_can_skip_waiting( assert "Check the status of your deployment at" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_creates_config_folder_and_creates_git_ignore( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -744,7 +746,7 @@ def test_creates_config_folder_and_creates_git_ignore( assert (tmp_path / ".fastapicloud" / ".gitignore").read_text() == "*" -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_does_not_duplicate_entry_in_git_ignore( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -756,7 +758,7 @@ def test_does_not_duplicate_entry_in_git_ignore( assert git_ignore_path.read_text() == ".fastapicloud\n" -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_for_invalid_waitlist_form_data( logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -787,7 +789,7 @@ def test_shows_error_for_invalid_waitlist_form_data( assert "Invalid form data. Please try again." in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_no_apps_found_message_when_team_has_no_apps( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -823,9 +825,9 @@ def test_shows_no_apps_found_message_when_team_has_no_apps( @pytest.mark.parametrize( "error", - [StreamLogError, TooManyRetriesError, TimeoutError], + [StreamLogError("stream error"), TooManyRetriesError(), TimeoutError()], ) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_message_on_build_exception( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter, error: Exception ) -> None: @@ -869,7 +871,7 @@ def test_shows_error_message_on_build_exception( assert deployment_data["dashboard_url"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_message_on_build_log_http_error( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -911,7 +913,7 @@ def test_shows_error_message_on_build_log_http_error( assert deployment_data["dashboard_url"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx @patch("fastapi_cloud_cli.commands.deploy.WAITING_MESSAGES", ["short wait message"]) def test_short_wait_messages( logged_in_cli: None, @@ -978,7 +980,7 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: assert "short wait message" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx @patch("fastapi_cloud_cli.commands.deploy.LONG_WAIT_MESSAGES", ["long wait message"]) def test_long_wait_messages( logged_in_cli: None, @@ -1046,7 +1048,7 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: assert "long wait message" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_calls_upload_cancelled_when_user_interrupts( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1081,7 +1083,7 @@ def test_calls_upload_cancelled_when_user_interrupts( assert upload_cancelled_route.called -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_cancel_upload_swallows_exceptions( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1117,7 +1119,7 @@ def test_cancel_upload_swallows_exceptions( assert "HTTPStatusError" not in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_successfully_with_token( logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1189,7 +1191,7 @@ def test_deploy_successfully_with_token( assert deployment_data["url"] in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_with_token_fails( logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1218,7 +1220,7 @@ def test_deploy_with_token_fails( ) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_with_app_id_arg( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1264,7 +1266,7 @@ def test_deploy_with_app_id_arg( assert f"Deploying to app {app_id}" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_with_app_id_from_env_var( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1310,7 +1312,7 @@ def test_deploy_with_app_id_from_env_var( assert f"Deploying to app {app_id}" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_with_app_id_matching_local_config( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1363,7 +1365,7 @@ def test_deploy_with_app_id_matching_local_config( assert f"Deploying to app {app_id}" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_with_app_id_mismatch_fails( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: @@ -1386,7 +1388,7 @@ def test_deploy_with_app_id_mismatch_fails( assert "FASTAPI_CLOUD_APP_ID" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_deploy_with_app_id_arg_app_not_found( logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: diff --git a/tests/test_cli_link.py b/tests/test_cli_link.py index 1f34e11..1b78918 100644 --- a/tests/test_cli_link.py +++ b/tests/test_cli_link.py @@ -7,13 +7,11 @@ from typer.testing import CliRunner from fastapi_cloud_cli.cli import cloud_app as app -from fastapi_cloud_cli.config import Settings from fastapi_cloud_cli.utils.apps import AppConfig from tests.conftest import ConfiguredApp from tests.utils import Keys, changing_dir runner = CliRunner() -settings = Settings.get() def test_shows_a_message_if_not_logged_in(logged_out_cli: None) -> None: @@ -33,7 +31,7 @@ def test_shows_a_message_if_already_linked( assert "This directory is already linked to an app." in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_no_teams( logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path ) -> None: @@ -46,7 +44,7 @@ def test_shows_a_message_if_no_teams( assert "No teams found" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_no_apps( logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path ) -> None: @@ -72,7 +70,7 @@ def test_shows_a_message_if_no_apps( assert "No apps found in this team." in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_links_successfully( logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path ) -> None: @@ -105,7 +103,7 @@ def test_links_successfully( assert config.team_id == "team-1" -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_on_teams_api_failure( logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path ) -> None: @@ -118,7 +116,7 @@ def test_shows_error_on_teams_api_failure( assert "Error fetching teams" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_error_on_apps_api_failure( logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path ) -> None: @@ -144,7 +142,7 @@ def test_shows_error_on_apps_api_failure( assert "Error fetching apps" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_links_with_multiple_teams_and_apps( logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path ) -> None: diff --git a/tests/test_cli_login.py b/tests/test_cli_login.py index f84501a..6674ca0 100644 --- a/tests/test_cli_login.py +++ b/tests/test_cli_login.py @@ -13,14 +13,13 @@ from tests.utils import create_jwt_token runner = CliRunner() -settings = Settings.get() assets_path = Path(__file__).parent / "assets" -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_something_is_wrong( - logged_out_cli: None, respx_mock: respx.MockRouter + logged_out_cli: None, respx_mock: respx.MockRouter, settings: Settings ) -> None: with patch("fastapi_cloud_cli.commands.login.typer.launch") as mock_open: respx_mock.post( @@ -38,8 +37,10 @@ def test_shows_a_message_if_something_is_wrong( assert not mock_open.called -@pytest.mark.respx(base_url=settings.base_api_url) -def test_full_login(respx_mock: respx.MockRouter, temp_auth_config: Path) -> None: +@pytest.mark.respx +def test_full_login( + respx_mock: respx.MockRouter, temp_auth_config: Path, settings: Settings +) -> None: with patch("fastapi_cloud_cli.commands.login.typer.launch") as mock_open: respx_mock.post( "/login/device/authorization", data={"client_id": settings.client_id} @@ -78,8 +79,10 @@ def test_full_login(respx_mock: respx.MockRouter, temp_auth_config: Path) -> Non assert '"access_token":"test_token_1234"' in temp_auth_config.read_text() -@pytest.mark.respx(base_url=settings.base_api_url) -def test_fetch_access_token_success_immediately(respx_mock: respx.MockRouter) -> None: +@pytest.mark.respx +def test_fetch_access_token_success_immediately( + respx_mock: respx.MockRouter, settings: Settings +) -> None: from fastapi_cloud_cli.commands.login import _fetch_access_token from fastapi_cloud_cli.utils.api import APIClient @@ -98,9 +101,10 @@ def test_fetch_access_token_success_immediately(respx_mock: respx.MockRouter) -> assert access_token == "test_token_success" -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_fetch_access_token_authorization_pending_then_success( respx_mock: respx.MockRouter, + settings: Settings, ) -> None: from fastapi_cloud_cli.commands.login import _fetch_access_token from fastapi_cloud_cli.utils.api import APIClient @@ -128,9 +132,10 @@ def test_fetch_access_token_authorization_pending_then_success( mock_sleep.assert_called_once_with(3) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_fetch_access_token_handles_400_error_not_authorization_pending( respx_mock: respx.MockRouter, + settings: Settings, ) -> None: from fastapi_cloud_cli.commands.login import _fetch_access_token from fastapi_cloud_cli.utils.api import APIClient @@ -149,8 +154,10 @@ def test_fetch_access_token_handles_400_error_not_authorization_pending( _fetch_access_token(client, "test_device_code", 5) -@pytest.mark.respx(base_url=settings.base_api_url) -def test_fetch_access_token_handles_500_error(respx_mock: respx.MockRouter) -> None: +@pytest.mark.respx +def test_fetch_access_token_handles_500_error( + respx_mock: respx.MockRouter, settings: Settings +) -> None: from fastapi_cloud_cli.commands.login import _fetch_access_token from fastapi_cloud_cli.utils.api import APIClient @@ -168,7 +175,7 @@ def test_fetch_access_token_handles_500_error(respx_mock: respx.MockRouter) -> N _fetch_access_token(client, "test_device_code", 5) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_notify_already_logged_in_user( respx_mock: respx.MockRouter, logged_in_cli: None ) -> None: @@ -182,9 +189,9 @@ def test_notify_already_logged_in_user( ) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_notify_expired_token_user( - respx_mock: respx.MockRouter, temp_auth_config: Path + respx_mock: respx.MockRouter, temp_auth_config: Path, settings: Settings ) -> None: past_exp = int(time.time()) - 3600 expired_token = create_jwt_token({"sub": "test_user_12345", "exp": past_exp}) diff --git a/tests/test_cli_whoami.py b/tests/test_cli_whoami.py index fb1d720..b007d34 100644 --- a/tests/test_cli_whoami.py +++ b/tests/test_cli_whoami.py @@ -6,15 +6,13 @@ from typer.testing import CliRunner from fastapi_cloud_cli.cli import cloud_app as app -from fastapi_cloud_cli.config import Settings runner = CliRunner() -settings = Settings.get() assets_path = Path(__file__).parent / "assets" -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_something_is_wrong( logged_in_cli: None, respx_mock: respx.MockRouter ) -> None: @@ -29,7 +27,7 @@ def test_shows_a_message_if_something_is_wrong( assert result.exit_code == 1 -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_when_token_is_invalid( logged_in_cli: None, respx_mock: respx.MockRouter ) -> None: @@ -41,7 +39,7 @@ def test_shows_a_message_when_token_is_invalid( assert "The specified token is not valid" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_email(logged_in_cli: None, respx_mock: respx.MockRouter) -> None: respx_mock.get("/users/me").mock( return_value=Response(200, json={"email": "email@fastapi.com"}) @@ -53,7 +51,7 @@ def test_shows_email(logged_in_cli: None, respx_mock: respx.MockRouter) -> None: assert "email@fastapi.com" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_handles_read_timeout( logged_in_cli: None, respx_mock: respx.MockRouter ) -> None: diff --git a/tests/test_config.py b/tests/test_config.py index 16bea00..1b36fd9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,10 @@ from pathlib import Path +import pytest +import typer + from fastapi_cloud_cli.config import Settings +from fastapi_cloud_cli.utils.config import get_config_folder def test_loads_default_values_when_file_does_not_exist() -> None: @@ -34,3 +38,19 @@ def test_loads_partial_settings(tmp_path: Path) -> None: assert settings.base_api_url == "https://example.com" assert settings.client_id == default_settings.client_id + + +def test_get_config_folder_reads_env_override( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setenv("FASTAPI_CLOUD_CLI_CONFIG_DIR", str(tmp_path)) + + assert get_config_folder() == tmp_path + + +def test_get_config_folder_defaults_to_app_dir( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("FASTAPI_CLOUD_CLI_CONFIG_DIR", raising=False) + + assert get_config_folder() == Path(typer.get_app_dir("fastapi-cli")) diff --git a/tests/test_env_delete.py b/tests/test_env_delete.py index ea2d39a..e591ec1 100644 --- a/tests/test_env_delete.py +++ b/tests/test_env_delete.py @@ -7,11 +7,9 @@ from typer.testing import CliRunner from fastapi_cloud_cli.cli import cloud_app as app -from fastapi_cloud_cli.config import Settings from tests.utils import Keys, changing_dir runner = CliRunner() -settings = Settings.get() assets_path = Path(__file__).parent / "assets" @@ -43,7 +41,7 @@ def test_shows_a_message_if_app_is_not_configured(logged_in_cli: None) -> None: assert "No app found" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_something_is_wrong( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -61,7 +59,7 @@ def test_shows_a_message_if_something_is_wrong( ) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_message_if_not_found( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -86,7 +84,7 @@ def test_shows_a_message_if_name_is_invalid( assert "The environment variable name aaa-aaa is invalid." in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_message_when_it_deletes( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -101,7 +99,7 @@ def test_shows_message_when_it_deletes( assert "Environment variable SOME_VAR deleted" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_selector_for_environment_variables( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -134,7 +132,7 @@ def test_shows_selector_for_environment_variables( assert "Environment variable SECRET_KEY deleted" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_message_if_no_environment_variable( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: diff --git a/tests/test_env_list.py b/tests/test_env_list.py index 860cc92..4d8c32d 100644 --- a/tests/test_env_list.py +++ b/tests/test_env_list.py @@ -6,12 +6,10 @@ from typer.testing import CliRunner from fastapi_cloud_cli.cli import cloud_app as app -from fastapi_cloud_cli.config import Settings from tests.conftest import ConfiguredApp from tests.utils import changing_dir runner = CliRunner() -settings = Settings.get() assets_path = Path(__file__).parent / "assets" @@ -30,7 +28,7 @@ def test_shows_a_message_if_app_is_not_configured(logged_in_cli: None) -> None: assert "No app found" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_something_is_wrong( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -48,7 +46,7 @@ def test_shows_a_message_if_something_is_wrong( ) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_no_env_variables( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -63,7 +61,7 @@ def test_shows_a_message_if_no_env_variables( assert "No environment variables found." in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_environment_variables_names( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -87,7 +85,7 @@ def test_shows_environment_variables_names( assert "API_KEY" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_secret_environment_variables_without_value( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: diff --git a/tests/test_env_set.py b/tests/test_env_set.py index 2c32893..c00210b 100644 --- a/tests/test_env_set.py +++ b/tests/test_env_set.py @@ -7,11 +7,9 @@ from typer.testing import CliRunner from fastapi_cloud_cli.cli import cloud_app as app -from fastapi_cloud_cli.config import Settings from tests.utils import Keys, changing_dir runner = CliRunner() -settings = Settings.get() assets_path = Path(__file__).parent / "assets" @@ -43,7 +41,7 @@ def test_shows_a_message_if_app_is_not_configured(logged_in_cli: None) -> None: assert "No app found" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_a_message_if_something_is_wrong( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -62,7 +60,7 @@ def test_shows_a_message_if_something_is_wrong( ) -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_message_when_it_sets( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -78,7 +76,7 @@ def test_shows_message_when_it_sets( assert "Environment variable SOME_VAR set" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_asks_for_name_and_value( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -103,7 +101,7 @@ def test_asks_for_name_and_value( assert "Environment variable SOME_VAR set" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_asks_for_name_and_value_for_secret( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: @@ -130,7 +128,7 @@ def test_asks_for_name_and_value_for_secret( assert "*" * 6 in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_sets_secret_flag( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: Path ) -> None: diff --git a/tests/test_logs.py b/tests/test_logs.py index bf540c4..778d14b 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -7,13 +7,11 @@ from typer.testing import CliRunner from fastapi_cloud_cli.cli import cloud_app as app -from fastapi_cloud_cli.config import Settings from fastapi_cloud_cli.utils.api import TooManyRetriesError from tests.conftest import ConfiguredApp from tests.utils import changing_dir runner = CliRunner() -settings = Settings.get() def test_shows_message_if_not_logged_in(logged_out_cli: None) -> None: @@ -30,7 +28,7 @@ def test_shows_message_if_app_not_configured(logged_in_cli: None) -> None: assert "No app linked to this directory" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_displays_logs( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -66,7 +64,7 @@ def test_displays_logs( assert "GET /health 200" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_passes_default_params( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -86,7 +84,7 @@ def test_passes_default_params( assert configured_app.app_id in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_passes_custom_params( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -106,7 +104,7 @@ def test_passes_custom_params( assert "follow=false" in url -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_displays_all_log_levels( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -156,7 +154,7 @@ def test_displays_all_log_levels( assert "Error message" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_handles_401_unauthorized( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -171,7 +169,7 @@ def test_handles_401_unauthorized( assert "token is not valid" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_handles_404( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -186,7 +184,7 @@ def test_handles_404( assert "App not found" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_shows_message_when_no_logs_found( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -201,7 +199,7 @@ def test_shows_message_when_no_logs_found( assert "No logs found" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_handles_server_error_message( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -221,7 +219,7 @@ def test_handles_server_error_message( assert "Log storage unavailable" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_handles_unknown_log_level( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -247,7 +245,7 @@ def test_handles_unknown_log_level( assert "Unknown level message" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_skips_invalid_json_lines( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -274,7 +272,7 @@ def test_skips_invalid_json_lines( assert "Valid log message" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx def test_skips_heartbeat_messages( logged_in_cli: None, respx_mock: respx.MockRouter, configured_app: ConfiguredApp ) -> None: @@ -349,7 +347,7 @@ def test_rejects_invalid_since_format( assert "Invalid format" in result.output -@pytest.mark.respx(base_url=settings.base_api_url) +@pytest.mark.respx @pytest.mark.parametrize( "valid_since", [