diff --git a/services/invitations/src/simcore_service_invitations/cli.py b/services/invitations/src/simcore_service_invitations/cli.py index 67838b046155..f186db081ed1 100644 --- a/services/invitations/src/simcore_service_invitations/cli.py +++ b/services/invitations/src/simcore_service_invitations/cli.py @@ -1,5 +1,6 @@ import getpass import logging +import os import typer from cryptography.fernet import Fernet @@ -14,7 +15,6 @@ print_as_envfile, ) -from . import web_server from ._meta import PROJECT_NAME, __version__ from .core.settings import ApplicationSettings, MinimalApplicationSettings from .services.invitations import ( @@ -50,7 +50,7 @@ def generate_key( export INVITATIONS_SECRET_KEY=$(invitations-maker generate-key) """ assert ctx # nosec - print(Fernet.generate_key().decode()) # noqa: T201 + typer.echo(Fernet.generate_key().decode()) @main.command() @@ -106,6 +106,10 @@ def invite( None, help=InvitationInputs.model_fields["trial_account_days"].description, ), + extra_credits_in_usd: int = typer.Option( + None, + help=InvitationInputs.model_fields["extra_credits_in_usd"].description, + ), product: str = typer.Option( None, help=InvitationInputs.model_fields["product"].description, @@ -119,7 +123,7 @@ def invite( issuer=issuer, guest=TypeAdapter(EmailStr).validate_python(email), trial_account_days=trial_account_days, - extra_credits_in_usd=None, + extra_credits_in_usd=extra_credits_in_usd, product=product, ) @@ -129,7 +133,7 @@ def invite( base_url=settings.INVITATIONS_OSPARC_URL, default_product=settings.INVITATIONS_DEFAULT_PRODUCT, ) - print(invitation_link) # noqa: T201 + typer.echo(invitation_link) @main.command() @@ -149,18 +153,8 @@ def extract(ctx: typer.Context, invitation_url: str): ) assert invitation.product is not None # nosec - print(invitation.model_dump_json(indent=1)) # noqa: T201 - - except (InvalidInvitationCodeError, ValidationError): - _err_console.print("[bold red]Invalid code[/bold red]") - + typer.echo(invitation.model_dump_json(indent=1)) -@main.command() -def serve( - ctx: typer.Context, - *, - reload: bool = False, -): - """Starts server with http API""" - assert ctx # nosec - web_server.start(log_level="info", reload=reload) + except (InvalidInvitationCodeError, ValidationError) as err: + typer.secho("Invalid code", fg=typer.colors.RED, bold=True, err=True) + raise typer.Exit(os.EX_DATAERR) from err diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index 84f97fb45fe3..1949a38a9662 100644 --- a/services/invitations/tests/unit/api/test_api_invitations.py +++ b/services/invitations/tests/unit/api/test_api_invitations.py @@ -40,6 +40,7 @@ def test_create_invitation( assert invitation.issuer == invitation_input.issuer assert invitation.guest == invitation_input.guest assert invitation.trial_account_days == invitation_input.trial_account_days + assert invitation.extra_credits_in_usd == invitation_input.extra_credits_in_usd # checks issue with `//` reported in https://github.com/ITISFoundation/osparc-simcore/issues/7055 assert invitation.invitation_url @@ -61,6 +62,7 @@ def test_check_invitation( "issuer": invitation_data.issuer, "guest": invitation_data.guest, "trial_account_days": invitation_data.trial_account_days, + "extra_credits_in_usd": invitation_data.extra_credits_in_usd, }, auth=basic_auth, ) @@ -85,6 +87,7 @@ def test_check_invitation( assert invitation.issuer == invitation_data.issuer assert invitation.guest == invitation_data.guest assert invitation.trial_account_days == invitation_data.trial_account_days + assert invitation.extra_credits_in_usd == invitation_data.extra_credits_in_usd def test_check_valid_invitation( diff --git a/services/invitations/tests/unit/conftest.py b/services/invitations/tests/unit/conftest.py index 1bed48254480..ef2ec565d545 100644 --- a/services/invitations/tests/unit/conftest.py +++ b/services/invitations/tests/unit/conftest.py @@ -10,6 +10,7 @@ from cryptography.fernet import Fernet from faker import Faker from models_library.products import ProductName +from pydantic import PositiveInt, TypeAdapter from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict from simcore_service_invitations.services.invitations import InvitationInputs @@ -85,6 +86,11 @@ def is_trial_account(request: pytest.FixtureRequest) -> bool: return request.param +@pytest.fixture +def extra_credits_in_usd(is_trial_account: bool) -> PositiveInt | None: + return TypeAdapter(PositiveInt).validate_python(123) if is_trial_account else None + + @pytest.fixture def default_product() -> ProductName: return "s4llite" @@ -98,7 +104,10 @@ def product(request: pytest.FixtureRequest) -> ProductName | None: @pytest.fixture def invitation_data( - is_trial_account: bool, faker: Faker, product: ProductName | None + is_trial_account: bool, + faker: Faker, + product: ProductName | None, + extra_credits_in_usd: PositiveInt | None, ) -> InvitationInputs: # first version kwargs = { @@ -110,4 +119,7 @@ def invitation_data( if product: kwargs["product"] = product + if extra_credits_in_usd is not None: + kwargs["extra_credits_in_usd"] = extra_credits_in_usd + return InvitationInputs.model_validate(kwargs) diff --git a/services/invitations/tests/unit/test_cli.py b/services/invitations/tests/unit/test_cli.py index 0c4bf15c7a8b..a26f094b88a5 100644 --- a/services/invitations/tests/unit/test_cli.py +++ b/services/invitations/tests/unit/test_cli.py @@ -45,19 +45,24 @@ def test_invite_user_and_check_invitation( "INVITATIONS_DEFAULT_PRODUCT": default_product, } - expected = { + expected_invitation = { **invitation_data.model_dump(exclude={"product"}), "product": environs["INVITATIONS_DEFAULT_PRODUCT"], } # invitations-maker invite guest@email.com --issuer=me --trial-account-days=3 - trial_account = "" + other_options = "" if invitation_data.trial_account_days: - trial_account = f"--trial-account-days={invitation_data.trial_account_days}" + other_options = f"--trial-account-days={invitation_data.trial_account_days}" + + if invitation_data.extra_credits_in_usd: + other_options += ( + f" --extra-credits-in-usd={invitation_data.extra_credits_in_usd}" + ) result = cli_runner.invoke( main, - f"invite {invitation_data.guest} --issuer={invitation_data.issuer} {trial_account}", + f"invite {invitation_data.guest} --issuer={invitation_data.issuer} {other_options}", env=environs, ) assert result.exit_code == os.EX_OK, result.output @@ -73,7 +78,7 @@ def test_invite_user_and_check_invitation( ) assert result.exit_code == os.EX_OK, result.output assert ( - expected + expected_invitation == TypeAdapter(InvitationInputs).validate_json(result.stdout).model_dump() ) @@ -99,3 +104,24 @@ def test_list_settings(cli_runner: CliRunner, app_environment: EnvVarsDict): print(result.output) settings = ApplicationSettings.model_validate_json(result.output) assert settings == ApplicationSettings.create_from_envs() + + +def test_extract_invalid_invitation_code( + cli_runner: CliRunner, faker: Faker, app_environment: EnvVarsDict +): + """Test that extract command handles invalid invitation codes properly""" + # Create an invalid invitation URL + invalid_invitation_url = f"{faker.url()}#invitation=invalid_code_123" + + # Run extract command with invalid invitation URL + result = cli_runner.invoke( + main, + f'extract "{invalid_invitation_url}"', + env=app_environment, + ) + + # Verify command exits with correct error code + assert result.exit_code == os.EX_DATAERR + + # Verify error message is displayed via stderr + assert "Invalid code" in result.stdout diff --git a/services/invitations/tests/unit/test_core_settings.py b/services/invitations/tests/unit/test_core_settings.py index f639cc0b0f38..cb89e8183ac9 100644 --- a/services/invitations/tests/unit/test_core_settings.py +++ b/services/invitations/tests/unit/test_core_settings.py @@ -42,3 +42,5 @@ def test_valid_application_settings(app_environment: EnvVarsDict): assert settings assert settings == ApplicationSettings.create_from_envs() + + assert settings.LOG_LEVEL == "INFO" diff --git a/services/web/server/src/simcore_service_webserver/wallets/_events.py b/services/web/server/src/simcore_service_webserver/wallets/_events.py index 3aea74cdb83a..fefd99006038 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_events.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_events.py @@ -19,6 +19,7 @@ async def _auto_add_default_wallet( app: web.Application, + *, user_id: UserID, product_name: ProductName, extra_credits_in_usd: PositiveInt | None = None,