@mondaydotcomorg/atp-providers 0.17.14
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/README.md +430 -0
- package/__tests__/oauth-integration.test.ts +457 -0
- package/__tests__/scope-checkers.test.ts +560 -0
- package/__tests__/token-expiration.test.ts +308 -0
- package/dist/audit/jsonl.d.ts +29 -0
- package/dist/audit/jsonl.d.ts.map +1 -0
- package/dist/audit/jsonl.js +163 -0
- package/dist/audit/jsonl.js.map +1 -0
- package/dist/audit/opentelemetry.d.ts +23 -0
- package/dist/audit/opentelemetry.d.ts.map +1 -0
- package/dist/audit/opentelemetry.js +240 -0
- package/dist/audit/opentelemetry.js.map +1 -0
- package/dist/audit/otel-metrics.d.ts +111 -0
- package/dist/audit/otel-metrics.d.ts.map +1 -0
- package/dist/audit/otel-metrics.js +115 -0
- package/dist/audit/otel-metrics.js.map +1 -0
- package/dist/auth/env.d.ts +21 -0
- package/dist/auth/env.d.ts.map +1 -0
- package/dist/auth/env.js +48 -0
- package/dist/auth/env.js.map +1 -0
- package/dist/cache/file.d.ts +47 -0
- package/dist/cache/file.d.ts.map +1 -0
- package/dist/cache/file.js +262 -0
- package/dist/cache/file.js.map +1 -0
- package/dist/cache/memory.d.ts +30 -0
- package/dist/cache/memory.d.ts.map +1 -0
- package/dist/cache/memory.js +81 -0
- package/dist/cache/memory.js.map +1 -0
- package/dist/cache/redis.d.ts +28 -0
- package/dist/cache/redis.d.ts.map +1 -0
- package/dist/cache/redis.js +124 -0
- package/dist/cache/redis.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/index.d.ts +2 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +2 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth/scope-checkers.d.ts +61 -0
- package/dist/oauth/scope-checkers.d.ts.map +1 -0
- package/dist/oauth/scope-checkers.js +166 -0
- package/dist/oauth/scope-checkers.js.map +1 -0
- package/package.json +26 -0
- package/project.json +31 -0
- package/src/audit/jsonl.ts +189 -0
- package/src/audit/opentelemetry.ts +286 -0
- package/src/audit/otel-metrics.ts +122 -0
- package/src/auth/env.ts +65 -0
- package/src/cache/file.ts +330 -0
- package/src/cache/memory.ts +105 -0
- package/src/cache/redis.ts +160 -0
- package/src/index.ts +32 -0
- package/src/oauth/index.ts +1 -0
- package/src/oauth/scope-checkers.ts +196 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { ScopeCheckerRegistry } from '../src/oauth/scope-checkers.js';
|
|
3
|
+
import type { AuthProvider, UserCredentialData } from '@mondaydotcomorg/atp-protocol';
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
global.fetch = vi.fn();
|
|
7
|
+
|
|
8
|
+
// Mock Auth Provider
|
|
9
|
+
class MockAuthProvider implements AuthProvider {
|
|
10
|
+
name = 'mock';
|
|
11
|
+
private userCredentials = new Map<string, Map<string, UserCredentialData>>();
|
|
12
|
+
|
|
13
|
+
async getCredential(key: string): Promise<string | null> {
|
|
14
|
+
return process.env[key] || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async setCredential(key: string, value: string): Promise<void> {}
|
|
18
|
+
|
|
19
|
+
async deleteCredential(key: string): Promise<void> {}
|
|
20
|
+
|
|
21
|
+
async getUserCredential(userId: string, provider: string): Promise<UserCredentialData | null> {
|
|
22
|
+
const userMap = this.userCredentials.get(userId);
|
|
23
|
+
const creds = userMap?.get(provider);
|
|
24
|
+
|
|
25
|
+
if (!creds) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check expiration
|
|
30
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return creds;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async setUserCredential(
|
|
38
|
+
userId: string,
|
|
39
|
+
provider: string,
|
|
40
|
+
data: UserCredentialData
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if (!this.userCredentials.has(userId)) {
|
|
43
|
+
this.userCredentials.set(userId, new Map());
|
|
44
|
+
}
|
|
45
|
+
this.userCredentials.get(userId)!.set(provider, data);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async deleteUserCredential(userId: string, provider: string): Promise<void> {
|
|
49
|
+
this.userCredentials.get(userId)?.delete(provider);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async listUserProviders(userId: string): Promise<string[]> {
|
|
53
|
+
const userMap = this.userCredentials.get(userId);
|
|
54
|
+
return userMap ? Array.from(userMap.keys()) : [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Mock scope checkers for testing
|
|
59
|
+
class TestGitHubChecker {
|
|
60
|
+
provider = 'github';
|
|
61
|
+
async check(token: string) {
|
|
62
|
+
const response = await fetch('https://api.github.com/user', {
|
|
63
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error('Invalid or expired GitHub token');
|
|
67
|
+
}
|
|
68
|
+
const scopesHeader = response.headers.get('X-OAuth-Scopes');
|
|
69
|
+
return scopesHeader ? scopesHeader.split(',').map((s) => s.trim()) : [];
|
|
70
|
+
}
|
|
71
|
+
async validate(token: string) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch('https://api.github.com/user', {
|
|
74
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
75
|
+
});
|
|
76
|
+
return response.ok;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class TestGoogleChecker {
|
|
84
|
+
provider = 'google';
|
|
85
|
+
async check(token: string) {
|
|
86
|
+
const response = await fetch(
|
|
87
|
+
`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`
|
|
88
|
+
);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error('Invalid or expired Google token');
|
|
91
|
+
}
|
|
92
|
+
const data = (await response.json()) as { scope?: string; exp?: number };
|
|
93
|
+
return data.scope ? data.scope.split(' ') : [];
|
|
94
|
+
}
|
|
95
|
+
async validate(token: string) {
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(
|
|
98
|
+
`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`
|
|
99
|
+
);
|
|
100
|
+
return response.ok;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe('OAuth Integration Tests', () => {
|
|
108
|
+
let scopeChecker: ScopeCheckerRegistry;
|
|
109
|
+
let authProvider: MockAuthProvider;
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
scopeChecker = new ScopeCheckerRegistry();
|
|
113
|
+
// Register test checkers
|
|
114
|
+
scopeChecker.register(new TestGitHubChecker() as any);
|
|
115
|
+
scopeChecker.register(new TestGoogleChecker() as any);
|
|
116
|
+
|
|
117
|
+
authProvider = new MockAuthProvider();
|
|
118
|
+
vi.clearAllMocks();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
scopeChecker.stopCleanup();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('End-to-end OAuth connection flow', () => {
|
|
126
|
+
it('should validate and store GitHub credentials', async () => {
|
|
127
|
+
const userId = 'user123';
|
|
128
|
+
const provider = 'github';
|
|
129
|
+
const accessToken = 'gho_test_token';
|
|
130
|
+
|
|
131
|
+
// Mock GitHub API responses
|
|
132
|
+
(global.fetch as any)
|
|
133
|
+
.mockResolvedValueOnce({
|
|
134
|
+
// check() call
|
|
135
|
+
ok: true,
|
|
136
|
+
headers: {
|
|
137
|
+
get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo, read:user' : null),
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
.mockResolvedValueOnce({
|
|
141
|
+
// validate() call
|
|
142
|
+
ok: true,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Validate token
|
|
146
|
+
const tokenInfo = await scopeChecker.getTokenInfo(provider, accessToken);
|
|
147
|
+
|
|
148
|
+
expect(tokenInfo.valid).toBe(true);
|
|
149
|
+
expect(tokenInfo.scopes).toEqual(['repo', 'read:user']);
|
|
150
|
+
|
|
151
|
+
// Store credentials
|
|
152
|
+
await authProvider.setUserCredential(userId, provider, {
|
|
153
|
+
token: accessToken,
|
|
154
|
+
scopes: tokenInfo.scopes,
|
|
155
|
+
expiresAt: Date.now() + 3600000,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Retrieve credentials
|
|
159
|
+
const storedCreds = await authProvider.getUserCredential(userId, provider);
|
|
160
|
+
|
|
161
|
+
expect(storedCreds).not.toBeNull();
|
|
162
|
+
expect(storedCreds?.token).toBe(accessToken);
|
|
163
|
+
expect(storedCreds?.scopes).toEqual(['repo', 'read:user']);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should reject invalid tokens', async () => {
|
|
167
|
+
const provider = 'github';
|
|
168
|
+
const invalidToken = 'invalid_token';
|
|
169
|
+
|
|
170
|
+
// Mock GitHub API error
|
|
171
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
172
|
+
ok: false,
|
|
173
|
+
status: 401,
|
|
174
|
+
statusText: 'Unauthorized',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await expect(scopeChecker.getTokenInfo(provider, invalidToken)).rejects.toThrow(
|
|
178
|
+
'Invalid or expired GitHub token'
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should handle Google OAuth flow with expiration', async () => {
|
|
183
|
+
const userId = 'user123';
|
|
184
|
+
const provider = 'google';
|
|
185
|
+
const accessToken = 'ya29.test_token';
|
|
186
|
+
|
|
187
|
+
const now = Math.floor(Date.now() / 1000);
|
|
188
|
+
const expiresIn = 3600; // 1 hour
|
|
189
|
+
|
|
190
|
+
// Mock Google tokeninfo
|
|
191
|
+
(global.fetch as any)
|
|
192
|
+
.mockResolvedValueOnce({
|
|
193
|
+
ok: true,
|
|
194
|
+
json: async () => ({
|
|
195
|
+
scope: 'https://www.googleapis.com/auth/calendar',
|
|
196
|
+
exp: now + expiresIn,
|
|
197
|
+
}),
|
|
198
|
+
})
|
|
199
|
+
.mockResolvedValueOnce({
|
|
200
|
+
ok: true,
|
|
201
|
+
json: async () => ({
|
|
202
|
+
exp: now + expiresIn,
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const tokenInfo = await scopeChecker.getTokenInfo(provider, accessToken);
|
|
207
|
+
|
|
208
|
+
expect(tokenInfo.valid).toBe(true);
|
|
209
|
+
expect(tokenInfo.scopes).toEqual(['https://www.googleapis.com/auth/calendar']);
|
|
210
|
+
|
|
211
|
+
// Store with expiration
|
|
212
|
+
await authProvider.setUserCredential(userId, provider, {
|
|
213
|
+
token: accessToken,
|
|
214
|
+
scopes: tokenInfo.scopes,
|
|
215
|
+
expiresAt: (now + expiresIn) * 1000, // Convert to milliseconds
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Should be retrievable immediately
|
|
219
|
+
const creds = await authProvider.getUserCredential(userId, provider);
|
|
220
|
+
expect(creds).not.toBeNull();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('Multi-user scenarios', () => {
|
|
225
|
+
it('should handle multiple users with same provider', async () => {
|
|
226
|
+
const provider = 'github';
|
|
227
|
+
|
|
228
|
+
// Mock API responses for both users
|
|
229
|
+
(global.fetch as any)
|
|
230
|
+
.mockResolvedValueOnce({
|
|
231
|
+
ok: true,
|
|
232
|
+
headers: {
|
|
233
|
+
get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
.mockResolvedValueOnce({
|
|
237
|
+
ok: true,
|
|
238
|
+
})
|
|
239
|
+
.mockResolvedValueOnce({
|
|
240
|
+
ok: true,
|
|
241
|
+
headers: {
|
|
242
|
+
get: (name: string) => (name === 'X-OAuth-Scopes' ? 'read:user' : null),
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
.mockResolvedValueOnce({
|
|
246
|
+
ok: true,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// User 1 connects
|
|
250
|
+
const token1Info = await scopeChecker.getTokenInfo(provider, 'token1');
|
|
251
|
+
await authProvider.setUserCredential('user1', provider, {
|
|
252
|
+
token: 'token1',
|
|
253
|
+
scopes: token1Info.scopes,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// User 2 connects
|
|
257
|
+
const token2Info = await scopeChecker.getTokenInfo(provider, 'token2');
|
|
258
|
+
await authProvider.setUserCredential('user2', provider, {
|
|
259
|
+
token: 'token2',
|
|
260
|
+
scopes: token2Info.scopes,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Verify both users have different credentials
|
|
264
|
+
const user1Creds = await authProvider.getUserCredential('user1', provider);
|
|
265
|
+
const user2Creds = await authProvider.getUserCredential('user2', provider);
|
|
266
|
+
|
|
267
|
+
expect(user1Creds?.token).toBe('token1');
|
|
268
|
+
expect(user1Creds?.scopes).toEqual(['repo']);
|
|
269
|
+
|
|
270
|
+
expect(user2Creds?.token).toBe('token2');
|
|
271
|
+
expect(user2Creds?.scopes).toEqual(['read:user']);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should handle user with multiple providers', async () => {
|
|
275
|
+
const userId = 'user123';
|
|
276
|
+
|
|
277
|
+
// Mock GitHub
|
|
278
|
+
(global.fetch as any)
|
|
279
|
+
.mockResolvedValueOnce({
|
|
280
|
+
ok: true,
|
|
281
|
+
headers: {
|
|
282
|
+
get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
.mockResolvedValueOnce({
|
|
286
|
+
ok: true,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const githubInfo = await scopeChecker.getTokenInfo('github', 'github_token');
|
|
290
|
+
await authProvider.setUserCredential(userId, 'github', {
|
|
291
|
+
token: 'github_token',
|
|
292
|
+
scopes: githubInfo.scopes,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Mock Google
|
|
296
|
+
(global.fetch as any)
|
|
297
|
+
.mockResolvedValueOnce({
|
|
298
|
+
ok: true,
|
|
299
|
+
json: async () => ({
|
|
300
|
+
scope: 'https://www.googleapis.com/auth/calendar',
|
|
301
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
302
|
+
}),
|
|
303
|
+
})
|
|
304
|
+
.mockResolvedValueOnce({
|
|
305
|
+
ok: true,
|
|
306
|
+
json: async () => ({
|
|
307
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const googleInfo = await scopeChecker.getTokenInfo('google', 'google_token');
|
|
312
|
+
await authProvider.setUserCredential(userId, 'google', {
|
|
313
|
+
token: 'google_token',
|
|
314
|
+
scopes: googleInfo.scopes,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// List providers
|
|
318
|
+
const providers = await authProvider.listUserProviders(userId);
|
|
319
|
+
|
|
320
|
+
expect(providers).toEqual(expect.arrayContaining(['github', 'google']));
|
|
321
|
+
expect(providers.length).toBe(2);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('Token disconnection', () => {
|
|
326
|
+
it('should remove provider credentials', async () => {
|
|
327
|
+
const userId = 'user123';
|
|
328
|
+
const provider = 'github';
|
|
329
|
+
|
|
330
|
+
// Connect
|
|
331
|
+
await authProvider.setUserCredential(userId, provider, {
|
|
332
|
+
token: 'token',
|
|
333
|
+
scopes: ['repo'],
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Verify connected
|
|
337
|
+
const beforeDisconnect = await authProvider.getUserCredential(userId, provider);
|
|
338
|
+
expect(beforeDisconnect).not.toBeNull();
|
|
339
|
+
|
|
340
|
+
// Disconnect
|
|
341
|
+
await authProvider.deleteUserCredential(userId, provider);
|
|
342
|
+
|
|
343
|
+
// Verify disconnected
|
|
344
|
+
const afterDisconnect = await authProvider.getUserCredential(userId, provider);
|
|
345
|
+
expect(afterDisconnect).toBeNull();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should only remove specified provider', async () => {
|
|
349
|
+
const userId = 'user123';
|
|
350
|
+
|
|
351
|
+
// Connect multiple providers
|
|
352
|
+
await authProvider.setUserCredential(userId, 'github', {
|
|
353
|
+
token: 'github_token',
|
|
354
|
+
scopes: ['repo'],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await authProvider.setUserCredential(userId, 'google', {
|
|
358
|
+
token: 'google_token',
|
|
359
|
+
scopes: ['calendar'],
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Disconnect only GitHub
|
|
363
|
+
await authProvider.deleteUserCredential(userId, 'github');
|
|
364
|
+
|
|
365
|
+
// Verify GitHub is gone but Google remains
|
|
366
|
+
const githubCreds = await authProvider.getUserCredential(userId, 'github');
|
|
367
|
+
const googleCreds = await authProvider.getUserCredential(userId, 'google');
|
|
368
|
+
|
|
369
|
+
expect(githubCreds).toBeNull();
|
|
370
|
+
expect(googleCreds).not.toBeNull();
|
|
371
|
+
expect(googleCreds?.token).toBe('google_token');
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('Scope caching behavior', () => {
|
|
376
|
+
it('should cache scope checks across requests', async () => {
|
|
377
|
+
const provider = 'github';
|
|
378
|
+
const token = 'gho_test_token';
|
|
379
|
+
|
|
380
|
+
// Mock single API call
|
|
381
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
382
|
+
ok: true,
|
|
383
|
+
headers: {
|
|
384
|
+
get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Make multiple requests
|
|
389
|
+
await scopeChecker.checkScopes(provider, token);
|
|
390
|
+
await scopeChecker.checkScopes(provider, token);
|
|
391
|
+
await scopeChecker.checkScopes(provider, token);
|
|
392
|
+
|
|
393
|
+
// Should only call API once
|
|
394
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should respect cache TTL', async () => {
|
|
398
|
+
vi.useFakeTimers();
|
|
399
|
+
|
|
400
|
+
const provider = 'github';
|
|
401
|
+
const token = 'gho_test_token';
|
|
402
|
+
|
|
403
|
+
// Mock API responses
|
|
404
|
+
(global.fetch as any).mockResolvedValue({
|
|
405
|
+
ok: true,
|
|
406
|
+
headers: {
|
|
407
|
+
get: (name: string) => (name === 'X-OAuth-Scopes' ? 'repo' : null),
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// First call
|
|
412
|
+
await scopeChecker.checkScopes(provider, token, 1); // 1 second TTL
|
|
413
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
414
|
+
|
|
415
|
+
// Within TTL - should use cache
|
|
416
|
+
await scopeChecker.checkScopes(provider, token, 1);
|
|
417
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
418
|
+
|
|
419
|
+
// Advance time past TTL
|
|
420
|
+
vi.advanceTimersByTime(2000);
|
|
421
|
+
|
|
422
|
+
// Should make new API call
|
|
423
|
+
await scopeChecker.checkScopes(provider, token, 1);
|
|
424
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
425
|
+
|
|
426
|
+
vi.useRealTimers();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('Error handling', () => {
|
|
431
|
+
it('should handle network errors gracefully', async () => {
|
|
432
|
+
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
|
|
433
|
+
|
|
434
|
+
await expect(scopeChecker.checkScopes('github', 'token')).rejects.toThrow('Network error');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should handle malformed API responses', async () => {
|
|
438
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
439
|
+
ok: true,
|
|
440
|
+
json: async () => {
|
|
441
|
+
throw new Error('Invalid JSON');
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await expect(scopeChecker.checkScopes('google', 'token')).rejects.toThrow();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should handle missing provider gracefully', async () => {
|
|
449
|
+
const userId = 'user123';
|
|
450
|
+
const nonexistentProvider = 'nonexistent';
|
|
451
|
+
|
|
452
|
+
const creds = await authProvider.getUserCredential(userId, nonexistentProvider);
|
|
453
|
+
|
|
454
|
+
expect(creds).toBeNull();
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|