From 053d0f9c014c0af1789dbd52ebbfed8da94dd1d6 Mon Sep 17 00:00:00 2001 From: Bruno Date: Fri, 25 Apr 2025 11:07:05 -0400 Subject: [PATCH 1/2] feat(hook): add new create user hook --- src/hooks/auth0-context.ts | 7 +++++++ src/hooks/auth0-provider.tsx | 16 ++++++++++++++++ src/hooks/use-auth0.ts | 1 + 3 files changed, 24 insertions(+) diff --git a/src/hooks/auth0-context.ts b/src/hooks/auth0-context.ts index dc7161dd..392f64c6 100644 --- a/src/hooks/auth0-context.ts +++ b/src/hooks/auth0-context.ts @@ -18,6 +18,7 @@ import type { ExchangeNativeSocialOptions, RevokeOptions, ResetPasswordOptions, + CreateUserOptions, } from '../types'; import BaseError from '../utils/baseError'; @@ -138,6 +139,11 @@ export interface Auth0ContextInterface *Request an email with instructions to change password of a user {@link Auth#resetPassword} */ resetPassword: (parameters: ResetPasswordOptions) => Promise; + + /** + *Creates a new user with the given email and password {@link Auth#createUser} + */ + createUser: (parameters: CreateUserOptions) => Promise>; } export interface AuthState { @@ -181,6 +187,7 @@ const initialContext = { authorizeWithExchangeNativeSocial: stub, revokeRefreshToken: stub, resetPassword: stub, + createUser: stub, }; const Auth0Context = createContext(initialContext); diff --git a/src/hooks/auth0-provider.tsx b/src/hooks/auth0-provider.tsx index 048e4e62..ba479f4d 100644 --- a/src/hooks/auth0-provider.tsx +++ b/src/hooks/auth0-provider.tsx @@ -25,6 +25,7 @@ import type { ExchangeNativeSocialOptions, RevokeOptions, ResetPasswordOptions, + CreateUserOptions, } from '../types'; import type { CustomJwtPayload } from '../internal-types'; import { convertUser } from '../utils/userConversion'; @@ -386,6 +387,19 @@ const Auth0Provider = ({ } }, [client]); + const createUser = useCallback( + async (parameters: CreateUserOptions) => { + try { + const user = await client.auth.createUser(parameters); + return user; + } catch (error) { + dispatch({ type: 'ERROR', error: error as BaseError }); + throw error; + } + }, + [client] + ); + const contextValue = useMemo( () => ({ ...state, @@ -407,6 +421,7 @@ const Auth0Provider = ({ authorizeWithExchangeNativeSocial, revokeRefreshToken, resetPassword, + createUser, }), [ state, @@ -428,6 +443,7 @@ const Auth0Provider = ({ authorizeWithExchangeNativeSocial, revokeRefreshToken, resetPassword, + createUser, ] ); diff --git a/src/hooks/use-auth0.ts b/src/hooks/use-auth0.ts index 35494df2..6ea40dfa 100644 --- a/src/hooks/use-auth0.ts +++ b/src/hooks/use-auth0.ts @@ -30,6 +30,7 @@ import type { Auth0ContextInterface } from './auth0-context'; * authorizeWithPasswordRealm, * authorizeWithExchangeNativeSocial, * revokeRefreshToken + * createUser * } = useAuth0(); * ``` * From 6565034dbfc0c768a3396d7ee96f844c2c2bc096 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 4 May 2025 20:00:23 -0400 Subject: [PATCH 2/2] test(createUserHook): add tests for the createUser hook --- src/hooks/__tests__/use-auth0.spec.jsx | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/hooks/__tests__/use-auth0.spec.jsx b/src/hooks/__tests__/use-auth0.spec.jsx index 1aa11099..07f70a81 100644 --- a/src/hooks/__tests__/use-auth0.spec.jsx +++ b/src/hooks/__tests__/use-auth0.spec.jsx @@ -67,6 +67,7 @@ const mockAuth0 = { passwordRealm: jest.fn().mockResolvedValue(mockCredentials), exchangeNativeSocial: jest.fn().mockResolvedValue(mockCredentials), revoke: jest.fn().mockResolvedValue(mockCredentials), + createUser: jest.fn(), }, credentialsManager: { getCredentials: jest.fn().mockResolvedValue(mockCredentials), @@ -114,6 +115,11 @@ describe('The useAuth0 hook', () => { expect(result.current.cancelWebAuth()).toBeDefined(); }); + it('defines createUser', () => { + const { result } = renderHook(() => useAuth0()); + expect(result.current.createUser).toBeDefined(); + }); + it('isLoading is true until initialization finishes', async () => { const { result } = renderHook(() => useAuth0(), { wrapper, @@ -1215,6 +1221,57 @@ describe('The useAuth0 hook', () => { mockAuth0.credentialsManager.hasValidCredentials ).toHaveBeenCalledWith(100); }); + + it('throws an error when createUser is called without a wrapper', () => { + const { result } = renderHook(() => useAuth0()); + expect(() => + result.current.createUser({ + email: 'foo@bar.com', + password: 'pass', + connection: 'Username-Password-Authentication', + }) + ).toThrowError(/no provider was set/i); + }); + + it('can create a user', async () => { + const mockUser = { email: 'foo@bar.com', user_id: 'user123' }; + mockAuth0.auth.createUser = jest.fn().mockResolvedValue(mockUser); + const { result } = renderHook(() => useAuth0(), { wrapper }); + let user; + await act(async () => { + user = await result.current.createUser({ + email: 'foo@bar.com', + password: 'pass', + connection: 'Username-Password-Authentication', + }); + }); + expect(mockAuth0.auth.createUser).toHaveBeenCalledWith({ + email: 'foo@bar.com', + password: 'pass', + connection: 'Username-Password-Authentication', + }); + expect(user).toEqual(mockUser); + }); + + it('sets the error property when an error is raised in createUser', async () => { + const errorToThrow = new Error('Create user error'); + mockAuth0.auth.createUser = jest.fn().mockRejectedValue(errorToThrow); + const { result } = renderHook(() => useAuth0(), { wrapper }); + let thrown; + await act(async () => { + try { + await result.current.createUser({ + email: 'foo@bar.com', + password: 'pass', + connection: 'Username-Password-Authentication', + }); + } catch (e) { + thrown = e; + } + }); + expect(result.current.error).toBe(errorToThrow); + expect(thrown).toBe(errorToThrow); + }); }); describe('The Auth0Provider component', () => {