@telicent-oss/fe-auth-lib 1.0.0 → 1.0.2-TELFE-1477.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/AuthServerOAuth2Client.d.ts +12 -15
- package/src/AuthServerOAuth2Client.js +21 -10
- package/src/__tests__/callback.failures.test.ts +285 -0
- package/src/__tests__/callback.success.test.ts +410 -0
- package/src/__tests__/core.failures.test.ts +122 -0
- package/src/__tests__/core.success.test.ts +196 -0
- package/src/__tests__/env.success.test.ts +17 -0
- package/src/__tests__/logout.success.test.ts +151 -0
- package/src/__tests__/methods/base64URLEncode.success.test.ts +39 -0
- package/src/__tests__/methods/generateCodeChallenge.success.test.ts +43 -0
- package/src/__tests__/methods/generateCodeVerifier.success.test.ts +43 -0
- package/src/__tests__/methods/generateNonce.success.test.ts +43 -0
- package/src/__tests__/methods/generateState.success.test.ts +43 -0
- package/src/__tests__/methods/getCsrfToken.failures.test.ts +54 -0
- package/src/__tests__/methods/getCsrfToken.success.test.ts +45 -0
- package/src/__tests__/methods/getRawIdToken.success.test.ts +39 -0
- package/src/__tests__/methods/getUserInfo.failures.test.ts +153 -0
- package/src/__tests__/methods/getUserInfo.success.test.ts +84 -0
- package/src/__tests__/methods/isAuthenticated.failures.test.ts +62 -0
- package/src/__tests__/methods/isAuthenticated.success.test.ts +84 -0
- package/src/__tests__/methods/isIdTokenExpired.failures.test.ts +77 -0
- package/src/__tests__/methods/isIdTokenExpired.success.test.ts +49 -0
- package/src/__tests__/methods/validateIdToken.failures.test.ts +177 -0
- package/src/__tests__/methods/validateIdToken.success.test.ts +55 -0
- package/src/__tests__/methods/validateIdTokenForRecovery.failures.test.ts +121 -0
- package/src/__tests__/methods/validateIdTokenForRecovery.success.test.ts +49 -0
- package/src/__tests__/popup.success.test.ts +277 -0
- package/src/__tests__/request-helpers.failures.test.ts +143 -0
- package/src/__tests__/request-helpers.success.test.ts +137 -0
- package/src/__tests__/schema-loading.failures.test.ts +44 -0
- package/src/__tests__/schema-loading.success.test.ts +106 -0
- package/src/__tests__/state.success.test.ts +217 -0
- package/src/__tests__/test-utils.node.success.test.ts +16 -0
- package/src/__tests__/test-utils.success.test.ts +188 -0
- package/src/__tests__/test-utils.ts +203 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telicent-oss/fe-auth-lib",
|
|
3
|
-
"version": "1.0.0",
|
|
3
|
+
"version": "1.0.2-TELFE-1477.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "OAuth2 client library for Telicent Authentication Server",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"INSTRUCTIONS.md"
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "jest",
|
|
35
|
+
"test": "jest --config ./jest.config.js",
|
|
36
36
|
"build": "echo \"No-op: Nothing to build\"",
|
|
37
37
|
"local-publish": "npm publish --registry http://localhost:4873"
|
|
38
38
|
},
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=20.19.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "4222468eba6c8be751a0de016110d35cd53322cd"
|
|
54
54
|
}
|
|
@@ -10,19 +10,17 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export interface AuthServerOAuth2ClientConfig {
|
|
12
12
|
/** OAuth2 client identifier registered with the auth server */
|
|
13
|
-
clientId
|
|
13
|
+
clientId: string;
|
|
14
14
|
/** Base URL of the authentication server (e.g. "http://auth.telicent.localhost") */
|
|
15
|
-
authServerUrl
|
|
15
|
+
authServerUrl: string;
|
|
16
16
|
/** URI to redirect to after OAuth authorization (must be registered with auth server) */
|
|
17
|
-
redirectUri
|
|
17
|
+
redirectUri: string;
|
|
18
18
|
/** Alternative redirect URI for popup-based authentication flows */
|
|
19
|
-
popupRedirectUri
|
|
19
|
+
popupRedirectUri: string;
|
|
20
20
|
/** OAuth2 scopes to request (e.g. "openid profile message.read") */
|
|
21
|
-
scope
|
|
22
|
-
/** Base URL for API requests (used for cross-domain detection) */
|
|
23
|
-
apiUrl?: string;
|
|
21
|
+
scope: string;
|
|
24
22
|
/** Callback function executed after successful logout */
|
|
25
|
-
onLogout
|
|
23
|
+
onLogout: () => void;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
/**
|
|
@@ -144,7 +142,7 @@ declare class AuthServerOAuth2Client {
|
|
|
144
142
|
static readonly OAUTH_ERROR: string;
|
|
145
143
|
|
|
146
144
|
/** Client configuration with defaults applied */
|
|
147
|
-
config:
|
|
145
|
+
config: AuthServerOAuth2ClientConfig;
|
|
148
146
|
/** Whether this client operates in cross-domain mode */
|
|
149
147
|
isCrossDomain: boolean;
|
|
150
148
|
|
|
@@ -184,6 +182,8 @@ declare class AuthServerOAuth2Client {
|
|
|
184
182
|
*/
|
|
185
183
|
base64URLEncode(array: Uint8Array): string;
|
|
186
184
|
|
|
185
|
+
base64URLEncodeString(str: string): string;
|
|
186
|
+
|
|
187
187
|
/**
|
|
188
188
|
* Generate random state
|
|
189
189
|
*/
|
|
@@ -255,7 +255,9 @@ declare class AuthServerOAuth2Client {
|
|
|
255
255
|
* });
|
|
256
256
|
* ```
|
|
257
257
|
*/
|
|
258
|
-
handleCallback(
|
|
258
|
+
handleCallback(
|
|
259
|
+
callbackParams?: string | Record<string, string> | URLSearchParams | null
|
|
260
|
+
): Promise<SessionData>;
|
|
259
261
|
|
|
260
262
|
/**
|
|
261
263
|
* Check if user has valid session
|
|
@@ -425,11 +427,6 @@ declare class AuthServerOAuth2Client {
|
|
|
425
427
|
*/
|
|
426
428
|
logout(): Promise<void>;
|
|
427
429
|
|
|
428
|
-
/**
|
|
429
|
-
* Track popup window and listen for OAuth callback messages
|
|
430
|
-
*/
|
|
431
|
-
trackPopup(popup: Window): void;
|
|
432
|
-
|
|
433
430
|
/**
|
|
434
431
|
* Complete popup OAuth flow
|
|
435
432
|
*
|
|
@@ -44,6 +44,9 @@ class AuthServerOAuth2Client {
|
|
|
44
44
|
this.config = {
|
|
45
45
|
...config,
|
|
46
46
|
};
|
|
47
|
+
this.derived = {
|
|
48
|
+
accountInactive: `${this.config.authServerUrl}/account-inactive`,
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
// Auto-detect if this is a cross-domain client
|
|
49
52
|
this.isCrossDomain = this.detectCrossDomain();
|
|
@@ -278,6 +281,8 @@ class AuthServerOAuth2Client {
|
|
|
278
281
|
);
|
|
279
282
|
|
|
280
283
|
if (!tokenResponse.ok) {
|
|
284
|
+
let errorMessage = "Unknown error";
|
|
285
|
+
const authUrl = `${this.config.authServerUrl}/oauth2/authorize`;
|
|
281
286
|
let errorDetails;
|
|
282
287
|
try {
|
|
283
288
|
// Try to parse JSON error response first
|
|
@@ -288,6 +293,9 @@ class AuthServerOAuth2Client {
|
|
|
288
293
|
errorDetails = { error: "unknown", error_description: errorText };
|
|
289
294
|
}
|
|
290
295
|
|
|
296
|
+
errorMessage =
|
|
297
|
+
errorDetails.error_description || errorDetails.error || "Unknown error";
|
|
298
|
+
|
|
291
299
|
// Handle specific consent_required error
|
|
292
300
|
if (errorDetails.error === "consent_required") {
|
|
293
301
|
console.log("Consent required - redirecting to proper OAuth2 flow");
|
|
@@ -295,15 +303,24 @@ class AuthServerOAuth2Client {
|
|
|
295
303
|
// The SPA should not directly call /oauth2/session without consent
|
|
296
304
|
// Instead, redirect to start the proper OAuth2 authorization flow
|
|
297
305
|
// which will handle consent properly through OAuth2AuthenticationSuccessHandler
|
|
298
|
-
const authUrl = this.buildAuthorizationUrl();
|
|
299
306
|
console.log("Redirecting to OAuth2 authorization flow:", authUrl);
|
|
300
|
-
|
|
307
|
+
this.clearLocalStorage();
|
|
308
|
+
window.location.href = `${authUrl}/access-denied`;
|
|
309
|
+
|
|
301
310
|
return; // Don't throw error, we're redirecting
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
if (errorDetails.error === "access_denied" && tokenResponse.status === 400) {
|
|
314
|
+
console.log(`${errorMessage}`);
|
|
315
|
+
this.clearLocalStorage();
|
|
316
|
+
|
|
317
|
+
window.location.href = this.derived.accountInactive;
|
|
318
|
+
|
|
319
|
+
throw new Error(`${errorMessage} redirecting to ${this.derived.accountInactive}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
304
322
|
// For other errors, throw as before
|
|
305
|
-
|
|
306
|
-
errorDetails.error_description || errorDetails.error || "Unknown error";
|
|
323
|
+
this.clearLocalStorage();
|
|
307
324
|
throw new Error(
|
|
308
325
|
`Token exchange and session creation failed: ${errorMessage}`
|
|
309
326
|
);
|
|
@@ -964,9 +981,3 @@ if (typeof module !== "undefined" && module.exports) {
|
|
|
964
981
|
exports.default = AuthServerOAuth2Client;
|
|
965
982
|
exports.AuthServerOAuth2Client = AuthServerOAuth2Client;
|
|
966
983
|
}
|
|
967
|
-
|
|
968
|
-
// Create global OAuth client instance for browser use
|
|
969
|
-
if (typeof window !== "undefined") {
|
|
970
|
-
window.AuthServerOAuth2Client = AuthServerOAuth2Client;
|
|
971
|
-
window.authServerOAuth2Client = new AuthServerOAuth2Client();
|
|
972
|
-
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import AuthServerOAuth2Client, {
|
|
2
|
+
AuthServerOAuth2ClientConfig,
|
|
3
|
+
} from "../AuthServerOAuth2Client";
|
|
4
|
+
import { installTestEnv, resetTestEnv } from "./test-utils";
|
|
5
|
+
|
|
6
|
+
const createConfig = (
|
|
7
|
+
overrides: Partial<AuthServerOAuth2ClientConfig> = {}
|
|
8
|
+
): AuthServerOAuth2ClientConfig => ({
|
|
9
|
+
clientId: "client-1",
|
|
10
|
+
authServerUrl: "http://auth.telicent.localhost",
|
|
11
|
+
redirectUri: "http://app.telicent.localhost/callback",
|
|
12
|
+
popupRedirectUri: "http://app.telicent.localhost/popup",
|
|
13
|
+
scope: "openid profile",
|
|
14
|
+
onLogout: jest.fn(),
|
|
15
|
+
...overrides,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const createFetchResponse = (options: {
|
|
19
|
+
ok?: boolean;
|
|
20
|
+
status?: number;
|
|
21
|
+
jsonData?: unknown;
|
|
22
|
+
textData?: string;
|
|
23
|
+
jsonThrows?: boolean;
|
|
24
|
+
}): Response =>
|
|
25
|
+
({
|
|
26
|
+
ok: options.ok ?? false,
|
|
27
|
+
status: options.status ?? 400,
|
|
28
|
+
json: options.jsonThrows
|
|
29
|
+
? jest.fn().mockRejectedValue(new Error("json error"))
|
|
30
|
+
: jest.fn().mockResolvedValue(options.jsonData ?? {}),
|
|
31
|
+
text: jest.fn().mockResolvedValue(options.textData ?? ""),
|
|
32
|
+
} as unknown as Response);
|
|
33
|
+
|
|
34
|
+
describe("failure path - handleCallback failure modes", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
installTestEnv();
|
|
37
|
+
Object.defineProperty(window, "location", {
|
|
38
|
+
value: {
|
|
39
|
+
href: "http://app.telicent.localhost/callback",
|
|
40
|
+
origin: "http://app.telicent.localhost",
|
|
41
|
+
search: "",
|
|
42
|
+
},
|
|
43
|
+
writable: true,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
resetTestEnv();
|
|
49
|
+
jest.useRealTimers();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("throws on missing code or state", async () => {
|
|
53
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
54
|
+
|
|
55
|
+
const cases = [
|
|
56
|
+
{ params: {}, error: "Missing code or state parameter" },
|
|
57
|
+
{ params: { code: "CODE_1" }, error: "Missing code or state parameter" },
|
|
58
|
+
{
|
|
59
|
+
params: { state: "STATE_1" },
|
|
60
|
+
error: "Missing code or state parameter",
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const results = [];
|
|
65
|
+
for (const item of cases) {
|
|
66
|
+
try {
|
|
67
|
+
await client.handleCallback(item.params as Record<string, string>);
|
|
68
|
+
results.push({ error: null });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
results.push({ error: (error as Error).message });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(results).toMatchInlineSnapshot(`
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
"error": "Missing code or state parameter",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"error": "Missing code or state parameter",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"error": "Missing code or state parameter",
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("throws on state mismatch or missing verifier/redirect uri", async () => {
|
|
90
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
91
|
+
sessionStorage.setItem("oauth_state", "STATE_1");
|
|
92
|
+
sessionStorage.setItem("oauth_code_verifier", "VERIFIER_1");
|
|
93
|
+
sessionStorage.setItem("oauth_redirect_uri", "http://app/callback");
|
|
94
|
+
|
|
95
|
+
const cases = [
|
|
96
|
+
{
|
|
97
|
+
params: { code: "CODE_1", state: "STATE_2" },
|
|
98
|
+
error: "Invalid state parameter",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
params: { code: "CODE_1", state: "STATE_1" },
|
|
102
|
+
clear: "oauth_code_verifier",
|
|
103
|
+
error: "Missing code verifier",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
params: { code: "CODE_1", state: "STATE_1" },
|
|
107
|
+
clear: "oauth_redirect_uri",
|
|
108
|
+
error: "Missing redirect URI",
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const results = [];
|
|
113
|
+
for (const item of cases) {
|
|
114
|
+
sessionStorage.setItem("oauth_state", "STATE_1");
|
|
115
|
+
sessionStorage.setItem("oauth_code_verifier", "VERIFIER_1");
|
|
116
|
+
sessionStorage.setItem("oauth_redirect_uri", "http://app/callback");
|
|
117
|
+
if (item.clear) sessionStorage.removeItem(item.clear);
|
|
118
|
+
try {
|
|
119
|
+
await client.handleCallback(item.params as Record<string, string>);
|
|
120
|
+
results.push({ error: null });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
results.push({ error: (error as Error).message });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
expect(results).toMatchInlineSnapshot(`
|
|
127
|
+
[
|
|
128
|
+
{
|
|
129
|
+
"error": "Invalid state parameter",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"error": "Missing code verifier",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"error": "Missing redirect URI",
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("throws on error param from callback params", async () => {
|
|
142
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
143
|
+
|
|
144
|
+
let error: Error | null = null;
|
|
145
|
+
try {
|
|
146
|
+
await client.handleCallback({ error: "access_denied" });
|
|
147
|
+
} catch (caught) {
|
|
148
|
+
error = caught as Error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
expect({ error: error?.message }).toMatchInlineSnapshot(`
|
|
152
|
+
{
|
|
153
|
+
"error": "OAuth error: access_denied",
|
|
154
|
+
}
|
|
155
|
+
`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("reads callback params from window.location.search", async () => {
|
|
159
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
160
|
+
Object.defineProperty(window, "location", {
|
|
161
|
+
value: {
|
|
162
|
+
href: "http://app.telicent.localhost/callback?error=access_denied",
|
|
163
|
+
origin: "http://app.telicent.localhost",
|
|
164
|
+
search: "?error=access_denied",
|
|
165
|
+
},
|
|
166
|
+
writable: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let error: Error | null = null;
|
|
170
|
+
try {
|
|
171
|
+
await client.handleCallback();
|
|
172
|
+
} catch (caught) {
|
|
173
|
+
error = caught as Error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expect({ error: error?.message }).toMatchInlineSnapshot(`
|
|
177
|
+
{
|
|
178
|
+
"error": "OAuth error: access_denied",
|
|
179
|
+
}
|
|
180
|
+
`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("redirects on consent_required without throwing", async () => {
|
|
184
|
+
jest.useFakeTimers();
|
|
185
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
186
|
+
sessionStorage.setItem("oauth_state", "STATE_1");
|
|
187
|
+
sessionStorage.setItem("oauth_code_verifier", "VERIFIER_1");
|
|
188
|
+
sessionStorage.setItem("oauth_redirect_uri", "http://app/callback");
|
|
189
|
+
|
|
190
|
+
const fetchMock = jest.fn().mockResolvedValue(
|
|
191
|
+
createFetchResponse({
|
|
192
|
+
ok: false,
|
|
193
|
+
status: 400,
|
|
194
|
+
jsonData: { error: "consent_required" },
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
globalThis.fetch = fetchMock;
|
|
198
|
+
|
|
199
|
+
await client.handleCallback({ code: "CODE_1", state: "STATE_1" });
|
|
200
|
+
|
|
201
|
+
expect({
|
|
202
|
+
href: window.location.href,
|
|
203
|
+
oauthState: sessionStorage.getItem("oauth_state"),
|
|
204
|
+
oauthVerifier: sessionStorage.getItem("oauth_code_verifier"),
|
|
205
|
+
}).toMatchInlineSnapshot(`
|
|
206
|
+
{
|
|
207
|
+
"href": "http://auth.telicent.localhost/oauth2/authorize/access-denied",
|
|
208
|
+
"oauthState": null,
|
|
209
|
+
"oauthVerifier": null,
|
|
210
|
+
}
|
|
211
|
+
`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("redirects on access_denied", async () => {
|
|
215
|
+
jest.useFakeTimers();
|
|
216
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
217
|
+
sessionStorage.setItem("oauth_state", "STATE_1");
|
|
218
|
+
sessionStorage.setItem("oauth_code_verifier", "VERIFIER_1");
|
|
219
|
+
sessionStorage.setItem("oauth_redirect_uri", "http://app/callback");
|
|
220
|
+
|
|
221
|
+
const fetchMock = jest.fn().mockResolvedValue(
|
|
222
|
+
createFetchResponse({
|
|
223
|
+
ok: false,
|
|
224
|
+
status: 400,
|
|
225
|
+
jsonData: { error: "access_denied" },
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
globalThis.fetch = fetchMock;
|
|
229
|
+
|
|
230
|
+
await expect(
|
|
231
|
+
client.handleCallback({ code: "CODE_1", state: "STATE_1" })
|
|
232
|
+
).rejects.toThrow(
|
|
233
|
+
"access_denied redirecting to http://auth.telicent.localhost/account-inactive"
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect({
|
|
237
|
+
href: window.location.href,
|
|
238
|
+
oauthState: sessionStorage.getItem("oauth_state"),
|
|
239
|
+
oauthVerifier: sessionStorage.getItem("oauth_code_verifier"),
|
|
240
|
+
}).toMatchInlineSnapshot(`
|
|
241
|
+
{
|
|
242
|
+
"href": "http://auth.telicent.localhost/account-inactive",
|
|
243
|
+
"oauthState": null,
|
|
244
|
+
"oauthVerifier": null,
|
|
245
|
+
}
|
|
246
|
+
`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("throws when token exchange fails with text response", async () => {
|
|
250
|
+
jest.useFakeTimers();
|
|
251
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
252
|
+
sessionStorage.setItem("oauth_state", "STATE_1");
|
|
253
|
+
sessionStorage.setItem("oauth_code_verifier", "VERIFIER_1");
|
|
254
|
+
sessionStorage.setItem("oauth_redirect_uri", "http://app/callback");
|
|
255
|
+
|
|
256
|
+
const fetchMock = jest.fn().mockResolvedValue(
|
|
257
|
+
createFetchResponse({
|
|
258
|
+
ok: false,
|
|
259
|
+
status: 500,
|
|
260
|
+
jsonThrows: true,
|
|
261
|
+
textData: "server down",
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
globalThis.fetch = fetchMock;
|
|
265
|
+
|
|
266
|
+
let error: Error | null = null;
|
|
267
|
+
try {
|
|
268
|
+
await client.handleCallback({ code: "CODE_1", state: "STATE_1" });
|
|
269
|
+
} catch (caught) {
|
|
270
|
+
error = caught as Error;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expect({
|
|
274
|
+
error: error?.message,
|
|
275
|
+
oauthState: sessionStorage.getItem("oauth_state"),
|
|
276
|
+
oauthVerifier: sessionStorage.getItem("oauth_code_verifier"),
|
|
277
|
+
}).toMatchInlineSnapshot(`
|
|
278
|
+
{
|
|
279
|
+
"error": "Token exchange and session creation failed: server down",
|
|
280
|
+
"oauthState": null,
|
|
281
|
+
"oauthVerifier": null,
|
|
282
|
+
}
|
|
283
|
+
`);
|
|
284
|
+
});
|
|
285
|
+
});
|