diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPageTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPageTest.java new file mode 100644 index 000000000..c76117aab --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPageTest.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.preferences; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jface.preference.FieldEditor; +import org.eclipse.jface.preference.PreferenceStore; +import org.eclipse.jface.preference.StringFieldEditor; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; + +class GeneralPreferencesPageTest { + + private Shell shell; + private TestPreferencesPage page; + + @AfterEach + void tearDown() { + SwtUtils.invokeOnDisplayThread(() -> { + if (page != null) { + page.dispose(); + } + if (shell != null && !shell.isDisposed()) { + shell.dispose(); + } + }); + } + + @ParameterizedTest + @ValueSource(strings = {"", "https://company.ghe.com", "https://company.ghe.com/", + "https://github.company.example:8443", "HTTPS://company.ghe.com", "HtTpS://company.ghe.com"}) + void testEnterpriseEndpoint_validStoredValue_pageIsValid(String endpoint) { + SwtUtils.invokeOnDisplayThread(() -> { + createPage(endpoint); + assertTrue(page.isValid()); + assertNull(page.getErrorMessage()); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"http://company.ghe.com/", "company.ghe.com", "ftp://company.ghe.com", + "https://", "https:///company.ghe.com", "https://company ghe.com", "https://company.ghe.com/%", + " ", " https://company.ghe.com", "https://company.ghe.com "}) + void testEnterpriseEndpoint_invalidStoredValue_pageShowsError(String endpoint) { + SwtUtils.invokeOnDisplayThread(() -> { + createPage(endpoint); + assertFalse(page.isValid()); + assertEquals(Messages.preferences_page_github_enterprise_validation_error, page.getErrorMessage()); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"", "https://company.ghe.com"}) + void testEnterpriseEndpoint_nativeHints_preserveValue(String endpoint) { + SwtUtils.invokeOnDisplayThread(() -> { + createPage(endpoint); + Text endpointText = findTextControl((Composite) page.getControl()); + assertNotNull(endpointText); + assertEquals(Messages.preferences_page_github_enterprise_placeholder, endpointText.getMessage()); + assertEquals(Messages.preferences_page_github_enterprise_tooltip, endpointText.getToolTipText()); + assertEquals(endpoint, endpointText.getText()); + + page.endpointField.store(); + assertEquals(endpoint, page.getPreferenceStore().getString(Constants.GITHUB_ENTERPRISE)); + }); + } + + @Test + void testEnterpriseEndpoint_editValue_validationUpdatesImmediately() { + SwtUtils.invokeOnDisplayThread(() -> { + createPage(""); + page.endpointField.setStringValue("http://company.ghe.com/"); + assertFalse(page.isValid()); + assertEquals(Messages.preferences_page_github_enterprise_validation_error, page.getErrorMessage()); + + page.endpointField.setStringValue("https://company.ghe.com/"); + assertTrue(page.isValid()); + assertNull(page.getErrorMessage()); + + page.endpointField.setStringValue(""); + assertTrue(page.isValid()); + assertNull(page.getErrorMessage()); + }); + } + + private void createPage(String endpoint) { + shell = new Shell(Display.getDefault()); + PreferenceStore store = new PreferenceStore(); + store.setValue(Constants.GITHUB_ENTERPRISE, endpoint); + page = new TestPreferencesPage(); + page.setPreferenceStore(store); + page.createControl(shell); + } + + private Text findTextControl(Composite parent) { + for (Control child : parent.getChildren()) { + if (child instanceof Text text) { + return text; + } + if (child instanceof Composite composite) { + Text text = findTextControl(composite); + if (text != null) { + return text; + } + } + } + return null; + } + + private static class TestPreferencesPage extends GeneralPreferencesPage { + private StringFieldEditor endpointField; + + @Override + protected void addField(FieldEditor editor) { + super.addField(editor); + if (Constants.GITHUB_ENTERPRISE.equals(editor.getPreferenceName())) { + endpointField = (StringFieldEditor) editor; + } + } + } +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPage.java index 6b7da3e3a..9264befa6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/GeneralPreferencesPage.java @@ -3,6 +3,9 @@ package com.microsoft.copilot.eclipse.ui.preferences; +import java.net.URI; + +import org.apache.commons.lang3.StringUtils; import org.eclipse.core.runtime.preferences.ConfigurationScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.jface.layout.GridDataFactory; @@ -14,6 +17,7 @@ import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; import org.osgi.service.prefs.BackingStoreException; @@ -82,8 +86,26 @@ public void createFieldEditors() { ctnGhe.setLayout(gl); ctnGhe.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); StringFieldEditor sftGhe = new StringFieldEditor(Constants.GITHUB_ENTERPRISE, - Messages.preferences_page_github_enterprise, ctnGhe); + Messages.preferences_page_github_enterprise, ctnGhe) { + @Override + protected boolean doCheckState() { + String endpoint = getStringValue(); + if (StringUtils.isEmpty(endpoint)) { + return true; + } + try { + URI uri = URI.create(endpoint); + return "https".equalsIgnoreCase(uri.getScheme()) && StringUtils.isNotBlank(uri.getHost()); + } catch (IllegalArgumentException e) { + return false; + } + } + }; + sftGhe.setErrorMessage(Messages.preferences_page_github_enterprise_validation_error); sftGhe.getLabelControl(ctnGhe).setToolTipText(Messages.preferences_page_github_enterprise_tooltip); + Text endpointText = sftGhe.getTextControl(ctnGhe); + endpointText.setMessage(Messages.preferences_page_github_enterprise_placeholder); + endpointText.setToolTipText(Messages.preferences_page_github_enterprise_tooltip); addField(sftGhe); // What's new group diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java index ae4051b53..a002e3472 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/Messages.java @@ -69,6 +69,8 @@ public class Messages extends NLS { public static String preferences_page_enable_strict_ssl_tooltip; public static String preferences_page_proxy_config_link_tooltip; public static String preferences_page_github_enterprise_tooltip; + public static String preferences_page_github_enterprise_placeholder; + public static String preferences_page_github_enterprise_validation_error; public static String preferences_page_mcp_tooltip; public static String preferences_page_mcp_note_content; public static String preferences_page_mcp_disabled_tip; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties index d81ecf669..2a98e46c3 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/messages.properties @@ -58,7 +58,9 @@ preferences_page_auth_settings= Authentication preferences_page_mcp_settings= MCP Servers preferences_page_enable_strict_ssl_tooltip= Only disable this if your proxy server is not configured to use SSL and do not have a trusted certificate installed. preferences_page_proxy_config_link_tooltip= Configure proxy settings in Eclipse -preferences_page_github_enterprise_tooltip= Ask your IT administrator for the GitHub Enterprise authentication endpoint if you are using GitHub Enterprise. +preferences_page_github_enterprise_tooltip= Ask your IT administrator for the GitHub Enterprise authentication endpoint. Use an HTTPS URL, for example https://your-company.ghe.com. Leave empty to sign in to GitHub.com. +preferences_page_github_enterprise_placeholder= https://your-company.ghe.com +preferences_page_github_enterprise_validation_error= Enter a valid GitHub Enterprise authentication endpoint starting with https://, or leave it empty to use GitHub.com. preferences_page_mcp_tooltip= Model Context Protocol server configurations. preferences_page_note_prefix= Note: preferences_page_note_content= You can configure proxy settings in Eclipse by clicking on the Configure Proxy link. The Strict SSL option takes effect only when you are accessing GitHub Copilot from a network protected by a proxy server.