@telicent-oss/fe-auth-lib 1.0.1 → 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 +22 -5
  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,188 @@
1
+ import {
2
+ buildJwt,
3
+ decodeJwtPayload,
4
+ installTestEnv,
5
+ installBase64,
6
+ installConsoleMocks,
7
+ resetCookies,
8
+ resetSessionStorage,
9
+ restoreConsoleMocks,
10
+ resetTestEnv,
11
+ setCookies,
12
+ } from "./test-utils";
13
+
14
+ describe("happy path - test utilities", () => {
15
+ beforeEach(() => {
16
+ installTestEnv();
17
+ });
18
+
19
+ afterEach(() => {
20
+ resetTestEnv();
21
+ });
22
+
23
+ it("builds and decodes jwt payloads", () => {
24
+ const payload = { sub: "user-1", exp: 123, aud: "client-1" };
25
+ const token = buildJwt(payload);
26
+ const decoded = decodeJwtPayload(token);
27
+
28
+ expect({ token, decoded }).toMatchInlineSnapshot(`
29
+ {
30
+ "decoded": {
31
+ "aud": "client-1",
32
+ "exp": 123,
33
+ "sub": "user-1",
34
+ },
35
+ "token": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTEiLCJleHAiOjEyMywiYXVkIjoiY2xpZW50LTEifQ.",
36
+ }
37
+ `);
38
+ });
39
+
40
+ it("returns null when jwt payload decoding fails", () => {
41
+ const payload = decodeJwtPayload("not-a-jwt");
42
+
43
+ expect({ payload }).toMatchInlineSnapshot(`
44
+ {
45
+ "payload": null,
46
+ }
47
+ `);
48
+ });
49
+
50
+ it("returns null when jwt payload is not valid json", () => {
51
+ const payload = decodeJwtPayload("header.bm90LWpzb24.signature");
52
+
53
+ expect({ payload }).toMatchInlineSnapshot(`
54
+ {
55
+ "payload": null,
56
+ }
57
+ `);
58
+ });
59
+
60
+ it("installs base64 helpers when missing", () => {
61
+ const originalBtoa = globalThis.btoa;
62
+ const originalAtob = globalThis.atob;
63
+ delete (globalThis as { btoa?: typeof btoa }).btoa;
64
+ delete (globalThis as { atob?: typeof atob }).atob;
65
+
66
+ installBase64();
67
+
68
+ const encoded = globalThis.btoa?.("hi");
69
+ const decoded = globalThis.atob?.(encoded ?? "");
70
+
71
+ expect({
72
+ encoded,
73
+ decoded,
74
+ hasBtoa: typeof globalThis.btoa !== "undefined",
75
+ hasAtob: typeof globalThis.atob !== "undefined",
76
+ }).toMatchInlineSnapshot(`
77
+ {
78
+ "decoded": "hi",
79
+ "encoded": "aGk=",
80
+ "hasAtob": true,
81
+ "hasBtoa": true,
82
+ }
83
+ `);
84
+
85
+ if (originalBtoa) globalThis.btoa = originalBtoa;
86
+ if (originalAtob) globalThis.atob = originalAtob;
87
+ });
88
+
89
+ it("manages console mock lifecycle", () => {
90
+ installConsoleMocks();
91
+ installConsoleMocks();
92
+ const beforeRestore = {
93
+ log: jest.isMockFunction(console.log),
94
+ warn: jest.isMockFunction(console.warn),
95
+ error: jest.isMockFunction(console.error),
96
+ info: jest.isMockFunction(console.info),
97
+ };
98
+ restoreConsoleMocks();
99
+ restoreConsoleMocks();
100
+ const afterRestore = {
101
+ log: jest.isMockFunction(console.log),
102
+ warn: jest.isMockFunction(console.warn),
103
+ error: jest.isMockFunction(console.error),
104
+ info: jest.isMockFunction(console.info),
105
+ };
106
+
107
+ expect({ beforeRestore, afterRestore }).toMatchInlineSnapshot(`
108
+ {
109
+ "afterRestore": {
110
+ "error": false,
111
+ "info": false,
112
+ "log": false,
113
+ "warn": false,
114
+ },
115
+ "beforeRestore": {
116
+ "error": true,
117
+ "info": true,
118
+ "log": true,
119
+ "warn": true,
120
+ },
121
+ }
122
+ `);
123
+ });
124
+
125
+ it("ignores empty cookie input", () => {
126
+ resetCookies();
127
+ setCookies("");
128
+
129
+ expect({ cookie: document.cookie }).toMatchInlineSnapshot(`
130
+ {
131
+ "cookie": "",
132
+ }
133
+ `);
134
+ });
135
+
136
+ it("installs runtime/browser mocks", async () => {
137
+ const randomBytes = new Uint8Array(4);
138
+ crypto.getRandomValues(randomBytes);
139
+ const digest = await crypto.subtle.digest("SHA-256", new Uint8Array([1]));
140
+
141
+ expect({
142
+ hasTextEncoder: typeof TextEncoder !== "undefined",
143
+ hasBtoa: typeof btoa !== "undefined",
144
+ hasAtob: typeof atob !== "undefined",
145
+ hasConsoleMocks: {
146
+ log: jest.isMockFunction(console.log),
147
+ warn: jest.isMockFunction(console.warn),
148
+ error: jest.isMockFunction(console.error),
149
+ info: jest.isMockFunction(console.info),
150
+ },
151
+ randomBytes: Array.from(randomBytes),
152
+ digestBytes: Array.from(new Uint8Array(digest)),
153
+ }).toMatchInlineSnapshot(`
154
+ {
155
+ "digestBytes": [
156
+ 1,
157
+ 2,
158
+ 3,
159
+ ],
160
+ "hasAtob": true,
161
+ "hasBtoa": true,
162
+ "hasConsoleMocks": {
163
+ "error": true,
164
+ "info": true,
165
+ "log": true,
166
+ "warn": true,
167
+ },
168
+ "hasTextEncoder": true,
169
+ "randomBytes": [
170
+ 1,
171
+ 1,
172
+ 1,
173
+ 1,
174
+ ],
175
+ }
176
+ `);
177
+ });
178
+
179
+ it("sets cookies", () => {
180
+ setCookies("XSRF-TOKEN=abc123");
181
+
182
+ expect({ cookie: document.cookie }).toMatchInlineSnapshot(`
183
+ {
184
+ "cookie": "XSRF-TOKEN=abc123",
185
+ }
186
+ `);
187
+ });
188
+ });
@@ -0,0 +1,203 @@
1
+ import { TextEncoder as NodeTextEncoder } from "util";
2
+
3
+ type JwtPayload = Record<string, unknown>;
4
+ type JwtHeader = Record<string, unknown>;
5
+
6
+ const defaultHeader: JwtHeader = { alg: "none", typ: "JWT" };
7
+
8
+ const base64UrlEncode = (input: string): string =>
9
+ Buffer.from(input, "utf8")
10
+ .toString("base64")
11
+ .replace(/\+/g, "-")
12
+ .replace(/\//g, "_")
13
+ .replace(/=/g, "");
14
+
15
+ const base64UrlDecode = (input: string): string => {
16
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
17
+ const padded = normalized.padEnd(
18
+ normalized.length + ((4 - (normalized.length % 4)) % 4),
19
+ "="
20
+ );
21
+ return Buffer.from(padded, "base64").toString("utf8");
22
+ };
23
+
24
+ export const buildJwt = (
25
+ payload: JwtPayload,
26
+ header: JwtHeader = defaultHeader
27
+ ): string => {
28
+ const headerPart = base64UrlEncode(JSON.stringify(header));
29
+ const payloadPart = base64UrlEncode(JSON.stringify(payload));
30
+ return `${headerPart}.${payloadPart}.`;
31
+ };
32
+
33
+ export const decodeJwtPayload = (token: string): JwtPayload | null => {
34
+ try {
35
+ const payloadPart = token.split(".")[1];
36
+ if (!payloadPart) return null;
37
+ return JSON.parse(base64UrlDecode(payloadPart)) as JwtPayload;
38
+ } catch {
39
+ return null;
40
+ }
41
+ };
42
+
43
+ export const installTextEncoder = (): void => {
44
+ if (typeof globalThis.TextEncoder === "undefined") {
45
+ globalThis.TextEncoder = NodeTextEncoder as typeof TextEncoder;
46
+ }
47
+ };
48
+
49
+ export const installBase64 = (): void => {
50
+ if (typeof globalThis.btoa === "undefined") {
51
+ globalThis.btoa = (input: string): string =>
52
+ Buffer.from(input, "binary").toString("base64");
53
+ }
54
+ if (typeof globalThis.atob === "undefined") {
55
+ globalThis.atob = (input: string): string =>
56
+ Buffer.from(input, "base64").toString("binary");
57
+ }
58
+ };
59
+
60
+ let consoleMocks:
61
+ | {
62
+ log: jest.SpyInstance;
63
+ warn: jest.SpyInstance;
64
+ error: jest.SpyInstance;
65
+ info: jest.SpyInstance;
66
+ }
67
+ | undefined;
68
+
69
+ export const installConsoleMocks = (): void => {
70
+ if (consoleMocks) return;
71
+ consoleMocks = {
72
+ log: jest.spyOn(console, "log").mockImplementation(() => {}),
73
+ warn: jest.spyOn(console, "warn").mockImplementation(() => {}),
74
+ error: jest.spyOn(console, "error").mockImplementation(() => {}),
75
+ info: jest.spyOn(console, "info").mockImplementation(() => {}),
76
+ };
77
+ };
78
+
79
+ export const restoreConsoleMocks = (): void => {
80
+ if (!consoleMocks) return;
81
+ consoleMocks.log.mockRestore();
82
+ consoleMocks.warn.mockRestore();
83
+ consoleMocks.error.mockRestore();
84
+ consoleMocks.info.mockRestore();
85
+ consoleMocks = undefined;
86
+ };
87
+
88
+ export const installCryptoMock = (options?: {
89
+ fillByte?: number;
90
+ digestBytes?: number[];
91
+ }): { mockDigest: jest.Mock } => {
92
+ const fillByte = options?.fillByte ?? 1;
93
+ const digestBytes = options?.digestBytes ?? [1, 2, 3];
94
+ const mockDigest = jest
95
+ .fn()
96
+ .mockResolvedValue(new Uint8Array(digestBytes).buffer);
97
+
98
+ Object.defineProperty(globalThis, "crypto", {
99
+ value: {
100
+ getRandomValues: <T extends ArrayBufferView>(arr: T): T => {
101
+ const bytes = new Uint8Array(arr.buffer);
102
+ bytes.fill(fillByte);
103
+ return arr;
104
+ },
105
+ subtle: { digest: mockDigest },
106
+ },
107
+ configurable: true,
108
+ });
109
+
110
+ return { mockDigest };
111
+ };
112
+
113
+ export const resetSessionStorage = (): void => {
114
+ if (typeof sessionStorage === "undefined") return;
115
+ sessionStorage.clear();
116
+ };
117
+
118
+ export const resetCookies = (): void => {
119
+ if (typeof document === "undefined") return;
120
+ const cookies = document.cookie
121
+ .split(";")
122
+ .map((cookie) => cookie.trim())
123
+ .filter(Boolean);
124
+ for (const cookie of cookies) {
125
+ const name = cookie.split("=")[0];
126
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
127
+ }
128
+ };
129
+
130
+ export const setCookies = (cookieString: string): void => {
131
+ if (typeof document === "undefined") return;
132
+ const parts = cookieString
133
+ .split(";")
134
+ .map((part) => part.trim())
135
+ .filter(Boolean);
136
+ if (parts.length === 0) return;
137
+ document.cookie = parts.join("; ");
138
+ };
139
+
140
+ export const installTestEnv = (): { mockDigest: jest.Mock } => {
141
+ installTextEncoder();
142
+ installBase64();
143
+ installConsoleMocks();
144
+ Object.defineProperty(window, "top", { value: window, writable: true });
145
+ resetSessionStorage();
146
+ resetCookies();
147
+ return installCryptoMock();
148
+ };
149
+
150
+ export const setWindowLocation = (href: string): void => {
151
+ const url = new URL(href);
152
+ Object.defineProperty(window, "location", {
153
+ value: {
154
+ href,
155
+ origin: url.origin,
156
+ search: url.search,
157
+ },
158
+ writable: true,
159
+ });
160
+ };
161
+
162
+ export const resetTestEnv = (): void => {
163
+ resetSessionStorage();
164
+ resetCookies();
165
+ restoreConsoleMocks();
166
+ };
167
+
168
+ export const mockPkceValues = (
169
+ client: {
170
+ generateCodeVerifier: () => string;
171
+ generateCodeChallenge: (value: string) => Promise<string>;
172
+ generateState: () => string;
173
+ generateNonce: () => string;
174
+ },
175
+ overrides?: Partial<{
176
+ codeVerifier: string;
177
+ codeChallenge: string;
178
+ state: string;
179
+ nonce: string;
180
+ }>
181
+ ): {
182
+ codeVerifier: string;
183
+ codeChallenge: string;
184
+ state: string;
185
+ nonce: string;
186
+ } => {
187
+ const values = {
188
+ codeVerifier: "ABC_codeVerifier_ABC",
189
+ codeChallenge: "ABC_codeChallenge_ABC",
190
+ state: "ABC_state_ABC",
191
+ nonce: "ABC_nonce_ABC",
192
+ ...overrides,
193
+ };
194
+
195
+ jest.spyOn(client, "generateCodeVerifier").mockReturnValue(values.codeVerifier);
196
+ jest
197
+ .spyOn(client, "generateCodeChallenge")
198
+ .mockResolvedValue(values.codeChallenge);
199
+ jest.spyOn(client, "generateState").mockReturnValue(values.state);
200
+ jest.spyOn(client, "generateNonce").mockReturnValue(values.nonce);
201
+
202
+ return values;
203
+ };