mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-20 02:51:55 -07:00
fix(extensions): resolve GitHub API 415 error for source tarballs (#13319)
This commit is contained in:
@@ -285,6 +285,131 @@ describe('github.ts', () => {
|
||||
expect(result.failureReason).toBe('failed to fetch release data');
|
||||
}
|
||||
});
|
||||
|
||||
it('should use correct headers for release assets', async () => {
|
||||
vi.mocked(fetchJson).mockResolvedValue({
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [{ name: 'asset.tar.gz', url: 'http://asset.url' }],
|
||||
});
|
||||
vi.mocked(os.platform).mockReturnValue('linux');
|
||||
vi.mocked(os.arch).mockReturnValue('x64');
|
||||
|
||||
// Mock https.get and fs.createWriteStream for downloadFile
|
||||
const mockReq = new EventEmitter();
|
||||
const mockRes =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockRes, { statusCode: 200, pipe: vi.fn() });
|
||||
|
||||
vi.mocked(https.get).mockImplementation((url, options, cb) => {
|
||||
if (typeof options === 'function') {
|
||||
cb = options;
|
||||
}
|
||||
if (cb) cb(mockRes);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
});
|
||||
|
||||
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
|
||||
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
|
||||
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
|
||||
|
||||
// Mock fs.promises.readdir to return empty array (no cleanup needed)
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue([]);
|
||||
// Mock fs.promises.unlink
|
||||
vi.mocked(fs.promises.unlink).mockResolvedValue(undefined);
|
||||
|
||||
const promise = downloadFromGitHubRelease(
|
||||
{
|
||||
type: 'github-release',
|
||||
source: 'owner/repo',
|
||||
ref: 'v1.0.0',
|
||||
} as unknown as ExtensionInstallMetadata,
|
||||
'/dest',
|
||||
{ owner: 'owner', repo: 'repo' },
|
||||
);
|
||||
|
||||
// Wait for downloadFile to be called and stream to be created
|
||||
await vi.waitUntil(
|
||||
() => vi.mocked(fs.createWriteStream).mock.calls.length > 0,
|
||||
);
|
||||
|
||||
// Trigger stream events to complete download
|
||||
mockRes.emit('end');
|
||||
mockStream.emit('finish');
|
||||
|
||||
await promise;
|
||||
|
||||
expect(https.get).toHaveBeenCalledWith(
|
||||
'http://asset.url',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Accept: 'application/octet-stream',
|
||||
}),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use correct headers for source tarballs', async () => {
|
||||
vi.mocked(fetchJson).mockResolvedValue({
|
||||
tag_name: 'v1.0.0',
|
||||
assets: [],
|
||||
tarball_url: 'http://tarball.url',
|
||||
});
|
||||
|
||||
// Mock https.get and fs.createWriteStream for downloadFile
|
||||
const mockReq = new EventEmitter();
|
||||
const mockRes =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockRes, { statusCode: 200, pipe: vi.fn() });
|
||||
|
||||
vi.mocked(https.get).mockImplementation((url, options, cb) => {
|
||||
if (typeof options === 'function') {
|
||||
cb = options;
|
||||
}
|
||||
if (cb) cb(mockRes);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
});
|
||||
|
||||
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
|
||||
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
|
||||
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
|
||||
|
||||
// Mock fs.promises.readdir to return empty array
|
||||
vi.mocked(fs.promises.readdir).mockResolvedValue([]);
|
||||
// Mock fs.promises.unlink
|
||||
vi.mocked(fs.promises.unlink).mockResolvedValue(undefined);
|
||||
|
||||
const promise = downloadFromGitHubRelease(
|
||||
{
|
||||
type: 'github-release',
|
||||
source: 'owner/repo',
|
||||
ref: 'v1.0.0',
|
||||
} as unknown as ExtensionInstallMetadata,
|
||||
'/dest',
|
||||
{ owner: 'owner', repo: 'repo' },
|
||||
);
|
||||
|
||||
// Wait for downloadFile to be called and stream to be created
|
||||
await vi.waitUntil(
|
||||
() => vi.mocked(fs.createWriteStream).mock.calls.length > 0,
|
||||
);
|
||||
|
||||
// Trigger stream events to complete download
|
||||
mockRes.emit('end');
|
||||
mockStream.emit('finish');
|
||||
|
||||
await promise;
|
||||
|
||||
expect(https.get).toHaveBeenCalledWith(
|
||||
'http://tarball.url',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Accept: 'application/vnd.github+json',
|
||||
}),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findReleaseAsset', () => {
|
||||
@@ -349,6 +474,120 @@ describe('github.ts', () => {
|
||||
'Request failed with status code 404',
|
||||
);
|
||||
});
|
||||
|
||||
it('should follow redirects', async () => {
|
||||
const mockReq = new EventEmitter();
|
||||
const mockResRedirect =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockResRedirect, {
|
||||
statusCode: 302,
|
||||
headers: { location: 'new-url' },
|
||||
});
|
||||
|
||||
const mockResSuccess =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockResSuccess, { statusCode: 200, pipe: vi.fn() });
|
||||
|
||||
vi.mocked(https.get)
|
||||
.mockImplementationOnce((url, options, cb) => {
|
||||
if (typeof options === 'function') cb = options;
|
||||
if (cb) cb(mockResRedirect);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
})
|
||||
.mockImplementationOnce((url, options, cb) => {
|
||||
if (typeof options === 'function') cb = options;
|
||||
if (cb) cb(mockResSuccess);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
});
|
||||
|
||||
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
|
||||
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
|
||||
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
|
||||
|
||||
const promise = downloadFile('url', '/dest');
|
||||
mockResSuccess.emit('end');
|
||||
mockStream.emit('finish');
|
||||
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
expect(https.get).toHaveBeenCalledTimes(2);
|
||||
expect(https.get).toHaveBeenLastCalledWith(
|
||||
'new-url',
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail after too many redirects', async () => {
|
||||
const mockReq = new EventEmitter();
|
||||
const mockResRedirect =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockResRedirect, {
|
||||
statusCode: 302,
|
||||
headers: { location: 'new-url' },
|
||||
});
|
||||
|
||||
vi.mocked(https.get).mockImplementation((url, options, cb) => {
|
||||
if (typeof options === 'function') cb = options;
|
||||
if (cb) cb(mockResRedirect);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
});
|
||||
|
||||
await expect(downloadFile('url', '/dest')).rejects.toThrow(
|
||||
'Too many redirects',
|
||||
);
|
||||
}, 10000); // Increase timeout for this test if needed, though with mocks it should be fast
|
||||
|
||||
it('should fail if redirect location is missing', async () => {
|
||||
const mockReq = new EventEmitter();
|
||||
const mockResRedirect =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockResRedirect, {
|
||||
statusCode: 302,
|
||||
headers: {}, // No location
|
||||
});
|
||||
|
||||
vi.mocked(https.get).mockImplementation((url, options, cb) => {
|
||||
if (typeof options === 'function') cb = options;
|
||||
if (cb) cb(mockResRedirect);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
});
|
||||
|
||||
await expect(downloadFile('url', '/dest')).rejects.toThrow(
|
||||
'Redirect response missing Location header',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass custom headers', async () => {
|
||||
const mockReq = new EventEmitter();
|
||||
const mockRes =
|
||||
new EventEmitter() as unknown as import('node:http').IncomingMessage;
|
||||
Object.assign(mockRes, { statusCode: 200, pipe: vi.fn() });
|
||||
|
||||
vi.mocked(https.get).mockImplementation((url, options, cb) => {
|
||||
if (typeof options === 'function') cb = options;
|
||||
if (cb) cb(mockRes);
|
||||
return mockReq as unknown as import('node:http').ClientRequest;
|
||||
});
|
||||
|
||||
const mockStream = new EventEmitter() as unknown as fs.WriteStream;
|
||||
Object.assign(mockStream, { close: vi.fn((cb) => cb && cb()) });
|
||||
vi.mocked(fs.createWriteStream).mockReturnValue(mockStream);
|
||||
|
||||
const promise = downloadFile('url', '/dest', {
|
||||
headers: { 'X-Custom': 'value' },
|
||||
});
|
||||
mockRes.emit('end');
|
||||
mockStream.emit('finish');
|
||||
|
||||
await expect(promise).resolves.toBeUndefined();
|
||||
expect(https.get).toHaveBeenCalledWith(
|
||||
'url',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'X-Custom': 'value' }),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFile', () => {
|
||||
|
||||
Reference in New Issue
Block a user