@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.
Files changed (36) hide show
  1. package/package.json +3 -3
  2. package/src/AuthServerOAuth2Client.d.ts +12 -15
  3. package/src/AuthServerOAuth2Client.js +21 -10
  4. package/src/__tests__/callback.failures.test.ts +285 -0
  5. package/src/__tests__/callback.success.test.ts +410 -0
  6. package/src/__tests__/core.failures.test.ts +122 -0
  7. package/src/__tests__/core.success.test.ts +196 -0
  8. package/src/__tests__/env.success.test.ts +17 -0
  9. package/src/__tests__/logout.success.test.ts +151 -0
  10. package/src/__tests__/methods/base64URLEncode.success.test.ts +39 -0
  11. package/src/__tests__/methods/generateCodeChallenge.success.test.ts +43 -0
  12. package/src/__tests__/methods/generateCodeVerifier.success.test.ts +43 -0
  13. package/src/__tests__/methods/generateNonce.success.test.ts +43 -0
  14. package/src/__tests__/methods/generateState.success.test.ts +43 -0
  15. package/src/__tests__/methods/getCsrfToken.failures.test.ts +54 -0
  16. package/src/__tests__/methods/getCsrfToken.success.test.ts +45 -0
  17. package/src/__tests__/methods/getRawIdToken.success.test.ts +39 -0
  18. package/src/__tests__/methods/getUserInfo.failures.test.ts +153 -0
  19. package/src/__tests__/methods/getUserInfo.success.test.ts +84 -0
  20. package/src/__tests__/methods/isAuthenticated.failures.test.ts +62 -0
  21. package/src/__tests__/methods/isAuthenticated.success.test.ts +84 -0
  22. package/src/__tests__/methods/isIdTokenExpired.failures.test.ts +77 -0
  23. package/src/__tests__/methods/isIdTokenExpired.success.test.ts +49 -0
  24. package/src/__tests__/methods/validateIdToken.failures.test.ts +177 -0
  25. package/src/__tests__/methods/validateIdToken.success.test.ts +55 -0
  26. package/src/__tests__/methods/validateIdTokenForRecovery.failures.test.ts +121 -0
  27. package/src/__tests__/methods/validateIdTokenForRecovery.success.test.ts +49 -0
  28. package/src/__tests__/popup.success.test.ts +277 -0
  29. package/src/__tests__/request-helpers.failures.test.ts +143 -0
  30. package/src/__tests__/request-helpers.success.test.ts +137 -0
  31. package/src/__tests__/schema-loading.failures.test.ts +44 -0
  32. package/src/__tests__/schema-loading.success.test.ts +106 -0
  33. package/src/__tests__/state.success.test.ts +217 -0
  34. package/src/__tests__/test-utils.node.success.test.ts +16 -0
  35. package/src/__tests__/test-utils.success.test.ts +188 -0
  36. package/src/__tests__/test-utils.ts +203 -0
@@ -0,0 +1,143 @@
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
+ status?: number;
20
+ ok?: boolean;
21
+ }): Response =>
22
+ ({
23
+ status: options.status ?? 401,
24
+ ok: options.ok ?? false,
25
+ } as unknown as Response);
26
+
27
+ describe("failure path - request helpers on 401", () => {
28
+ beforeEach(() => {
29
+ installTestEnv();
30
+ Object.defineProperty(window, "location", {
31
+ value: {
32
+ href: "http://app.telicent.localhost/home",
33
+ origin: "http://app.telicent.localhost",
34
+ search: "",
35
+ },
36
+ writable: true,
37
+ });
38
+ });
39
+
40
+ afterEach(() => {
41
+ resetTestEnv();
42
+ });
43
+
44
+ it("logs out and throws on 401 for makeAuthenticatedRequest", async () => {
45
+ const client = new AuthServerOAuth2Client(createConfig());
46
+ const fetchMock = jest.fn().mockResolvedValue(createFetchResponse({}));
47
+ globalThis.fetch = fetchMock;
48
+ jest.spyOn(client, "login").mockResolvedValue();
49
+ const clearSpy = jest.spyOn(client, "clearLocalStorage");
50
+
51
+ let error: Error | null = null;
52
+ try {
53
+ await client.makeAuthenticatedRequest("http://app.telicent.localhost/data");
54
+ } catch (caught) {
55
+ error = caught as Error;
56
+ }
57
+
58
+ expect({
59
+ error: error?.message,
60
+ clearCalls: clearSpy.mock.calls.length,
61
+ loginCalls: (client.login as jest.Mock).mock.calls.length,
62
+ }).toMatchInlineSnapshot(`
63
+ {
64
+ "clearCalls": 1,
65
+ "error": "Session expired",
66
+ "loginCalls": 1,
67
+ }
68
+ `);
69
+ });
70
+
71
+ it("warns and does not logout when skipAutoLogout is true", async () => {
72
+ const client = new AuthServerOAuth2Client(createConfig());
73
+ const fetchMock = jest.fn().mockResolvedValue(createFetchResponse({}));
74
+ globalThis.fetch = fetchMock;
75
+
76
+ const response = await client.makeAuthenticatedRequest(
77
+ "http://app.telicent.localhost/data",
78
+ { skipAutoLogout: true }
79
+ );
80
+
81
+ expect({
82
+ status: response.status,
83
+ warnings: (console.warn as jest.Mock).mock.calls.map((call) => call[0]),
84
+ }).toMatchInlineSnapshot(`
85
+ {
86
+ "status": 401,
87
+ "warnings": [
88
+ "401 response during protected operation, not auto-logging out to prevent loops",
89
+ ],
90
+ }
91
+ `);
92
+ });
93
+
94
+ it("throws on 401 for afterRequest and triggers login", () => {
95
+ const client = new AuthServerOAuth2Client(createConfig());
96
+ jest.spyOn(client, "login").mockResolvedValue();
97
+ const clearSpy = jest.spyOn(client, "clearLocalStorage");
98
+ const response = createFetchResponse({});
99
+
100
+ let error: Error | null = null;
101
+ try {
102
+ client.afterRequest(
103
+ response,
104
+ "http://app.telicent.localhost/data"
105
+ );
106
+ } catch (caught) {
107
+ error = caught as Error;
108
+ }
109
+
110
+ expect({
111
+ error: error?.message,
112
+ clearCalls: clearSpy.mock.calls.length,
113
+ loginCalls: (client.login as jest.Mock).mock.calls.length,
114
+ }).toMatchInlineSnapshot(`
115
+ {
116
+ "clearCalls": 1,
117
+ "error": "Session expired",
118
+ "loginCalls": 1,
119
+ }
120
+ `);
121
+ });
122
+
123
+ it("warns and does not logout when afterRequest skipAutoLogout is true", () => {
124
+ const client = new AuthServerOAuth2Client(createConfig());
125
+ const response = createFetchResponse({});
126
+
127
+ const result = client.afterRequest(response, "http://app/data", {
128
+ skipAutoLogout: true,
129
+ });
130
+
131
+ expect({
132
+ status: result.status,
133
+ warnings: (console.warn as jest.Mock).mock.calls.map((call) => call[0]),
134
+ }).toMatchInlineSnapshot(`
135
+ {
136
+ "status": 401,
137
+ "warnings": [
138
+ "401 response during protected operation, not auto-logging out to prevent loops",
139
+ ],
140
+ }
141
+ `);
142
+ });
143
+ });
@@ -0,0 +1,137 @@
1
+ import AuthServerOAuth2Client, {
2
+ AuthServerOAuth2ClientConfig,
3
+ } from "../AuthServerOAuth2Client";
4
+ import { installTestEnv, resetTestEnv, setCookies } 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
+ status?: number;
20
+ ok?: boolean;
21
+ }): Response =>
22
+ ({
23
+ status: options.status ?? 200,
24
+ ok: options.ok ?? true,
25
+ } as unknown as Response);
26
+
27
+ describe("happy path - request helpers add auth headers", () => {
28
+ beforeEach(() => {
29
+ installTestEnv();
30
+ Object.defineProperty(window, "location", {
31
+ value: {
32
+ href: "http://app.telicent.localhost/home",
33
+ origin: "http://app.telicent.localhost",
34
+ search: "",
35
+ },
36
+ writable: true,
37
+ });
38
+ });
39
+
40
+ afterEach(() => {
41
+ resetTestEnv();
42
+ });
43
+
44
+ it("adds Authorization header for cross-domain requests", async () => {
45
+ const client = new AuthServerOAuth2Client(
46
+ createConfig({ authServerUrl: "https://auth.telicent.io" })
47
+ );
48
+ sessionStorage.setItem("auth_session_id", "SESSION_123");
49
+ const fetchMock = jest.fn().mockResolvedValue(createFetchResponse({}));
50
+ globalThis.fetch = fetchMock;
51
+
52
+ await client.makeAuthenticatedRequest("https://api.telicent.io/data");
53
+
54
+ expect({
55
+ headers: fetchMock.mock.calls[0][1]?.headers,
56
+ }).toMatchInlineSnapshot(`
57
+ {
58
+ "headers": {
59
+ "Accept": "application/json",
60
+ "Authorization": "Bearer SESSION_123",
61
+ },
62
+ }
63
+ `);
64
+ });
65
+
66
+ it("adds CSRF token for same-domain state-changing requests", async () => {
67
+ const client = new AuthServerOAuth2Client(createConfig());
68
+ setCookies("XSRF-TOKEN=csrf-123");
69
+ const fetchMock = jest.fn().mockResolvedValue(createFetchResponse({}));
70
+ globalThis.fetch = fetchMock;
71
+
72
+ await client.makeAuthenticatedRequest(
73
+ "http://auth.telicent.localhost/data",
74
+ { method: "POST" }
75
+ );
76
+
77
+ expect({
78
+ headers: fetchMock.mock.calls[0][1]?.headers,
79
+ }).toMatchInlineSnapshot(`
80
+ {
81
+ "headers": {
82
+ "Accept": "application/json",
83
+ "X-XSRF-TOKEN": "csrf-123",
84
+ },
85
+ }
86
+ `);
87
+ });
88
+
89
+ it("prepares request headers for cross-domain and same-domain", () => {
90
+ const crossDomainClient = new AuthServerOAuth2Client(
91
+ createConfig({ authServerUrl: "https://auth.telicent.io" })
92
+ );
93
+ const sameDomainClient = new AuthServerOAuth2Client(createConfig());
94
+ sessionStorage.setItem("auth_session_id", "SESSION_ABC");
95
+ setCookies("XSRF-TOKEN=csrf-456");
96
+
97
+ const crossHeaders = crossDomainClient.beforeRequest().headers;
98
+ const sameHeaders = sameDomainClient.beforeRequest({
99
+ method: "DELETE",
100
+ }).headers;
101
+
102
+ expect({
103
+ crossHeaders,
104
+ sameHeaders,
105
+ }).toMatchInlineSnapshot(`
106
+ {
107
+ "crossHeaders": {
108
+ "Accept": "application/json",
109
+ "Authorization": "Bearer SESSION_ABC",
110
+ },
111
+ "sameHeaders": {
112
+ "Accept": "application/json",
113
+ "X-XSRF-TOKEN": "csrf-456",
114
+ },
115
+ }
116
+ `);
117
+ });
118
+
119
+ it("returns response from afterRequest on allowed 401", () => {
120
+ const client = new AuthServerOAuth2Client(createConfig());
121
+ const response = createFetchResponse({ status: 401, ok: false });
122
+
123
+ const result = client.afterRequest(
124
+ response,
125
+ "http://auth.telicent.localhost/session/check"
126
+ );
127
+
128
+ expect({ result }).toMatchInlineSnapshot(`
129
+ {
130
+ "result": {
131
+ "ok": false,
132
+ "status": 401,
133
+ },
134
+ }
135
+ `);
136
+ });
137
+ });
@@ -0,0 +1,44 @@
1
+ import { installTestEnv, resetTestEnv } from "./test-utils";
2
+
3
+ describe("failure path - schema loading fails", () => {
4
+ beforeEach(() => {
5
+ installTestEnv();
6
+ jest.resetModules();
7
+ });
8
+
9
+ afterEach(() => {
10
+ resetTestEnv();
11
+ jest.resetModules();
12
+ jest.dontMock("../schemas.js");
13
+ });
14
+
15
+ it("warns when schemas cannot be required", () => {
16
+ jest.doMock("../schemas.js", () => {
17
+ throw new Error("missing schemas");
18
+ });
19
+
20
+ let AuthServerOAuth2Client:
21
+ | typeof import("../AuthServerOAuth2Client").default
22
+ | undefined;
23
+ jest.isolateModules(() => {
24
+ AuthServerOAuth2Client = require("../AuthServerOAuth2Client").default;
25
+ });
26
+ if (!AuthServerOAuth2Client) {
27
+ throw new Error("AuthServerOAuth2Client not loaded");
28
+ }
29
+
30
+ const warnCalls = (console.warn as jest.Mock).mock.calls;
31
+
32
+ expect({
33
+ hasClient: typeof AuthServerOAuth2Client === "function",
34
+ warnings: warnCalls.map((call) => call[0]),
35
+ }).toMatchInlineSnapshot(`
36
+ {
37
+ "hasClient": true,
38
+ "warnings": [
39
+ "Zod schema not available, validation will be skipped:",
40
+ ],
41
+ }
42
+ `);
43
+ });
44
+ });
@@ -0,0 +1,106 @@
1
+ import { installTestEnv, resetTestEnv } from "./test-utils";
2
+
3
+ describe("happy path - schema loading succeeds", () => {
4
+ beforeEach(() => {
5
+ installTestEnv();
6
+ });
7
+
8
+ afterEach(() => {
9
+ resetTestEnv();
10
+ jest.resetModules();
11
+ });
12
+
13
+ it("parses config when schema is available", () => {
14
+ let AuthServerOAuth2Client:
15
+ | typeof import("../AuthServerOAuth2Client").default
16
+ | undefined;
17
+ jest.isolateModules(() => {
18
+ AuthServerOAuth2Client = require("../AuthServerOAuth2Client").default;
19
+ });
20
+ if (!AuthServerOAuth2Client) {
21
+ throw new Error("AuthServerOAuth2Client not loaded");
22
+ }
23
+
24
+ const client = new AuthServerOAuth2Client({
25
+ clientId: "client-1",
26
+ authServerUrl: "http://auth.telicent.localhost",
27
+ redirectUri: "http://app.telicent.localhost/callback",
28
+ popupRedirectUri: "http://app.telicent.localhost/popup",
29
+ scope: "openid profile",
30
+ onLogout: jest.fn(),
31
+ });
32
+
33
+ expect({
34
+ clientId: client.config.clientId,
35
+ }).toMatchInlineSnapshot(`
36
+ {
37
+ "clientId": "client-1",
38
+ }
39
+ `);
40
+ });
41
+
42
+ it("validates user info via schema when available", () => {
43
+ let AuthServerOAuth2Client:
44
+ | typeof import("../AuthServerOAuth2Client").default
45
+ | undefined;
46
+ jest.isolateModules(() => {
47
+ AuthServerOAuth2Client = require("../AuthServerOAuth2Client").default;
48
+ });
49
+ if (!AuthServerOAuth2Client) {
50
+ throw new Error("AuthServerOAuth2Client not loaded");
51
+ }
52
+
53
+ const client = new AuthServerOAuth2Client({
54
+ clientId: "client-1",
55
+ authServerUrl: "http://auth.telicent.localhost",
56
+ redirectUri: "http://app.telicent.localhost/callback",
57
+ popupRedirectUri: "http://app.telicent.localhost/popup",
58
+ scope: "openid profile",
59
+ onLogout: jest.fn(),
60
+ });
61
+
62
+ const now = 1_700_000_000_000;
63
+ jest.spyOn(Date, "now").mockReturnValue(now);
64
+ const token =
65
+ "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTEiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJwcmVmZXJyZWRfbmFtZSI6IlVzZXIgT25lIiwiaXNzIjoiaHR0cDovL2F1dGgudGVsaWNlbnQubG9jYWxob3N0IiwiYXVkIjoiY2xpZW50LTEiLCJleHAiOjE3MDAwMDAzMDAsImlhdCI6MTcwMDAwMDAwMCwianRpIjoiaWQtMSJ9.";
66
+
67
+ sessionStorage.setItem("auth_id_token", token);
68
+ const info = client.getUserInfo();
69
+
70
+ expect({ info }).toMatchInlineSnapshot(`
71
+ {
72
+ "info": {
73
+ "aud": "client-1",
74
+ "auth_time": undefined,
75
+ "azp": undefined,
76
+ "email": "user@example.com",
77
+ "exp": 1700000300,
78
+ "externalProvider": {
79
+ "aud": "client-1",
80
+ "email": "user@example.com",
81
+ "exp": 1700000300,
82
+ "iat": 1700000000,
83
+ "iss": "http://auth.telicent.localhost",
84
+ "jti": "id-1",
85
+ "preferred_name": "User One",
86
+ "sub": "user-1",
87
+ },
88
+ "iat": 1700000000,
89
+ "isActive": undefined,
90
+ "iss": "http://auth.telicent.localhost",
91
+ "jti": "id-1",
92
+ "name": "User One",
93
+ "nonce": undefined,
94
+ "preferred_name": "User One",
95
+ "sid": undefined,
96
+ "source": "id_token",
97
+ "sub": "user-1",
98
+ "token_expired": false,
99
+ "token_expires_at": "2023-11-14T22:18:20.000Z",
100
+ },
101
+ }
102
+ `);
103
+
104
+ (Date.now as jest.Mock).mockRestore();
105
+ });
106
+ });
@@ -0,0 +1,217 @@
1
+ import AuthServerOAuth2Client, {
2
+ AuthServerOAuth2ClientConfig,
3
+ } from "../AuthServerOAuth2Client";
4
+ import { buildJwt, 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
+ describe("happy path - jwt decode, validation, and user info", () => {
19
+ beforeEach(() => {
20
+ installTestEnv();
21
+ });
22
+
23
+ afterEach(() => {
24
+ resetTestEnv();
25
+ });
26
+
27
+ it("decodes jwt payload", () => {
28
+ const client = new AuthServerOAuth2Client(createConfig());
29
+ const token = buildJwt({ sub: "user-1", aud: "client-1" });
30
+ const payload = client.decodeJWT(token);
31
+
32
+ expect({ payload }).toMatchInlineSnapshot(`
33
+ {
34
+ "payload": {
35
+ "aud": "client-1",
36
+ "sub": "user-1",
37
+ },
38
+ }
39
+ `);
40
+ });
41
+
42
+ it("validates id token and removes nonce", () => {
43
+ const now = 1_700_000_000_000;
44
+ jest.spyOn(Date, "now").mockReturnValue(now);
45
+
46
+ const client = new AuthServerOAuth2Client(createConfig());
47
+ sessionStorage.setItem("oauth_nonce", "ABC_nonce_ABC");
48
+ const token = buildJwt({
49
+ sub: "user-1",
50
+ aud: "client-1",
51
+ exp: Math.floor(now / 1000) + 300,
52
+ iat: Math.floor(now / 1000),
53
+ nonce: "ABC_nonce_ABC",
54
+ });
55
+
56
+ const result = client.validateIdToken(token);
57
+
58
+ expect({
59
+ result,
60
+ nonceAfter: sessionStorage.getItem("oauth_nonce"),
61
+ }).toMatchInlineSnapshot(`
62
+ {
63
+ "nonceAfter": null,
64
+ "result": true,
65
+ }
66
+ `);
67
+
68
+ (Date.now as jest.Mock).mockRestore();
69
+ });
70
+
71
+ it("validates id token for recovery", () => {
72
+ const now = 1_700_000_000_000;
73
+ jest.spyOn(Date, "now").mockReturnValue(now);
74
+
75
+ const client = new AuthServerOAuth2Client(createConfig());
76
+ const token = buildJwt({
77
+ sub: "user-1",
78
+ aud: "client-1",
79
+ exp: Math.floor(now / 1000) + 300,
80
+ iat: Math.floor(now / 1000),
81
+ });
82
+
83
+ const result = client.validateIdTokenForRecovery(token);
84
+
85
+ expect({ result }).toMatchInlineSnapshot(`
86
+ {
87
+ "result": true,
88
+ }
89
+ `);
90
+
91
+ (Date.now as jest.Mock).mockRestore();
92
+ });
93
+
94
+ it("returns user info from id token", () => {
95
+ const now = 1_700_000_000_000;
96
+ jest.spyOn(Date, "now").mockReturnValue(now);
97
+
98
+ const client = new AuthServerOAuth2Client(createConfig());
99
+ const token = buildJwt({
100
+ sub: "user-1",
101
+ aud: "client-1",
102
+ exp: Math.floor(now / 1000) + 300,
103
+ iat: Math.floor(now / 1000),
104
+ email: "user@example.com",
105
+ preferred_name: "User One",
106
+ iss: "http://auth.telicent.localhost",
107
+ jti: "id-1",
108
+ sid: "session-1",
109
+ azp: "azp-1",
110
+ auth_time: Math.floor(now / 1000) - 100,
111
+ nonce: "ABC_nonce_ABC",
112
+ isActive: true,
113
+ name: "User One",
114
+ });
115
+ sessionStorage.setItem("auth_id_token", token);
116
+
117
+ const info = client.getUserInfo();
118
+
119
+ expect({ info }).toMatchInlineSnapshot(`
120
+ {
121
+ "info": {
122
+ "aud": "client-1",
123
+ "auth_time": 1699999900,
124
+ "azp": "azp-1",
125
+ "email": "user@example.com",
126
+ "exp": 1700000300,
127
+ "externalProvider": {
128
+ "aud": "client-1",
129
+ "auth_time": 1699999900,
130
+ "azp": "azp-1",
131
+ "email": "user@example.com",
132
+ "exp": 1700000300,
133
+ "iat": 1700000000,
134
+ "isActive": true,
135
+ "iss": "http://auth.telicent.localhost",
136
+ "jti": "id-1",
137
+ "name": "User One",
138
+ "nonce": "ABC_nonce_ABC",
139
+ "preferred_name": "User One",
140
+ "sid": "session-1",
141
+ "sub": "user-1",
142
+ },
143
+ "iat": 1700000000,
144
+ "isActive": true,
145
+ "iss": "http://auth.telicent.localhost",
146
+ "jti": "id-1",
147
+ "name": "User One",
148
+ "nonce": "ABC_nonce_ABC",
149
+ "preferred_name": "User One",
150
+ "sid": "session-1",
151
+ "source": "id_token",
152
+ "sub": "user-1",
153
+ "token_expired": false,
154
+ "token_expires_at": "2023-11-14T22:18:20.000Z",
155
+ },
156
+ }
157
+ `);
158
+
159
+ (Date.now as jest.Mock).mockRestore();
160
+ });
161
+
162
+ it("returns user info from API as token claims", async () => {
163
+ const now = 1_700_000_000_000;
164
+ jest.spyOn(Date, "now").mockReturnValue(now);
165
+
166
+ const client = new AuthServerOAuth2Client(createConfig());
167
+ const token = buildJwt({
168
+ sub: "user-1",
169
+ aud: "client-1",
170
+ exp: Math.floor(now / 1000) + 300,
171
+ iat: Math.floor(now / 1000),
172
+ email: "user@example.com",
173
+ preferred_name: "User One",
174
+ iss: "http://auth.telicent.localhost",
175
+ jti: "id-1",
176
+ });
177
+ sessionStorage.setItem("auth_id_token", token);
178
+
179
+ const info = await client.getUserInfoFromAPI();
180
+
181
+ expect({ info }).toMatchInlineSnapshot(`
182
+ {
183
+ "info": {
184
+ "aud": "client-1",
185
+ "auth_time": undefined,
186
+ "azp": undefined,
187
+ "email": "user@example.com",
188
+ "exp": 1700000300,
189
+ "externalProvider": {
190
+ "aud": "client-1",
191
+ "email": "user@example.com",
192
+ "exp": 1700000300,
193
+ "iat": 1700000000,
194
+ "iss": "http://auth.telicent.localhost",
195
+ "jti": "id-1",
196
+ "preferred_name": "User One",
197
+ "sub": "user-1",
198
+ },
199
+ "iat": 1700000000,
200
+ "isActive": undefined,
201
+ "iss": "http://auth.telicent.localhost",
202
+ "jti": "id-1",
203
+ "name": "User One",
204
+ "nonce": undefined,
205
+ "preferred_name": "User One",
206
+ "sid": undefined,
207
+ "source": "id_token",
208
+ "sub": "user-1",
209
+ "token_expired": false,
210
+ "token_expires_at": "2023-11-14T22:18:20.000Z",
211
+ },
212
+ }
213
+ `);
214
+
215
+ (Date.now as jest.Mock).mockRestore();
216
+ });
217
+ });
@@ -0,0 +1,16 @@
1
+ /** @jest-environment node */
2
+ import { resetCookies, resetSessionStorage, setCookies } from "./test-utils";
3
+
4
+ describe("happy path - test utils in node", () => {
5
+ it("handles missing document and sessionStorage safely", () => {
6
+ resetSessionStorage();
7
+ resetCookies();
8
+ setCookies("XSRF-TOKEN=abc123");
9
+
10
+ expect({ ok: true }).toMatchInlineSnapshot(`
11
+ {
12
+ "ok": true,
13
+ }
14
+ `);
15
+ });
16
+ });