From cd2acb4f7bdb93ee2930818682590e8210af85a9 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Mon, 13 Apr 2026 17:00:31 +0530 Subject: [PATCH 1/6] Refactor export view to remove hacky ManagementLink usage and improve error handling --- .../plugins/casc/ConfigurationAsCode.java | 36 ++++++------------- .../casc/ConfigurationAsCode/viewExport.jelly | 6 ++-- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index a6197eee08..d3d70e97b8 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -143,12 +143,10 @@ public String getDescription() { return "Reload your configuration or update configuration source."; } - /** - * Name of the category for this management link. - * TODO: Use getCategory when core requirement is greater or equal to 2.226 - */ - public @NonNull String getCategoryName() { - return "CONFIGURATION"; + @NonNull + @Override + public Category getCategory() { + return Category.CONFIGURATION; } @NonNull @@ -228,7 +226,7 @@ public void doReplace(StaplerRequest2 request, StaplerResponse2 response) throws candidateSources.add(candidateSource); } else { LOGGER.log(Level.WARNING, "Source {0} could not be applied", candidateSource); - // todo: show message in UI + throw new ConfiguratorException("Source " + candidateSource + " could not be applied or does not exist."); } } if (!candidateSources.isEmpty()) { @@ -244,7 +242,7 @@ public void doReplace(StaplerRequest2 request, StaplerResponse2 response) throws LOGGER.log(Level.FINE, "Replace configuration with: " + normalizedSource); } else { LOGGER.log(Level.WARNING, "Provided sources could not be applied"); - // todo: show message in UI + throw new ConfiguratorException("Provided sources could not be applied. Please check the syntax and validity of the provided configuration."); } } else { LOGGER.log(Level.FINE, "No such source exists, applying default"); @@ -453,7 +451,9 @@ public List getBundledCasCURIs() { URL bundled = servletContext.getResource(cascItem); if (bundled != null && matcher.matches(new File(bundled.getPath()).toPath())) { res.add(bundled.toString()); - } // TODO: else do some handling? + } else if (bundled != null) { + LOGGER.log(Level.FINE, "Skipped bundled resource {0} as it does not match the YAML pattern.", bundled.getPath()); + } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to execute " + res, e); } @@ -569,23 +569,7 @@ public void doViewExport(StaplerRequest2 req, StaplerResponse2 res) throws Excep ByteArrayOutputStream out = new ByteArrayOutputStream(); export(out); - req.setAttribute("viewExport", new ManagementLink() { - @Override - public String getIconFileName() { - return ""; - } - - // TODO - FIX - EXTREMELY HACKY - couldn't expose a public method for some reason - @Override - public String getUrlName() { - return out.toString(StandardCharsets.UTF_8); - } - - @Override - public String getDisplayName() { - return "Export configuration"; - } - }); + req.setAttribute("exportedYaml", out.toString(StandardCharsets.UTF_8)); req.getView(this, "viewExport.jelly").forward(req, res); } diff --git a/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly b/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly index d826cbdcb7..de100f264f 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly +++ b/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly @@ -3,7 +3,7 @@ xmlns:f="/lib/form"> - + @@ -11,7 +11,7 @@ - +
@@ -20,7 +20,7 @@
-      ${viewExport.urlName}
+      ${exportedYaml}
     
From ac34cc9e4e5016dfdd5cf2a9995bb38c05247542 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Mon, 13 Apr 2026 22:54:39 +0530 Subject: [PATCH 2/6] Added tests --- .../plugins/casc/ConfigurationAsCode.java | 11 +++-- .../casc/ConfigurationAsCodeApiTest.java | 26 +++++++++++ .../jenkins/plugins/casc/ErrorPageTest.java | 45 +++++++++++++++++++ .../plugins/casc/ConfigurationAsCodeTest.java | 2 +- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java index d3d70e97b8..d3bebfd4a3 100644 --- a/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java +++ b/plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationAsCode.java @@ -226,7 +226,8 @@ public void doReplace(StaplerRequest2 request, StaplerResponse2 response) throws candidateSources.add(candidateSource); } else { LOGGER.log(Level.WARNING, "Source {0} could not be applied", candidateSource); - throw new ConfiguratorException("Source " + candidateSource + " could not be applied or does not exist."); + throw new ConfiguratorException( + "Source " + candidateSource + " could not be applied or does not exist."); } } if (!candidateSources.isEmpty()) { @@ -242,7 +243,8 @@ public void doReplace(StaplerRequest2 request, StaplerResponse2 response) throws LOGGER.log(Level.FINE, "Replace configuration with: " + normalizedSource); } else { LOGGER.log(Level.WARNING, "Provided sources could not be applied"); - throw new ConfiguratorException("Provided sources could not be applied. Please check the syntax and validity of the provided configuration."); + throw new ConfiguratorException( + "Provided sources could not be applied. Please check the syntax and validity of the provided configuration."); } } else { LOGGER.log(Level.FINE, "No such source exists, applying default"); @@ -452,7 +454,10 @@ public List getBundledCasCURIs() { if (bundled != null && matcher.matches(new File(bundled.getPath()).toPath())) { res.add(bundled.toString()); } else if (bundled != null) { - LOGGER.log(Level.FINE, "Skipped bundled resource {0} as it does not match the YAML pattern.", bundled.getPath()); + LOGGER.log( + Level.FINE, + "Skipped bundled resource {0} as it does not match the YAML pattern.", + bundled.getPath()); } } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to execute " + res, e); diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java index b69e8774e0..34f0ec7a3e 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeApiTest.java @@ -5,10 +5,14 @@ import static org.hamcrest.Matchers.is; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; import jenkins.model.Jenkins; import org.htmlunit.HttpMethod; import org.htmlunit.WebRequest; import org.htmlunit.WebResponse; +import org.htmlunit.util.NameValuePair; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -159,4 +163,26 @@ public void testDoConfigure_ValidYaml_NoChanges() throws Exception { assertThat(j.jenkins.getSystemMessage(), is("Idempotency Test")); } } + + @Test + public void testDoReplace_ValidSource() throws Exception { + configureAdminSecurity(); + + Path configFile = Files.createTempFile("valid", ".yaml"); + Files.writeString(configFile, "jenkins:\n systemMessage: 'Hello Replace'"); + + try (JenkinsRule.WebClient wc = j.createWebClient().withBasicApiToken(ADMIN)) { + wc.setThrowExceptionOnFailingStatusCode(false); + + WebRequest request = + new WebRequest(new URL(j.getURL(), "manage/configuration-as-code/replace"), HttpMethod.POST); + + request.setRequestParameters(List.of(new NameValuePair("_.newSource", configFile.toString()))); + + WebResponse response = wc.getPage(request).getWebResponse(); + + assertThat(response.getStatusCode(), is(200)); + assertThat(j.jenkins.getSystemMessage(), is("Hello Replace")); + } + } } diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java index 5819bfa60e..71dccd544d 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java @@ -1,7 +1,10 @@ package io.jenkins.plugins.casc; +import static hudson.model.ManagementLink.Category.CONFIGURATION; +import static jenkins.model.Jenkins.ADMINISTER; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.io.IOException; @@ -16,6 +19,7 @@ import org.junit.jupiter.api.io.TempDir; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.MockAuthorizationStrategy; import org.jvnet.hudson.test.junit.jupiter.WithJenkins; @WithJenkins @@ -87,4 +91,45 @@ void noImplementationFoundForSymbol() throws Exception { assertThat(pageContent, containsString("Attribute was:")); assertThat(pageContent, containsString("unknown")); } + + @Test + void replaceWithInvalidSource() throws Exception { + String pageContent = replaceConfiguration(); + + assertThat( + pageContent, containsString("Source non-existent-file.yaml could not be applied or does not exist.")); + } + + private String replaceConfiguration() throws Exception { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy( + new MockAuthorizationStrategy().grant(ADMINISTER).everywhere().to("admin")); + + try (WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false)) { + webClient.login("admin", "admin"); + + HtmlPage htmlPage = webClient.goTo("manage/configuration-as-code/"); + + HtmlButton button = (HtmlButton) htmlPage.getElementById("btn-open-apply-configuration"); + HtmlElementUtil.click(button); + + HtmlForm replaceForm = htmlPage.getFormByName("replace"); + replaceForm.getInputByName("_.newSource").setValue("non-existent-file.yaml"); + + HtmlPage submit = r.submit(replaceForm); + + return submit.asNormalizedText(); + } + } + + @Test + void verifyManagementLinkProperties() { + ConfigurationAsCode casc = ConfigurationAsCode.get(); + + assertEquals(CONFIGURATION, casc.getCategory()); + + assertEquals("configuration-as-code", casc.getUrlName()); + assertEquals("Configuration as Code", casc.getDisplayName()); + assertEquals("symbol-logo plugin-configuration-as-code", casc.getIconFileName()); + } } diff --git a/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java b/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java index f2fb1f2f2b..48f3721696 100644 --- a/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java +++ b/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java @@ -343,7 +343,7 @@ void testHtmlDocStringRetrieval(JenkinsConfiguredWithCodeRule j) throws Exceptio @Test void configurationCategory(JenkinsConfiguredWithCodeRule j) { ConfigurationAsCode configurationAsCode = ConfigurationAsCode.get(); - assertThat(configurationAsCode.getCategoryName(), is("CONFIGURATION")); + assertThat(configurationAsCode.getCategory(), is("CONFIGURATION")); } private static File newFolder(File root, String... subDirs) throws IOException { From a65e83f27e33bea1e33f23b2e255a3c4adad9952 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Tue, 14 Apr 2026 00:07:59 +0530 Subject: [PATCH 3/6] Cheange expectation from string to enum --- .../java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java b/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java index 48f3721696..896861a1d9 100644 --- a/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java +++ b/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java @@ -1,5 +1,6 @@ package io.jenkins.plugins.casc; +import static hudson.model.ManagementLink.Category.CONFIGURATION; import static io.jenkins.plugins.casc.ConfigurationAsCode.CASC_JENKINS_CONFIG_PROPERTY; import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; import static io.jenkins.plugins.casc.misc.Util.toYamlString; @@ -343,7 +344,7 @@ void testHtmlDocStringRetrieval(JenkinsConfiguredWithCodeRule j) throws Exceptio @Test void configurationCategory(JenkinsConfiguredWithCodeRule j) { ConfigurationAsCode configurationAsCode = ConfigurationAsCode.get(); - assertThat(configurationAsCode.getCategory(), is("CONFIGURATION")); + assertThat(configurationAsCode.getCategory(), is(CONFIGURATION)); } private static File newFolder(File root, String... subDirs) throws IOException { From 93e91a59c21186f17316c15204649710189100b1 Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Tue, 14 Apr 2026 00:23:36 +0530 Subject: [PATCH 4/6] Remove configurationCategory test --- .../io/jenkins/plugins/casc/ConfigurationAsCodeTest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java b/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java index 896861a1d9..90364a27e2 100644 --- a/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java +++ b/test-harness/src/test/java/io/jenkins/plugins/casc/ConfigurationAsCodeTest.java @@ -1,6 +1,5 @@ package io.jenkins.plugins.casc; -import static hudson.model.ManagementLink.Category.CONFIGURATION; import static io.jenkins.plugins.casc.ConfigurationAsCode.CASC_JENKINS_CONFIG_PROPERTY; import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; import static io.jenkins.plugins.casc.misc.Util.toYamlString; @@ -341,12 +340,6 @@ void testHtmlDocStringRetrieval(JenkinsConfiguredWithCodeRule j) throws Exceptio assertEquals(expectedDocString, actualDocString); } - @Test - void configurationCategory(JenkinsConfiguredWithCodeRule j) { - ConfigurationAsCode configurationAsCode = ConfigurationAsCode.get(); - assertThat(configurationAsCode.getCategory(), is(CONFIGURATION)); - } - private static File newFolder(File root, String... subDirs) throws IOException { String subFolder = String.join("/", subDirs); File result = new File(root, subFolder); From 0a1cc159c2915490f5f255eabaf9496e531808c2 Mon Sep 17 00:00:00 2001 From: somil jain <89907422+somiljain2006@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:47:48 +0530 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com> --- .../jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly b/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly index de100f264f..3067e809f3 100644 --- a/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly +++ b/plugin/src/main/resources/io/jenkins/plugins/casc/ConfigurationAsCode/viewExport.jelly @@ -11,7 +11,7 @@ - +
From 27e5223feb8808045b0454a5c220eb5b0bde06dc Mon Sep 17 00:00:00 2001 From: somiljain2006 Date: Wed, 15 Apr 2026 02:25:17 +0530 Subject: [PATCH 6/6] Remove irrelevant test --- .../java/io/jenkins/plugins/casc/ErrorPageTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java b/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java index 71dccd544d..ccf4a03099 100644 --- a/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java +++ b/plugin/src/test/java/io/jenkins/plugins/casc/ErrorPageTest.java @@ -1,10 +1,8 @@ package io.jenkins.plugins.casc; -import static hudson.model.ManagementLink.Category.CONFIGURATION; import static jenkins.model.Jenkins.ADMINISTER; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.io.IOException; @@ -121,15 +119,4 @@ private String replaceConfiguration() throws Exception { return submit.asNormalizedText(); } } - - @Test - void verifyManagementLinkProperties() { - ConfigurationAsCode casc = ConfigurationAsCode.get(); - - assertEquals(CONFIGURATION, casc.getCategory()); - - assertEquals("configuration-as-code", casc.getUrlName()); - assertEquals("Configuration as Code", casc.getDisplayName()); - assertEquals("symbol-logo plugin-configuration-as-code", casc.getIconFileName()); - } }