mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 20:14:44 -07:00
Support redirects in fetchJson, add tests for it (#11993)
This commit is contained in:
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||||
|
import * as https from 'node:https';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import { fetchJson, getGitHubToken } from './github_fetch.js';
|
||||||
|
import type { ClientRequest, IncomingMessage } from 'node:http';
|
||||||
|
|
||||||
|
vi.mock('node:https');
|
||||||
|
|
||||||
|
describe('getGitHubToken', () => {
|
||||||
|
const originalToken = process.env['GITHUB_TOKEN'];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalToken) {
|
||||||
|
process.env['GITHUB_TOKEN'] = originalToken;
|
||||||
|
} else {
|
||||||
|
delete process.env['GITHUB_TOKEN'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the token if GITHUB_TOKEN is set', () => {
|
||||||
|
process.env['GITHUB_TOKEN'] = 'test-token';
|
||||||
|
expect(getGitHubToken()).toBe('test-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if GITHUB_TOKEN is not set', () => {
|
||||||
|
delete process.env['GITHUB_TOKEN'];
|
||||||
|
expect(getGitHubToken()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchJson', () => {
|
||||||
|
const getMock = vi.mocked(https.get);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch and parse JSON successfully', async () => {
|
||||||
|
getMock.mockImplementationOnce((_url, _options, callback) => {
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 200;
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('data', Buffer.from('{"foo":'));
|
||||||
|
res.emit('data', Buffer.from('"bar"}'));
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
await expect(fetchJson('https://example.com/data.json')).resolves.toEqual({
|
||||||
|
foo: 'bar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle redirects (301 and 302)', async () => {
|
||||||
|
// Test 302
|
||||||
|
getMock.mockImplementationOnce((_url, _options, callback) => {
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.headers = { location: 'https://example.com/final' };
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
getMock.mockImplementationOnce((url, _options, callback) => {
|
||||||
|
expect(url).toBe('https://example.com/final');
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 200;
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('data', Buffer.from('{"success": true}'));
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fetchJson('https://example.com/redirect')).resolves.toEqual({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 301
|
||||||
|
getMock.mockImplementationOnce((_url, _options, callback) => {
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 301;
|
||||||
|
res.headers = { location: 'https://example.com/final-permanent' };
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
getMock.mockImplementationOnce((url, _options, callback) => {
|
||||||
|
expect(url).toBe('https://example.com/final-permanent');
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 200;
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('data', Buffer.from('{"permanent": true}'));
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fetchJson('https://example.com/redirect-perm'),
|
||||||
|
).resolves.toEqual({ permanent: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject on non-200/30x status code', async () => {
|
||||||
|
getMock.mockImplementationOnce((_url, _options, callback) => {
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 404;
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fetchJson('https://example.com/error')).rejects.toThrow(
|
||||||
|
'Request failed with status code 404',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject on request error', async () => {
|
||||||
|
const error = new Error('Network error');
|
||||||
|
getMock.mockImplementationOnce(() => {
|
||||||
|
const req = new EventEmitter() as ClientRequest;
|
||||||
|
req.emit('error', error);
|
||||||
|
return req;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fetchJson('https://example.com/error')).rejects.toThrow(
|
||||||
|
'Network error',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with GITHUB_TOKEN', () => {
|
||||||
|
const originalToken = process.env['GITHUB_TOKEN'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env['GITHUB_TOKEN'] = 'my-secret-token';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalToken) {
|
||||||
|
process.env['GITHUB_TOKEN'] = originalToken;
|
||||||
|
} else {
|
||||||
|
delete process.env['GITHUB_TOKEN'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include Authorization header if token is present', async () => {
|
||||||
|
getMock.mockImplementationOnce((_url, options, callback) => {
|
||||||
|
expect(options.headers).toEqual({
|
||||||
|
'User-Agent': 'gemini-cli',
|
||||||
|
Authorization: 'token my-secret-token',
|
||||||
|
});
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 200;
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('data', Buffer.from('{"foo": "bar"}'));
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
await expect(fetchJson('https://api.github.com/user')).resolves.toEqual({
|
||||||
|
foo: 'bar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('without GITHUB_TOKEN', () => {
|
||||||
|
const originalToken = process.env['GITHUB_TOKEN'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env['GITHUB_TOKEN'];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalToken) {
|
||||||
|
process.env['GITHUB_TOKEN'] = originalToken;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include Authorization header if token is not present', async () => {
|
||||||
|
getMock.mockImplementationOnce((_url, options, callback) => {
|
||||||
|
expect(options.headers).toEqual({
|
||||||
|
'User-Agent': 'gemini-cli',
|
||||||
|
});
|
||||||
|
const res = new EventEmitter() as IncomingMessage;
|
||||||
|
res.statusCode = 200;
|
||||||
|
(callback as (res: IncomingMessage) => void)(res);
|
||||||
|
res.emit('data', Buffer.from('{"foo": "bar"}'));
|
||||||
|
res.emit('end');
|
||||||
|
return new EventEmitter() as ClientRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fetchJson('https://api.github.com/user')).resolves.toEqual({
|
||||||
|
foo: 'bar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,7 +10,10 @@ export function getGitHubToken(): string | undefined {
|
|||||||
return process.env['GITHUB_TOKEN'];
|
return process.env['GITHUB_TOKEN'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchJson<T>(url: string): Promise<T> {
|
export async function fetchJson<T>(
|
||||||
|
url: string,
|
||||||
|
redirectCount: number = 0,
|
||||||
|
): Promise<T> {
|
||||||
const headers: { 'User-Agent': string; Authorization?: string } = {
|
const headers: { 'User-Agent': string; Authorization?: string } = {
|
||||||
'User-Agent': 'gemini-cli',
|
'User-Agent': 'gemini-cli',
|
||||||
};
|
};
|
||||||
@@ -21,6 +24,18 @@ export async function fetchJson<T>(url: string): Promise<T> {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
https
|
https
|
||||||
.get(url, { headers }, (res) => {
|
.get(url, { headers }, (res) => {
|
||||||
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
||||||
|
if (redirectCount >= 10) {
|
||||||
|
return reject(new Error('Too many redirects'));
|
||||||
|
}
|
||||||
|
if (!res.headers.location) {
|
||||||
|
return reject(new Error('No location header in redirect response'));
|
||||||
|
}
|
||||||
|
fetchJson<T>(res.headers.location!, redirectCount++)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
return reject(
|
return reject(
|
||||||
new Error(`Request failed with status code ${res.statusCode}`),
|
new Error(`Request failed with status code ${res.statusCode}`),
|
||||||
|
|||||||
Reference in New Issue
Block a user