diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98acbb8b..e429edac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: pip install pydantic==1.10.12 make tests_only - name: Upload coverage to Coveralls + if: ${{ matrix.python-version }} == "3.12" uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/supabase_auth/_async/gotrue_client.py b/supabase_auth/_async/gotrue_client.py index 041ac63b..fc4b0732 100644 --- a/supabase_auth/_async/gotrue_client.py +++ b/supabase_auth/_async/gotrue_client.py @@ -349,7 +349,7 @@ async def sign_in_with_sso(self, credentials: SignInWithSSOCredentials): If you have built an organization-specific login page, you can use the organization's SSO Identity Provider UUID directly instead. """ - self._remove_session() + await self._remove_session() provider_id = credentials.get("provider_id") domain = credentials.get("domain") options = credentials.get("options", {}) @@ -361,7 +361,7 @@ async def sign_in_with_sso(self, credentials: SignInWithSSOCredentials): skip_http_redirect = options.get("skip_http_redirect", True) if domain: - return self._request( + return await self._request( "POST", "sso", body={ @@ -375,7 +375,7 @@ async def sign_in_with_sso(self, credentials: SignInWithSSOCredentials): xform=parse_sso_response, ) if provider_id: - return self._request( + return await self._request( "POST", "sso", body={ diff --git a/tests/_async/test_gotrue_admin_api.py b/tests/_async/test_gotrue_admin_api.py index 5eebd8eb..701f46c8 100644 --- a/tests/_async/test_gotrue_admin_api.py +++ b/tests/_async/test_gotrue_admin_api.py @@ -1,6 +1,15 @@ -from supabase_auth.errors import AuthError +import pytest + +from supabase_auth.errors import ( + AuthApiError, + AuthError, + AuthInvalidCredentialsError, + AuthSessionMissingError, + AuthWeakPasswordError, +) from .clients import ( + auth_client, auth_client_with_session, client_api_auto_confirm_disabled_client, client_api_auto_confirm_off_signups_enabled_client, @@ -36,21 +45,6 @@ async def test_create_user_with_user_metadata(): assert "profile_image" in response.user.user_metadata -async def test_create_user_with_app_metadata(): - app_metadata = mock_app_metadata() - credentials = mock_user_credentials() - response = await service_role_api_client().create_user( - { - "email": credentials.get("email"), - "password": credentials.get("password"), - "app_metadata": app_metadata, - } - ) - assert response.user.email == credentials.get("email") - assert "provider" in response.user.app_metadata - assert "providers" in response.user.app_metadata - - async def test_create_user_with_user_and_app_metadata(): user_metadata = mock_user_metadata() app_metadata = mock_app_metadata() @@ -160,6 +154,102 @@ async def test_modify_confirm_email_using_update_user_by_id(): assert response.user.email_confirmed_at +async def test_invalid_credential_sign_in_with_phone(): + try: + response = await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "phone": "+123456789", + "password": "strong_pwd", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_invalid_credential_sign_in_with_email(): + try: + response = await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "email": "unknown_user@unknowndomain.com", + "password": "strong_pwd", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_sign_in_with_otp_email(): + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "email": "unknown_user@unknowndomain.com", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_sign_in_with_otp_phone(): + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "phone": "+112345678", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_resend(): + try: + await client_api_auto_confirm_off_signups_enabled_client().resend( + {"phone": "+112345678", "type": "sms"} + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_reauthenticate(): + try: + response = await auth_client_with_session().reauthenticate() + except AuthSessionMissingError: + pass + + +async def test_refresh_session(): + try: + response = await auth_client_with_session().refresh_session() + except AuthSessionMissingError: + pass + + +async def test_reset_password_for_email(): + credentials = mock_user_credentials() + try: + response = await auth_client_with_session().reset_password_email( + email=credentials.get("email") + ) + except AuthSessionMissingError: + pass + + +async def test_resend_missing_credentials(): + try: + await client_api_auto_confirm_off_signups_enabled_client().resend( + {"type": "email_change"} + ) + except AuthInvalidCredentialsError as e: + assert e.to_dict() + + +async def test_sign_in_anonymously(): + try: + response = await auth_client_with_session().sign_in_anonymously() + assert response + except AuthApiError: + pass + + async def test_delete_user_should_be_able_delete_an_existing_user(): credentials = mock_user_credentials() user = await create_new_user_with_email(email=credentials.get("email")) @@ -271,3 +361,230 @@ async def test_verify_otp_with_invalid_phone_number(): assert False except AuthError as e: assert e.message == "Invalid phone number format (E.164 required)" + + +async def test_sign_in_with_id_token(): + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_id_token( + { + "provider": "google", + "token": "123456", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_sign_in_with_sso(): + with pytest.raises(AuthApiError, match=r"SAML 2.0 is disabled") as exc: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_sso( + { + "domain": "google", + } + ) + assert exc.value is not None + + +async def test_sign_in_with_oauth(): + assert ( + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_oauth( + { + "provider": "google", + } + ) + ) + + +async def test_link_identity_missing_session(): + + with pytest.raises(AuthSessionMissingError) as exc: + await client_api_auto_confirm_off_signups_enabled_client().link_identity( + { + "provider": "google", + } + ) + assert exc.value is not None + + +async def test_get_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert await client._storage.get_item(client._storage_key) is not None + + +async def test_remove_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + await client._storage.remove_item(client._storage_key) + assert client._storage_key not in client._storage.storage + + +async def test_list_factors(): + credentials = mock_user_credentials() + client = auth_client() + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + factors = await client._list_factors() + assert factors + assert isinstance(factors.totp, list) and isinstance(factors.phone, list) + + +async def test_start_auto_refresh_token(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + assert await client._start_auto_refresh_token(2.0) is None + + +async def test_recover_and_refresh(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + await client._recover_and_refresh() + assert client._storage_key in client._storage.storage + + +async def test_get_user_identities(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert (await client.get_user_identities()).identities[0].identity_data[ + "email" + ] == credentials.get("email") + + +async def test_update_user(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + await client.update_user({"password": "123e5a"}) + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": "123e5a", + } + ) + + +async def test_create_user_with_app_metadata(): + app_metadata = mock_app_metadata() + credentials = mock_user_credentials() + response = await service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "app_metadata": app_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert "provider" in response.user.app_metadata + assert "providers" in response.user.app_metadata + + +async def test_weak_email_password_error(): + credentials = mock_user_credentials() + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "email": credentials.get("email"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() + + +async def test_weak_phone_password_error(): + credentials = mock_user_credentials() + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "phone": credentials.get("phone"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() diff --git a/tests/_sync/test_gotrue_admin_api.py b/tests/_sync/test_gotrue_admin_api.py index 44d24999..c885d4bf 100644 --- a/tests/_sync/test_gotrue_admin_api.py +++ b/tests/_sync/test_gotrue_admin_api.py @@ -1,6 +1,15 @@ -from supabase_auth.errors import AuthError +import pytest + +from supabase_auth.errors import ( + AuthApiError, + AuthError, + AuthInvalidCredentialsError, + AuthSessionMissingError, + AuthWeakPasswordError, +) from .clients import ( + auth_client, auth_client_with_session, client_api_auto_confirm_disabled_client, client_api_auto_confirm_off_signups_enabled_client, @@ -8,6 +17,7 @@ ) from .utils import ( create_new_user_with_email, + mock_access_token, mock_app_metadata, mock_user_credentials, mock_user_metadata, @@ -21,21 +31,6 @@ def test_create_user_should_create_a_new_user(): assert response.email == credentials.get("email") -def test_create_user_with_user_metadata(): - user_metadata = mock_user_metadata() - credentials = mock_user_credentials() - response = service_role_api_client().create_user( - { - "email": credentials.get("email"), - "password": credentials.get("password"), - "user_metadata": user_metadata, - } - ) - assert response.user.email == credentials.get("email") - assert response.user.user_metadata == user_metadata - assert "profile_image" in response.user.user_metadata - - def test_create_user_with_app_metadata(): app_metadata = mock_app_metadata() credentials = mock_user_credentials() @@ -160,6 +155,102 @@ def test_modify_confirm_email_using_update_user_by_id(): assert response.user.email_confirmed_at +def test_invalid_credential_sign_in_with_phone(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "phone": "+123456789", + "password": "strong_pwd", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_invalid_credential_sign_in_with_email(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "email": "unknown_user@unknowndomain.com", + "password": "strong_pwd", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_sign_in_with_otp_email(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "email": "unknown_user@unknowndomain.com", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_sign_in_with_otp_phone(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "phone": "+112345678", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_resend(): + try: + client_api_auto_confirm_off_signups_enabled_client().resend( + {"phone": "+112345678", "type": "sms"} + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_reauthenticate(): + try: + response = auth_client_with_session().reauthenticate() + except AuthSessionMissingError: + pass + + +def test_refresh_session(): + try: + response = auth_client_with_session().refresh_session() + except AuthSessionMissingError: + pass + + +def test_reset_password_for_email(): + credentials = mock_user_credentials() + try: + response = auth_client_with_session().reset_password_email( + email=credentials.get("email") + ) + except AuthSessionMissingError: + pass + + +def test_resend_missing_credentials(): + try: + client_api_auto_confirm_off_signups_enabled_client().resend( + {"type": "email_change"} + ) + except AuthInvalidCredentialsError as e: + assert e.to_dict() + + +def test_sign_in_anonymously(): + try: + response = auth_client_with_session().sign_in_anonymously() + assert response + except AuthApiError: + pass + + def test_delete_user_should_be_able_delete_an_existing_user(): credentials = mock_user_credentials() user = create_new_user_with_email(email=credentials.get("email")) @@ -271,3 +362,222 @@ def test_verify_otp_with_invalid_phone_number(): assert False except AuthError as e: assert e.message == "Invalid phone number format (E.164 required)" + + +def test_sign_in_with_oauth(): + assert client_api_auto_confirm_off_signups_enabled_client().sign_in_with_oauth( + { + "provider": "google", + } + ) + + +def test_decode_jwt(): + assert auth_client_with_session()._decode_jwt(mock_access_token()) + + +def test_link_identity_missing_session(): + + with pytest.raises(AuthSessionMissingError) as exc: + client_api_auto_confirm_off_signups_enabled_client().link_identity( + { + "provider": "google", + } + ) + assert exc.value is not None + + +def test_sign_in_with_id_token(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_id_token( + { + "provider": "google", + "token": "123456", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_get_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert client._storage.get_item(client._storage_key) is not None + + +def test_remove_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + client._storage.remove_item(client._storage_key) + assert client._storage_key not in client._storage.storage + + +def test_list_factors(): + credentials = mock_user_credentials() + client = auth_client() + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + factors = client._list_factors() + assert factors + assert isinstance(factors.totp, list) and isinstance(factors.phone, list) + + +def test_start_auto_refresh_token(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + assert client._start_auto_refresh_token(2.0) is None + + +def test_recover_and_refresh(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + client._recover_and_refresh() + assert client._storage_key in client._storage.storage + + +def test_get_user_identities(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert client.get_user_identities().identities[0].identity_data[ + "email" + ] == credentials.get("email") + + +def test_update_user(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + client.update_user({"password": "123e5a"}) + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": "123e5a", + } + ) + + +def test_create_user_with_user_metadata(): + user_metadata = mock_user_metadata() + credentials = mock_user_credentials() + response = service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "user_metadata": user_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert response.user.user_metadata == user_metadata + assert "profile_image" in response.user.user_metadata + + +def test_weak_email_password_error(): + credentials = mock_user_credentials() + try: + client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "email": credentials.get("email"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() + + +def test_weak_phone_password_error(): + credentials = mock_user_credentials() + try: + client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "phone": credentials.get("phone"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index afb8e128..57dcb66a 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -7,7 +7,17 @@ from supabase_auth.constants import API_VERSION_HEADER_NAME from supabase_auth.errors import AuthApiError, AuthWeakPasswordError -from supabase_auth.helpers import parse_response_api_version +from supabase_auth.helpers import ( + decode_jwt_payload, + generate_pkce_challenge, + generate_pkce_verifier, + get_error_code, + is_valid_jwt, + parse_link_identity_response, + parse_response_api_version, +) + +from ._sync.utils import mock_access_token TEST_URL = f"http://localhost" @@ -90,3 +100,44 @@ def test_parse_response_api_version_with_invalid_dates(): response = Response(headers=headers, status_code=200) api_ver = parse_response_api_version(response) assert api_ver == None + + +def test_parse_link_identity_response(): + assert parse_link_identity_response({"url": f"{TEST_URL}/hello-world"}) + + +def test_get_error_code(): + assert get_error_code({}) == None + assert get_error_code({"error_code": "500"}) == "500" + + +def test_decode_jwt_payload(): + assert decode_jwt_payload(mock_access_token()) + + with pytest.raises( + ValueError, match=r"JWT is not valid: not a JWT structure" + ) as exc: + decode_jwt_payload("non-valid-jwt") + assert exc.value is not None + + +def test_generate_pkce_verifier(): + assert isinstance(generate_pkce_verifier(45), str) + with pytest.raises( + ValueError, match=r"PKCE verifier length must be between 43 and 128 characters" + ) as exc: + generate_pkce_verifier(42) + assert exc.value is not None + + +def test_generate_pkce_challenge(): + pkce = generate_pkce_verifier(45) + assert isinstance(generate_pkce_challenge(pkce), str) + + +def test_is_valid_jwt(): + jwt = mock_access_token() + assert not is_valid_jwt(1) + assert not is_valid_jwt("") + assert not is_valid_jwt("Bearer ") + assert is_valid_jwt(jwt)