Path: blob/main/extensions/copilot/src/platform/authentication/test/node/copilotToken.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { afterEach, beforeEach, describe, expect, it } from 'vitest';6import { DeferredPromise } from '../../../../util/vs/base/common/async';7import { Event } from '../../../../util/vs/base/common/event';8import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';9import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';10import { ICAPIClientService } from '../../../endpoint/common/capiClient';11import { IDomainService } from '../../../endpoint/common/domainService';12import { IEnvService } from '../../../env/common/envService';13import { NullBaseOctoKitService } from '../../../github/common/nullOctokitServiceImpl';14import { ILogService } from '../../../log/common/logService';15import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response, WebSocketConnection } from '../../../networking/common/fetcherService';16import { ITelemetryService } from '../../../telemetry/common/telemetry';17import { createFakeResponse } from '../../../test/node/fetcher';18import { createPlatformServices, ITestingServicesAccessor } from '../../../test/node/services';19import { CopilotToken, createTestExtendedTokenInfo, isErrorEnvelope, isStandardErrorEnvelope, isTokenEnvelope, validateTokenEnvelope } from '../../common/copilotToken';20import { BaseCopilotTokenManager, CopilotTokenManagerFromGitHubToken } from '../../node/copilotTokenManager';2122// This is a fake version of CopilotTokenManagerFromGitHubToken.23class RefreshFakeCopilotTokenManager extends BaseCopilotTokenManager {24calls = 0;25constructor(26private readonly throwErrorCount: number,27@ILogService logService: ILogService,28@ITelemetryService telemetryService: ITelemetryService,29@IDomainService domainService: IDomainService,30@ICAPIClientService capiClientService: ICAPIClientService,31@IFetcherService fetcherService: IFetcherService,32@IEnvService envService: IEnvService,33) {34super(new NullBaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService);35}3637async getCopilotToken(force?: boolean): Promise<CopilotToken> {38this.calls++;39await new Promise(resolve => setTimeout(resolve, 10));40if (this.calls === this.throwErrorCount) {41throw new Error('fake error');42}43if (!force && this.copilotToken) {44return new CopilotToken(this.copilotToken);45}46this.copilotToken = createTestExtendedTokenInfo({ token: 'done', username: 'fake', copilot_plan: 'unknown' });47return new CopilotToken(this.copilotToken);48}49}5051describe('Copilot token unit tests', function () {52let accessor: ITestingServicesAccessor;53let disposables: DisposableStore;5455beforeEach(() => {56disposables = new DisposableStore();57accessor = disposables.add(createPlatformServices().createTestingAccessor());58});5960afterEach(() => {61disposables.dispose();62});6364it('includes editor information in token request', async function () {65const fetcher = new StaticFetcherService({66token: 'token',67expires_at: 1,68refresh_in: 1,69});70const testingServiceCollection = createPlatformServices();71testingServiceCollection.define(IFetcherService, fetcher);72accessor = disposables.add(testingServiceCollection.createTestingAccessor());7374const tokenManager = disposables.add(accessor.get(IInstantiationService).createInstance(RefreshFakeCopilotTokenManager, 1));75await tokenManager.authFromGitHubToken('fake-token', 'fake-user');7677expect(fetcher.requests.size).toBe(2);78});7980it(`notifies about token on token retrieval`, async function () {81const tokenManager = disposables.add(accessor.get(IInstantiationService).createInstance(RefreshFakeCopilotTokenManager, 3));82const deferredTokenPromise = new DeferredPromise<CopilotToken>();83tokenManager.onDidCopilotTokenRefresh(async () => {84const notifiedValue = await tokenManager.getCopilotToken();85deferredTokenPromise.complete(notifiedValue);86});87await tokenManager.getCopilotToken(true);88const notifiedValue = await deferredTokenPromise.p;89expect(notifiedValue.token).toBe('done');90});9192it('invalid GitHub token', async function () {93const fetcher = new StaticFetcherService({94can_signup_for_limited: false,95message: 'You do not have access to Copilot',96error_details: {97message: 'fake error message',98url: 'https://github.com/settings?param={EDITOR}',99notification_id: 'fake-notification-id',100title: 'Access Denied',101},102});103104const testingServiceCollection = createPlatformServices();105testingServiceCollection.define(IFetcherService, fetcher);106accessor = disposables.add(testingServiceCollection.createTestingAccessor());107108const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'invalid', 'invalid-user');109const result = await tokenManager.checkCopilotToken();110expect(result).toEqual({111kind: 'failure',112reason: 'NotAuthorized',113message: 'fake error message',114notification_id: 'fake-notification-id',115url: 'https://github.com/settings?param={EDITOR}',116title: 'Access Denied',117});118});119120it('network request failed', async function () {121const fetcher = new StaticFetcherService('NETWORK_FAILURE'); // special sentinel simulates network failure122123const testingServiceCollection = createPlatformServices();124testingServiceCollection.define(IFetcherService, fetcher);125accessor = disposables.add(testingServiceCollection.createTestingAccessor());126127const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'valid', 'valid-user');128const result = await tokenManager.checkCopilotToken();129expect(result).toEqual({130kind: 'failure',131message: 'Network request failed',132reason: 'RequestFailed',133});134});135136it('JSON parse failed', async function () {137const fetcher = new StaticFetcherService(null); // null tokenInfo simulates parse failure (JSON.parse returns null)138139const testingServiceCollection = createPlatformServices();140testingServiceCollection.define(IFetcherService, fetcher);141accessor = disposables.add(testingServiceCollection.createTestingAccessor());142143const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'valid', 'valid-user');144const result = await tokenManager.checkCopilotToken();145expect(result).toEqual({146kind: 'failure',147message: 'Response is not valid: null',148reason: 'ParseFailed',149});150});151152it('properly propagates errors', async function () {153const expectedError = new Error('to be handled');154155const testingServiceCollection = createPlatformServices();156testingServiceCollection.define(IFetcherService, new ErrorFetcherService(expectedError));157accessor = disposables.add(testingServiceCollection.createTestingAccessor());158159const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'invalid', 'invalid-user');160try {161await tokenManager.checkCopilotToken();162} catch (err: any) {163expect(err).toBe(expectedError);164}165});166167it('ignore v1 token', async function () {168const token =169'0123456789abcdef0123456789abcdef:org1.com:1674258990:0000000000000000000000000000000000000000000000000000000000000000';170171const copilotToken = new CopilotToken(createTestExtendedTokenInfo({ token, username: 'fake', copilot_plan: 'unknown' }));172expect(copilotToken.getTokenValue('tid')).toBeUndefined();173});174175it('parsing v2 token', async function () {176const token =177'tid=0123456789abcdef0123456789abcdef;dom=org1.com;ol=org1,org2;exp=1674258990:0000000000000000000000000000000000000000000000000000000000000000';178179const copilotToken = new CopilotToken(createTestExtendedTokenInfo({ token, username: 'fake', copilot_plan: 'unknown' }));180expect(copilotToken.getTokenValue('tid')).toBe('0123456789abcdef0123456789abcdef');181});182183it('parsing v2 token, multiple values', async function () {184const token =185'tid=0123456789abcdef0123456789abcdef;rt=1;ssc=0;dom=org1.com;ol=org1,org2;exp=1674258990:0000000000000000000000000000000000000000000000000000000000000000';186187const copilotToken = new CopilotToken(createTestExtendedTokenInfo({ token, username: 'fake', copilot_plan: 'unknown' }));188expect(copilotToken.getTokenValue('rt')).toBe('1');189expect(copilotToken.getTokenValue('ssc')).toBe('0');190expect(copilotToken.getTokenValue('foo')).toBeUndefined();191});192193it('With a GitHub Enterprise configuration, retrieves token from the GHEC server', async () => {194const ghecConfig: IDomainService = {195_serviceBrand: undefined,196onDidChangeDomains: Event.None,197};198const fetcher = new StaticFetcherService({199token: 'token',200expires_at: 1,201refresh_in: 1,202});203204const testingServiceCollection = createPlatformServices();205testingServiceCollection.define(IDomainService, ghecConfig);206testingServiceCollection.define(IFetcherService, fetcher);207accessor = disposables.add(testingServiceCollection.createTestingAccessor());208209const tokenManager = disposables.add(accessor.get(IInstantiationService).createInstance(RefreshFakeCopilotTokenManager, 1));210await tokenManager.authFromGitHubToken('fake-token', 'invalid-user');211212expect(fetcher.requests.size).toBe(2);213});214215it('rate limiting (StandardErrorEnvelope)', async function () {216const fetcher = new StaticFetcherService({217message: 'API rate limit exceeded for user ID 12345.',218documentation_url: 'https://developer.github.com/rest/overview/rate-limits-for-the-rest-api',219status: '403',220});221222const testingServiceCollection = createPlatformServices();223testingServiceCollection.define(IFetcherService, fetcher);224accessor = disposables.add(testingServiceCollection.createTestingAccessor());225226const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'valid', 'valid-user');227const result = await tokenManager.checkCopilotToken();228expect(result).toEqual({229kind: 'failure',230reason: 'RateLimited',231});232});233234it('HTTP 401 unauthorized', async function () {235const fetcher = new HttpStatusFetcherService(401);236237const testingServiceCollection = createPlatformServices();238testingServiceCollection.define(IFetcherService, fetcher);239accessor = disposables.add(testingServiceCollection.createTestingAccessor());240241const tokenManager = accessor.get(IInstantiationService).createInstance(CopilotTokenManagerFromGitHubToken, 'bad-token', 'bad-user');242const result = await tokenManager.checkCopilotToken();243expect(result).toEqual({244kind: 'failure',245reason: 'HTTP401',246});247});248});249250describe('Token envelope validators', function () {251it('isTokenEnvelope returns true for valid token', function () {252const validToken = {253token: 'test-token',254expires_at: 1234567890,255refresh_in: 300,256sku: 'free_limited_copilot',257individual: true,258blackbird_clientside_indexing: false,259code_quote_enabled: false,260code_review_enabled: false,261codesearch: false,262copilotignore_enabled: false,263vsc_electron_fetcher_v2: false,264public_suggestions: 'enabled',265telemetry: 'enabled',266};267expect(isTokenEnvelope(validToken)).toBe(true);268});269270it('isTokenEnvelope returns true when limited_user_quotas and limited_user_reset_date are null', function () {271// Enterprise/paid users get null for these fields272const validToken = {273token: 'test-token',274expires_at: 1234567890,275refresh_in: 300,276sku: 'free_limited_copilot',277individual: true,278blackbird_clientside_indexing: false,279code_quote_enabled: false,280code_review_enabled: false,281codesearch: false,282copilotignore_enabled: false,283vsc_electron_fetcher_v2: false,284public_suggestions: 'enabled',285telemetry: 'enabled',286limited_user_quotas: null,287limited_user_reset_date: null,288};289expect(isTokenEnvelope(validToken)).toBe(true);290});291292it('isTokenEnvelope returns false for missing required fields', function () {293expect(isTokenEnvelope({})).toBe(false);294expect(isTokenEnvelope({ token: 'test' })).toBe(false);295expect(isTokenEnvelope({ token: 'test', expires_at: 123 })).toBe(false);296expect(isTokenEnvelope(null)).toBe(false);297expect(isTokenEnvelope(undefined)).toBe(false);298});299300it('isErrorEnvelope returns true for valid error envelope', function () {301const validError = {302can_signup_for_limited: false,303message: 'Access denied',304error_details: {305message: 'You do not have access',306notification_id: 'no_copilot_access',307title: 'No Access',308url: 'https://github.com/settings/copilot',309},310};311expect(isErrorEnvelope(validError)).toBe(true);312});313314it('isErrorEnvelope returns false for invalid structures', function () {315expect(isErrorEnvelope({})).toBe(false);316expect(isErrorEnvelope({ message: 'error' })).toBe(false);317expect(isErrorEnvelope({ error_details: {} })).toBe(false);318expect(isErrorEnvelope(null)).toBe(false);319});320321it('isStandardErrorEnvelope returns true for rate limit response', function () {322const rateLimitError = {323message: 'API rate limit exceeded for user ID 12345.',324documentation_url: 'https://developer.github.com/rest/overview/rate-limits-for-the-rest-api',325status: '403',326};327expect(isStandardErrorEnvelope(rateLimitError)).toBe(true);328});329330it('isStandardErrorEnvelope returns false for invalid structures', function () {331expect(isStandardErrorEnvelope({})).toBe(false);332expect(isStandardErrorEnvelope({ message: 'error' })).toBe(false);333expect(isStandardErrorEnvelope(null)).toBe(false);334});335336describe('validateTokenEnvelope', function () {337it('returns strict strategy for fully valid token envelope', function () {338const validToken = {339token: 'test-token',340expires_at: 1234567890,341refresh_in: 300,342sku: 'free_limited_copilot',343individual: true,344blackbird_clientside_indexing: false,345code_quote_enabled: false,346code_review_enabled: false,347codesearch: false,348copilotignore_enabled: false,349vsc_electron_fetcher_v2: false,350public_suggestions: 'enabled',351telemetry: 'enabled',352};353const result = validateTokenEnvelope(validToken);354expect(result.valid).toBe(true);355expect(result.strategy).toBe('strict');356if (result.strategy === 'strict') {357expect(result.envelope).toBeDefined();358expect(result.envelope.token).toBe('test-token');359expect(result.envelope.expires_at).toBe(1234567890);360expect(result.envelope.refresh_in).toBe(300);361expect(result.envelope.sku).toBe('free_limited_copilot');362}363});364365it('returns strict strategy for minimal token with only required fields', function () {366// The strict validator only requires token, expires_at, refresh_in367// Other fields are optional, so a minimal token passes strict validation368const minimalToken = {369token: 'test-token',370expires_at: 1234567890,371refresh_in: 300,372};373const result = validateTokenEnvelope(minimalToken);374expect(result.valid).toBe(true);375expect(result.strategy).toBe('strict');376if (result.strategy === 'strict') {377expect(result.envelope).toBeDefined();378expect(result.envelope.token).toBe('test-token');379expect(result.envelope.expires_at).toBe(1234567890);380expect(result.envelope.refresh_in).toBe(300);381}382});383384it('returns fallback strategy when optional field has wrong type', function () {385// Server changes sku from string to number - strict fails, fallback succeeds386const tokenWithWrongOptionalType = {387token: 'test-token',388expires_at: 1234567890,389refresh_in: 300,390sku: 12345, // wrong type - should be string391};392const result = validateTokenEnvelope(tokenWithWrongOptionalType);393expect(result.valid).toBe(true);394expect(result.strategy).toBe('fallback');395if (result.strategy === 'fallback') {396expect(result.strictError).toContain('sku');397expect(result.fallbackError).toBeUndefined();398// Envelope is returned with critical fields even when fallback is used399expect(result.envelope).toBeDefined();400expect(result.envelope.token).toBe('test-token');401expect(result.envelope.expires_at).toBe(1234567890);402expect(result.envelope.refresh_in).toBe(300);403}404});405406it('returns fallback strategy when server changes enum values', function () {407const tokenWithNewEnumValue = {408token: 'test-token',409expires_at: 1234567890,410refresh_in: 300,411public_suggestions: 'new_unknown_value', // not in enum412};413const result = validateTokenEnvelope(tokenWithNewEnumValue);414expect(result.valid).toBe(true);415expect(result.strategy).toBe('fallback');416if (result.strategy === 'fallback') {417expect(result.strictError).toContain('public_suggestions');418// Envelope is returned with critical fields419expect(result.envelope).toBeDefined();420expect(result.envelope.token).toBe('test-token');421}422});423424it('returns failed strategy when missing critical token field', function () {425const missingToken = {426expires_at: 1234567890,427refresh_in: 300,428};429const result = validateTokenEnvelope(missingToken);430expect(result.valid).toBe(false);431expect(result.strategy).toBe('failed');432if (result.strategy === 'failed') {433expect(result.strictError).toBeDefined();434expect(result.fallbackError).toContain('token');435}436});437438it('returns failed strategy when missing critical expires_at field', function () {439const missingExpiresAt = {440token: 'test-token',441refresh_in: 300,442};443const result = validateTokenEnvelope(missingExpiresAt);444expect(result.valid).toBe(false);445expect(result.strategy).toBe('failed');446if (result.strategy === 'failed') {447expect(result.fallbackError).toContain('expires_at');448}449});450451it('returns failed strategy when missing critical refresh_in field', function () {452const missingRefreshIn = {453token: 'test-token',454expires_at: 1234567890,455};456const result = validateTokenEnvelope(missingRefreshIn);457expect(result.valid).toBe(false);458expect(result.strategy).toBe('failed');459if (result.strategy === 'failed') {460expect(result.fallbackError).toContain('refresh_in');461}462});463464it('returns failed strategy for null input', function () {465const result = validateTokenEnvelope(null);466expect(result.valid).toBe(false);467expect(result.strategy).toBe('failed');468});469470it('returns failed strategy for undefined input', function () {471const result = validateTokenEnvelope(undefined);472expect(result.valid).toBe(false);473expect(result.strategy).toBe('failed');474});475476it('returns failed strategy when critical field has wrong type', function () {477const wrongTypeToken = {478token: 12345, // should be string479expires_at: 1234567890,480refresh_in: 300,481};482const result = validateTokenEnvelope(wrongTypeToken);483expect(result.valid).toBe(false);484expect(result.strategy).toBe('failed');485if (result.strategy === 'failed') {486expect(result.fallbackError).toContain('token');487}488});489});490});491492describe('CopilotToken class', function () {493it('isFreeUser returns true for free_limited_copilot sku', function () {494const token = new CopilotToken(createTestExtendedTokenInfo({ sku: 'free_limited_copilot' }));495expect(token.isFreeUser).toBe(true);496expect(token.isNoAuthUser).toBe(false);497});498499it('isNoAuthUser returns true for no_auth_limited_copilot sku', function () {500const token = new CopilotToken(createTestExtendedTokenInfo({ sku: 'no_auth_limited_copilot' }));501expect(token.isFreeUser).toBe(false);502expect(token.isNoAuthUser).toBe(true);503});504505it('isTelemetryEnabled reflects token state', function () {506const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ telemetry: 'enabled' }));507const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ telemetry: 'disabled' }));508expect(enabledToken.isTelemetryEnabled()).toBe(true);509expect(disabledToken.isTelemetryEnabled()).toBe(false);510});511512it('isPublicSuggestionsEnabled reflects token state', function () {513const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ public_suggestions: 'enabled' }));514const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ public_suggestions: 'disabled' }));515const unconfiguredToken = new CopilotToken(createTestExtendedTokenInfo({ public_suggestions: 'unconfigured' }));516expect(enabledToken.isPublicSuggestionsEnabled()).toBe(true);517expect(disabledToken.isPublicSuggestionsEnabled()).toBe(false);518expect(unconfiguredToken.isPublicSuggestionsEnabled()).toBe(false);519});520521it('copilotPlan returns correct plan type', function () {522const freeToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'free_limited_copilot', copilot_plan: 'free' }));523const individualToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_individual', copilot_plan: 'individual' }));524const businessToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_business', copilot_plan: 'business' }));525const enterpriseToken = new CopilotToken(createTestExtendedTokenInfo({ sku: 'copilot_enterprise', copilot_plan: 'enterprise' }));526527expect(freeToken.copilotPlan).toBe('free');528expect(individualToken.copilotPlan).toBe('individual');529expect(businessToken.copilotPlan).toBe('business');530expect(enterpriseToken.copilotPlan).toBe('enterprise');531});532533it('isChatQuotaExceeded for free users with zero quota', function () {534const exceededToken = new CopilotToken(createTestExtendedTokenInfo({535sku: 'free_limited_copilot',536limited_user_quotas: { chat: 0, completions: 10 }537}));538const notExceededToken = new CopilotToken(createTestExtendedTokenInfo({539sku: 'free_limited_copilot',540limited_user_quotas: { chat: 5, completions: 10 }541}));542const nonFreeToken = new CopilotToken(createTestExtendedTokenInfo({543sku: 'copilot_individual',544limited_user_quotas: { chat: 0, completions: 0 }545}));546547expect(exceededToken.isChatQuotaExceeded).toBe(true);548expect(notExceededToken.isChatQuotaExceeded).toBe(false);549expect(nonFreeToken.isChatQuotaExceeded).toBe(false); // Non-free users don't have quota limits550});551552it('isCompletionsQuotaExceeded for free users with zero quota', function () {553const exceededToken = new CopilotToken(createTestExtendedTokenInfo({554sku: 'free_limited_copilot',555limited_user_quotas: { chat: 10, completions: 0 }556}));557const notExceededToken = new CopilotToken(createTestExtendedTokenInfo({558sku: 'free_limited_copilot',559limited_user_quotas: { chat: 10, completions: 5 }560}));561562expect(exceededToken.isCompletionsQuotaExceeded).toBe(true);563expect(notExceededToken.isCompletionsQuotaExceeded).toBe(false);564});565566it('isInternal detects GitHub and Microsoft organizations', function () {567const githubOrgToken = new CopilotToken(createTestExtendedTokenInfo({568organization_list: ['4535c7beffc844b46bb1ed4aa04d759a']569}));570const microsoftOrgToken = new CopilotToken(createTestExtendedTokenInfo({571organization_list: ['a5db0bcaae94032fe715fb34a5e4bce2']572}));573const externalToken = new CopilotToken(createTestExtendedTokenInfo({574organization_list: ['some-other-org']575}));576const noOrgToken = new CopilotToken(createTestExtendedTokenInfo({577organization_list: []578}));579580expect(githubOrgToken.isInternal).toBe(true);581expect(githubOrgToken.isGitHubInternal).toBe(true);582expect(githubOrgToken.isMicrosoftInternal).toBe(false);583584expect(microsoftOrgToken.isInternal).toBe(true);585expect(microsoftOrgToken.isGitHubInternal).toBe(false);586expect(microsoftOrgToken.isMicrosoftInternal).toBe(true);587588expect(externalToken.isInternal).toBe(false);589expect(noOrgToken.isInternal).toBe(false);590});591592it('codeQuoteEnabled reflects token state', function () {593const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_quote_enabled: true }));594const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_quote_enabled: false }));595expect(enabledToken.codeQuoteEnabled).toBe(true);596expect(disabledToken.codeQuoteEnabled).toBe(false);597});598599it('isCopilotCodeReviewEnabled reflects token state', function () {600const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_review_enabled: true }));601const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ code_review_enabled: false }));602expect(enabledToken.isCopilotCodeReviewEnabled).toBe(true);603expect(disabledToken.isCopilotCodeReviewEnabled).toBe(false);604});605606it('isExpandedClientSideIndexingEnabled reflects token state', function () {607const enabledToken = new CopilotToken(createTestExtendedTokenInfo({ blackbird_clientside_indexing: true }));608const disabledToken = new CopilotToken(createTestExtendedTokenInfo({ blackbird_clientside_indexing: false }));609expect(enabledToken.isExpandedClientSideIndexingEnabled()).toBe(true);610expect(disabledToken.isExpandedClientSideIndexingEnabled()).toBe(false);611});612});613614class StaticFetcherService implements IFetcherService {615616declare readonly _serviceBrand: undefined;617readonly onDidFetch = Event.None;618readonly onDidCompleteFetch = Event.None;619620public requests = new Map<string, FetchOptions>();621constructor(readonly tokenResponse: any) {622}623624fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {625throw new Error('Method not implemented.');626}627628getUserAgentLibrary(): string {629return 'test';630}631async fetch(url: string, options: FetchOptions): Promise<Response> {632this.requests.set(url, options);633if (url.endsWith('copilot_internal/v2/token')) {634if (this.tokenResponse === 'NETWORK_FAILURE') {635// Simulate network failure - fetch throws636throw new Error('Network request failed');637}638// null will parse successfully as JSON (returns null) but fails tokenInfo check639return createFakeResponse(200, this.tokenResponse);640} else if (url.endsWith('copilot_internal/notification')) {641return createFakeResponse(200, '');642}643return createFakeResponse(404, '');644}645createWebSocket(_url: string): WebSocketConnection {646throw new Error('Method not implemented.');647}648disconnectAll(): Promise<unknown> {649throw new Error('Method not implemented.');650}651makeAbortController(): IAbortController {652throw new Error('Method not implemented.');653}654isAbortError(e: any): boolean {655throw new Error('Method not implemented.');656}657isInternetDisconnectedError(e: any): boolean {658throw new Error('Method not implemented.');659}660isFetcherError(err: any): boolean {661throw new Error('Method not implemented.');662}663isNetworkProcessCrashedError(err: any): boolean {664throw new Error('Method not implemented.');665}666getUserMessageForFetcherError(err: any): string {667throw new Error('Method not implemented.');668}669}670671class ErrorFetcherService extends StaticFetcherService {672constructor(private readonly error: any) {673super({});674}675676override fetch(url: string, options: FetchOptions): Promise<Response> {677throw this.error;678}679}680681class HttpStatusFetcherService extends StaticFetcherService {682constructor(private readonly status: number) {683super({});684}685686override async fetch(url: string, options: FetchOptions): Promise<Response> {687this.requests.set(url, options);688return createFakeResponse(this.status, {});689}690}691692693