Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add support for any type in value field instead of only string #525

Merged
merged 11 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/pre-commit-hook-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ jobs:
pr-title:
name: Pre commit hook check
runs-on: ubuntu-latest
container: rishabhpoddar/supertokens_python_driver_testing
steps:
- uses: actions/checkout@v2
- name: Set up node
uses: actions/setup-node@v1
with:
node-version: '12'
- name: Create virtual environment and install dependencies
run: |
python3 -m venv venv
source venv/bin/activate
pip install "cython<3.0.0" wheel
pip install "PyYAML==5.4.1" --no-build-isolation
make dev-install && rm -rf src
- name: Make a dummy change to README.md
run: |
echo "# Dummy change for PR check" >> README.md
- run: git init && git add --all && git -c user.name='test' -c user.email='[email protected]' commit -m 'init for pr action'
- run: make dev-install && rm -rf src
- run: ./hooks/pre-commit.sh
- run: |
source venv/bin/activate
./hooks/pre-commit.sh

5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [0.24.3] - 2024-09-24

- Adds support for form field related improvements by making fields accept any type of values
- Adds support for optional fields to properly optional

## [0.24.2] - 2024-09-03
- Makes optional input form fields truly optional instead of just being able to accept `""`.

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@

setup(
name="supertokens_python",
version="0.24.2",
version="0.24.3",
author="SuperTokens",
license="Apache 2.0",
author_email="[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion supertokens_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from __future__ import annotations

SUPPORTED_CDI_VERSIONS = ["3.0"]
VERSION = "0.24.2"
VERSION = "0.24.3"
TELEMETRY = "/telemetry"
USER_COUNT = "/users/count"
USER_DELETE = "/user/remove"
Expand Down
20 changes: 18 additions & 2 deletions supertokens_python/recipe/emailpassword/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
from typing import Any, Dict, List, Union

from supertokens_python.exceptions import raise_bad_input_exception
from supertokens_python.recipe.emailpassword.constants import FORM_FIELD_EMAIL_ID
from supertokens_python.recipe.emailpassword.constants import (
FORM_FIELD_EMAIL_ID,
FORM_FIELD_PASSWORD_ID,
)
from supertokens_python.recipe.emailpassword.exceptions import (
raise_form_field_exception,
)
Expand All @@ -41,7 +44,9 @@ async def validate_form_or_throw_error(
input_field: Union[None, FormField] = find_first_occurrence_in_list(
lambda x: x.id == field.id, inputs
)
is_invalid_value = input_field is None or input_field.value == ""
is_invalid_value = input_field is None or (
isinstance(input_field.value, str) and input_field.value == ""
)
deepjyoti30-st marked this conversation as resolved.
Show resolved Hide resolved
if not field.optional and is_invalid_value:
validation_errors.append(ErrorFormField(field.id, "Field is not optional"))
continue
Expand Down Expand Up @@ -83,7 +88,18 @@ async def validate_form_fields_or_throw_error(
raise_bad_input_exception(
"All elements of formFields must contain an 'id' and 'value' field"
)

value = current_form_field["value"]
if current_form_field["id"] in [
FORM_FIELD_EMAIL_ID,
FORM_FIELD_PASSWORD_ID,
] and not isinstance(value, str):
# Ensure that the type is string else we will throw a bad input
# error.
raise_bad_input_exception(
f"{current_form_field['id']} value must be a string"
)

if current_form_field["id"] == FORM_FIELD_EMAIL_ID and isinstance(value, str):
value = value.strip()
form_fields.append(FormField(current_form_field["id"], value))
Expand Down
7 changes: 4 additions & 3 deletions supertokens_python/recipe/emailpassword/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from typing import Awaitable, Callable, List, TypeVar, Union

from typing import Any, Awaitable, Callable, List, TypeVar, Union

from supertokens_python.ingredients.emaildelivery import EmailDeliveryIngredient
from supertokens_python.ingredients.emaildelivery.types import (
Expand Down Expand Up @@ -53,9 +54,9 @@ def __init__(self, id: str, error: str): # pylint: disable=redefined-builtin


class FormField:
def __init__(self, id: str, value: str): # pylint: disable=redefined-builtin
def __init__(self, id: str, value: Any): # pylint: disable=redefined-builtin
self.id: str = id
self.value: str = value
self.value: Any = value


class InputFormField:
Expand Down
18 changes: 6 additions & 12 deletions supertokens_python/recipe/emailpassword/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from __future__ import annotations

from re import fullmatch
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union, Dict
from supertokens_python.framework import BaseRequest
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union

from supertokens_python.framework import BaseRequest
from supertokens_python.ingredients.emaildelivery.types import (
EmailDeliveryConfig,
EmailDeliveryConfigWithService,
Expand All @@ -26,17 +26,14 @@
)

from .interfaces import APIInterface, RecipeInterface
from .types import InputFormField, NormalisedFormField, EmailTemplateVars
from .types import EmailTemplateVars, InputFormField, NormalisedFormField

if TYPE_CHECKING:
from supertokens_python.supertokens import AppInfo

from supertokens_python.utils import get_filtered_list

from .constants import (
FORM_FIELD_EMAIL_ID,
FORM_FIELD_PASSWORD_ID,
)
from .constants import FORM_FIELD_EMAIL_ID, FORM_FIELD_PASSWORD_ID


async def default_validator(_: str, __: str) -> Union[str, None]:
Expand Down Expand Up @@ -261,11 +258,8 @@ def validate_and_normalise_user_input(
email_delivery: Union[EmailDeliveryConfig[EmailTemplateVars], None] = None,
) -> EmailPasswordConfig:

if sign_up_feature is not None and not isinstance(sign_up_feature, InputSignUpFeature): # type: ignore
raise ValueError("sign_up_feature must be of type InputSignUpFeature or None")

if override is not None and not isinstance(override, InputOverrideConfig): # type: ignore
raise ValueError("override must be of type InputOverrideConfig or None")
# NOTE: We don't need to check the instance of sign_up_feature and override
# as they will always be either None or the specified type.

if override is None:
override = InputOverrideConfig()
Expand Down
47 changes: 44 additions & 3 deletions tests/emailpassword/test_passwordreset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

from fastapi import FastAPI
from fastapi.requests import Request
from tests.testclient import TestClientWithNoCookieJar as TestClient
from pytest import fixture, mark, raises

from supertokens_python import InputAppInfo, SupertokensConfig, init
from supertokens_python.framework import BaseRequest
from supertokens_python.framework.fastapi import get_middleware
Expand All @@ -34,6 +34,7 @@
get_session,
refresh_session,
)
from tests.testclient import TestClientWithNoCookieJar as TestClient
from tests.utils import clean_st, reset, setup_st, sign_up_request, start_st


Expand Down Expand Up @@ -178,9 +179,11 @@ async def send_email(

assert response_1.status_code == 200
assert reset_url == "http://supertokens.io/auth/reset-password"
assert token_info is not None and "token=" in token_info # type: ignore pylint: disable=unsupported-membership-test
# type: ignore pylint: disable=unsupported-membership-test
assert token_info is not None and "token=" in token_info
assert query_length == 2
assert tenant_info is not None and "tenantId=public" in tenant_info # type: ignore pylint: disable=unsupported-membership-test
# type: ignore pylint: disable=unsupported-membership-test
assert tenant_info is not None and "tenantId=public" in tenant_info


@mark.asyncio
Expand Down Expand Up @@ -223,6 +226,44 @@ async def test_password_validation(driver_config_client: TestClient):
assert dict_response["status"] != "FIELD_ERROR"


@mark.asyncio
async def test_invalid_type_for_password_and_email(driver_config_client: TestClient):
init(
supertokens_config=SupertokensConfig("http://localhost:3567"),
app_info=InputAppInfo(
app_name="SuperTokens Demo",
api_domain="http://api.supertokens.io",
website_domain="http://supertokens.io",
api_base_path="/auth",
),
framework="fastapi",
recipe_list=[emailpassword.init()],
)
start_st()

response_1 = driver_config_client.post(
url="/auth/user/password/reset",
json={
"formFields": [{"id": "password", "value": 12345}],
"token": "random",
},
)

assert response_1.status_code == 400
assert json.loads(response_1.text)["message"] == "password value must be a string"

response_2 = driver_config_client.post(
url="/auth/user/password/reset/token",
json={
"formFields": [{"id": "email", "value": 123456}],
"token": "randomToken",
},
)

assert response_2.status_code == 400
assert json.loads(response_2.text)["message"] == "email value must be a string"


@mark.asyncio
async def test_token_missing_from_input(driver_config_client: TestClient):
init(
Expand Down
121 changes: 121 additions & 0 deletions tests/emailpassword/test_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,127 @@ async def test_optional_custom_field_without_input(driver_config_client: TestCli
assert dict_response["status"] == "OK"


@mark.asyncio
async def test_non_optional_custom_field_with_boolean_value(
driver_config_client: TestClient,
):
init(
supertokens_config=SupertokensConfig("http://localhost:3567"),
app_info=InputAppInfo(
app_name="SuperTokens Demo",
api_domain="http://api.supertokens.io",
website_domain="http://supertokens.io",
api_base_path="/auth",
),
framework="fastapi",
recipe_list=[
emailpassword.init(
sign_up_feature=emailpassword.InputSignUpFeature(
form_fields=[
emailpassword.InputFormField("autoVerify", optional=False)
]
)
),
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
],
)
start_st()

response_1 = driver_config_client.post(
url="/auth/signup",
headers={"Content-Type": "application/json"},
json={
"formFields": [
{"id": "email", "value": "[email protected]"},
{"id": "password", "value": "validpassword123"},
{"id": "autoVerify", "value": False},
]
},
)
assert response_1.status_code == 200
dict_response = json.loads(response_1.text)
assert dict_response["status"] == "OK"


@mark.asyncio
async def test_invalid_type_for_email_and_password(
driver_config_client: TestClient,
):
init(
supertokens_config=SupertokensConfig("http://localhost:3567"),
app_info=InputAppInfo(
app_name="SuperTokens Demo",
api_domain="http://api.supertokens.io",
website_domain="http://supertokens.io",
api_base_path="/auth",
),
framework="fastapi",
recipe_list=[
emailpassword.init(
sign_up_feature=emailpassword.InputSignUpFeature(form_fields=[])
),
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
],
)
start_st()

response_1 = driver_config_client.post(
url="/auth/signup",
headers={"Content-Type": "application/json"},
json={
"formFields": [
{"id": "email", "value": 123},
{"id": "password", "value": "validpassword123"},
]
},
)
assert response_1.status_code == 400
dict_response = json.loads(response_1.text)
assert dict_response["message"] == "email value must be a string"

response_1_signin = driver_config_client.post(
url="/auth/signin",
headers={"Content-Type": "application/json"},
json={
"formFields": [
{"id": "email", "value": 123},
{"id": "password", "value": "validpassword123"},
]
},
)
assert response_1_signin.status_code == 400
dict_response_signin = json.loads(response_1_signin.text)
assert dict_response_signin["message"] == "email value must be a string"

response_2 = driver_config_client.post(
url="/auth/signup",
headers={"Content-Type": "application/json"},
json={
"formFields": [
{"id": "email", "value": "[email protected]"},
{"id": "password", "value": 12345},
]
},
)
assert response_2.status_code == 400
dict_response = json.loads(response_2.text)
assert dict_response["message"] == "password value must be a string"

response_2_signin = driver_config_client.post(
url="/auth/signin",
headers={"Content-Type": "application/json"},
json={
"formFields": [
{"id": "email", "value": "[email protected]"},
{"id": "password", "value": 12345},
]
},
)
assert response_2_signin.status_code == 400
dict_response_signin = json.loads(response_2_signin.text)
assert dict_response_signin["message"] == "password value must be a string"


@mark.asyncio
async def test_too_many_fields(driver_config_client: TestClient):
init(
Expand Down
8 changes: 5 additions & 3 deletions tests/emailpassword/test_signup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from fastapi import FastAPI
import json
from tests.testclient import TestClientWithNoCookieJar as TestClient

from fastapi import FastAPI
from pytest import fixture, mark

from supertokens_python import init
from supertokens_python.framework.fastapi import get_middleware
from supertokens_python.recipe import emailpassword, session
from tests.testclient import TestClientWithNoCookieJar as TestClient
from tests.utils import (
get_st_init_args,
setup_function,
sign_up_request,
start_st,
teardown_function,
sign_up_request,
)

_ = setup_function # type: ignore
Expand Down
Loading