mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Fix extension headers (#5000)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ExtensionModal from './ExtensionModal';
|
||||
import { ExtensionFormData } from '../utils';
|
||||
|
||||
describe('ExtensionModal', () => {
|
||||
it('creates a http_streamable extension', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOnSubmit = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
|
||||
const initialData: ExtensionFormData = {
|
||||
name: '',
|
||||
description: '',
|
||||
type: 'stdio', // Default type
|
||||
cmd: '',
|
||||
endpoint: '',
|
||||
enabled: true,
|
||||
timeout: 300,
|
||||
envVars: [],
|
||||
headers: [],
|
||||
};
|
||||
|
||||
render(
|
||||
<ExtensionModal
|
||||
title="Add custom extension"
|
||||
initialData={initialData}
|
||||
onClose={mockOnClose}
|
||||
onSubmit={mockOnSubmit}
|
||||
submitLabel="Add Extension"
|
||||
modalType="add"
|
||||
/>
|
||||
);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Enter extension name...');
|
||||
const submitButton = screen.getByTestId('extension-submit-btn');
|
||||
|
||||
await user.type(nameInput, 'Test MCP');
|
||||
|
||||
const typeSelect = screen.getByRole('combobox');
|
||||
await user.click(typeSelect);
|
||||
|
||||
const httpOption = screen.getByText('Streamable HTTP');
|
||||
await user.click(httpOption);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Request Headers')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const endpointInput = screen.getByPlaceholderText('Enter endpoint URL...');
|
||||
await user.type(endpointInput, 'https://foo.bar.com/mcp/');
|
||||
|
||||
const descriptionInput = screen.getByPlaceholderText('Optional description...');
|
||||
await user.type(descriptionInput, 'Test MCP extension');
|
||||
|
||||
const headerNameInput = screen.getByPlaceholderText('Header name');
|
||||
const headerValueInput = screen
|
||||
.getAllByPlaceholderText('Value')
|
||||
.find(
|
||||
(input) =>
|
||||
input.closest('div')?.textContent?.includes('Request Headers') ||
|
||||
input.parentElement?.parentElement?.textContent?.includes('Request Headers')
|
||||
);
|
||||
|
||||
await user.type(headerNameInput, 'Authorization');
|
||||
if (headerValueInput) {
|
||||
await user.type(headerValueInput, 'Bearer abc123');
|
||||
}
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const submittedData = mockOnSubmit.mock.calls[0][0];
|
||||
|
||||
expect(submittedData.name).toBe('Test MCP');
|
||||
expect(submittedData.type).toBe('streamable_http');
|
||||
expect(submittedData.endpoint).toBe('https://foo.bar.com/mcp/');
|
||||
expect(submittedData.description).toBe('Test MCP extension');
|
||||
expect(submittedData.timeout).toBe(300);
|
||||
expect(submittedData.headers).toHaveLength(1);
|
||||
expect(submittedData.headers).toEqual([
|
||||
{ key: 'Authorization', value: 'Bearer abc123', isEdited: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '../../../ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -43,6 +43,7 @@ export default function ExtensionModal({
|
||||
const [showCloseConfirmation, setShowCloseConfirmation] = useState(false);
|
||||
const [hasPendingEnvVars, setHasPendingEnvVars] = useState(false);
|
||||
const [hasPendingHeaders, setHasPendingHeaders] = useState(false);
|
||||
const [pendingHeader, setPendingHeader] = useState<{ key: string; value: string } | null>(null);
|
||||
|
||||
// Function to check if form has been modified
|
||||
const hasFormChanges = (): boolean => {
|
||||
@@ -72,7 +73,7 @@ export default function ExtensionModal({
|
||||
envVar.value !== '••••••••'
|
||||
);
|
||||
|
||||
// Check if there are pending environment variables being typed
|
||||
// Check if there are pending environment variables or headers being typed
|
||||
const hasPendingInput = hasPendingEnvVars || hasPendingHeaders;
|
||||
|
||||
return (
|
||||
@@ -168,6 +169,14 @@ export default function ExtensionModal({
|
||||
});
|
||||
};
|
||||
|
||||
const handlePendingHeaderChange = useCallback(
|
||||
(hasPending: boolean, header: { key: string; value: string } | null) => {
|
||||
setHasPendingHeaders(hasPending);
|
||||
setPendingHeader(header);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Function to store a secret value
|
||||
const storeSecret = async (key: string, value: string) => {
|
||||
try {
|
||||
@@ -217,8 +226,16 @@ export default function ExtensionModal({
|
||||
);
|
||||
};
|
||||
|
||||
const getFinalHeaders = () => {
|
||||
const finalHeaders = [...formData.headers];
|
||||
if (pendingHeader && pendingHeader.key.trim() !== '' && pendingHeader.value.trim() !== '') {
|
||||
finalHeaders.push({ ...pendingHeader, isEdited: true });
|
||||
}
|
||||
return finalHeaders;
|
||||
};
|
||||
|
||||
const isHeadersValid = () => {
|
||||
return formData.headers.every(
|
||||
return getFinalHeaders().every(
|
||||
({ key, value }) => (key === '' && value === '') || (key !== '' && value !== '')
|
||||
);
|
||||
};
|
||||
@@ -249,8 +266,13 @@ export default function ExtensionModal({
|
||||
setSubmitAttempted(true);
|
||||
|
||||
if (isFormValid()) {
|
||||
const finalFormData = {
|
||||
...formData,
|
||||
headers: getFinalHeaders(),
|
||||
};
|
||||
|
||||
// Only store env vars that have been edited (which includes new)
|
||||
const secretPromises = formData.envVars
|
||||
const secretPromises = finalFormData.envVars
|
||||
.filter((envVar) => envVar.isEdited)
|
||||
.map(({ key, value }) => storeSecret(key, value));
|
||||
|
||||
@@ -261,9 +283,11 @@ export default function ExtensionModal({
|
||||
if (results.every((success) => success)) {
|
||||
// Convert timeout to number if needed
|
||||
const dataToSubmit = {
|
||||
...formData,
|
||||
...finalFormData,
|
||||
timeout:
|
||||
typeof formData.timeout === 'string' ? Number(formData.timeout) : formData.timeout,
|
||||
typeof finalFormData.timeout === 'string'
|
||||
? Number(finalFormData.timeout)
|
||||
: finalFormData.timeout,
|
||||
};
|
||||
onSubmit(dataToSubmit);
|
||||
onClose();
|
||||
@@ -366,7 +390,7 @@ export default function ExtensionModal({
|
||||
onRemove={handleRemoveHeader}
|
||||
onChange={handleHeaderChange}
|
||||
submitAttempted={submitAttempted}
|
||||
onPendingInputChange={setHasPendingHeaders}
|
||||
onPendingInputChange={handlePendingHeaderChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -10,7 +10,10 @@ interface HeadersSectionProps {
|
||||
onRemove: (index: number) => void;
|
||||
onChange: (index: number, field: 'key' | 'value', value: string) => void;
|
||||
submitAttempted: boolean;
|
||||
onPendingInputChange?: (hasPending: boolean) => void;
|
||||
onPendingInputChange: (
|
||||
hasPendingInput: boolean,
|
||||
pendingHeader: { key: string; value: string } | null
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default function HeadersSection({
|
||||
@@ -29,10 +32,12 @@ export default function HeadersSection({
|
||||
value: false,
|
||||
});
|
||||
|
||||
// Track pending input changes
|
||||
// Notify parent when pending input changes
|
||||
React.useEffect(() => {
|
||||
const hasPendingInput = newKey.trim() !== '' || newValue.trim() !== '';
|
||||
onPendingInputChange?.(hasPendingInput);
|
||||
const pendingHeader =
|
||||
newKey.trim() && newValue.trim() ? { key: newKey, value: newValue } : null;
|
||||
onPendingInputChange(hasPendingInput, pendingHeader);
|
||||
}, [newKey, newValue, onPendingInputChange]);
|
||||
|
||||
const handleAdd = () => {
|
||||
|
||||
Reference in New Issue
Block a user