@palantir/pack.auth.foundry 0.0.1-beta.1
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/.turbo/turbo-lint.log +20 -0
- package/.turbo/turbo-transpileBrowser.log +5 -0
- package/.turbo/turbo-transpileCjs.log +5 -0
- package/.turbo/turbo-transpileEsm.log +5 -0
- package/.turbo/turbo-transpileTypes.log +5 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE.txt +13 -0
- package/README.md +159 -0
- package/build/browser/index.js +374 -0
- package/build/browser/index.js.map +1 -0
- package/build/cjs/index.cjs +381 -0
- package/build/cjs/index.cjs.map +1 -0
- package/build/cjs/index.d.cts +90 -0
- package/build/esm/index.js +374 -0
- package/build/esm/index.js.map +1 -0
- package/build/types/AuthModuleConfig.d.ts +1 -0
- package/build/types/AuthModuleConfig.d.ts.map +1 -0
- package/build/types/__tests__/AuthServiceConfidential.test.d.ts +1 -0
- package/build/types/__tests__/AuthServiceConfidential.test.d.ts.map +1 -0
- package/build/types/__tests__/AuthServicePublic.test.d.ts +1 -0
- package/build/types/__tests__/AuthServicePublic.test.d.ts.map +1 -0
- package/build/types/__tests__/AuthServiceStatic.test.d.ts +1 -0
- package/build/types/__tests__/AuthServiceStatic.test.d.ts.map +1 -0
- package/build/types/auth/AuthServiceBase.d.ts +32 -0
- package/build/types/auth/AuthServiceBase.d.ts.map +1 -0
- package/build/types/auth/AuthServiceConfidential.d.ts +20 -0
- package/build/types/auth/AuthServiceConfidential.d.ts.map +1 -0
- package/build/types/auth/AuthServicePublic.d.ts +23 -0
- package/build/types/auth/AuthServicePublic.d.ts.map +1 -0
- package/build/types/auth/AuthServiceStatic.d.ts +17 -0
- package/build/types/auth/AuthServiceStatic.d.ts.map +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/utils/jwtUtils.d.ts +28 -0
- package/build/types/utils/jwtUtils.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/AuthModuleConfig.ts +17 -0
- package/src/__tests__/AuthServiceConfidential.test.ts +407 -0
- package/src/__tests__/AuthServicePublic.test.ts +413 -0
- package/src/__tests__/AuthServiceStatic.test.ts +341 -0
- package/src/auth/AuthServiceBase.ts +201 -0
- package/src/auth/AuthServiceConfidential.ts +96 -0
- package/src/auth/AuthServicePublic.ts +124 -0
- package/src/auth/AuthServiceStatic.ts +91 -0
- package/src/index.ts +20 -0
- package/src/utils/jwtUtils.ts +127 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mjs +26 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@palantir/pack.auth.foundry",
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
|
+
"description": "Foundry-specific authentication implementations for PACK applications",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/palantir/pack.git"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"browser": "./build/browser/index.js",
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./build/types/index.d.ts",
|
|
15
|
+
"default": "./build/esm/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": "./build/cjs/index.js",
|
|
18
|
+
"default": "./build/browser/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./*": {
|
|
21
|
+
"browser": "./build/browser/public/*.js",
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./build/types/public/*.d.ts",
|
|
24
|
+
"default": "./build/js/public/*.js"
|
|
25
|
+
},
|
|
26
|
+
"require": "./build/cjs/public/*.js",
|
|
27
|
+
"default": "./build/browser/public/*.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"tiny-invariant": "^1.3.3",
|
|
32
|
+
"@palantir/pack.auth": "0.0.1-beta.1",
|
|
33
|
+
"@palantir/pack.core": "0.1.0-beta.2"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@osdk/api": "~2.4.2",
|
|
37
|
+
"@osdk/client": "~2.4.2",
|
|
38
|
+
"@osdk/foundry.admin": "~2.32.0",
|
|
39
|
+
"@osdk/oauth": "~1.4.0",
|
|
40
|
+
"@osdk/shared.client2": "~1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"happy-dom": "^18.0.1",
|
|
44
|
+
"rimraf": "^6.0.1",
|
|
45
|
+
"tslib": "^2.8.1",
|
|
46
|
+
"typescript": "^5.9.2",
|
|
47
|
+
"vitest-mock-extended": "^2.0.2",
|
|
48
|
+
"@palantir/pack.monorepo.tsconfig": "~0.4.0-beta.1"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"pack",
|
|
55
|
+
"auth",
|
|
56
|
+
"foundry"
|
|
57
|
+
],
|
|
58
|
+
"main": "./build/cjs/index.js",
|
|
59
|
+
"module": "./build/esm/index.js",
|
|
60
|
+
"types": "./build/cjs/index.d.ts",
|
|
61
|
+
"type": "module",
|
|
62
|
+
"scripts": {
|
|
63
|
+
"clean": "rimraf .turbo build dist lib test-output *.tgz tsconfig.tsbuildinfo",
|
|
64
|
+
"lint": "eslint ./src ; dprint check --config $(find-up dprint.json) --allow-no-files",
|
|
65
|
+
"lint:fix": "eslint ./src --fix ; dprint fmt --config $(find-up dprint.json) --allow-no-files",
|
|
66
|
+
"test": "vitest run --passWithNoTests -u",
|
|
67
|
+
"test:watch": "vitest --passWithNoTests",
|
|
68
|
+
"transpileBrowser": "monorepo-transpile -f esm -m bundle -t browser",
|
|
69
|
+
"transpileCjs": "monorepo-transpile -f cjs -m bundle -t node",
|
|
70
|
+
"transpileEsm": "monorepo-transpile -f esm -m bundle -t node",
|
|
71
|
+
"transpileTypes": "monorepo-transpile -f esm -m types -t node",
|
|
72
|
+
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { createAuthModuleConfig } from "@palantir/pack.auth";
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Logger } from "@osdk/api";
|
|
18
|
+
import type { Client } from "@osdk/client";
|
|
19
|
+
import type { createConfidentialOauthClient } from "@osdk/oauth";
|
|
20
|
+
import { AuthState, type BaseAuthService } from "@palantir/pack.auth";
|
|
21
|
+
import type { PackAppInternal } from "@palantir/pack.core";
|
|
22
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
+
import type { MockProxy } from "vitest-mock-extended";
|
|
24
|
+
import { mock } from "vitest-mock-extended";
|
|
25
|
+
import { createConfidentialOauthService } from "../auth/AuthServiceConfidential.js";
|
|
26
|
+
|
|
27
|
+
type ConfidentialOauthClient = ReturnType<typeof createConfidentialOauthClient>;
|
|
28
|
+
|
|
29
|
+
describe("ConfidentialOauthService", () => {
|
|
30
|
+
let mockOauthClient: ConfidentialOauthClient;
|
|
31
|
+
let mockApp: PackAppInternal;
|
|
32
|
+
let mockLogger: MockProxy<Logger>;
|
|
33
|
+
let service: BaseAuthService;
|
|
34
|
+
let eventListeners: Record<string, (() => void)[]>;
|
|
35
|
+
|
|
36
|
+
// Store references to mocks for easy access in tests
|
|
37
|
+
let mockGetTokenOrUndefined: ReturnType<typeof vi.fn>;
|
|
38
|
+
let mockSignIn: ReturnType<typeof vi.fn>;
|
|
39
|
+
let mockSignOut: ReturnType<typeof vi.fn>;
|
|
40
|
+
let mockAddEventListener: ReturnType<typeof vi.fn>;
|
|
41
|
+
let mockRemoveEventListener: ReturnType<typeof vi.fn>;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
eventListeners = {};
|
|
45
|
+
|
|
46
|
+
mockLogger = mock<Logger>();
|
|
47
|
+
mockLogger.child.mockReturnValue(mockLogger);
|
|
48
|
+
|
|
49
|
+
const mockOsdkClient = mock<Client>();
|
|
50
|
+
|
|
51
|
+
mockApp = {
|
|
52
|
+
config: {
|
|
53
|
+
app: { appId: "test-app" },
|
|
54
|
+
isTestMode: false,
|
|
55
|
+
logger: mockLogger,
|
|
56
|
+
osdkClient: mockOsdkClient,
|
|
57
|
+
remote: {
|
|
58
|
+
packWsPath: "/pack/ws",
|
|
59
|
+
baseUrl: "https://test.example.com",
|
|
60
|
+
fetchFn: globalThis.fetch,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
getModule: vi.fn(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const tokenProvider = vi.fn().mockResolvedValue("oauth-token");
|
|
67
|
+
mockGetTokenOrUndefined = vi.fn().mockReturnValue(undefined);
|
|
68
|
+
mockSignIn = vi.fn().mockResolvedValue({
|
|
69
|
+
access_token: "test-token",
|
|
70
|
+
expires_in: 3600,
|
|
71
|
+
expires_at: Date.now() + 3600000,
|
|
72
|
+
});
|
|
73
|
+
mockSignOut = vi.fn().mockResolvedValue(undefined);
|
|
74
|
+
mockAddEventListener = vi.fn().mockImplementation(
|
|
75
|
+
(event: string, listener: (evt: unknown) => void) => {
|
|
76
|
+
if (!eventListeners[event]) {
|
|
77
|
+
eventListeners[event] = [];
|
|
78
|
+
}
|
|
79
|
+
eventListeners[event].push(() => {
|
|
80
|
+
listener({});
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
mockRemoveEventListener = vi.fn().mockImplementation(
|
|
85
|
+
(event: string, listener: (evt: unknown) => void) => {
|
|
86
|
+
if (eventListeners[event]) {
|
|
87
|
+
const index = eventListeners[event].findIndex(storedListener =>
|
|
88
|
+
storedListener.toString() === listener.toString()
|
|
89
|
+
);
|
|
90
|
+
if (index > -1) {
|
|
91
|
+
eventListeners[event].splice(index, 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
mockOauthClient = Object.assign(tokenProvider, {
|
|
98
|
+
getTokenOrUndefined: mockGetTokenOrUndefined,
|
|
99
|
+
signIn: mockSignIn,
|
|
100
|
+
signOut: mockSignOut,
|
|
101
|
+
addEventListener: mockAddEventListener,
|
|
102
|
+
removeEventListener: mockRemoveEventListener,
|
|
103
|
+
}) as ConfidentialOauthClient;
|
|
104
|
+
|
|
105
|
+
service = createConfidentialOauthService(mockApp, mockOauthClient);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Helper to trigger OAuth events
|
|
109
|
+
const triggerEvent = (eventType: string) => {
|
|
110
|
+
if (eventListeners[eventType]) {
|
|
111
|
+
eventListeners[eventType].forEach(listener => {
|
|
112
|
+
listener();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
describe("initialization", () => {
|
|
118
|
+
it("should start in unauthenticated state", () => {
|
|
119
|
+
expect(service.getCurrentState()).toBe(AuthState.Unauthenticated);
|
|
120
|
+
expect(service.isAuthenticated()).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should start authenticated if token exists", () => {
|
|
124
|
+
const getTokenOrUndefined = vi.fn().mockReturnValue("existing-token");
|
|
125
|
+
const tokenProvider = vi.fn().mockResolvedValue("oauth-token");
|
|
126
|
+
const signIn = vi.fn().mockResolvedValue({
|
|
127
|
+
access_token: "test-token",
|
|
128
|
+
expires_in: 3600,
|
|
129
|
+
expires_at: Date.now() + 3600000,
|
|
130
|
+
});
|
|
131
|
+
const signOut = vi.fn().mockResolvedValue(undefined);
|
|
132
|
+
const addEventListener = vi.fn();
|
|
133
|
+
const removeEventListener = vi.fn();
|
|
134
|
+
|
|
135
|
+
const authenticatedClient = Object.assign(tokenProvider, {
|
|
136
|
+
getTokenOrUndefined,
|
|
137
|
+
signIn,
|
|
138
|
+
signOut,
|
|
139
|
+
addEventListener,
|
|
140
|
+
removeEventListener,
|
|
141
|
+
}) as ConfidentialOauthClient;
|
|
142
|
+
|
|
143
|
+
const authenticatedService = createConfidentialOauthService(mockApp, authenticatedClient);
|
|
144
|
+
expect(authenticatedService.getCurrentState()).toBe(AuthState.Authenticated);
|
|
145
|
+
expect(authenticatedService.isAuthenticated()).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should setup event listeners (no refresh for confidential)", () => {
|
|
149
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("signIn", expect.any(Function));
|
|
150
|
+
expect(mockAddEventListener).toHaveBeenCalledWith(
|
|
151
|
+
"signOut",
|
|
152
|
+
expect.any(Function),
|
|
153
|
+
);
|
|
154
|
+
expect(mockAddEventListener).not.toHaveBeenCalledWith(
|
|
155
|
+
"refresh",
|
|
156
|
+
expect.any(Function),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("TokenProvider interface", () => {
|
|
162
|
+
it("should be callable and return token", async () => {
|
|
163
|
+
const token = await service.getToken();
|
|
164
|
+
expect(token).toBe("oauth-token");
|
|
165
|
+
expect(mockOauthClient).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("authentication methods", () => {
|
|
170
|
+
it("should delegate to OAuth client for signIn", async () => {
|
|
171
|
+
await service.signIn();
|
|
172
|
+
expect(mockSignIn).toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should delegate to OAuth client for signOut", async () => {
|
|
176
|
+
await service.signOut();
|
|
177
|
+
expect(mockSignOut).toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should not have refresh capability", () => {
|
|
181
|
+
expect(service.refresh).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should delegate to OAuth client for getTokenOrUndefined", () => {
|
|
185
|
+
mockGetTokenOrUndefined.mockReturnValue("test-token");
|
|
186
|
+
|
|
187
|
+
expect(service.getTokenOrUndefined()).toBe("test-token");
|
|
188
|
+
expect(mockGetTokenOrUndefined).toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should delegate to OAuth client for isAuthenticated", () => {
|
|
192
|
+
mockGetTokenOrUndefined.mockReturnValue("test-token");
|
|
193
|
+
|
|
194
|
+
expect(service.isAuthenticated()).toBe(true);
|
|
195
|
+
|
|
196
|
+
mockGetTokenOrUndefined.mockReturnValue(undefined);
|
|
197
|
+
expect(service.isAuthenticated()).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("state management", () => {
|
|
202
|
+
it("should update state on signIn event", () => {
|
|
203
|
+
const callback = vi.fn();
|
|
204
|
+
service.onAuthStateChange(callback);
|
|
205
|
+
|
|
206
|
+
triggerEvent("signIn");
|
|
207
|
+
|
|
208
|
+
expect(service.getCurrentState()).toBe(AuthState.Authenticated);
|
|
209
|
+
expect(callback).toHaveBeenCalledWith({
|
|
210
|
+
previousState: AuthState.Unauthenticated,
|
|
211
|
+
state: AuthState.Authenticated,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should update state on signOut event", () => {
|
|
216
|
+
// Start authenticated
|
|
217
|
+
triggerEvent("signIn");
|
|
218
|
+
const callback = vi.fn();
|
|
219
|
+
service.onAuthStateChange(callback);
|
|
220
|
+
|
|
221
|
+
triggerEvent("signOut");
|
|
222
|
+
|
|
223
|
+
expect(service.getCurrentState()).toBe(AuthState.Unauthenticated);
|
|
224
|
+
expect(callback).toHaveBeenCalledWith({
|
|
225
|
+
previousState: AuthState.Authenticated,
|
|
226
|
+
state: AuthState.Unauthenticated,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should not respond to refresh events (confidential clients don't have refresh)", () => {
|
|
231
|
+
const callback = vi.fn();
|
|
232
|
+
service.onAuthStateChange(callback);
|
|
233
|
+
|
|
234
|
+
// Clear the initial callback that fires immediately
|
|
235
|
+
callback.mockClear();
|
|
236
|
+
|
|
237
|
+
// Trigger a refresh event (should be ignored)
|
|
238
|
+
triggerEvent("refresh");
|
|
239
|
+
|
|
240
|
+
expect(service.getCurrentState()).toBe(AuthState.Unauthenticated);
|
|
241
|
+
expect(callback).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should update state to authenticating during signIn", async () => {
|
|
245
|
+
const callback = vi.fn();
|
|
246
|
+
service.onAuthStateChange(callback);
|
|
247
|
+
|
|
248
|
+
const signInPromise = service.signIn();
|
|
249
|
+
|
|
250
|
+
expect(callback).toHaveBeenCalledWith({
|
|
251
|
+
previousState: AuthState.Unauthenticated,
|
|
252
|
+
state: AuthState.Authenticating,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await signInPromise;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should update state to error on signIn failure", async () => {
|
|
259
|
+
const error = new Error("Sign in failed");
|
|
260
|
+
mockSignIn.mockRejectedValue(error);
|
|
261
|
+
|
|
262
|
+
const callback = vi.fn();
|
|
263
|
+
service.onAuthStateChange(callback);
|
|
264
|
+
|
|
265
|
+
await expect(service.signIn()).rejects.toThrow("Sign in failed");
|
|
266
|
+
|
|
267
|
+
expect(callback).toHaveBeenCalledWith({
|
|
268
|
+
previousState: AuthState.Authenticating,
|
|
269
|
+
state: AuthState.Error,
|
|
270
|
+
error,
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should handle multiple state change listeners", () => {
|
|
275
|
+
const callback1 = vi.fn();
|
|
276
|
+
const callback2 = vi.fn();
|
|
277
|
+
|
|
278
|
+
service.onAuthStateChange(callback1);
|
|
279
|
+
service.onAuthStateChange(callback2);
|
|
280
|
+
|
|
281
|
+
triggerEvent("signIn");
|
|
282
|
+
|
|
283
|
+
expect(callback1).toHaveBeenCalled();
|
|
284
|
+
expect(callback2).toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should allow unsubscribing from state changes", () => {
|
|
288
|
+
const callback = vi.fn();
|
|
289
|
+
const unsubscribe = service.onAuthStateChange(callback);
|
|
290
|
+
|
|
291
|
+
// Clear the initial callback that fires immediately
|
|
292
|
+
callback.mockClear();
|
|
293
|
+
|
|
294
|
+
unsubscribe();
|
|
295
|
+
triggerEvent("signIn");
|
|
296
|
+
|
|
297
|
+
expect(callback).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("error handling", () => {
|
|
302
|
+
it("should handle errors in state change callbacks", () => {
|
|
303
|
+
const errorCallback = vi.fn().mockImplementation(() => {
|
|
304
|
+
throw new Error("Callback error");
|
|
305
|
+
});
|
|
306
|
+
const goodCallback = vi.fn();
|
|
307
|
+
|
|
308
|
+
// Add callbacks - errors should be handled gracefully
|
|
309
|
+
service.onAuthStateChange(errorCallback);
|
|
310
|
+
service.onAuthStateChange(goodCallback);
|
|
311
|
+
|
|
312
|
+
// Clear initial calls to test the actual event-triggered behavior
|
|
313
|
+
errorCallback.mockClear();
|
|
314
|
+
goodCallback.mockClear();
|
|
315
|
+
mockLogger.error.mockClear();
|
|
316
|
+
|
|
317
|
+
triggerEvent("signIn");
|
|
318
|
+
|
|
319
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
320
|
+
"Error in auth state callback:",
|
|
321
|
+
expect.any(Error),
|
|
322
|
+
);
|
|
323
|
+
expect(goodCallback).toHaveBeenCalled(); // Other callbacks should still work
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("should convert non-Error objects to Error in error state", async () => {
|
|
327
|
+
mockSignIn.mockRejectedValue("string error");
|
|
328
|
+
|
|
329
|
+
const callback = vi.fn();
|
|
330
|
+
service.onAuthStateChange(callback);
|
|
331
|
+
|
|
332
|
+
await expect(service.signIn()).rejects.toThrow("string error");
|
|
333
|
+
|
|
334
|
+
expect(callback).toHaveBeenCalledWith({
|
|
335
|
+
previousState: AuthState.Authenticating,
|
|
336
|
+
state: AuthState.Error,
|
|
337
|
+
error: expect.objectContaining({
|
|
338
|
+
message: "string error",
|
|
339
|
+
}) as unknown,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("cleanup", () => {
|
|
345
|
+
it("should remove event listeners on dispose", () => {
|
|
346
|
+
service.dispose();
|
|
347
|
+
|
|
348
|
+
expect(mockRemoveEventListener).toHaveBeenCalledWith(
|
|
349
|
+
"signIn",
|
|
350
|
+
expect.any(Function),
|
|
351
|
+
);
|
|
352
|
+
expect(mockRemoveEventListener).toHaveBeenCalledWith(
|
|
353
|
+
"signOut",
|
|
354
|
+
expect.any(Function),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Should not try to remove refresh listener for confidential clients
|
|
358
|
+
expect(mockRemoveEventListener).not.toHaveBeenCalledWith(
|
|
359
|
+
"refresh",
|
|
360
|
+
expect.any(Function),
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should clear state callbacks on dispose", () => {
|
|
365
|
+
const callback = vi.fn();
|
|
366
|
+
service.onAuthStateChange(callback);
|
|
367
|
+
|
|
368
|
+
// Clear the initial callback that fires immediately
|
|
369
|
+
callback.mockClear();
|
|
370
|
+
|
|
371
|
+
service.dispose();
|
|
372
|
+
triggerEvent("signIn");
|
|
373
|
+
|
|
374
|
+
expect(callback).not.toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("confidential client specific behavior", () => {
|
|
379
|
+
it("should handle client credentials flow via signIn", async () => {
|
|
380
|
+
// For confidential clients, signIn typically performs client credentials grant
|
|
381
|
+
await service.signIn();
|
|
382
|
+
|
|
383
|
+
expect(mockSignIn).toHaveBeenCalled();
|
|
384
|
+
expect(service.getCurrentState()).toBe(AuthState.Authenticating); // Should be in authenticating state during the call
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("should not have refresh functionality", () => {
|
|
388
|
+
expect(service.refresh).toBeUndefined();
|
|
389
|
+
expect(typeof service.refresh).toBe("undefined");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should work with server-side authentication patterns", async () => {
|
|
393
|
+
// Simulate successful client credentials flow
|
|
394
|
+
mockGetTokenOrUndefined
|
|
395
|
+
.mockReturnValueOnce(undefined) // Initial state
|
|
396
|
+
.mockReturnValue("server-token"); // After sign in
|
|
397
|
+
|
|
398
|
+
expect(service.isAuthenticated()).toBe(false);
|
|
399
|
+
|
|
400
|
+
await service.signIn();
|
|
401
|
+
triggerEvent("signIn"); // Simulate the event being fired
|
|
402
|
+
|
|
403
|
+
expect(service.getCurrentState()).toBe(AuthState.Authenticated);
|
|
404
|
+
expect(service.isAuthenticated()).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|