Skip to content

Commit 806f6af

Browse files
Refactor authentication handling and improve error management
- Updated ClassApp to handle errors without logging the error object. - Enhanced Auth0Provider tests to simulate loading states and credential retrieval. - Implemented unimplemented methods in NativeWebAuthProvider to throw AuthError. - Refactored WebAuth0Client to use a singleton pattern for Auth0Client instance. - Improved WebWebAuthProvider to handle redirect callbacks and errors more gracefully. - Added comprehensive tests for WebAuth0Client and WebCredentialsManager to ensure correct behavior. - Created tests for UnimplementedWebAuthenticationProvider to validate error handling. - Added tests for WebCredentialsManager to verify credential management functionality.
1 parent 7d9a929 commit 806f6af

File tree

9 files changed

+1138
-67
lines changed

9 files changed

+1138
-67
lines changed

example/src/App.web.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class ClassApp extends React.Component<{}, ClassAppState> {
209209
token: credentials.accessToken,
210210
});
211211
this.setState({ user, result: credentials, isLoading: false });
212-
} catch (e) {
212+
} catch {
213213
this.setState({ user: null, isLoading: false });
214214
}
215215
};

src/hooks/__tests__/Auth0Provider.spec.tsx

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const createMockClient = () => {
8787
authorize: jest.fn().mockResolvedValue(mockCredentials),
8888
clearSession: jest.fn().mockResolvedValue(undefined),
8989
cancelWebAuth: jest.fn().mockResolvedValue(undefined),
90+
handleRedirectCallback: jest.fn().mockResolvedValue(undefined),
9091
},
9192
credentialsManager: {
9293
hasValidCredentials: jest.fn().mockResolvedValue(false),
@@ -112,6 +113,7 @@ const createMockClient = () => {
112113
refreshToken: jest.fn(),
113114
revoke: jest.fn(),
114115
userInfo: jest.fn(),
116+
passwordRealm: jest.fn(),
115117
},
116118
users: jest.fn(),
117119
};
@@ -185,21 +187,45 @@ describe('Auth0Provider', () => {
185187
MockAuth0User.fromIdToken.mockReturnValue(mockUser as any);
186188
});
187189

188-
it('should render a loading state initially', () => {
189-
render(
190-
<Auth0Provider domain="test.com" clientId="123">
191-
<TestConsumer />
192-
</Auth0Provider>
190+
it('should render a loading state initially', async () => {
191+
// Make getCredentials return a promise that we can control
192+
let resolveCredentials: (value: any) => void;
193+
const credentialsPromise = new Promise((resolve) => {
194+
resolveCredentials = resolve;
195+
});
196+
mockClientInstance.credentialsManager.getCredentials.mockReturnValue(
197+
credentialsPromise
193198
);
199+
200+
await act(async () => {
201+
render(
202+
<Auth0Provider domain="test.com" clientId="123">
203+
<TestConsumer />
204+
</Auth0Provider>
205+
);
206+
});
207+
208+
// Should show loading state initially
194209
expect(screen.getByTestId('loading')).toBeDefined();
210+
211+
// Resolve the credentials promise
212+
await act(async () => {
213+
resolveCredentials!(null);
214+
});
215+
216+
// Now it should show the "not logged in" state
217+
await waitFor(() => expect(screen.queryByTestId('loading')).toBeNull());
195218
});
196219

197220
it('should initialize with no user if no valid credentials exist', async () => {
198-
render(
199-
<Auth0Provider domain="test.com" clientId="123">
200-
<TestConsumer />
201-
</Auth0Provider>
202-
);
221+
await act(async () => {
222+
render(
223+
<Auth0Provider domain="test.com" clientId="123">
224+
<TestConsumer />
225+
</Auth0Provider>
226+
);
227+
});
228+
203229
await waitFor(() => expect(screen.queryByTestId('loading')).toBeNull());
204230
expect(
205231
mockClientInstance.credentialsManager.getCredentials
@@ -216,11 +242,13 @@ describe('Auth0Provider', () => {
216242
expiresAt: Date.now() / 1000 + 3600,
217243
} as any);
218244

219-
render(
220-
<Auth0Provider domain="test.com" clientId="123">
221-
<TestConsumer />
222-
</Auth0Provider>
223-
);
245+
await act(async () => {
246+
render(
247+
<Auth0Provider domain="test.com" clientId="123">
248+
<TestConsumer />
249+
</Auth0Provider>
250+
);
251+
});
224252

225253
await waitFor(() => {
226254
expect(screen.getByTestId('user-status')).toHaveTextContent(
@@ -232,11 +260,14 @@ describe('Auth0Provider', () => {
232260
});
233261

234262
it('should update the state correctly after a successful authorize call', async () => {
235-
render(
236-
<Auth0Provider domain="test.com" clientId="123">
237-
<TestConsumer />
238-
</Auth0Provider>
239-
);
263+
await act(async () => {
264+
render(
265+
<Auth0Provider domain="test.com" clientId="123">
266+
<TestConsumer />
267+
</Auth0Provider>
268+
);
269+
});
270+
240271
await waitFor(() =>
241272
expect(screen.getByTestId('user-status')).toHaveTextContent(
242273
'Not logged in'
@@ -268,11 +299,14 @@ describe('Auth0Provider', () => {
268299
expiresAt: Date.now() / 1000 + 3600,
269300
} as any);
270301

271-
render(
272-
<Auth0Provider domain="test.com" clientId="123">
273-
<TestConsumer />
274-
</Auth0Provider>
275-
);
302+
await act(async () => {
303+
render(
304+
<Auth0Provider domain="test.com" clientId="123">
305+
<TestConsumer />
306+
</Auth0Provider>
307+
);
308+
});
309+
276310
await waitFor(() =>
277311
expect(screen.getByTestId('user-status')).toHaveTextContent(
278312
'Logged in as: Test User'
@@ -302,11 +336,14 @@ describe('Auth0Provider', () => {
302336
};
303337
mockClientInstance.webAuth.authorize.mockRejectedValueOnce(loginError);
304338

305-
render(
306-
<Auth0Provider domain="test.com" clientId="123">
307-
<TestConsumer />
308-
</Auth0Provider>
309-
);
339+
await act(async () => {
340+
render(
341+
<Auth0Provider domain="test.com" clientId="123">
342+
<TestConsumer />
343+
</Auth0Provider>
344+
);
345+
});
346+
310347
await waitFor(() =>
311348
expect(screen.getByTestId('user-status')).toHaveTextContent(
312349
'Not logged in'
@@ -334,11 +371,14 @@ describe('Auth0Provider', () => {
334371
});
335372

336373
it('should call createUser but not change the login state', async () => {
337-
render(
338-
<Auth0Provider domain="test.com" clientId="123">
339-
<TestConsumer />
340-
</Auth0Provider>
341-
);
374+
await act(async () => {
375+
render(
376+
<Auth0Provider domain="test.com" clientId="123">
377+
<TestConsumer />
378+
</Auth0Provider>
379+
);
380+
});
381+
342382
await waitFor(() =>
343383
expect(screen.getByTestId('user-status')).toHaveTextContent(
344384
'Not logged in'
@@ -358,11 +398,14 @@ describe('Auth0Provider', () => {
358398
});
359399

360400
it('should call resetPassword and not change the login state', async () => {
361-
render(
362-
<Auth0Provider domain="test.com" clientId="123">
363-
<TestConsumer />
364-
</Auth0Provider>
365-
);
401+
await act(async () => {
402+
render(
403+
<Auth0Provider domain="test.com" clientId="123">
404+
<TestConsumer />
405+
</Auth0Provider>
406+
);
407+
});
408+
366409
await waitFor(() =>
367410
expect(screen.getByTestId('user-status')).toHaveTextContent(
368411
'Not logged in'

src/platforms/native/adapters/NativeWebAuthProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import type {
1111
} from '../../../types/platform-specific';
1212
import type { INativeBridge } from '../bridge';
1313
import { finalizeScope } from '../../../core/utils';
14+
import { AuthError } from '../../../core/models';
15+
16+
const webAuthNotSupported = 'This Method is only available in web platform.';
1417

1518
/**
1619
* A native platform-specific implementation of the IWebAuthProvider.
@@ -21,6 +24,9 @@ export class NativeWebAuthProvider implements IWebAuthProvider {
2124
private bridge: INativeBridge,
2225
private domain: string
2326
) {}
27+
handleRedirectCallback(): Promise<void> {
28+
throw new AuthError('NotImplemented', webAuthNotSupported);
29+
}
2430

2531
async authorize(
2632
parameters: WebAuthorizeParameters = {},

src/platforms/web/adapters/WebAuth0Client.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,40 @@ import {
1414
import { HttpClient } from '../../../core/services/HttpClient';
1515
import { AuthError } from '../../../core/models';
1616

17-
let spaClient: Auth0Client | null = null;
18-
let redirectHandled = false;
19-
20-
/**
21-
* Factory function to get a singleton instance of Auth0Client.
22-
* This ensures that the client is only created once and reused.
23-
*
24-
* @param options - The Auth0ClientOptions to configure the client.
25-
* @returns An instance of Auth0Client.
26-
*/
27-
const getSpaClient = (options: Auth0ClientOptions): Auth0Client => {
28-
if (spaClient) {
29-
return spaClient;
30-
}
31-
spaClient = new Auth0Client(options);
32-
return spaClient;
33-
};
34-
3517
export class WebAuth0Client implements IAuth0Client {
3618
readonly webAuth: WebWebAuthProvider;
3719
readonly credentialsManager: WebCredentialsManager;
3820
readonly auth: AuthenticationOrchestrator;
3921

4022
private readonly httpClient: HttpClient;
4123
public readonly client: Auth0Client;
24+
private static spaClient: Auth0Client | null = null;
4225

4326
private logoutInProgress = false;
4427

28+
/**
29+
* Factory method to get a singleton instance of Auth0Client.
30+
* This ensures that the client is only created once and reused.
31+
*
32+
* @param options - The Auth0ClientOptions to configure the client.
33+
* @returns An instance of Auth0Client.
34+
*/
35+
private static getSpaClient(options: Auth0ClientOptions): Auth0Client {
36+
if (WebAuth0Client.spaClient) {
37+
return WebAuth0Client.spaClient;
38+
}
39+
WebAuth0Client.spaClient = new Auth0Client(options);
40+
return WebAuth0Client.spaClient;
41+
}
42+
43+
/**
44+
* Reset the singleton instance. Used for testing purposes.
45+
* @internal
46+
*/
47+
public static resetSpaClientSingleton(): void {
48+
WebAuth0Client.spaClient = null;
49+
}
50+
4551
constructor(options: WebAuth0Options) {
4652
const baseUrl = `https://${options.domain}`;
4753

@@ -69,7 +75,7 @@ export class WebAuth0Client implements IAuth0Client {
6975
};
7076

7177
// Use the singleton factory to get the spa-js client instance.
72-
const client = getSpaClient(clientOptions);
78+
const client = WebAuth0Client.getSpaClient(clientOptions);
7379
this.client = client;
7480

7581
this.webAuth = new WebWebAuthProvider(this.client);

src/platforms/web/adapters/WebWebAuthProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export class WebWebAuthProvider implements IWebAuthProvider {
3131
return new Promise(() => {});
3232
}
3333

34-
async handleRedirectCallback(url?: string): Promise<RedirectLoginResult> {
34+
async handleRedirectCallback(url?: string): Promise<void> {
3535
try {
36-
return await this.client.handleRedirectCallback(url);
36+
await this.client.handleRedirectCallback(url);
3737
} catch (e: any) {
3838
throw new AuthError(
3939
e.error ?? 'RedirectCallbackError',

0 commit comments

Comments
 (0)