From 4dd65562f8036d6c05f3c18c547ac3bc2409eafb Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sun, 22 Jun 2025 17:55:17 +0200 Subject: [PATCH 1/6] adds test --- .../tests/unit/api/test_api_invitations.py | 1 + services/invitations/tests/unit/conftest.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index 84f97fb45fe3..bd0459e75d7f 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 diff --git a/services/invitations/tests/unit/conftest.py b/services/invitations/tests/unit/conftest.py index 1bed48254480..a8c859d8fffe 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 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,10 @@ def is_trial_account(request: pytest.FixtureRequest) -> bool: return request.param +def extra_credits_in_usd(is_trial_account: bool) -> PositiveInt | None: + return PositiveInt(123) if is_trial_account else None + + @pytest.fixture def default_product() -> ProductName: return "s4llite" @@ -98,7 +103,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 +118,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) From 5423f04e1d9abb77a3e6cdcb1803ff44f5402ca2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:04:31 +0200 Subject: [PATCH 2/6] enhances tests by adding extra credits handling and updating validation logic --- services/invitations/tests/unit/api/test_api_invitations.py | 2 ++ services/invitations/tests/unit/conftest.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/invitations/tests/unit/api/test_api_invitations.py b/services/invitations/tests/unit/api/test_api_invitations.py index bd0459e75d7f..1949a38a9662 100644 --- a/services/invitations/tests/unit/api/test_api_invitations.py +++ b/services/invitations/tests/unit/api/test_api_invitations.py @@ -62,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, ) @@ -86,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 a8c859d8fffe..ef2ec565d545 100644 --- a/services/invitations/tests/unit/conftest.py +++ b/services/invitations/tests/unit/conftest.py @@ -10,7 +10,7 @@ from cryptography.fernet import Fernet from faker import Faker from models_library.products import ProductName -from pydantic import PositiveInt +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 @@ -86,8 +86,9 @@ 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 PositiveInt(123) if is_trial_account else None + return TypeAdapter(PositiveInt).validate_python(123) if is_trial_account else None @pytest.fixture From e44d70eccf6a7ffd15307b6e5d9339c08651b4fd Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:25:31 +0200 Subject: [PATCH 3/6] adapt test --- services/invitations/tests/unit/test_cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/invitations/tests/unit/test_cli.py b/services/invitations/tests/unit/test_cli.py index 0c4bf15c7a8b..7d3870e712de 100644 --- a/services/invitations/tests/unit/test_cli.py +++ b/services/invitations/tests/unit/test_cli.py @@ -47,17 +47,18 @@ def test_invite_user_and_check_invitation( expected = { **invitation_data.model_dump(exclude={"product"}), + "extra_credits_in_usd": None, # Cannot be set from CLI "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}" 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 From e0305f0885166a5aaf0efc60d56dc206b68822e5 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:19:15 +0200 Subject: [PATCH 4/6] minor --- .../web/server/src/simcore_service_webserver/wallets/_events.py | 1 + 1 file changed, 1 insertion(+) 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, From 56c9701685679e6912cc322c622c10ef16971dda Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:43:45 +0200 Subject: [PATCH 5/6] Extends CLI --- .../invitations/src/simcore_service_invitations/cli.py | 6 +++++- services/invitations/tests/unit/test_cli.py | 10 +++++++--- services/invitations/tests/unit/test_core_settings.py | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/services/invitations/src/simcore_service_invitations/cli.py b/services/invitations/src/simcore_service_invitations/cli.py index 67838b046155..5b5825c75c99 100644 --- a/services/invitations/src/simcore_service_invitations/cli.py +++ b/services/invitations/src/simcore_service_invitations/cli.py @@ -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, ) diff --git a/services/invitations/tests/unit/test_cli.py b/services/invitations/tests/unit/test_cli.py index 7d3870e712de..8b9ebaf58eb5 100644 --- a/services/invitations/tests/unit/test_cli.py +++ b/services/invitations/tests/unit/test_cli.py @@ -45,9 +45,8 @@ def test_invite_user_and_check_invitation( "INVITATIONS_DEFAULT_PRODUCT": default_product, } - expected = { + expected_invitation = { **invitation_data.model_dump(exclude={"product"}), - "extra_credits_in_usd": None, # Cannot be set from CLI "product": environs["INVITATIONS_DEFAULT_PRODUCT"], } @@ -56,6 +55,11 @@ def test_invite_user_and_check_invitation( if 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} {other_options}", @@ -74,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() ) 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" From 696a6611eac99f3320841a6f7cbcd0524481c2e0 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:59:14 +0200 Subject: [PATCH 6/6] increased converage and dropped serve CLI --- .../src/simcore_service_invitations/cli.py | 24 ++++++------------- services/invitations/tests/unit/test_cli.py | 21 ++++++++++++++++ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/services/invitations/src/simcore_service_invitations/cli.py b/services/invitations/src/simcore_service_invitations/cli.py index 5b5825c75c99..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() @@ -133,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() @@ -153,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 + typer.echo(invitation.model_dump_json(indent=1)) - except (InvalidInvitationCodeError, ValidationError): - _err_console.print("[bold red]Invalid code[/bold red]") - - -@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/test_cli.py b/services/invitations/tests/unit/test_cli.py index 8b9ebaf58eb5..a26f094b88a5 100644 --- a/services/invitations/tests/unit/test_cli.py +++ b/services/invitations/tests/unit/test_cli.py @@ -104,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