diff --git a/routes/connectors-home/default-connectors.tsx b/routes/connectors-home/default-connectors.tsx index f65ed8e3bf33bb..abe4ed52191a28 100644 --- a/routes/connectors-home/default-connectors.tsx +++ b/routes/connectors-home/default-connectors.tsx @@ -216,6 +216,10 @@ function ApiKeyConnector( { } ); const isExternallyConfigured = keySource === 'env' || keySource === 'constant'; + // A stored key is only available masked, so it can't be edited, even when + // it can't be verified. It has to be removed before entering a new one. + const isReadOnly = + isConnected || isExternallyConfigured || !! currentApiKey; const showUnavailableBadge = ( pluginStatus === 'not-installed' && canInstallPlugins === false ) || ( pluginStatus === 'inactive' && canActivatePlugins === false ); @@ -248,14 +252,14 @@ function ApiKeyConnector( { pluginStatus === 'active' && hasResolvedSettings && ( + ( { + useConnectorPlugin: vi.fn(), + } ) as unknown as typeof import( '../use-connector-plugin' ) +); + +const { store: connectorsStore } = unlock( connectorsPrivateApis ); +const mockedUseConnectorPlugin = vi.mocked( useConnectorPlugin ); + +const MASKED_API_KEY = '•'.repeat( 16 ) + 'fj39'; + +function mockConnectorPlugin( { + isConnected, + currentApiKey, +}: { + isConnected: boolean; + currentApiKey: string; +} ) { + mockedUseConnectorPlugin.mockReturnValue( { + pluginStatus: 'active', + canInstallPlugins: true, + canActivatePlugins: true, + isExpanded: true, + setIsExpanded: vi.fn(), + isBusy: false, + isConnected, + currentApiKey, + hasResolvedSettings: true, + keySource: currentApiKey ? 'database' : 'none', + handleButtonClick: vi.fn(), + getButtonLabel: () => ( isConnected ? 'Edit' : 'Set up' ), + saveApiKey: vi.fn(), + removeApiKey: vi.fn(), + } as unknown as ReturnType< typeof useConnectorPlugin > ); +} + +function renderConnector( connector: ConnectorConfig ) { + if ( ! connector.render ) { + throw new Error( 'The connector has no render.' ); + } + + return ( + + ); +} + +describe( 'API key connector', () => { + let connector: ConnectorConfig; + + beforeEach( () => { + document.body.innerHTML = ``; + + registerDefaultConnectors(); + connector = unlock( select( connectorsStore ) ).getConnector( + 'openai' + ); + } ); + + it( 'shows a stored key as read-only when it cannot be verified', () => { + mockConnectorPlugin( { + isConnected: false, + currentApiKey: MASKED_API_KEY, + } ); + + render( renderConnector( connector ) ); + + const input = screen.getByRole( 'textbox', { name: 'API Key' } ); + expect( input ).toHaveValue( MASKED_API_KEY ); + expect( input ).toBeDisabled(); + expect( + screen.getByRole( 'button', { name: 'Remove and replace' } ) + ).toBeVisible(); + expect( + screen.queryByRole( 'button', { name: 'Save' } ) + ).not.toBeInTheDocument(); + } ); + + it( 'shows an empty editable field once the stored key is removed', () => { + mockConnectorPlugin( { + isConnected: false, + currentApiKey: MASKED_API_KEY, + } ); + + const { rerender } = render( renderConnector( connector ) ); + + mockConnectorPlugin( { isConnected: false, currentApiKey: '' } ); + rerender( renderConnector( connector ) ); + + const input = screen.getByRole( 'textbox', { name: 'API Key' } ); + expect( input ).toHaveValue( '' ); + expect( input ).toBeEnabled(); + expect( + screen.getByRole( 'button', { name: 'Save' } ) + ).toBeInTheDocument(); + } ); +} );