diff --git a/ui/desktop/src/components/settings/extensions/deeplink.test.ts b/ui/desktop/src/components/settings/extensions/deeplink.test.ts new file mode 100644 index 0000000000..92dde56010 --- /dev/null +++ b/ui/desktop/src/components/settings/extensions/deeplink.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { addExtensionFromDeepLink } from './deeplink'; + +vi.mock('../../../toasts', () => ({ + toastService: { + handleError: vi.fn(), + success: vi.fn(), + }, +})); + +describe('addExtensionFromDeepLink', () => { + const mockAddExtension = vi.fn().mockResolvedValue(undefined); + const mockSetView = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('header parsing', () => { + it('should preserve = characters in header values', async () => { + const url = + 'goose://extension?name=Remote&url=https%3A%2F%2Fexample.com%2Fmcp&header=Authorization%3DBasic%20abc%3D%3D'; + + await addExtensionFromDeepLink(url, mockAddExtension, mockSetView); + + expect(mockSetView).toHaveBeenCalledWith( + 'extensions', + expect.objectContaining({ + showEnvVars: true, + deepLinkConfig: expect.objectContaining({ + headers: { Authorization: 'Basic abc==' }, + }), + }) + ); + }); + + it('should handle header values without = characters', async () => { + const url = + 'goose://extension?name=Remote&url=https%3A%2F%2Fexample.com%2Fmcp&header=X-Token%3Dabc123'; + + await addExtensionFromDeepLink(url, mockAddExtension, mockSetView); + + expect(mockSetView).toHaveBeenCalledWith( + 'extensions', + expect.objectContaining({ + deepLinkConfig: expect.objectContaining({ + headers: { 'X-Token': 'abc123' }, + }), + }) + ); + }); + + it('should handle multiple headers', async () => { + const url = + 'goose://extension?name=Remote&url=https%3A%2F%2Fexample.com%2Fmcp&header=Authorization%3DBearer%20tok%3D%3D&header=X-Key%3Dval'; + + await addExtensionFromDeepLink(url, mockAddExtension, mockSetView); + + expect(mockSetView).toHaveBeenCalledWith( + 'extensions', + expect.objectContaining({ + deepLinkConfig: expect.objectContaining({ + headers: { + Authorization: 'Bearer tok==', + 'X-Key': 'val', + }, + }), + }) + ); + }); + + it('should handle header with empty value', async () => { + const url = + 'goose://extension?name=Remote&url=https%3A%2F%2Fexample.com%2Fmcp&header=X-Empty%3D'; + + await addExtensionFromDeepLink(url, mockAddExtension, mockSetView); + + expect(mockSetView).toHaveBeenCalledWith( + 'extensions', + expect.objectContaining({ + deepLinkConfig: expect.objectContaining({ + headers: { 'X-Empty': '' }, + }), + }) + ); + }); + }); +}); diff --git a/ui/desktop/src/components/settings/extensions/deeplink.ts b/ui/desktop/src/components/settings/extensions/deeplink.ts index adb321dd65..f8d59b40c6 100644 --- a/ui/desktop/src/components/settings/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings/extensions/deeplink.ts @@ -141,8 +141,8 @@ export async function addExtensionFromDeepLink( headerParams.length > 0 ? Object.fromEntries( headerParams.map((header) => { - const [key, value] = header.split('='); - return [key, decodeURIComponent(value || '')]; + const [key, ...rest] = header.split('='); + return [key, decodeURIComponent(rest.join('=') || '')]; }) ) : undefined;