Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/authentication/test/node/copilotToken.spec.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
import { DeferredPromise } from '../../../../util/vs/base/common/async';
8
import { Event } from '../../../../util/vs/base/common/event';
9
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
10
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
11
import { ICAPIClientService } from '../../../endpoint/common/capiClient';
12
import { IDomainService } from '../../../endpoint/common/domainService';
13
import { IEnvService } from '../../../env/common/envService';
14
import { NullBaseOctoKitService } from '../../../github/common/nullOctokitServiceImpl';
15
import { ILogService } from '../../../log/common/logService';
16
import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response, WebSocketConnection } from '../../../networking/common/fetcherService';
17
import { ITelemetryService } from '../../../telemetry/common/telemetry';
18
import { createFakeResponse } from '../../../test/node/fetcher';
19
import { createPlatformServices, ITestingServicesAccessor } from '../../../test/node/services';
20
import { CopilotToken, createTestExtendedTokenInfo, isErrorEnvelope, isStandardErrorEnvelope, isTokenEnvelope, validateTokenEnvelope } from '../../common/copilotToken';
21
import { BaseCopilotTokenManager, CopilotTokenManagerFromGitHubToken } from '../../node/copilotTokenManager';
22
23
// This is a fake version of CopilotTokenManagerFromGitHubToken.
24
class RefreshFakeCopilotTokenManager extends BaseCopilotTokenManager {
25
calls = 0;
26
constructor(
27
private readonly throwErrorCount: number,
28
@ILogService logService: ILogService,
29
@ITelemetryService telemetryService: ITelemetryService,
30
@IDomainService domainService: IDomainService,
31
@ICAPIClientService capiClientService: ICAPIClientService,
32
@IFetcherService fetcherService: IFetcherService,
33
@IEnvService envService: IEnvService,
34
) {
35
super(new NullBaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService);
36
}
37
38
async getCopilotToken(force?: boolean): Promise<CopilotToken> {
39
this.calls++;
40
await new Promise(resolve => setTimeout(resolve, 10));
41
if (this.calls === this.throwErrorCount) {
42
throw new Error('fake error');
43
}
44
if (!force && this.copilotToken) {
45
return new CopilotToken(this.copilotToken);
46
}
47
this.copilotToken = createTestExtendedTokenInfo({ token: 'done', username: 'fake', copilot_plan: 'unknown' });
48
return new CopilotToken(this.copilotToken);
49
}
50
}
51
52
describe('Copilot token unit tests', function () {
53
let accessor: ITestingServicesAccessor;
54
let disposables: DisposableStore;
55
56
beforeEach(() => {
57
disposables = new DisposableStore();
58
accessor = disposables.add(createPlatformServices().createTestingAccessor());
59
});
60
61
afterEach(() => {
62
disposables.dispose();
63
});
64
65
it('includes editor information in token request', async function () {
66
const fetcher = new StaticFetcherService({
67
token: 'token',
68
expires_at: 1,
69
refresh_in: 1,
70
});
71
const testingServiceCollection = createPlatformServices();
72
testingServiceCollection.define(IFetcherService, fetcher);
73
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
74
75
const tokenManager = disposables.add(accessor.get(IInstantiationService).createInstance(RefreshFakeCopilotTokenManager, 1));
76
await tokenManager.authFromGitHubToken('fake-token', 'fake-user');
77
78
expect(fetcher.requests.size).toBe(2);
79
});
80
81
it(`notifies about token on token retrieval`, async function () {
82
const tokenManager = disposables.add(accessor.get(IInstantiationService).createInstance(RefreshFakeCopilotTokenManager, 3));
83
const deferredTokenPromise = new DeferredPromise<CopilotToken>();
84
tokenManager.onDidCopilotTokenRefresh(async () => {
85
const notifiedValue = await tokenManager.getCopilotToken();
86
deferredTokenPromise.complete(notifiedValue);
87
});
88
await tokenManager.getCopilotToken(true);
89
const notifiedValue = await deferredTokenPromise.p;
90
expect(notifiedValue.token).toBe('done');
91
});
92
93
it('invalid GitHub token', async function () {
94
const fetcher = new StaticFetcherService({
95
can_signup_for_limited: false,
96
message: 'You do not have access to Copilot',
97
error_details: {
98
message: 'fake error message',
99
url: 'https://github.com/settings?param={EDITOR}',
100
notification_id: 'fake-notification-id',
101
title: 'Access Denied',
102
},
103
});
104
105
const testingServiceCollection = createPlatformServices();
106
testingServiceCollection.define(IFetcherService, fetcher);
107
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
108
109
const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'invalid', 'invalid-user');
110
const result = await tokenManager.checkCopilotToken();
111
expect(result).toEqual({
112
kind: 'failure',
113
reason: 'NotAuthorized',
114
message: 'fake error message',
115
notification_id: 'fake-notification-id',
116
url: 'https://github.com/settings?param={EDITOR}',
117
title: 'Access Denied',
118
});
119
});
120
121
it('network request failed', async function () {
122
const fetcher = new StaticFetcherService('NETWORK_FAILURE'); // special sentinel simulates network failure
123
124
const testingServiceCollection = createPlatformServices();
125
testingServiceCollection.define(IFetcherService, fetcher);
126
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
127
128
const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'valid', 'valid-user');
129
const result = await tokenManager.checkCopilotToken();
130
expect(result).toEqual({
131
kind: 'failure',
132
message: 'Network request failed',
133
reason: 'RequestFailed',
134
});
135
});
136
137
it('JSON parse failed', async function () {
138
const fetcher = new StaticFetcherService(null); // null tokenInfo simulates parse failure (JSON.parse returns null)
139
140
const testingServiceCollection = createPlatformServices();
141
testingServiceCollection.define(IFetcherService, fetcher);
142
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
143
144
const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'valid', 'valid-user');
145
const result = await tokenManager.checkCopilotToken();
146
expect(result).toEqual({
147
kind: 'failure',
148
message: 'Response is not valid: null',
149
reason: 'ParseFailed',
150
});
151
});
152
153
it('properly propagates errors', async function () {
154
const expectedError = new Error('to be handled');
155
156
const testingServiceCollection = createPlatformServices();
157
testingServiceCollection.define(IFetcherService, new ErrorFetcherService(expectedError));
158
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
159
160
const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'invalid', 'invalid-user');
161
try {
162
await tokenManager.checkCopilotToken();
163
} catch (err: any) {
164
expect(err).toBe(expectedError);
165
}
166
});
167
168
it('ignore v1 token', async function () {
169
const token =
170
'0123456789abcdef0123456789abcdef:org1.com:1674258990:0000000000000000000000000000000000000000000000000000000000000000';
171
172
const copilotToken = new CopilotToken(createTestExtendedTokenInfo({ token, username: 'fake', copilot_plan: 'unknown' }));
173
expect(copilotToken.getTokenValue('tid')).toBeUndefined();
174
});
175
176
it('parsing v2 token', async function () {
177
const token =
178
'tid=0123456789abcdef0123456789abcdef;dom=org1.com;ol=org1,org2;exp=1674258990:0000000000000000000000000000000000000000000000000000000000000000';
179
180
const copilotToken = new CopilotToken(createTestExtendedTokenInfo({ token, username: 'fake', copilot_plan: 'unknown' }));
181
expect(copilotToken.getTokenValue('tid')).toBe('0123456789abcdef0123456789abcdef');
182
});
183
184
it('parsing v2 token, multiple values', async function () {
185
const token =
186
'tid=0123456789abcdef0123456789abcdef;rt=1;ssc=0;dom=org1.com;ol=org1,org2;exp=1674258990:0000000000000000000000000000000000000000000000000000000000000000';
187
188
const copilotToken = new CopilotToken(createTestExtendedTokenInfo({ token, username: 'fake', copilot_plan: 'unknown' }));
189
expect(copilotToken.getTokenValue('rt')).toBe('1');
190
expect(copilotToken.getTokenValue('ssc')).toBe('0');
191
expect(copilotToken.getTokenValue('foo')).toBeUndefined();
192
});
193
194
it('With a GitHub Enterprise configuration, retrieves token from the GHEC server', async () => {
195
const ghecConfig: IDomainService = {
196
_serviceBrand: undefined,
197
onDidChangeDomains: Event.None,
198
};
199
const fetcher = new StaticFetcherService({
200
token: 'token',
201
expires_at: 1,
202
refresh_in: 1,
203
});
204
205
const testingServiceCollection = createPlatformServices();
206
testingServiceCollection.define(IDomainService, ghecConfig);
207
testingServiceCollection.define(IFetcherService, fetcher);
208
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
209
210
const tokenManager = disposables.add(accessor.get(IInstantiationService).createInstance(RefreshFakeCopilotTokenManager, 1));
211
await tokenManager.authFromGitHubToken('fake-token', 'invalid-user');
212
213
expect(fetcher.requests.size).toBe(2);
214
});
215
216
it('rate limiting (StandardErrorEnvelope)', async function () {
217
const fetcher = new StaticFetcherService({
218
message: 'API rate limit exceeded for user ID 12345.',
219
documentation_url: 'https://developer.github.com/rest/overview/rate-limits-for-the-rest-api',
220
status: '403',
221
});
222
223
const testingServiceCollection = createPlatformServices();
224
testingServiceCollection.define(IFetcherService, fetcher);
225
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
226
227
const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'valid', 'valid-user');
228
const result = await tokenManager.checkCopilotToken();
229
expect(result).toEqual({
230
kind: 'failure',
231
reason: 'RateLimited',
232
});
233
});
234
235
it('HTTP 401 unauthorized', async function () {
236
const fetcher = new HttpStatusFetcherService(401);
237
238
const testingServiceCollection = createPlatformServices();
239
testingServiceCollection.define(IFetcherService, fetcher);
240
accessor = disposables.add(testingServiceCollection.createTestingAccessor());
241
242
const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'bad-token', 'bad-user');
243
const result = await tokenManager.checkCopilotToken();
244
expect(result).toEqual({
245
kind: 'failure',
246
reason: 'HTTP401',
247
});
248
});
249
});
250
251
describe('Token envelope validators', function () {
252
it('isTokenEnvelope returns true for valid token', function () {
253
const validToken = {
254
token: 'test-token',
255
expires_at: 1234567890,
256
refresh_in: 300,
257
sku: 'free_limited_copilot',
258
individual: true,
259
blackbird_clientside_indexing: false,
260
code_quote_enabled: false,
261
code_review_enabled: false,
262
codesearch: false,
263
copilotignore_enabled: false,
264
vsc_electron_fetcher_v2: false,
265
public_suggestions: 'enabled',
266
telemetry: 'enabled',
267
};
268
expect(isTokenEnvelope(validToken)).toBe(true);
269
});
270
271
it('isTokenEnvelope returns true when limited_user_quotas and limited_user_reset_date are null', function () {
272
// Enterprise/paid users get null for these fields
273
const validToken = {
274
token: 'test-token',
275
expires_at: 1234567890,
276
refresh_in: 300,
277
sku: 'free_limited_copilot',
278
individual: true,
279
blackbird_clientside_indexing: false,
280
code_quote_enabled: false,
281
code_review_enabled: false,
282
codesearch: false,
283
copilotignore_enabled: false,
284
vsc_electron_fetcher_v2: false,
285
public_suggestions: 'enabled',
286
telemetry: 'enabled',
287
limited_user_quotas: null,
288
limited_user_reset_date: null,
289
};
290
expect(isTokenEnvelope(validToken)).toBe(true);
291
});
292
293
it('isTokenEnvelope returns false for missing required fields', function () {
294
expect(isTokenEnvelope({})).toBe(false);
295
expect(isTokenEnvelope({ token: 'test' })).toBe(false);
296
expect(isTokenEnvelope({ token: 'test', expires_at: 123 })).toBe(false);
297
expect(isTokenEnvelope(null)).toBe(false);
298
expect(isTokenEnvelope(undefined)).toBe(false);
299
});
300
301
it('isErrorEnvelope returns true for valid error envelope', function () {
302
const validError = {
303
can_signup_for_limited: false,
304
message: 'Access denied',
305
error_details: {
306
message: 'You do not have access',
307
notification_id: 'no_copilot_access',
308
title: 'No Access',
309
url: 'https://github.com/settings/copilot',
310
},
311
};
312
expect(isErrorEnvelope(validError)).toBe(true);
313
});
314
315
it('isErrorEnvelope returns false for invalid structures', function () {
316
expect(isErrorEnvelope({})).toBe(false);
317
expect(isErrorEnvelope({ message: 'error' })).toBe(false);
318
expect(isErrorEnvelope({ error_details: {} })).toBe(false);
319
expect(isErrorEnvelope(null)).toBe(false);
320
});
321
322
it('isStandardErrorEnvelope returns true for rate limit response', function () {
323
const rateLimitError = {
324
message: 'API rate limit exceeded for user ID 12345.',
325
documentation_url: 'https://developer.github.com/rest/overview/rate-limits-for-the-rest-api',
326
status: '403',
327
};
328
expect(isStandardErrorEnvelope(rateLimitError)).toBe(true);
329
});
330
331
it('isStandardErrorEnvelope returns false for invalid structures', function () {
332
expect(isStandardErrorEnvelope({})).toBe(false);
333
expect(isStandardErrorEnvelope({ message: 'error' })).toBe(false);
334
expect(isStandardErrorEnvelope(null)).toBe(false);
335
});
336
337
describe('validateTokenEnvelope', function () {
338
it('returns strict strategy for fully valid token envelope', function () {
339
const validToken = {
340
token: 'test-token',
341
expires_at: 1234567890,
342
refresh_in: 300,
343
sku: 'free_limited_copilot',
344
individual: true,
345
blackbird_clientside_indexing: false,
346
code_quote_enabled: false,
347
code_review_enabled: false,
348
codesearch: false,
349
copilotignore_enabled: false,
350
vsc_electron_fetcher_v2: false,
351
public_suggestions: 'enabled',
352
telemetry: 'enabled',
353
};
354
const result = validateTokenEnvelope(validToken);
355
expect(result.valid).toBe(true);
356
expect(result.strategy).toBe('strict');
357
if (result.strategy === 'strict') {
358
expect(result.envelope).toBeDefined();
359
expect(result.envelope.token).toBe('test-token');
360
expect(result.envelope.expires_at).toBe(1234567890);
361
expect(result.envelope.refresh_in).toBe(300);
362
expect(result.envelope.sku).toBe('free_limited_copilot');
363
}
364
});
365
366
it('returns strict strategy for minimal token with only required fields', function () {
367
// The strict validator only requires token, expires_at, refresh_in
368
// Other fields are optional, so a minimal token passes strict validation
369
const minimalToken = {
370
token: 'test-token',
371
expires_at: 1234567890,
372
refresh_in: 300,
373
};
374
const result = validateTokenEnvelope(minimalToken);
375
expect(result.valid).toBe(true);
376
expect(result.strategy).toBe('strict');
377
if (result.strategy === 'strict') {
378
expect(result.envelope).toBeDefined();
379
expect(result.envelope.token).toBe('test-token');
380
expect(result.envelope.expires_at).toBe(1234567890);
381
expect(result.envelope.refresh_in).toBe(300);
382
}
383
});
384
385
it('returns fallback strategy when optional field has wrong type', function () {
386
// Server changes sku from string to number - strict fails, fallback succeeds
387
const tokenWithWrongOptionalType = {
388
token: 'test-token',
389
expires_at: 1234567890,
390
refresh_in: 300,
391
sku: 12345, // wrong type - should be string
392
};
393
const result = validateTokenEnvelope(tokenWithWrongOptionalType);
394
expect(result.valid).toBe(true);
395
expect(result.strategy).toBe('fallback');
396
if (result.strategy === 'fallback') {
397
expect(result.strictError).toContain('sku');
398
expect(result.fallbackError).toBeUndefined();
399
// Envelope is returned with critical fields even when fallback is used
400
expect(result.envelope).toBeDefined();
401
expect(result.envelope.token).toBe('test-token');
402
expect(result.envelope.expires_at).toBe(1234567890);
403
expect(result.envelope.refresh_in).toBe(300);
404
}
405
});
406
407
it('returns fallback strategy when server changes enum values', function () {
408
const tokenWithNewEnumValue = {
409
token: 'test-token',
410
expires_at: 1234567890,
411
refresh_in: 300,
412
public_suggestions: 'new_unknown_value', // not in enum
413
};
414
const result = validateTokenEnvelope(tokenWithNewEnumValue);
415
expect(result.valid).toBe(true);
416
expect(result.strategy).toBe('fallback');
417
if (result.strategy === 'fallback') {
418
expect(result.strictError).toContain('public_suggestions');
419
// Envelope is returned with critical fields
420
expect(result.envelope).toBeDefined();
421
expect(result.envelope.token).toBe('test-token');
422
}
423
});
424
425
it('returns failed strategy when missing critical token field', function () {
426
const missingToken = {
427
expires_at: 1234567890,
428
refresh_in: 300,
429
};
430
const result = validateTokenEnvelope(missingToken);
431
expect(result.valid).toBe(false);
432
expect(result.strategy).toBe('failed');
433
if (result.strategy === 'failed') {
434
expect(result.strictError).toBeDefined();
435
expect(result.fallbackError).toContain('token');
436
}
437
});
438
439
it('returns failed strategy when missing critical expires_at field', function () {
440
const missingExpiresAt = {
441
token: 'test-token',
442
refresh_in: 300,
443
};
444
const result = validateTokenEnvelope(missingExpiresAt);
445
expect(result.valid).toBe(false);
446
expect(result.strategy).toBe('failed');
447
if (result.strategy === 'failed') {
448
expect(result.fallbackError).toContain('expires_at');
449
}
450
});
451
452
it('returns failed strategy when missing critical refresh_in field', function () {
453
const missingRefreshIn = {
454
token: 'test-token',
455
expires_at: 1234567890,
456
};
457
const result = validateTokenEnvelope(missingRefreshIn);
458
expect(result.valid).toBe(false);
459
expect(result.strategy).toBe('failed');
460
if (result.strategy === 'failed') {
461
expect(result.fallbackError).toContain('refresh_in');
462
}
463
});
464
465
it('returns failed strategy for null input', function () {
466
const result = validateTokenEnvelope(null);
467
expect(result.valid).toBe(false);
468
expect(result.strategy).toBe('failed');
469
});
470
471
it('returns failed strategy for undefined input', function () {
472
const result = validateTokenEnvelope(undefined);
473
expect(result.valid).toBe(false);
474
expect(result.strategy).toBe('failed');
475
});
476
477
it('returns failed strategy when critical field has wrong type', function () {
478
const wrongTypeToken = {
479
token: 12345, // should be string
480
expires_at: 1234567890,
481
refresh_in: 300,
482
};
483
const result = validateTokenEnvelope(wrongTypeToken);
484
expect(result.valid).toBe(false);
485
expect(result.strategy).toBe('failed');
486
if (result.strategy === 'failed') {
487
expect(result.fallbackError).toContain('token');
488
}
489
});
490
});
491
});
492
493
describe('CopilotToken class', function () {
494
it('isFreeUser returns true for free_limited_copilot sku', function () {
495
const token = new CopilotToken(createTestExtendedTokenInfo({ sku: 'free_limited_copilot' }));
496
expect(token.isFreeUser).toBe(true);
497
expect(token.isNoAuthUser).toBe(false);
498
});
499
500
it('isNoAuthUser returns true for no_auth_limited_copilot sku', function () {
501
const token = new CopilotToken(createTestExtendedTokenInfo({ sku: 'no_auth_limited_copilot' }));
502
expect(token.isFreeUser).toBe(false);
503
expect(token.isNoAuthUser).toBe(true);
504
});
505
506
it('isTelemetryEnabled reflects token state', function () {
507
const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ telemetry: 'enabled' }));
508
const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ telemetry: 'disabled' }));
509
expect(enabledToken.isTelemetryEnabled()).toBe(true);
510
expect(disabledToken.isTelemetryEnabled()).toBe(false);
511
});
512
513
it('isPublicSuggestionsEnabled reflects token state', function () {
514
const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ public_suggestions: 'enabled' }));
515
const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ public_suggestions: 'disabled' }));
516
const unconfiguredToken = new CopilotToken(createTestExtendedTokenInfo({ public_suggestions: 'unconfigured' }));
517
expect(enabledToken.isPublicSuggestionsEnabled()).toBe(true);
518
expect(disabledToken.isPublicSuggestionsEnabled()).toBe(false);
519
expect(unconfiguredToken.isPublicSuggestionsEnabled()).toBe(false);
520
});
521
522
it('copilotPlan returns correct plan type', function () {
523
const freeToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'free_limited_copilot', copilot_plan: 'free' }));
524
const individualToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_individual', copilot_plan: 'individual' }));
525
const businessToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_business', copilot_plan: 'business' }));
526
const enterpriseToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_enterprise', copilot_plan: 'enterprise' }));
527
528
expect(freeToken.copilotPlan).toBe('free');
529
expect(individualToken.copilotPlan).toBe('individual');
530
expect(businessToken.copilotPlan).toBe('business');
531
expect(enterpriseToken.copilotPlan).toBe('enterprise');
532
});
533
534
it('isChatQuotaExceeded for free users with zero quota', function () {
535
const exceededToken = new CopilotToken(createTestExtendedTokenInfo({
536
sku: 'free_limited_copilot',
537
limited_user_quotas: { chat: 0, completions: 10 }
538
}));
539
const notExceededToken = new CopilotToken(createTestExtendedTokenInfo({
540
sku: 'free_limited_copilot',
541
limited_user_quotas: { chat: 5, completions: 10 }
542
}));
543
const nonFreeToken = new CopilotToken(createTestExtendedTokenInfo({
544
sku: 'copilot_individual',
545
limited_user_quotas: { chat: 0, completions: 0 }
546
}));
547
548
expect(exceededToken.isChatQuotaExceeded).toBe(true);
549
expect(notExceededToken.isChatQuotaExceeded).toBe(false);
550
expect(nonFreeToken.isChatQuotaExceeded).toBe(false); // Non-free users don't have quota limits
551
});
552
553
it('isCompletionsQuotaExceeded for free users with zero quota', function () {
554
const exceededToken = new CopilotToken(createTestExtendedTokenInfo({
555
sku: 'free_limited_copilot',
556
limited_user_quotas: { chat: 10, completions: 0 }
557
}));
558
const notExceededToken = new CopilotToken(createTestExtendedTokenInfo({
559
sku: 'free_limited_copilot',
560
limited_user_quotas: { chat: 10, completions: 5 }
561
}));
562
563
expect(exceededToken.isCompletionsQuotaExceeded).toBe(true);
564
expect(notExceededToken.isCompletionsQuotaExceeded).toBe(false);
565
});
566
567
it('isInternal detects GitHub and Microsoft organizations', function () {
568
const githubOrgToken = new CopilotToken(createTestExtendedTokenInfo({
569
organization_list: ['4535c7beffc844b46bb1ed4aa04d759a']
570
}));
571
const microsoftOrgToken = new CopilotToken(createTestExtendedTokenInfo({
572
organization_list: ['a5db0bcaae94032fe715fb34a5e4bce2']
573
}));
574
const externalToken = new CopilotToken(createTestExtendedTokenInfo({
575
organization_list: ['some-other-org']
576
}));
577
const noOrgToken = new CopilotToken(createTestExtendedTokenInfo({
578
organization_list: []
579
}));
580
581
expect(githubOrgToken.isInternal).toBe(true);
582
expect(githubOrgToken.isGitHubInternal).toBe(true);
583
expect(githubOrgToken.isMicrosoftInternal).toBe(false);
584
585
expect(microsoftOrgToken.isInternal).toBe(true);
586
expect(microsoftOrgToken.isGitHubInternal).toBe(false);
587
expect(microsoftOrgToken.isMicrosoftInternal).toBe(true);
588
589
expect(externalToken.isInternal).toBe(false);
590
expect(noOrgToken.isInternal).toBe(false);
591
});
592
593
it('codeQuoteEnabled reflects token state', function () {
594
const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_quote_enabled: true }));
595
const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_quote_enabled: false }));
596
expect(enabledToken.codeQuoteEnabled).toBe(true);
597
expect(disabledToken.codeQuoteEnabled).toBe(false);
598
});
599
600
it('isCopilotCodeReviewEnabled reflects token state', function () {
601
const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_review_enabled: true }));
602
const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_review_enabled: false }));
603
expect(enabledToken.isCopilotCodeReviewEnabled).toBe(true);
604
expect(disabledToken.isCopilotCodeReviewEnabled).toBe(false);
605
});
606
607
it('isExpandedClientSideIndexingEnabled reflects token state', function () {
608
const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ blackbird_clientside_indexing: true }));
609
const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ blackbird_clientside_indexing: false }));
610
expect(enabledToken.isExpandedClientSideIndexingEnabled()).toBe(true);
611
expect(disabledToken.isExpandedClientSideIndexingEnabled()).toBe(false);
612
});
613
});
614
615
class StaticFetcherService implements IFetcherService {
616
617
declare readonly _serviceBrand: undefined;
618
readonly onDidFetch = Event.None;
619
readonly onDidCompleteFetch = Event.None;
620
621
public requests = new Map<string, FetchOptions>();
622
constructor(readonly tokenResponse: any) {
623
}
624
625
fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {
626
throw new Error('Method not implemented.');
627
}
628
629
getUserAgentLibrary(): string {
630
return 'test';
631
}
632
async fetch(url: string, options: FetchOptions): Promise<Response> {
633
this.requests.set(url, options);
634
if (url.endsWith('copilot_internal/v2/token')) {
635
if (this.tokenResponse === 'NETWORK_FAILURE') {
636
// Simulate network failure - fetch throws
637
throw new Error('Network request failed');
638
}
639
// null will parse successfully as JSON (returns null) but fails tokenInfo check
640
return createFakeResponse(200, this.tokenResponse);
641
} else if (url.endsWith('copilot_internal/notification')) {
642
return createFakeResponse(200, '');
643
}
644
return createFakeResponse(404, '');
645
}
646
createWebSocket(_url: string): WebSocketConnection {
647
throw new Error('Method not implemented.');
648
}
649
disconnectAll(): Promise<unknown> {
650
throw new Error('Method not implemented.');
651
}
652
makeAbortController(): IAbortController {
653
throw new Error('Method not implemented.');
654
}
655
isAbortError(e: any): boolean {
656
throw new Error('Method not implemented.');
657
}
658
isInternetDisconnectedError(e: any): boolean {
659
throw new Error('Method not implemented.');
660
}
661
isFetcherError(err: any): boolean {
662
throw new Error('Method not implemented.');
663
}
664
isNetworkProcessCrashedError(err: any): boolean {
665
throw new Error('Method not implemented.');
666
}
667
getUserMessageForFetcherError(err: any): string {
668
throw new Error('Method not implemented.');
669
}
670
}
671
672
class ErrorFetcherService extends StaticFetcherService {
673
constructor(private readonly error: any) {
674
super({});
675
}
676
677
override fetch(url: string, options: FetchOptions): Promise<Response> {
678
throw this.error;
679
}
680
}
681
682
class HttpStatusFetcherService extends StaticFetcherService {
683
constructor(private readonly status: number) {
684
super({});
685
}
686
687
override async fetch(url: string, options: FetchOptions): Promise<Response> {
688
this.requests.set(url, options);
689
return createFakeResponse(this.status, {});
690
}
691
}
692
693