diff --git a/tests/test-server/__init__.py b/tests/test-server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test-server/app.py b/tests/test-server/app.py index c6690d72..87a63489 100644 --- a/tests/test-server/app.py +++ b/tests/test-server/app.py @@ -2,11 +2,11 @@ from flask import Flask, request, jsonify from supertokens_python.framework import BaseRequest from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig -from supertokens_python.recipe import accountlinking +from supertokens_python.recipe import accountlinking, multifactorauth from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe from supertokens_python.recipe.multifactorauth.recipe import MultiFactorAuthRecipe from supertokens_python.recipe.totp.recipe import TOTPRecipe -from utils import init_test_claims +from utils import init_test_claims # pylint: disable=import-error from supertokens_python.process_state import ProcessState from supertokens_python.recipe.dashboard.recipe import DashboardRecipe from supertokens_python.recipe.emailpassword.recipe import EmailPasswordRecipe @@ -18,10 +18,17 @@ from supertokens_python.recipe.thirdparty.recipe import ThirdPartyRecipe from supertokens_python.recipe.usermetadata.recipe import UserMetadataRecipe from supertokens_python.recipe.userroles.recipe import UserRolesRecipe -from test_functions_mapper import get_func, get_override_params, reset_override_params -from emailpassword import add_emailpassword_routes -from multitenancy import add_multitenancy_routes -from session import add_session_routes +from test_functions_mapper import ( # pylint: disable=import-error + get_func, + get_override_params, + reset_override_params, +) # pylint: disable=import-error +from emailpassword import add_emailpassword_routes # pylint: disable=import-error +from multitenancy import add_multitenancy_routes # pylint: disable=import-error +from emailverification import ( + add_emailverification_routes, +) # pylint: disable=import-error +from session import add_session_routes # pylint: disable=import-error from supertokens_python import ( AppInfo, Supertokens, @@ -52,8 +59,11 @@ def default_st_init(): - def origin_func( - request: Optional[BaseRequest] = None, context: Dict[str, Any] = {} + def origin_func( # pylint: disable=unused-argument, dangerous-default-value + request: Optional[BaseRequest] = None, + context: Dict[ # pylint: disable=unused-argument, dangerous-default-value + str, Any + ] = {}, # pylint: disable=unused-argument, dangerous-default-value ) -> str: if request is None: return "http://localhost:8080" @@ -150,7 +160,9 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: impl = get_func(override_name) else: - async def default_func(*args: Any, **kwargs: Any) -> Any: + async def default_func( # pylint: disable=unused-argument + *args: Any, **kwargs: Any # pylint: disable=unused-argument + ) -> Any: # pylint: disable=unused-argument return default_value impl = default_func @@ -388,6 +400,21 @@ def init_st(config: Dict[str, Any]): functions=override_functions ) recipe_list.append(emailverification.init(**ev_config)) + elif recipe_id == "multifactorauth": + recipe_config_json = json.loads(recipe_config.get("config", "{}")) + recipe_list.append( + multifactorauth.init( + first_factors=recipe_config_json.get("firstFactors", None), + override=multifactorauth.OverrideConfig( + functions=override_builder_with_logging( + "MultifactorAuth.override.functions", + recipe_config_json.get("override", {}).get( + "functions", None + ), + ), + ), + ) + ) interceptor_func = None if config.get("supertokens", {}).get("networkInterceptor") is not None: @@ -536,13 +563,14 @@ def verify_session_route(): @app.errorhandler(404) -def not_found(error: Any) -> Any: +def not_found(error: Any) -> Any: # pylint: disable=unused-argument return jsonify({"error": f"Route not found: {request.method} {request.path}"}), 404 add_emailpassword_routes(app) add_multitenancy_routes(app) add_session_routes(app) +add_emailverification_routes(app) init_test_claims() diff --git a/tests/test-server/emailpassword.py b/tests/test-server/emailpassword.py index 3b6f73b4..9691fa21 100644 --- a/tests/test-server/emailpassword.py +++ b/tests/test-server/emailpassword.py @@ -6,8 +6,14 @@ UnknownUserIdError, UpdateEmailOrPasswordEmailChangeNotAllowedError, UpdateEmailOrPasswordOkResult, + WrongCredentialsError, ) import supertokens_python.recipe.emailpassword.syncio as emailpassword +from session import convert_session_to_container # pylint: disable=import-error +from utils import ( # pylint: disable=import-error + serialize_user, + serialize_recipe_user_id, +) # pylint: disable=import-error def add_emailpassword_routes(app: Flask): @@ -20,23 +26,35 @@ def emailpassword_signup(): # type: ignore email = data["email"] password = data["password"] user_context = data.get("userContext") + session = ( + convert_session_to_container(data["session"]) if "session" in data else None + ) - response = emailpassword.sign_up(tenant_id, email, password, user_context) + response = emailpassword.sign_up( + tenant_id, email, password, session, user_context + ) if isinstance(response, SignUpOkResult): return jsonify( { "status": "OK", - "user": { - "id": response.user.id, - "email": response.user.emails[0], - "timeJoined": response.user.time_joined, - "tenantIds": response.user.tenant_ids, - }, + **serialize_user( + response.user, request.headers.get("fdi-version", "") + ), + **serialize_recipe_user_id( + response.recipe_user_id, request.headers.get("fdi-version", "") + ), } ) - else: + elif isinstance(response, EmailAlreadyExistsError): return jsonify({"status": "EMAIL_ALREADY_EXISTS_ERROR"}) + else: + return jsonify( + { + "status": response.status, + "reason": response.reason, + } + ) @app.route("/test/emailpassword/signin", methods=["POST"]) # type: ignore def emailpassword_signin(): # type: ignore @@ -55,16 +73,23 @@ def emailpassword_signin(): # type: ignore return jsonify( { "status": "OK", - "user": { - "id": response.user.id, - "email": response.user.emails[0], - "timeJoined": response.user.time_joined, - "tenantIds": response.user.tenant_ids, - }, + **serialize_user( + response.user, request.headers.get("fdi-version", "") + ), + **serialize_recipe_user_id( + response.recipe_user_id, request.headers.get("fdi-version", "") + ), } ) - else: + elif isinstance(response, WrongCredentialsError): return jsonify({"status": "WRONG_CREDENTIALS_ERROR"}) + else: + return jsonify( + { + "status": response.status, + "reason": response.reason, + } + ) @app.route("/test/emailpassword/createresetpasswordlink", methods=["POST"]) # type: ignore def emailpassword_create_reset_password_link(): # type: ignore diff --git a/tests/test-server/emailverification.py b/tests/test-server/emailverification.py new file mode 100644 index 00000000..9707ec24 --- /dev/null +++ b/tests/test-server/emailverification.py @@ -0,0 +1,67 @@ +from flask import Flask, request, jsonify + +from supertokens_python.recipe.emailverification.interfaces import ( + CreateEmailVerificationTokenOkResult, + VerifyEmailUsingTokenOkResult, +) +from supertokens_python.recipe.emailverification.syncio import ( + create_email_verification_token, +) + + +def add_emailverification_routes(app: Flask): + @app.route("/test/emailverification/createemailverificationtoken", methods=["POST"]) # type: ignore + def f(): # type: ignore + from supertokens_python import convert_to_recipe_user_id + + data = request.json + if data is None: + return jsonify({"status": "MISSING_DATA_ERROR"}) + + recipe_user_id = convert_to_recipe_user_id(data["recipeUserId"]) + tenant_id = data.get("tenantId", "public") + email = None if "email" not in data else data["email"] + user_context = data.get("userContext") + + response = create_email_verification_token( + tenant_id, recipe_user_id, email, user_context + ) + + if isinstance(response, CreateEmailVerificationTokenOkResult): + return jsonify({"status": "OK", "token": response.token}) + else: + return jsonify({"status": "EMAIL_ALREADY_VERIFIED_ERROR"}) + + @app.route("/test/emailverification/verifyemailusingtoken", methods=["POST"]) # type: ignore + def f2(): # type: ignore + from supertokens_python.recipe.emailverification.syncio import ( + verify_email_using_token, + ) + + data = request.json + if data is None: + return jsonify({"status": "MISSING_DATA_ERROR"}) + + tenant_id = data.get("tenantId", "public") + token = data["token"] + attempt_account_linking = data.get("attemptAccountLinking", False) + user_context = data.get("userContext", {}) + + response = verify_email_using_token( + tenant_id, token, attempt_account_linking, user_context + ) + + if isinstance(response, VerifyEmailUsingTokenOkResult): + return jsonify( + { + "status": "OK", + "user": { + "email": response.user.email, + "recipeUserId": { + "recipeUserId": response.user.recipe_user_id.get_as_string() + }, + }, + } + ) + else: + return jsonify({"status": "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"}) diff --git a/tests/test-server/session.py b/tests/test-server/session.py index f9749221..be43bcb4 100644 --- a/tests/test-server/session.py +++ b/tests/test-server/session.py @@ -5,7 +5,10 @@ parse_jwt_without_signature_verification, ) from supertokens_python.types import RecipeUserId -from utils import deserialize_validator, get_max_version +from utils import ( # pylint: disable=import-error + deserialize_validator, + get_max_version, +) from supertokens_python.recipe.session.recipe import SessionRecipe from supertokens_python.recipe.session.session_class import Session import supertokens_python.recipe.session.syncio as session @@ -175,12 +178,12 @@ def convert_session_to_container(data: Any) -> Session: jwt_info = parse_jwt_without_signature_verification(data["session"]["accessToken"]) jwt_payload = jwt_info.payload - user_id = jwt_info.version == 2 and jwt_payload["userId"] or jwt_payload["sub"] + user_id = jwt_payload["userId"] if jwt_info.version == 2 else jwt_payload["sub"] session_handle = jwt_payload["sessionHandle"] recipe_user_id = RecipeUserId(jwt_payload.get("rsub", user_id)) anti_csrf_token = jwt_payload.get("antiCsrfToken") - tenant_id = jwt_info.version >= 4 and jwt_payload["tId"] or "public" + tenant_id = jwt_payload["tId"] if jwt_info.version >= 4 else "public" return Session( recipe_implementation=SessionRecipe.get_instance().recipe_implementation, diff --git a/tests/test-server/test_functions_mapper.py b/tests/test-server/test_functions_mapper.py index 83b4be17..188e9728 100644 --- a/tests/test-server/test_functions_mapper.py +++ b/tests/test-server/test_functions_mapper.py @@ -26,7 +26,7 @@ def func(*args): # type: ignore elif eval_str.startswith("accountlinking.init.shouldDoAutomaticAccountLinking"): async def func( - i: Any, l: Any, o: Any, u: Any, a: Any + i: Any, l: Any, o: Any, u: Any, a: Any # pylint: disable=unused-argument ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if ( "()=>({shouldAutomaticallyLink:!0,shouldRequireVerification:!1})" diff --git a/tests/test-server/utils.py b/tests/test-server/utils.py index 073d006f..1791d24f 100644 --- a/tests/test-server/utils.py +++ b/tests/test-server/utils.py @@ -2,6 +2,7 @@ from supertokens_python.recipe.session.claims import SessionClaim from supertokens_python.recipe.session.interfaces import SessionClaimValidator +from supertokens_python.types import RecipeUserId, User test_claims: Dict[str, SessionClaim] = {} # type: ignore @@ -56,3 +57,32 @@ def get_max_version(v1: str, v2: str) -> str: return v1 return v2 + + +def serialize_user(user: User, fdi_version: str) -> Dict[str, Any]: + if get_max_version("1.17", fdi_version) == "1.17" or ( + get_max_version("2.0", fdi_version) == fdi_version + and get_max_version("3.0", fdi_version) != fdi_version + ): + return { + "user": { + "id": user.id, + "email": user.emails[0], + "timeJoined": user.time_joined, + "tenantIds": user.tenant_ids, + } + } + else: + return {"user": user.to_json()} + + +def serialize_recipe_user_id( + recipe_user_id: RecipeUserId, fdi_version: str +) -> Dict[str, Any]: + if get_max_version("1.17", fdi_version) == "1.17" or ( + get_max_version("2.0", fdi_version) == fdi_version + and get_max_version("3.0", fdi_version) != fdi_version + ): + return {} + else: + return {"recipeUserId": recipe_user_id.get_as_string()}