Skip to content

Commit 3997d41

Browse files
refactor: Update WebAuth0Client, WebCredentialsManager, and WebWebAuthProvider to use Auth0Client directly and add unit tests for WebWebAuthProvider
1 parent 659a260 commit 3997d41

4 files changed

Lines changed: 135 additions & 14 deletions

File tree

src/platforms/web/adapters/WebAuth0Client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ export class WebAuth0Client implements IAuth0Client {
7474

7575
this.handleRedirect(client);
7676

77-
this.webAuth = new WebWebAuthProvider(this);
78-
this.credentialsManager = new WebCredentialsManager(this);
77+
this.webAuth = new WebWebAuthProvider(this.client);
78+
this.credentialsManager = new WebCredentialsManager(this.client);
7979
}
8080

8181
users(token: string): IUsersClient {

src/platforms/web/adapters/WebCredentialsManager.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import {
44
AuthError,
55
Credentials as CredentialsModel,
66
} from '../../../core/models';
7-
import type { WebAuth0Client } from './WebAuth0Client';
7+
import type { Auth0Client } from '@auth0/auth0-spa-js';
88

99
export class WebCredentialsManager implements ICredentialsManager {
10-
constructor(private parent: WebAuth0Client) {}
10+
constructor(private client: Auth0Client) {}
1111

1212
async saveCredentials(_credentials: Credentials): Promise<void> {
1313
console.warn(
@@ -23,13 +23,13 @@ export class WebCredentialsManager implements ICredentialsManager {
2323
forceRefresh?: boolean
2424
): Promise<Credentials> {
2525
try {
26-
const tokenResponse = await this.parent.client.getTokenSilently({
26+
const tokenResponse = await this.client.getTokenSilently({
2727
cacheMode: forceRefresh ? 'off' : 'on',
2828
authorizationParams: { ...parameters, scope },
2929
detailedResponse: true,
3030
});
3131

32-
const claims = await this.parent.client.getIdTokenClaims();
32+
const claims = await this.client.getIdTokenClaims();
3333
if (!claims || !claims.exp) {
3434
throw new AuthError(
3535
'IdTokenMissing',
@@ -61,10 +61,10 @@ export class WebCredentialsManager implements ICredentialsManager {
6161
}
6262

6363
async hasValidCredentials(): Promise<boolean> {
64-
return this.parent.client.isAuthenticated();
64+
return this.client.isAuthenticated();
6565
}
6666

6767
async clearCredentials(): Promise<void> {
68-
await this.parent.client.logout({ openUrl: false });
68+
await this.client.logout({ openUrl: false });
6969
}
7070
}

src/platforms/web/adapters/WebWebAuthProvider.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@ import type {
66
} from '../../../types';
77
import { AuthError } from '../../../core/models';
88
import { finalizeScope } from '../../../core/utils';
9-
import type { WebAuth0Client } from './WebAuth0Client';
9+
import type { Auth0Client } from '@auth0/auth0-spa-js';
1010

1111
export class WebWebAuthProvider implements IWebAuthProvider {
12-
constructor(private parent: WebAuth0Client) {}
12+
constructor(private client: Auth0Client) {}
1313

1414
async authorize(
1515
parameters: WebAuthorizeParameters = {}
1616
): Promise<Credentials> {
1717
const finalScope = finalizeScope(parameters.scope);
18-
await this.parent.client.loginWithRedirect({
18+
const { redirectUrl, ...restParams } = parameters;
19+
await this.client.loginWithRedirect({
1920
authorizationParams: {
20-
...parameters,
21+
...restParams,
2122
scope: finalScope,
22-
redirect_uri: parameters.redirectUrl,
23+
redirect_uri: redirectUrl,
2324
},
2425
});
2526

@@ -32,7 +33,7 @@ export class WebWebAuthProvider implements IWebAuthProvider {
3233

3334
async clearSession(parameters: ClearSessionParameters = {}): Promise<void> {
3435
try {
35-
await this.parent.client.logout({
36+
await this.client.logout({
3637
logoutParams: {
3738
federated: parameters.federated,
3839
returnTo: parameters.returnToUrl,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Auth0Client } from '@auth0/auth0-spa-js';
2+
import { WebWebAuthProvider } from '../WebWebAuthProvider';
3+
import { finalizeScope } from '../../../../core/utils';
4+
import { AuthError } from '../../../../core/models';
5+
6+
// Mock the direct dependencies
7+
jest.mock('@auth0/auth0-spa-js');
8+
jest.mock('../../../../core/utils/scope');
9+
10+
const MockAuth0Client = Auth0Client as jest.MockedClass<typeof Auth0Client>;
11+
const mockFinalizeScope = finalizeScope as jest.Mock;
12+
13+
describe('WebWebAuthProvider', () => {
14+
let mockSpaClient: jest.Mocked<Auth0Client>;
15+
let provider: WebWebAuthProvider;
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
20+
// Create a mock instance of the underlying spa-js client.
21+
// This is the object that will be passed into our provider's constructor.
22+
mockSpaClient = new MockAuth0Client({} as any);
23+
24+
// Construct the provider with the mock dependency.
25+
provider = new WebWebAuthProvider(mockSpaClient);
26+
27+
// Provide a default mock implementation for the scope utility.
28+
mockFinalizeScope.mockImplementation(
29+
(scope) => scope || 'openid profile email'
30+
);
31+
});
32+
33+
describe('authorize', () => {
34+
it('should call finalizeScope with the provided scope', () => {
35+
// The promise from authorize() is designed to never resolve, so we don't await it.
36+
// We are only testing the synchronous calls that happen before the redirect.
37+
provider.authorize({ scope: 'read:data' });
38+
expect(mockFinalizeScope).toHaveBeenCalledWith('read:data');
39+
});
40+
41+
it('should call loginWithRedirect on the spa-js client with all parameters', () => {
42+
const parameters = {
43+
audience: 'https://api.example.com',
44+
connection: 'Username-Password-Authentication',
45+
redirectUrl: 'https://app.com/callback',
46+
scope: 'openid profile read:data',
47+
};
48+
49+
// Re-configure the mock scope utility for this specific test
50+
mockFinalizeScope.mockReturnValue('openid profile read:data');
51+
52+
provider.authorize(parameters);
53+
54+
expect(mockSpaClient.loginWithRedirect).toHaveBeenCalledTimes(1);
55+
expect(mockSpaClient.loginWithRedirect).toHaveBeenCalledWith({
56+
authorizationParams: {
57+
audience: parameters.audience,
58+
connection: parameters.connection,
59+
scope: 'openid profile read:data',
60+
redirect_uri: parameters.redirectUrl,
61+
},
62+
});
63+
});
64+
65+
it('should throw an error to interrupt the app flow, as it redirects', async () => {
66+
// This confirms the promise doesn't resolve with credentials, which is correct behavior for a redirect.
67+
// It should hang until Jest times it out, but we can check if it throws our specific Redirecting error.
68+
// In your latest implementation, it just returns a hanging promise. Let's test that it doesn't resolve or reject quickly.
69+
const authorizePromise = provider.authorize({});
70+
71+
const timeout = new Promise((resolve) => setTimeout(resolve, 100));
72+
73+
// We expect the test to finish (due to timeout) before the authorizePromise resolves.
74+
// This confirms it's a hanging promise.
75+
await expect(
76+
Promise.race([authorizePromise, timeout])
77+
).resolves.toBeUndefined();
78+
});
79+
});
80+
81+
describe('clearSession', () => {
82+
it("should call the client's logout method with correct parameters", async () => {
83+
const parameters = {
84+
returnToUrl: 'https://app.com/logout',
85+
federated: true,
86+
};
87+
88+
await provider.clearSession(parameters);
89+
90+
expect(mockSpaClient.logout).toHaveBeenCalledTimes(1);
91+
expect(mockSpaClient.logout).toHaveBeenCalledWith({
92+
logoutParams: {
93+
returnTo: parameters.returnToUrl,
94+
federated: parameters.federated,
95+
},
96+
});
97+
});
98+
99+
it('should throw an AuthError if the client logout fails', async () => {
100+
const logoutError = {
101+
error: 'logout_failed',
102+
error_description: 'Could not log out',
103+
};
104+
mockSpaClient.logout.mockRejectedValue(logoutError);
105+
106+
await expect(provider.clearSession()).rejects.toThrow(AuthError);
107+
await expect(provider.clearSession()).rejects.toMatchObject({
108+
name: 'logout_failed',
109+
});
110+
});
111+
});
112+
113+
describe('cancelWebAuth', () => {
114+
it('should resolve immediately as it is a no-op on the web', async () => {
115+
await expect(provider.cancelWebAuth()).resolves.toBeUndefined();
116+
expect(mockSpaClient.loginWithRedirect).not.toHaveBeenCalled();
117+
expect(mockSpaClient.logout).not.toHaveBeenCalled();
118+
});
119+
});
120+
});

0 commit comments

Comments
 (0)