diff --git a/packages/models-library/src/models_library/api_schemas_webserver/product.py b/packages/models-library/src/models_library/api_schemas_webserver/product.py index 4148c325e7b..c4dde433e65 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/product.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/product.py @@ -29,6 +29,7 @@ class Config(OutputSchema.Config): class GenerateInvitation(InputSchema): guest: LowerCaseEmailStr trial_account_days: PositiveInt | None = None + extra_credits: PositiveInt | None = None class InvitationGenerated(OutputSchema): @@ -36,6 +37,7 @@ class InvitationGenerated(OutputSchema): issuer: LowerCaseEmailStr guest: LowerCaseEmailStr trial_account_days: PositiveInt | None = None + extra_credits: PositiveInt | None = None created: datetime invitation_link: HttpUrl @@ -47,6 +49,15 @@ class Config(OutputSchema.Config): "issuer": "john.doe@email.com", "guest": "guest@example.com", "trialAccountDays": 7, + "extraCredits": 30, + "created": "2023-09-27T15:30:00", + "invitationLink": "https://example.com/invitation#1234", + }, + # w/o optional + { + "productName": "osparc", + "issuer": "john.doe@email.com", + "guest": "guest@example.com", "created": "2023-09-27T15:30:00", "invitationLink": "https://example.com/invitation#1234", }, diff --git a/packages/models-library/src/models_library/invitations.py b/packages/models-library/src/models_library/invitations.py index 75a0fcbf0d5..7a64b0c7e68 100644 --- a/packages/models-library/src/models_library/invitations.py +++ b/packages/models-library/src/models_library/invitations.py @@ -23,6 +23,10 @@ class InvitationInputs(BaseModel): description="If set, this invitation will activate a trial account." "Sets the number of days from creation until the account expires", ) + extra_credits: PositiveInt | None = Field( + None, + description="If set, the account's primary wallet will add these extra credits", + ) class InvitationContent(InvitationInputs): diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/utils_login.py b/packages/pytest-simcore/src/pytest_simcore/helpers/utils_login.py index bcee6ddd9bb..52c8c5fa88c 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/utils_login.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/utils_login.py @@ -126,6 +126,7 @@ def __init__( guest_email: str | None = None, host: dict | None = None, trial_days: int | None = None, + extra_credits: int | None = None, ): assert client.app super().__init__(params=host, app=client.app) @@ -133,6 +134,7 @@ def __init__( self.tag = f"Created by {guest_email or FAKE.email()}" self.confirmation = None self.trial_days = trial_days + self.extra_credits = extra_credits async def __aenter__(self) -> "NewInvitation": # creates host user @@ -146,6 +148,7 @@ async def __aenter__(self) -> "NewInvitation": user_email=self.user["email"], tag=self.tag, trial_days=self.trial_days, + extra_credits=self.extra_credits, ) return self diff --git a/services/invitations/VERSION b/services/invitations/VERSION index 6d7de6e6abe..9084fa2f716 100644 --- a/services/invitations/VERSION +++ b/services/invitations/VERSION @@ -1 +1 @@ -1.0.2 +1.1.0 diff --git a/services/invitations/openapi.json b/services/invitations/openapi.json index 577093831f8..9aa20746979 100644 --- a/services/invitations/openapi.json +++ b/services/invitations/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "simcore-service-invitations web API", "description": " Service that manages creation and validation of registration invitations", - "version": "1.0.2" + "version": "1.1.0" }, "paths": { "/": { @@ -188,6 +188,13 @@ "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires", "minimum": 0 }, + "extra_credits": { + "title": "Extra Credits", + "exclusiveMinimum": true, + "type": "integer", + "description": "If set, the account's primary wallet will add these extra credits", + "minimum": 0 + }, "created": { "title": "Created", "type": "string", @@ -233,6 +240,13 @@ "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires", "minimum": 0 }, + "extra_credits": { + "title": "Extra Credits", + "exclusiveMinimum": true, + "type": "integer", + "description": "If set, the account's primary wallet will add these extra credits", + "minimum": 0 + }, "created": { "title": "Created", "type": "string", @@ -284,6 +298,13 @@ "type": "integer", "description": "If set, this invitation will activate a trial account.Sets the number of days from creation until the account expires", "minimum": 0 + }, + "extra_credits": { + "title": "Extra Credits", + "exclusiveMinimum": true, + "type": "integer", + "description": "If set, the account's primary wallet will add these extra credits", + "minimum": 0 } }, "description": "Input data necessary to create an invitation", diff --git a/services/invitations/setup.cfg b/services/invitations/setup.cfg index 07dce244171..02f932c4921 100644 --- a/services/invitations/setup.cfg +++ b/services/invitations/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.2 +current_version = 1.1.0 commit = True message = services/invitations version: {current_version} → {new_version} tag = False diff --git a/services/invitations/src/simcore_service_invitations/invitations.py b/services/invitations/src/simcore_service_invitations/invitations.py index 89319bfddd8..3cbb9f8175c 100644 --- a/services/invitations/src/simcore_service_invitations/invitations.py +++ b/services/invitations/src/simcore_service_invitations/invitations.py @@ -60,6 +60,9 @@ class Config: "trial_account_days": { "alias": "t", }, + "extra_credits": { + "alias": "e", + }, "created": { "alias": "c", }, diff --git a/services/web/server/VERSION b/services/web/server/VERSION index 9eb2aa3f109..be386c9ede3 100644 --- a/services/web/server/VERSION +++ b/services/web/server/VERSION @@ -1 +1 @@ -0.32.0 +0.33.0 diff --git a/services/web/server/setup.cfg b/services/web/server/setup.cfg index 79d605e09da..1e3b46f7274 100644 --- a/services/web/server/setup.cfg +++ b/services/web/server/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.32.0 +current_version = 0.33.0 commit = True message = services/webserver api version: {current_version} → {new_version} tag = False diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 921f3d1d086..6d4365c5bc2 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -3,7 +3,7 @@ info: title: simcore-service-webserver description: ' Main service with an interface (http-API & websockets) to the web front-end' - version: 0.32.0 + version: 0.33.0 servers: - url: '' description: webserver @@ -6272,6 +6272,11 @@ components: exclusiveMinimum: true type: integer minimum: 0 + extraCredits: + title: Extracredits + exclusiveMinimum: true + type: integer + minimum: 0 GetWalletAutoRecharge: title: GetWalletAutoRecharge required: @@ -6468,6 +6473,11 @@ components: exclusiveMinimum: true type: integer minimum: 0 + extraCredits: + title: Extracredits + exclusiveMinimum: true + type: integer + minimum: 0 created: title: Created type: string @@ -7472,7 +7482,7 @@ components: required: - pricingUnitId - unitName - - unitAttributes + - unitExtraInfo - currentCostPerUnit - default type: object @@ -7485,8 +7495,8 @@ components: unitName: title: Unitname type: string - unitAttributes: - title: Unitattributes + unitExtraInfo: + title: Unitextrainfo type: object currentCostPerUnit: title: Currentcostperunit diff --git a/services/web/server/src/simcore_service_webserver/application.py b/services/web/server/src/simcore_service_webserver/application.py index 06112490f4d..b0837436c8b 100644 --- a/services/web/server/src/simcore_service_webserver/application.py +++ b/services/web/server/src/simcore_service_webserver/application.py @@ -93,6 +93,10 @@ def create_application() -> web.Application: setup_products(app) setup_statics(app) + # users + setup_users(app) + setup_groups(app) + # resource tracking / billing setup_resource_tracker(app) setup_payments(app) @@ -119,10 +123,6 @@ def create_application() -> web.Application: setup_resource_manager(app) setup_garbage_collector(app) - # users - setup_users(app) - setup_groups(app) - # projects setup_projects(app) # project add-ons diff --git a/services/web/server/src/simcore_service_webserver/login/_registration.py b/services/web/server/src/simcore_service_webserver/login/_registration.py index 64a14a3768d..e403c1ea365 100644 --- a/services/web/server/src/simcore_service_webserver/login/_registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_registration.py @@ -29,7 +29,7 @@ from .storage import AsyncpgStorage, BaseConfirmationTokenDict, ConfirmationTokenDict from .utils import CONFIRMATION_PENDING -log = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) class ConfirmationTokenInfoDict(ConfirmationTokenDict): @@ -50,6 +50,7 @@ class InvitationData(BaseModel): description="If set, this invitation will activate a trial account." "Sets the number of days from creation until the account expires", ) + extra_credits: PositiveInt | None = None class _InvitationValidator(BaseModel): @@ -100,7 +101,7 @@ async def check_other_registrations( user=user, confirmation=_confirmation ) - log.warning( + _logger.warning( "Re-registration of %s with expired %s" "Deleting user and proceeding to a new registration", f"{user=}", @@ -120,6 +121,7 @@ async def create_invitation_token( user_email: LowerCaseEmailStr | None = None, tag: str | None = None, trial_days: PositiveInt | None = None, + extra_credits: PositiveInt | None = None, ) -> ConfirmationTokenDict: """Creates an invitation token for a guest to register in the platform and returns @@ -130,12 +132,11 @@ async def create_invitation_token( :type host: Dict-like :param guest: some description of the guest, e.g. email, name or a json """ - data_model = InvitationData.parse_obj( - { - "issuer": user_email, - "guest": tag, - "trial_account_days": trial_days, - } + data_model = InvitationData( + issuer=user_email, + guest=tag, + trial_account_days=trial_days, + extra_credits=extra_credits, ) return await db.create_confirmation( user_id=user_id, @@ -191,7 +192,7 @@ async def check_and_consume_invitation( guest_email: str, db: AsyncpgStorage, cfg: LoginOptions, - app=web.Application, + app: web.Application, ) -> InvitationData: """Consumes invitation: the code is validated, the invitation retrieives and then deleted since it only has one use @@ -210,15 +211,17 @@ async def check_and_consume_invitation( invitation_url=f"{url}", ) - log.info("Consuming invitation from service:\n%s", content.json(indent=1)) + _logger.info( + "Consuming invitation from service:\n%s", content.json(indent=1) + ) return InvitationData( issuer=content.issuer, guest=content.guest, trial_account_days=content.trial_account_days, + extra_credits=content.extra_credits, ) # database-type invitations - if confirmation_token := await validate_confirmation_code(invitation_code, db, cfg): try: invitation_data: InvitationData = _InvitationValidator.parse_obj( @@ -227,7 +230,7 @@ async def check_and_consume_invitation( return invitation_data except ValidationError as err: - log.warning( + _logger.warning( "%s is associated with an invalid %s.\nDetails: %s", f"{invitation_code=}", f"{confirmation_token=}", @@ -236,7 +239,7 @@ async def check_and_consume_invitation( finally: await db.delete_confirmation(confirmation_token) - log.info("Invitation with %s was consumed", f"{confirmation_token=}") + _logger.info("Invitation with %s was consumed", f"{confirmation_token=}") raise web.HTTPForbidden( reason=( diff --git a/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py b/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py index 6a8c99ec96c..cf8685134e7 100644 --- a/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/handlers_confirmation.py @@ -1,9 +1,20 @@ import logging +from contextlib import suppress +from json import JSONDecodeError from aiohttp import web from aiohttp.web import RouteTableDef from models_library.emails import LowerCaseEmailStr -from pydantic import BaseModel, Field, SecretStr, parse_obj_as, validator +from models_library.products import ProductName +from pydantic import ( + BaseModel, + Field, + PositiveInt, + SecretStr, + ValidationError, + parse_obj_as, + validator, +) from servicelib.aiohttp.requests_validation import ( parse_request_body_as, parse_request_path_parameters_as, @@ -27,6 +38,7 @@ MSG_UNAUTHORIZED_PHONE_CONFIRMATION, ) from ._models import InputSchema, check_confirm_password_match +from ._registration import InvitationData from ._security import login_granted_response from .settings import ( LoginOptions, @@ -44,7 +56,7 @@ notify_user_confirmation, ) -log = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) routes = RouteTableDef() @@ -54,6 +66,52 @@ class _PathParam(BaseModel): code: SecretStr +def _parse_extra_credits_or_none( + confirmation: ConfirmationTokenDict, +) -> PositiveInt | None: + with suppress(ValidationError, JSONDecodeError): + invitation = InvitationData.parse_raw(confirmation.get("data", "EMPTY")) + return invitation.extra_credits + return None + + +async def _handle_confirm_registration( + app: web.Application, + product_name: ProductName, + confirmation: ConfirmationTokenDict, +): + db: AsyncpgStorage = get_plugin_storage(app) + user_id = confirmation["user_id"] + + # activate user and consume confirmation token + await db.delete_confirmation_and_update_user( + user_id=user_id, + updates={"status": ACTIVE}, + confirmation=confirmation, + ) + + await notify_user_confirmation( + app, + user_id=user_id, + product_name=product_name, + extra_credits=_parse_extra_credits_or_none(confirmation), + ) + + +async def _handle_confirm_change_email( + app: web.Application, confirmation: ConfirmationTokenDict +): + db: AsyncpgStorage = get_plugin_storage(app) + user_id = confirmation["user_id"] + + # update and consume confirmation token + await db.delete_confirmation_and_update_user( + user_id=user_id, + updates={"email": parse_obj_as(LowerCaseEmailStr, confirmation["data"])}, + confirmation=confirmation, + ) + + @routes.get("/v0/auth/confirmation/{code}", name="auth_confirmation") async def validate_confirmation_and_redirect(request: web.Request): """Handles email confirmation by checking a code passed as query parameter @@ -78,37 +136,27 @@ async def validate_confirmation_and_redirect(request: web.Request): path_params = parse_request_path_parameters_as(_PathParam, request) confirmation: ConfirmationTokenDict | None = await validate_confirmation_code( - path_params.code.get_secret_value(), db=db, cfg=cfg + path_params.code.get_secret_value(), + db=db, + cfg=cfg, ) redirect_to_login_url = URL(cfg.LOGIN_REDIRECT) if confirmation and (action := confirmation["action"]): try: - user_id = confirmation["user_id"] if action == REGISTRATION: - # activate user and consume confirmation token - await db.delete_confirmation_and_update_user( - user_id=user_id, - updates={"status": ACTIVE}, + await _handle_confirm_registration( + app=request.app, + product_name=product.name, confirmation=confirmation, ) - - await notify_user_confirmation( - request.app, user_id=user_id, product_name=product.name - ) - redirect_to_login_url = redirect_to_login_url.with_fragment( "?registered=true" ) elif action == CHANGE_EMAIL: - # update and consume confirmation token - await db.delete_confirmation_and_update_user( - user_id=user_id, - updates={ - "email": parse_obj_as(LowerCaseEmailStr, confirmation["data"]) - }, - confirmation=confirmation, + await _handle_confirm_change_email( + app=request.app, confirmation=confirmation ) elif action == RESET_PASSWORD: @@ -120,17 +168,16 @@ async def validate_confirmation_and_redirect(request: web.Request): f"reset-password?code={path_params.code.get_secret_value()}" ) - log.debug( - "Confirms %s of %s with %s -> %s", - action, - f"{user_id=}", + _logger.info( + "Confirms %s %s -> %s", + action.upper(), f"{confirmation=}", f"{redirect_to_login_url=}", ) except Exception as err: # pylint: disable=broad-except error_code = create_error_code(err) - log.exception( + _logger.exception( "Failed during email_confirmation [%s]", f"{error_code}", extra={"error_code": error_code}, @@ -139,7 +186,8 @@ async def validate_confirmation_and_redirect(request: web.Request): request.app, page="error", message=f"Sorry, we cannot confirm your {action}." - "Please try again in a few moments ({error_code})", + "Please try again in a few moments. " + "If the problem persist please contact support attaching this code ({error_code})", status_code=web.HTTPServiceUnavailable.status_code, ) from err diff --git a/services/web/server/src/simcore_service_webserver/login/handlers_registration.py b/services/web/server/src/simcore_service_webserver/login/handlers_registration.py index e4610270645..9aca34cb7b6 100644 --- a/services/web/server/src/simcore_service_webserver/login/handlers_registration.py +++ b/services/web/server/src/simcore_service_webserver/login/handlers_registration.py @@ -55,6 +55,7 @@ envelope_response, flash_response, get_client_ip, + notify_user_confirmation, ) from .utils_email import get_template_path, send_email_from_template @@ -169,6 +170,7 @@ async def register(request: web.Request): ) expires_at: datetime | None = None # = does not expire + invitation = None if settings.LOGIN_REGISTRATION_INVITATION_REQUIRED: # Only requests with INVITATION can register user # to either a permanent or to a trial account @@ -215,7 +217,7 @@ async def register(request: web.Request): if settings.LOGIN_REGISTRATION_CONFIRMATION_REQUIRED: # Confirmation required: send confirmation email _confirmation: ConfirmationTokenDict = await db.create_confirmation( - user["id"], REGISTRATION + user["id"], REGISTRATION, data=invitation.json() if invitation else None ) try: @@ -257,6 +259,15 @@ async def register(request: web.Request): "INFO", ) + # NOTE: Here confirmation is disabled + assert settings.LOGIN_REGISTRATION_CONFIRMATION_REQUIRED is False # nosec + await notify_user_confirmation( + request.app, + user_id=user["id"], + product_name=product.name, + extra_credits=invitation.extra_credits if invitation else None, + ) + # No confirmation required: authorize login assert not settings.LOGIN_REGISTRATION_CONFIRMATION_REQUIRED # nosec assert not settings.LOGIN_2FA_REQUIRED # nosec diff --git a/services/web/server/src/simcore_service_webserver/login/utils.py b/services/web/server/src/simcore_service_webserver/login/utils.py index 4b577f0bbe2..37791836a9d 100644 --- a/services/web/server/src/simcore_service_webserver/login/utils.py +++ b/services/web/server/src/simcore_service_webserver/login/utils.py @@ -8,6 +8,7 @@ from models_library.products import ProductName from models_library.users import UserID from passlib import pwd +from pydantic import PositiveInt from servicelib.aiohttp import observer from servicelib.aiohttp.rest_models import LogMessageType from servicelib.json_serialization import json_dumps @@ -69,6 +70,7 @@ async def notify_user_confirmation( app: web.Application, user_id: UserID, product_name: ProductName, + extra_credits: PositiveInt | None, ): """Broadcast that user with 'user_id' has login for the first-time in 'product_name'""" # NOTE: Follow up in https://github.com/ITISFoundation/osparc-simcore/issues/4822 @@ -77,6 +79,7 @@ async def notify_user_confirmation( "SIGNAL_ON_USER_CONFIRMATION", user_id=user_id, product_name=product_name, + extra_credits=extra_credits, ) diff --git a/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py b/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py index 69232e23602..6e4aee44b50 100644 --- a/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py +++ b/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py @@ -48,6 +48,7 @@ async def generate_invitation(request: web.Request): issuer=user_email, trial_account_days=body.trial_account_days, guest=body.guest, + extra_credits=body.extra_credits, ), ) assert request.url.host # nosec @@ -60,6 +61,7 @@ async def generate_invitation(request: web.Request): issuer=generated.issuer, guest=generated.guest, trial_account_days=generated.trial_account_days, + extra_credits=generated.extra_credits, created=generated.created, invitation_link=f"{invitation_link}", ) 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 e76eeb5af5b..e8221e72baf 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_events.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_events.py @@ -3,18 +3,24 @@ from aiohttp import web from models_library.products import ProductName from models_library.users import UserID +from pydantic import PositiveInt from servicelib.aiohttp.observer import register_observer, setup_observer_registry +from ..resource_usage.api import add_credits_to_wallet +from ..users.api import get_user_name_and_email from ._api import any_wallet_owned_by_user, create_wallet async def _auto_add_default_wallet( - app: web.Application, user_id: UserID, product_name: ProductName + app: web.Application, + user_id: UserID, + product_name: ProductName, + extra_credits: PositiveInt | None = None, ): if not await any_wallet_owned_by_user( app, user_id=user_id, product_name=product_name ): - await create_wallet( + wallet = await create_wallet( app, user_id=user_id, wallet_name="Credits", @@ -23,11 +29,30 @@ async def _auto_add_default_wallet( product_name=product_name, ) + if extra_credits: + user = await get_user_name_and_email(app, user_id=user_id) + await add_credits_to_wallet( + app, + product_name=product_name, + wallet_id=wallet.wallet_id, + wallet_name=wallet.name, + user_id=user_id, + user_email=user.email, + osparc_credits=extra_credits, # type: ignore + payment_id="INVITATION", # TODO: invitation id??? + created_at=wallet.created, + ) + async def _on_user_confirmation( - app: web.Application, user_id: UserID, product_name: ProductName + app: web.Application, + user_id: UserID, + product_name: ProductName, + extra_credits: PositiveInt, ): - await _auto_add_default_wallet(app, user_id=user_id, product_name=product_name) + await _auto_add_default_wallet( + app, user_id=user_id, product_name=product_name, extra_credits=extra_credits + ) def setup_wallets_events(app: web.Application): diff --git a/services/web/server/tests/unit/with_dbs/03/invitations/test_invitations.py b/services/web/server/tests/unit/with_dbs/03/invitations/test_invitations.py index d719744370e..25ba244b0a8 100644 --- a/services/web/server/tests/unit/with_dbs/03/invitations/test_invitations.py +++ b/services/web/server/tests/unit/with_dbs/03/invitations/test_invitations.py @@ -179,6 +179,7 @@ async def test_product_owner_generate_invitation( before_dt = datetime.now(tz=timezone.utc) guest_email = faker.email() trial_account_days = 3 + extra_credits = 10 # request response = await client.post( @@ -186,6 +187,7 @@ async def test_product_owner_generate_invitation( json={ "guest": guest_email, "trialAccountDays": trial_account_days, + "extraCredits": extra_credits, }, ) @@ -197,6 +199,7 @@ async def test_product_owner_generate_invitation( "issuer": logged_user["email"], "guest": guest_email, "trial_account_days": trial_account_days, + "extra_credits": extra_credits, } assert got.dict(include=set(expected), by_alias=False) == expected diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py index 639dc5857cf..8b39631ddd9 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py @@ -325,14 +325,12 @@ async def test_registration_without_confirmation( "confirm": fake_user_password, }, ) - data, error = unwrap_envelope(await response.json()) - assert response.status == 200, (data, error) + data, _ = await assert_status(response, web.HTTPOk) assert MSG_LOGGED_IN in data["message"] user = await db.get_user({"email": fake_user_email}) assert user - await db.delete_user(user) async def test_registration_with_confirmation( diff --git a/services/web/server/tests/unit/with_dbs/03/wallets/test_wallets.py b/services/web/server/tests/unit/with_dbs/03/wallets/test_wallets.py index 3b6b23fae2f..52d48d87127 100644 --- a/services/web/server/tests/unit/with_dbs/03/wallets/test_wallets.py +++ b/services/web/server/tests/unit/with_dbs/03/wallets/test_wallets.py @@ -29,8 +29,10 @@ def mock_rut_sum_total_available_credits_in_the_wallet( mocker: MockerFixture, ) -> mock.Mock: + # NOTE: PC->MD should rather use aioresponse to mock RUT responses return mocker.patch( "simcore_service_webserver.wallets._api.get_wallet_total_available_credits", + autospec=True, return_value=WalletTotalCredits( wallet_id=1, available_osparc_credits=Decimal(10.2) ), @@ -167,16 +169,26 @@ async def test_auto_wallet_on_user_registration_confirmation( wallets_clean_db: AsyncIterator[None], osparc_product_name: ProductName, mock_rut_sum_total_available_credits_in_the_wallet: mock.Mock, + mocker: MockerFixture, ): assert client.app + mocker.patch( + "simcore_service_webserver.wallets._events.add_credits_to_wallet", + autospec=True, + return_value=None, + ) + url = client.app.router["list_wallets"].url_for() resp = await client.get(f"{url}") data, _ = await assert_status(resp, web.HTTPOk) assert len(data) == 0 await notify_user_confirmation( - client.app, user_id=logged_user["id"], product_name=osparc_product_name + client.app, + user_id=logged_user["id"], + product_name=osparc_product_name, + extra_credits=10, ) resp = await client.get(f"{url}")