Skip to content

Commit b6851db

Browse files
Merge pull request #525 from supertokens/fix/any-type-of-value
fix: Add support for any type in value field instead of only string
2 parents acf2184 + fff32b0 commit b6851db

File tree

10 files changed

+218
-28
lines changed

10 files changed

+218
-28
lines changed

.github/workflows/pre-commit-hook-run.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ jobs:
1212
pr-title:
1313
name: Pre commit hook check
1414
runs-on: ubuntu-latest
15-
container: rishabhpoddar/supertokens_python_driver_testing
1615
steps:
1716
- uses: actions/checkout@v2
1817
- name: Set up node
1918
uses: actions/setup-node@v1
2019
with:
2120
node-version: '12'
21+
- name: Create virtual environment and install dependencies
22+
run: |
23+
python3 -m venv venv
24+
source venv/bin/activate
25+
pip install "cython<3.0.0" wheel
26+
pip install "PyYAML==5.4.1" --no-build-isolation
27+
make dev-install && rm -rf src
28+
- name: Make a dummy change to README.md
29+
run: |
30+
echo "# Dummy change for PR check" >> README.md
2231
- run: git init && git add --all && git -c user.name='test' -c user.email='[email protected]' commit -m 'init for pr action'
23-
- run: make dev-install && rm -rf src
24-
- run: ./hooks/pre-commit.sh
32+
- run: |
33+
source venv/bin/activate
34+
./hooks/pre-commit.sh
2535

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [unreleased]
1010

11+
## [0.24.3] - 2024-09-24
12+
13+
- Adds support for form field related improvements by making fields accept any type of values
14+
- Adds support for optional fields to properly optional
15+
1116
## [0.24.2] - 2024-09-03
1217
- Makes optional input form fields truly optional instead of just being able to accept `""`.
1318

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383

8484
setup(
8585
name="supertokens_python",
86-
version="0.24.2",
86+
version="0.24.3",
8787
author="SuperTokens",
8888
license="Apache 2.0",
8989
author_email="[email protected]",

supertokens_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from __future__ import annotations
1515

1616
SUPPORTED_CDI_VERSIONS = ["3.0"]
17-
VERSION = "0.24.2"
17+
VERSION = "0.24.3"
1818
TELEMETRY = "/telemetry"
1919
USER_COUNT = "/users/count"
2020
USER_DELETE = "/user/remove"

supertokens_python/recipe/emailpassword/api/utils.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
from typing import Any, Dict, List, Union
1717

1818
from supertokens_python.exceptions import raise_bad_input_exception
19-
from supertokens_python.recipe.emailpassword.constants import FORM_FIELD_EMAIL_ID
19+
from supertokens_python.recipe.emailpassword.constants import (
20+
FORM_FIELD_EMAIL_ID,
21+
FORM_FIELD_PASSWORD_ID,
22+
)
2023
from supertokens_python.recipe.emailpassword.exceptions import (
2124
raise_form_field_exception,
2225
)
@@ -41,7 +44,9 @@ async def validate_form_or_throw_error(
4144
input_field: Union[None, FormField] = find_first_occurrence_in_list(
4245
lambda x: x.id == field.id, inputs
4346
)
44-
is_invalid_value = input_field is None or input_field.value == ""
47+
is_invalid_value = input_field is None or (
48+
isinstance(input_field.value, str) and input_field.value == ""
49+
)
4550
if not field.optional and is_invalid_value:
4651
validation_errors.append(ErrorFormField(field.id, "Field is not optional"))
4752
continue
@@ -83,7 +88,18 @@ async def validate_form_fields_or_throw_error(
8388
raise_bad_input_exception(
8489
"All elements of formFields must contain an 'id' and 'value' field"
8590
)
91+
8692
value = current_form_field["value"]
93+
if current_form_field["id"] in [
94+
FORM_FIELD_EMAIL_ID,
95+
FORM_FIELD_PASSWORD_ID,
96+
] and not isinstance(value, str):
97+
# Ensure that the type is string else we will throw a bad input
98+
# error.
99+
raise_bad_input_exception(
100+
f"{current_form_field['id']} value must be a string"
101+
)
102+
87103
if current_form_field["id"] == FORM_FIELD_EMAIL_ID and isinstance(value, str):
88104
value = value.strip()
89105
form_fields.append(FormField(current_form_field["id"], value))

supertokens_python/recipe/emailpassword/types.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414
from __future__ import annotations
15-
from typing import Awaitable, Callable, List, TypeVar, Union
15+
16+
from typing import Any, Awaitable, Callable, List, TypeVar, Union
1617

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

5455

5556
class FormField:
56-
def __init__(self, id: str, value: str): # pylint: disable=redefined-builtin
57+
def __init__(self, id: str, value: Any): # pylint: disable=redefined-builtin
5758
self.id: str = id
58-
self.value: str = value
59+
self.value: Any = value
5960

6061

6162
class InputFormField:

supertokens_python/recipe/emailpassword/utils.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
from __future__ import annotations
1515

1616
from re import fullmatch
17-
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union, Dict
18-
from supertokens_python.framework import BaseRequest
17+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
1918

19+
from supertokens_python.framework import BaseRequest
2020
from supertokens_python.ingredients.emaildelivery.types import (
2121
EmailDeliveryConfig,
2222
EmailDeliveryConfigWithService,
@@ -26,17 +26,14 @@
2626
)
2727

2828
from .interfaces import APIInterface, RecipeInterface
29-
from .types import InputFormField, NormalisedFormField, EmailTemplateVars
29+
from .types import EmailTemplateVars, InputFormField, NormalisedFormField
3030

3131
if TYPE_CHECKING:
3232
from supertokens_python.supertokens import AppInfo
3333

3434
from supertokens_python.utils import get_filtered_list
3535

36-
from .constants import (
37-
FORM_FIELD_EMAIL_ID,
38-
FORM_FIELD_PASSWORD_ID,
39-
)
36+
from .constants import FORM_FIELD_EMAIL_ID, FORM_FIELD_PASSWORD_ID
4037

4138

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

264-
if sign_up_feature is not None and not isinstance(sign_up_feature, InputSignUpFeature): # type: ignore
265-
raise ValueError("sign_up_feature must be of type InputSignUpFeature or None")
266-
267-
if override is not None and not isinstance(override, InputOverrideConfig): # type: ignore
268-
raise ValueError("override must be of type InputOverrideConfig or None")
261+
# NOTE: We don't need to check the instance of sign_up_feature and override
262+
# as they will always be either None or the specified type.
269263

270264
if override is None:
271265
override = InputOverrideConfig()

tests/emailpassword/test_passwordreset.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
from fastapi import FastAPI
2020
from fastapi.requests import Request
21-
from tests.testclient import TestClientWithNoCookieJar as TestClient
2221
from pytest import fixture, mark, raises
22+
2323
from supertokens_python import InputAppInfo, SupertokensConfig, init
2424
from supertokens_python.framework import BaseRequest
2525
from supertokens_python.framework.fastapi import get_middleware
@@ -34,6 +34,7 @@
3434
get_session,
3535
refresh_session,
3636
)
37+
from tests.testclient import TestClientWithNoCookieJar as TestClient
3738
from tests.utils import clean_st, reset, setup_st, sign_up_request, start_st
3839

3940

@@ -178,9 +179,11 @@ async def send_email(
178179

179180
assert response_1.status_code == 200
180181
assert reset_url == "http://supertokens.io/auth/reset-password"
181-
assert token_info is not None and "token=" in token_info # type: ignore pylint: disable=unsupported-membership-test
182+
# type: ignore pylint: disable=unsupported-membership-test
183+
assert token_info is not None and "token=" in token_info
182184
assert query_length == 2
183-
assert tenant_info is not None and "tenantId=public" in tenant_info # type: ignore pylint: disable=unsupported-membership-test
185+
# type: ignore pylint: disable=unsupported-membership-test
186+
assert tenant_info is not None and "tenantId=public" in tenant_info
184187

185188

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

225228

229+
@mark.asyncio
230+
async def test_invalid_type_for_password_and_email(driver_config_client: TestClient):
231+
init(
232+
supertokens_config=SupertokensConfig("http://localhost:3567"),
233+
app_info=InputAppInfo(
234+
app_name="SuperTokens Demo",
235+
api_domain="http://api.supertokens.io",
236+
website_domain="http://supertokens.io",
237+
api_base_path="/auth",
238+
),
239+
framework="fastapi",
240+
recipe_list=[emailpassword.init()],
241+
)
242+
start_st()
243+
244+
response_1 = driver_config_client.post(
245+
url="/auth/user/password/reset",
246+
json={
247+
"formFields": [{"id": "password", "value": 12345}],
248+
"token": "random",
249+
},
250+
)
251+
252+
assert response_1.status_code == 400
253+
assert json.loads(response_1.text)["message"] == "password value must be a string"
254+
255+
response_2 = driver_config_client.post(
256+
url="/auth/user/password/reset/token",
257+
json={
258+
"formFields": [{"id": "email", "value": 123456}],
259+
"token": "randomToken",
260+
},
261+
)
262+
263+
assert response_2.status_code == 400
264+
assert json.loads(response_2.text)["message"] == "email value must be a string"
265+
266+
226267
@mark.asyncio
227268
async def test_token_missing_from_input(driver_config_client: TestClient):
228269
init(

tests/emailpassword/test_signin.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,127 @@ async def test_optional_custom_field_without_input(driver_config_client: TestCli
719719
assert dict_response["status"] == "OK"
720720

721721

722+
@mark.asyncio
723+
async def test_non_optional_custom_field_with_boolean_value(
724+
driver_config_client: TestClient,
725+
):
726+
init(
727+
supertokens_config=SupertokensConfig("http://localhost:3567"),
728+
app_info=InputAppInfo(
729+
app_name="SuperTokens Demo",
730+
api_domain="http://api.supertokens.io",
731+
website_domain="http://supertokens.io",
732+
api_base_path="/auth",
733+
),
734+
framework="fastapi",
735+
recipe_list=[
736+
emailpassword.init(
737+
sign_up_feature=emailpassword.InputSignUpFeature(
738+
form_fields=[
739+
emailpassword.InputFormField("autoVerify", optional=False)
740+
]
741+
)
742+
),
743+
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
744+
],
745+
)
746+
start_st()
747+
748+
response_1 = driver_config_client.post(
749+
url="/auth/signup",
750+
headers={"Content-Type": "application/json"},
751+
json={
752+
"formFields": [
753+
{"id": "email", "value": "[email protected]"},
754+
{"id": "password", "value": "validpassword123"},
755+
{"id": "autoVerify", "value": False},
756+
]
757+
},
758+
)
759+
assert response_1.status_code == 200
760+
dict_response = json.loads(response_1.text)
761+
assert dict_response["status"] == "OK"
762+
763+
764+
@mark.asyncio
765+
async def test_invalid_type_for_email_and_password(
766+
driver_config_client: TestClient,
767+
):
768+
init(
769+
supertokens_config=SupertokensConfig("http://localhost:3567"),
770+
app_info=InputAppInfo(
771+
app_name="SuperTokens Demo",
772+
api_domain="http://api.supertokens.io",
773+
website_domain="http://supertokens.io",
774+
api_base_path="/auth",
775+
),
776+
framework="fastapi",
777+
recipe_list=[
778+
emailpassword.init(
779+
sign_up_feature=emailpassword.InputSignUpFeature(form_fields=[])
780+
),
781+
session.init(get_token_transfer_method=lambda _, __, ___: "cookie"),
782+
],
783+
)
784+
start_st()
785+
786+
response_1 = driver_config_client.post(
787+
url="/auth/signup",
788+
headers={"Content-Type": "application/json"},
789+
json={
790+
"formFields": [
791+
{"id": "email", "value": 123},
792+
{"id": "password", "value": "validpassword123"},
793+
]
794+
},
795+
)
796+
assert response_1.status_code == 400
797+
dict_response = json.loads(response_1.text)
798+
assert dict_response["message"] == "email value must be a string"
799+
800+
response_1_signin = driver_config_client.post(
801+
url="/auth/signin",
802+
headers={"Content-Type": "application/json"},
803+
json={
804+
"formFields": [
805+
{"id": "email", "value": 123},
806+
{"id": "password", "value": "validpassword123"},
807+
]
808+
},
809+
)
810+
assert response_1_signin.status_code == 400
811+
dict_response_signin = json.loads(response_1_signin.text)
812+
assert dict_response_signin["message"] == "email value must be a string"
813+
814+
response_2 = driver_config_client.post(
815+
url="/auth/signup",
816+
headers={"Content-Type": "application/json"},
817+
json={
818+
"formFields": [
819+
{"id": "email", "value": "[email protected]"},
820+
{"id": "password", "value": 12345},
821+
]
822+
},
823+
)
824+
assert response_2.status_code == 400
825+
dict_response = json.loads(response_2.text)
826+
assert dict_response["message"] == "password value must be a string"
827+
828+
response_2_signin = driver_config_client.post(
829+
url="/auth/signin",
830+
headers={"Content-Type": "application/json"},
831+
json={
832+
"formFields": [
833+
{"id": "email", "value": "[email protected]"},
834+
{"id": "password", "value": 12345},
835+
]
836+
},
837+
)
838+
assert response_2_signin.status_code == 400
839+
dict_response_signin = json.loads(response_2_signin.text)
840+
assert dict_response_signin["message"] == "password value must be a string"
841+
842+
722843
@mark.asyncio
723844
async def test_too_many_fields(driver_config_client: TestClient):
724845
init(

tests/emailpassword/test_signup.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from fastapi import FastAPI
1514
import json
16-
from tests.testclient import TestClientWithNoCookieJar as TestClient
15+
16+
from fastapi import FastAPI
1717
from pytest import fixture, mark
18+
1819
from supertokens_python import init
1920
from supertokens_python.framework.fastapi import get_middleware
2021
from supertokens_python.recipe import emailpassword, session
22+
from tests.testclient import TestClientWithNoCookieJar as TestClient
2123
from tests.utils import (
2224
get_st_init_args,
2325
setup_function,
26+
sign_up_request,
2427
start_st,
2528
teardown_function,
26-
sign_up_request,
2729
)
2830

2931
_ = setup_function # type: ignore

0 commit comments

Comments
 (0)