PluginUpgradeStrategyTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.cling.invoker.mvnup.goals;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
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.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for the {@link PluginUpgradeStrategy} class. Tests plugin version upgrades, plugin management additions,
* and Maven 4 compatibility.
*/
@DisplayName("PluginUpgradeStrategy")
class PluginUpgradeStrategyTest {
private PluginUpgradeStrategy strategy;
@BeforeEach
void setUp() {
strategy = new PluginUpgradeStrategy();
}
private UpgradeContext createMockContext() {
return TestUtils.createMockContext();
}
private UpgradeContext createMockContext(UpgradeOptions options) {
return TestUtils.createMockContext(options);
}
private UpgradeOptions createDefaultOptions() {
return TestUtils.createDefaultOptions();
}
@Nested
@DisplayName("Applicability")
class ApplicabilityTests {
@Test
@DisplayName("should be applicable when --plugins option is true")
void shouldBeApplicableWhenPluginsOptionTrue() {
UpgradeOptions options = mock(UpgradeOptions.class);
when(options.plugins()).thenReturn(Optional.of(true));
when(options.all()).thenReturn(Optional.empty());
UpgradeContext context = createMockContext(options);
assertTrue(strategy.isApplicable(context), "Strategy should be applicable when --plugins is true");
}
@Test
@DisplayName("should be applicable when --all option is specified")
void shouldBeApplicableWhenAllOptionSpecified() {
UpgradeOptions options = mock(UpgradeOptions.class);
when(options.all()).thenReturn(Optional.of(true));
when(options.plugins()).thenReturn(Optional.empty());
UpgradeContext context = createMockContext(options);
assertTrue(strategy.isApplicable(context), "Strategy should be applicable when --all is specified");
}
@Test
@DisplayName("should be applicable by default when no specific options provided")
void shouldBeApplicableByDefaultWhenNoSpecificOptions() {
UpgradeOptions options = createDefaultOptions();
UpgradeContext context = createMockContext(options);
assertTrue(strategy.isApplicable(context), "Strategy should be applicable by default");
}
@Test
@DisplayName("should not be applicable when --plugins option is false")
void shouldNotBeApplicableWhenPluginsOptionFalse() {
UpgradeOptions options = mock(UpgradeOptions.class);
when(options.plugins()).thenReturn(Optional.of(false));
when(options.all()).thenReturn(Optional.empty());
UpgradeContext context = createMockContext(options);
assertFalse(strategy.isApplicable(context), "Strategy should not be applicable when --plugins is false");
}
}
@Nested
@DisplayName("Plugin Upgrades")
class PluginUpgradeTests {
@Test
@DisplayName("should upgrade plugin version when below minimum")
void shouldUpgradePluginVersionWhenBelowMinimum() throws Exception {
String pomXml = PomBuilder.create()
.groupId("test")
.artifactId("test")
.version("1.0.0")
.plugin("org.apache.maven.plugins", "maven-compiler-plugin", "3.8.1")
.build();
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
// Note: POM may or may not be modified depending on whether upgrades are needed
// Verify the plugin version was upgraded
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
// The exact version depends on the plugin upgrades configuration
assertNotNull(version, "Plugin should have a version");
}
@Test
@DisplayName("should not modify plugin when version is already sufficient")
void shouldNotModifyPluginWhenVersionAlreadySufficient() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
// POM might still be marked as modified due to other plugin management additions
}
@Test
@DisplayName("should upgrade milestone version below release minimum")
void shouldUpgradeMilestoneVersionBelowRelease() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M1</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded 3.0.0-M1 to 3.5.0");
Editor editor = new Editor(document);
String version = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.0", version, "3.0.0-M1 should be upgraded to 3.5.0");
}
@Test
@DisplayName("should upgrade plugin in pluginManagement")
void shouldUpgradePluginInPluginManagement() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>2.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded maven-enforcer-plugin");
// Verify the version was upgraded
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "pluginManagement", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.0", version);
}
@Test
@DisplayName("should upgrade plugin with property version")
void shouldUpgradePluginWithPropertyVersion() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<properties>
<shade.plugin.version>3.0.0</shade.plugin.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${shade.plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded shade plugin property");
// Verify the property was upgraded
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("properties", "shade.plugin.version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.0", version);
}
@Test
@DisplayName("should upgrade surefire plugin when below minimum")
void shouldUpgradeSurefirePluginWhenBelowMinimum() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded maven-surefire-plugin");
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.2", version);
}
@Test
@DisplayName("should upgrade failsafe plugin when below minimum")
void shouldUpgradeFailsafePluginWhenBelowMinimum() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded maven-failsafe-plugin");
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.2", version);
}
@Test
@DisplayName("should upgrade surefire-report plugin when below minimum")
void shouldUpgradeSurefireReportPluginWhenBelowMinimum() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded maven-surefire-report-plugin");
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.2", version);
}
@Test
@DisplayName("should upgrade jaxb2-maven-plugin when below minimum")
void shouldUpgradeJaxb2MavenPluginWhenBelowMinimum() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded jaxb2-maven-plugin");
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.2.0", version);
}
@Test
@DisplayName("should not upgrade when version is already higher")
void shouldNotUpgradeWhenVersionAlreadyHigher() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.3.0</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
// Verify the version was not changed
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("1.3.0", version);
}
@Test
@DisplayName("should upgrade plugin without explicit groupId")
void shouldUpgradePluginWithoutExplicitGroupId() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(
result.modifiedCount() > 0,
"Should have upgraded maven-shade-plugin even without explicit groupId");
// Verify the version was upgraded
Editor editor = new Editor(document);
Element root = editor.root();
String version = root.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.0", version);
}
@Test
@DisplayName("should not upgrade plugin without version")
void shouldNotUpgradePluginWithoutVersion() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<!-- No version - inherited from parent or pluginManagement -->
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
// Note: POM might still be modified due to plugin management additions
}
@Test
@DisplayName("should not upgrade when property is not found")
void shouldNotUpgradeWhenPropertyNotFound() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec.plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
// Note: POM might still be modified due to plugin management additions
}
}
@Nested
@DisplayName("Plugin Dependency Upgrades")
class PluginDependencyUpgradeTests {
@Test
@DisplayName("should upgrade extra-enforcer-rules dependency when below minimum")
void shouldUpgradeExtraEnforcerRulesDependency() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<dependencies>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>extra-enforcer-rules</artifactId>
<version>1.0-beta-4</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin dependency upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded extra-enforcer-rules");
String xml = document.toXml();
assertTrue(xml.contains("<version>1.4</version>"), "extra-enforcer-rules should be upgraded to 1.4");
assertFalse(xml.contains("1.0-beta-4"), "Old version should be gone");
}
@Test
@DisplayName("should not upgrade extra-enforcer-rules when version is already sufficient")
void shouldNotUpgradeExtraEnforcerRulesWhenSufficient() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<dependencies>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>extra-enforcer-rules</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
String xml = document.toXml();
assertTrue(xml.contains("1.8.0"), "Version 1.8.0 should be preserved");
}
}
@Nested
@DisplayName("Plugin Management")
class PluginManagementTests {
@Test
@DisplayName("should add pluginManagement before existing plugins section")
void shouldAddPluginManagementBeforeExistingPluginsSection() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
// Verify the structure
Editor editor = new Editor(document);
Element root = editor.root();
Element buildElement = root.childElement("build").orElse(null);
assertNotNull(buildElement, "Build element should exist");
List<Element> buildChildren = buildElement.childElements().toList();
// Find the indices of pluginManagement and plugins
int pluginManagementIndex = -1;
int pluginsIndex = -1;
for (int i = 0; i < buildChildren.size(); i++) {
Element child = buildChildren.get(i);
if ("pluginManagement".equals(child.name())) {
pluginManagementIndex = i;
} else if ("plugins".equals(child.name())) {
pluginsIndex = i;
}
}
assertTrue(pluginsIndex >= 0, "plugins should be present");
if (pluginManagementIndex >= 0) {
assertTrue(
pluginManagementIndex < pluginsIndex,
"pluginManagement should come before plugins when both are present");
}
}
}
@Nested
@DisplayName("Plugin Upgrade Configuration")
class PluginUpgradeConfigurationTests {
@Test
@DisplayName("should have predefined plugin upgrades")
void shouldHavePredefinedPluginUpgrades() throws Exception {
List<PluginUpgrade> upgrades = PluginUpgradeStrategy.getPluginUpgrades();
assertFalse(upgrades.isEmpty(), "Should have predefined plugin upgrades");
// Verify some expected plugins are included
boolean hasCompilerPlugin =
upgrades.stream().anyMatch(upgrade -> "maven-compiler-plugin".equals(upgrade.artifactId()));
boolean hasExecPlugin =
upgrades.stream().anyMatch(upgrade -> "exec-maven-plugin".equals(upgrade.artifactId()));
boolean hasSurefirePlugin =
upgrades.stream().anyMatch(upgrade -> "maven-surefire-plugin".equals(upgrade.artifactId()));
boolean hasFailsafePlugin =
upgrades.stream().anyMatch(upgrade -> "maven-failsafe-plugin".equals(upgrade.artifactId()));
boolean hasSurefireReportPlugin =
upgrades.stream().anyMatch(upgrade -> "maven-surefire-report-plugin".equals(upgrade.artifactId()));
assertTrue(hasCompilerPlugin, "Should include maven-compiler-plugin upgrade");
assertTrue(hasExecPlugin, "Should include exec-maven-plugin upgrade");
assertTrue(hasSurefirePlugin, "Should include maven-surefire-plugin upgrade");
assertTrue(hasFailsafePlugin, "Should include maven-failsafe-plugin upgrade");
assertTrue(hasSurefireReportPlugin, "Should include maven-surefire-report-plugin upgrade");
}
@Test
@DisplayName("should have valid plugin upgrade definitions")
void shouldHaveValidPluginUpgradeDefinitions() throws Exception {
List<PluginUpgrade> upgrades = PluginUpgradeStrategy.getPluginUpgrades();
for (PluginUpgrade upgrade : upgrades) {
assertNotNull(upgrade.groupId(), "Plugin upgrade should have groupId");
assertNotNull(upgrade.artifactId(), "Plugin upgrade should have artifactId");
assertNotNull(upgrade.minVersion(), "Plugin upgrade should have minVersion");
// configuration can be null for some plugins
}
}
}
@Nested
@DisplayName("Inherited Plugin Detection")
class InheritedPluginDetectionTests {
@Test
@DisplayName("should inject pluginManagement with comment for plugins inherited from remote parent")
void shouldInjectPluginManagementWithCommentForRemoteParentPlugins() throws Exception {
// org.apache:apache:23 defines maven-enforcer-plugin in pluginManagement and
// build/plugins. A child POM that does NOT itself declare the plugin should
// still get a pluginManagement override with a comment explaining it overrides
// the parent, so that Maven 4 incompatible plugin versions get upgraded.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>23</version>
</parent>
<groupId>org.example</groupId>
<artifactId>test-child</artifactId>
<version>1.0.0-SNAPSHOT</version>
</project>
""";
Path tempDir = Files.createTempDirectory("mvnup-test-");
try {
Files.createDirectories(tempDir.resolve(".mvn"));
Path pomPath = tempDir.resolve("pom.xml");
Files.writeString(pomPath, pomXml);
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(pomPath, document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Strategy should succeed");
assertTrue(result.modifiedCount() > 0, "Should have modified POM for remote parent plugin upgrade");
String xml = DomUtils.toXml(document);
assertTrue(
xml.contains("<pluginManagement>"),
"Should inject pluginManagement for plugins from remote parent");
assertTrue(
xml.contains("Override version inherited from parent"),
"Should add comment explaining the override");
assertTrue(
xml.contains("<artifactId>maven-enforcer-plugin</artifactId>"),
"Should add pluginManagement for maven-enforcer-plugin");
// Verify the comment is on its own line, not appended to the previous closing tag
assertFalse(
xml.contains("</plugin><!--"), "Comment should be on its own line, not appended to </plugin>");
} finally {
try (var walk = Files.walk(tempDir)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
try {
Files.delete(p);
} catch (IOException ignored) {
}
});
}
}
}
@Test
@DisplayName("should override remote parent plugin via pluginManagement with comment, not direct build/plugins")
void shouldOverrideRemoteParentPluginViaPluginManagementWithComment() throws Exception {
// org.apache:apache:23 has maven-enforcer-plugin in build/plugins WITHOUT
// an explicit version ��� the version (1.4.1) comes from pluginManagement.
// In this case, adding a pluginManagement override with a comment in the child
// is sufficient; no direct build/plugins entry should be added for enforcer.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>23</version>
</parent>
<groupId>org.example</groupId>
<artifactId>test-child</artifactId>
<version>1.0.0-SNAPSHOT</version>
</project>
""";
Document document = Document.of(pomXml);
Path pomPath = Paths.get("/project/pom.xml").toAbsolutePath();
Map<Path, Document> pomMap = Map.of(pomPath, document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Strategy should succeed");
Editor editor = new Editor(document);
Element root = editor.root();
// Verify pluginManagement entry exists for enforcer
Element pmPlugins =
root.path("build", "pluginManagement", "plugins").orElse(null);
assertNotNull(pmPlugins, "Should have pluginManagement/plugins");
boolean hasEnforcerInPM = pmPlugins
.childElements("plugin")
.anyMatch(p -> "maven-enforcer-plugin"
.equals(p.childElement("artifactId")
.map(Element::textContentTrimmed)
.orElse("")));
assertTrue(hasEnforcerInPM, "Should have enforcer in pluginManagement");
String xml = DomUtils.toXml(document);
assertTrue(
xml.contains("Override version inherited from parent"),
"Should add comment explaining the override");
// Verify the comment is on its own line, not appended to the previous closing tag
assertFalse(xml.contains("</plugin><!--"), "Comment should be on its own line, not appended to </plugin>");
// Verify NO direct build/plugins entry for enforcer (PM override is sufficient)
Element buildPlugins = root.childElement("build")
.flatMap(b -> b.childElement("plugins"))
.orElse(null);
if (buildPlugins != null) {
boolean hasEnforcerInPlugins = buildPlugins
.childElements("plugin")
.anyMatch(p -> "maven-enforcer-plugin"
.equals(p.childElement("artifactId")
.map(Element::textContentTrimmed)
.orElse("")));
assertFalse(
hasEnforcerInPlugins,
"Should NOT add enforcer in build/plugins when pluginManagement override suffices");
}
}
@Test
@DisplayName("should not duplicate plugin in build/plugins when already locally declared")
void shouldNotDuplicatePluginInBuildPluginsWhenAlreadyDeclared() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>23</version>
</parent>
<groupId>org.example</groupId>
<artifactId>test-child</artifactId>
<version>1.0.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Path pomPath = Paths.get("/project/pom.xml").toAbsolutePath();
Map<Path, Document> pomMap = Map.of(pomPath, document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Strategy should succeed");
Editor editor = new Editor(document);
Element root = editor.root();
Element buildPlugins = root.childElement("build")
.flatMap(b -> b.childElement("plugins"))
.orElse(null);
assertNotNull(buildPlugins, "Should have build/plugins section");
long enforcerCount = buildPlugins
.childElements("plugin")
.filter(p -> "maven-enforcer-plugin"
.equals(p.childElement("artifactId")
.map(Element::textContentTrimmed)
.orElse("")))
.count();
assertEquals(1, enforcerCount, "Should have exactly one maven-enforcer-plugin in build/plugins");
String version = buildPlugins
.childElements("plugin")
.filter(p -> "maven-enforcer-plugin"
.equals(p.childElement("artifactId")
.map(Element::textContentTrimmed)
.orElse("")))
.findFirst()
.flatMap(p -> p.childElement("version"))
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.5.0", version, "Existing enforcer-plugin version should be upgraded to 3.5.0");
}
@Test
@DisplayName("should inject pluginManagement when plugin is locally declared without version")
void shouldInjectPluginManagementForLocallyDeclaredPluginWithoutVersion() throws Exception {
// Child POM explicitly declares maven-enforcer-plugin in build/plugins without
// a version. The version comes from the remote parent's pluginManagement.
// Since the child does declare the plugin, mvnup should add a pluginManagement
// entry to override the inherited version.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>23</version>
</parent>
<groupId>org.example</groupId>
<artifactId>test-child</artifactId>
<version>1.0.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
""";
Path tempDir = Files.createTempDirectory("mvnup-test-");
try {
Files.createDirectories(tempDir.resolve(".mvn"));
Path pomPath = tempDir.resolve("pom.xml");
Files.writeString(pomPath, pomXml);
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(pomPath, document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Strategy should succeed");
assertTrue(
result.modifiedCount() > 0, "Should have added pluginManagement for locally declared plugin");
String xml = DomUtils.toXml(document);
assertTrue(
xml.contains("<pluginManagement>"), "Should add pluginManagement for locally declared plugin");
assertTrue(
xml.contains("<artifactId>maven-enforcer-plugin</artifactId>"),
"Should add pluginManagement for maven-enforcer-plugin");
} finally {
try (var walk = Files.walk(tempDir)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
try {
Files.delete(p);
} catch (IOException ignored) {
}
});
}
}
}
}
@Nested
@DisplayName("Error Handling")
class ErrorHandlingTests {
@Test
@DisplayName("should warn when effective model analysis fails for POM with unresolvable remote parent")
void shouldWarnWhenEffectiveModelAnalysisFailsForUnresolvableRemoteParent() throws Exception {
// POM inherits from a remote parent that does not exist.
// The effective model analysis should warn (not silently swallow) the failure.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.nonexistent.test</groupId>
<artifactId>nonexistent-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>test-child</artifactId>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
// Strategy should complete successfully even when effective model analysis fails
assertNotNull(result, "Result should not be null");
assertTrue(result.success(), "Strategy should succeed even when effective model analysis fails");
assertTrue(result.processedPoms().contains(Paths.get("pom.xml")), "POM should be marked as processed");
// The warning should have been logged (not silently swallowed at debug level)
verify(context.logger, atLeastOnce())
.warn(argThat(msg -> msg.contains("Failed to analyze effective model")));
}
@Test
@DisplayName("should handle malformed POM gracefully")
void shouldHandleMalformedPOMGracefully() throws Exception {
String malformedPomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<!-- Missing required elements -->
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(malformedPomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
// Strategy should handle malformed POMs gracefully
assertNotNull(result, "Result should not be null");
assertTrue(result.processedPoms().contains(Paths.get("pom.xml")), "POM should be marked as processed");
}
}
@Nested
@DisplayName("Quarkus Plugin Upgrades")
class QuarkusPluginUpgradeTests {
@Test
@DisplayName("should upgrade quarkus-maven-plugin with io.quarkus groupId when below minimum")
void shouldUpgradeQuarkusPluginWithIoQuarkusGroupId() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>3.16.3</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded quarkus-maven-plugin");
Editor editor = new Editor(document);
String version = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.26.0", version, "quarkus-maven-plugin should be upgraded to 3.26.0");
}
@Test
@DisplayName("should upgrade quarkus-maven-plugin with io.quarkus.platform groupId when below minimum")
void shouldUpgradeQuarkusPluginWithPlatformGroupId() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>3.16.3</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded quarkus-maven-plugin");
Editor editor = new Editor(document);
String version = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.26.0", version, "quarkus-maven-plugin should be upgraded to 3.26.0");
}
@Test
@DisplayName("should not upgrade quarkus-maven-plugin when version is already sufficient")
void shouldNotUpgradeQuarkusPluginWhenVersionSufficient() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>3.31.4</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
Editor editor = new Editor(document);
String version = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.31.4", version, "Version 3.31.4 should be preserved");
}
@Test
@DisplayName("should decouple plugin version from shared BOM property")
void shouldDecouplePluginVersionFromSharedBomProperty() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<properties>
<quarkus.platform.version>3.16.3</quarkus.platform.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have modified POM");
// BOM property should be unchanged
Editor editor = new Editor(document);
String bomVersion = editor.root()
.path("properties", "quarkus.platform.version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.16.3", bomVersion, "BOM property should remain unchanged at 3.16.3");
// New property should be introduced
String pluginVersion = editor.root()
.path("properties", "quarkus-plugin.version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.26.0", pluginVersion, "New quarkus-plugin.version property should be 3.26.0");
// Plugin version should reference the new property
String pluginVersionRef = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals(
"${quarkus-plugin.version}",
pluginVersionRef,
"Plugin should reference the new quarkus-plugin.version property");
}
@Test
@DisplayName("should not decouple when plugin has its own property not shared with BOM")
void shouldNotDecoupleWhenPluginHasOwnProperty() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<properties>
<quarkus-plugin.version>3.16.3</quarkus-plugin.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus-plugin.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded quarkus-maven-plugin");
// The property should be upgraded directly (no decoupling needed)
Editor editor = new Editor(document);
String version = editor.root()
.path("properties", "quarkus-plugin.version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.26.0", version, "Property should be upgraded directly to 3.26.0");
// Plugin should still reference the same property
String pluginVersionRef = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals(
"${quarkus-plugin.version}", pluginVersionRef, "Plugin should still reference the same property");
}
@Test
@DisplayName("should not decouple when BOM version is already sufficient")
void shouldNotDecoupleWhenBomVersionSufficient() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<properties>
<quarkus.platform.version>3.31.4</quarkus.platform.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
// No new property should be introduced ��� version is already sufficient
Editor editor = new Editor(document);
String bomVersion = editor.root()
.path("properties", "quarkus.platform.version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.31.4", bomVersion, "BOM property should remain unchanged");
// No quarkus-plugin.version should exist
Element newProp =
editor.root().path("properties", "quarkus-plugin.version").orElse(null);
assertTrue(newProp == null, "Should not introduce new property when version is already sufficient");
}
@Test
@DisplayName("should emit version gap warning when decoupling")
void shouldEmitVersionGapWarningWhenDecoupling() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<properties>
<quarkus.platform.version>3.16.3</quarkus.platform.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
// Verify warning was emitted about the version gap
verify(context.logger, atLeastOnce())
.warn(argThat(msg -> msg.contains("quarkus-maven-plugin upgraded to 3.26.0")
&& msg.contains("3.16.3")
&& msg.contains("mismatched plugin and platform")));
}
@Test
@DisplayName("should include quarkus-maven-plugin in predefined plugin upgrades")
void shouldIncludeQuarkusPluginInPredefinedUpgrades() {
List<PluginUpgrade> upgrades = PluginUpgradeStrategy.getPluginUpgrades();
boolean hasIoQuarkus = upgrades.stream()
.anyMatch(u -> "io.quarkus".equals(u.groupId()) && "quarkus-maven-plugin".equals(u.artifactId()));
boolean hasIoQuarkusPlatform = upgrades.stream()
.anyMatch(u ->
"io.quarkus.platform".equals(u.groupId()) && "quarkus-maven-plugin".equals(u.artifactId()));
assertTrue(hasIoQuarkus, "Should include io.quarkus:quarkus-maven-plugin upgrade");
assertTrue(hasIoQuarkusPlatform, "Should include io.quarkus.platform:quarkus-maven-plugin upgrade");
// Verify the reason text
upgrades.stream()
.filter(u -> "quarkus-maven-plugin".equals(u.artifactId()))
.forEach(u -> assertEquals(
"Maven 4 compatibility (Aether API changes)",
u.reason(),
"Quarkus plugin upgrade should have the correct reason"));
}
@Test
@DisplayName("should upgrade quarkus-maven-plugin in pluginManagement")
void shouldUpgradeQuarkusPluginInPluginManagement() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>3.16.3</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Plugin upgrade should succeed");
assertTrue(result.modifiedCount() > 0, "Should have upgraded quarkus-maven-plugin in pluginManagement");
Editor editor = new Editor(document);
String version = editor.root()
.path("build", "pluginManagement", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals("3.26.0", version, "quarkus-maven-plugin in pluginManagement should be upgraded to 3.26.0");
}
@Test
@DisplayName("should not decouple when shared property is inherited from parent POM")
void shouldNotDecoupleWhenSharedPropertyIsInherited() throws Exception {
// The shared property is NOT declared in this POM ��� it's inherited from a parent.
// We cannot resolve its value, so decoupling should be skipped to avoid
// introducing a quarkus-plugin.version=3.26.0 that might downgrade an already-sufficient version.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
Editor editor = new Editor(document);
Element newProp =
editor.root().path("properties", "quarkus-plugin.version").orElse(null);
assertTrue(
newProp == null, "Should not introduce quarkus-plugin.version when shared property is inherited");
// The plugin version reference should remain unchanged
String pluginVersion = editor.root()
.path("build", "plugins", "plugin", "version")
.map(Element::textContentTrimmed)
.orElse(null);
assertEquals(
"${quarkus.platform.version}",
pluginVersion,
"Plugin version should remain as the inherited property reference");
}
}
@Nested
@DisplayName("Strategy Description")
class StrategyDescriptionTests {
@Test
@DisplayName("should provide meaningful description")
void shouldProvideMeaningfulDescription() {
String description = strategy.getDescription();
assertNotNull(description, "Description should not be null");
assertFalse(description.trim().isEmpty(), "Description should not be empty");
assertTrue(description.toLowerCase().contains("plugin"), "Description should mention plugins");
}
}
@Nested
@DisplayName("XML Formatting")
class XmlFormattingTests {
@Test
@DisplayName("should format pluginManagement with proper indentation")
void shouldFormatPluginManagementWithProperIndentation() throws Exception {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
// Convert to string to check formatting
String result = DomUtils.toXml(document);
// Check that the plugin version was upgraded
assertTrue(result.contains("<version>3.2</version>"), "Plugin version should be upgraded to 3.2");
// Verify that the XML formatting is correct - no malformed closing tags
assertFalse(result.contains("</plugin></plugins>"), "Should not have malformed closing tags");
assertFalse(result.contains("</plugins></pluginManagement>"), "Should not have malformed closing tags");
// Check that proper indentation is maintained
assertTrue(result.contains(" <build>"), "Build element should be properly indented");
assertTrue(result.contains(" <plugins>"), "Plugins element should be properly indented");
assertTrue(result.contains(" <plugin>"), "Plugin element should be properly indented");
}
@Test
@DisplayName("should format pluginManagement with proper indentation when added")
void shouldFormatPluginManagementWithProperIndentationWhenAdded() throws Exception {
// Use a POM that will trigger pluginManagement addition by having a plugin without version
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<!-- No version - should trigger pluginManagement addition -->
</plugin>
</plugins>
</build>
</project>
""";
Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
UpgradeContext context = createMockContext();
strategy.doApply(context, pomMap);
// Convert to string to check formatting
String result = DomUtils.toXml(document);
// If pluginManagement was added, verify proper formatting
if (result.contains("<pluginManagement>")) {
// Verify that the XML formatting is correct - no malformed closing tags
assertFalse(result.contains("</plugin></plugins>"), "Should not have malformed closing tags");
assertFalse(result.contains("</plugins></pluginManagement>"), "Should not have malformed closing tags");
// Check that proper indentation is maintained for pluginManagement
assertTrue(
result.contains(" <pluginManagement>"), "PluginManagement should be properly indented");
assertTrue(
result.contains(" <plugins>"),
"Plugins in pluginManagement should be properly indented");
assertTrue(
result.contains(" </pluginManagement>"),
"PluginManagement closing tag should be properly indented");
}
}
}
}