diff --git a/pom.xml b/pom.xml index af6f70762..900d746f6 100644 --- a/pom.xml +++ b/pom.xml @@ -370,14 +370,6 @@ under the License. test - - - org.apache.maven - maven-compat - ${mavenVersion} - test - - org.eclipse.jetty jetty-server diff --git a/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java b/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java index 48d37908d..fafb54fe4 100644 --- a/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java +++ b/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java @@ -38,10 +38,9 @@ import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.dependency.utils.RepositorySessionInjector; import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.ProjectBuildingRequest; -import org.apache.maven.repository.RepositorySystem; -import org.apache.maven.settings.Settings; import org.apache.maven.shared.transfer.artifact.ArtifactCoordinate; import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate; import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver; @@ -73,9 +72,10 @@ public class GetMojo extends AbstractMojo { private final Map repositoryLayouts; /** - * The repository system. + * Applies the mirrors, proxies and credentials of the repository session to the repositories named by + * {@link #remoteRepositories}. */ - private final RepositorySystem repositorySystem; + private final RepositorySessionInjector repositorySessionInjector; private final DefaultDependableCoordinate coordinate = new DefaultDependableCoordinate(); @@ -115,14 +115,13 @@ public GetMojo( ArtifactResolver artifactResolver, DependencyResolver dependencyResolver, ArtifactHandlerManager artifactHandlerManager, - Map repositoryLayouts, - RepositorySystem repositorySystem) { + Map repositoryLayouts) { this.session = session; this.artifactResolver = artifactResolver; this.dependencyResolver = dependencyResolver; this.artifactHandlerManager = artifactHandlerManager; this.repositoryLayouts = repositoryLayouts; - this.repositorySystem = repositorySystem; + this.repositorySessionInjector = new RepositorySessionInjector(repositoryLayouts); } @Override @@ -174,10 +173,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest()); - Settings settings = session.getSettings(); - repositorySystem.injectMirror(repoList, settings.getMirrors()); - repositorySystem.injectProxy(repoList, settings.getProxies()); - repositorySystem.injectAuthentication(repoList, settings.getServers()); + repositorySessionInjector.inject(session.getRepositorySession(), repoList); buildingRequest.setRemoteRepositories(repoList); diff --git a/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java b/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java new file mode 100644 index 000000000..e11a947f8 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java @@ -0,0 +1,193 @@ +/* + * 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.plugins.dependency.utils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; +import org.apache.maven.artifact.repository.Authentication; +import org.apache.maven.artifact.repository.MavenArtifactRepository; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout2; +import org.apache.maven.repository.Proxy; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.AuthenticationSelector; +import org.eclipse.aether.repository.MirrorSelector; +import org.eclipse.aether.repository.ProxySelector; +import org.eclipse.aether.repository.RemoteRepository; + +/** + * Applies the mirror, proxy and authentication configuration of a {@link RepositorySystemSession} to repositories + * that were built by hand rather than obtained from the project, so that they can be used for resolution. + *

+ * This is the work {@code org.apache.maven.bridge.MavenRepositorySystem} does in its session-based + * {@code injectMirror}/{@code injectProxy}/{@code injectAuthentication} methods, reimplemented here because + * {@code org.apache.maven.bridge} is not one of the packages maven-core exports to plugin class realms — a plugin + * that references it compiles and unit-tests cleanly and then fails at runtime with + * {@code NoClassDefFoundError: org/apache/maven/bridge/MavenRepositorySystem}. Everything used below is in a package + * maven-core does export: {@code org.apache.maven} (for {@link RepositoryUtils}), {@code org.apache.maven.artifact}, + * {@code org.apache.maven.repository} and {@code org.eclipse.aether.repository}. + *

+ * The session's selectors are populated by Maven from the mirrors, proxies and servers of + * settings.xml, with the servers and proxies already decrypted, and they are the same selectors + * Resolver consults when it performs the transfer. + */ +public class RepositorySessionInjector { + + private final Map repositoryLayouts; + + public RepositorySessionInjector(Map repositoryLayouts) { + this.repositoryLayouts = repositoryLayouts; + } + + /** + * Applies the session's mirror, then proxy, then authentication configuration to each repository, in that order: + * mirroring rewrites the id and URL a repository is known by, and the proxy and the credentials are selected for + * the mirror rather than for the repository it replaced. + * + * @param session the repository session, may be {@code null} + * @param repositories the repositories to modify in place, may be {@code null} + */ + public void inject(RepositorySystemSession session, List repositories) { + if (session == null || repositories == null) { + return; + } + + for (ArtifactRepository repository : repositories) { + injectMirror(session, repository); + repository.setProxy(getProxy(session, repository)); + repository.setAuthentication(getAuthentication(session, repository)); + } + } + + private void injectMirror(RepositorySystemSession session, ArtifactRepository repository) { + MirrorSelector selector = session.getMirrorSelector(); + if (selector == null) { + return; + } + + RemoteRepository mirror = selector.getMirror(RepositoryUtils.toRepo(repository)); + if (mirror == null) { + return; + } + + repository.setMirroredRepositories(Collections.singletonList(createArtifactRepository( + repository.getId(), + repository.getUrl(), + repository.getLayout(), + repository.getSnapshots(), + repository.getReleases()))); + + repository.setId(mirror.getId()); + repository.setUrl(mirror.getUrl()); + + String layoutId = mirror.getContentType(); + if (layoutId != null && !layoutId.isEmpty()) { + repository.setLayout(getLayout(layoutId)); + } + + repository.setBlocked(mirror.isBlocked()); + } + + /** + * Same as {@code MavenRepositorySystem.createArtifactRepository}, including its substitution of default policies + * for null ones — {@link MavenArtifactRepository} stores null and fails later at whichever call site validates. + */ + private static ArtifactRepository createArtifactRepository( + String id, + String url, + ArtifactRepositoryLayout layout, + ArtifactRepositoryPolicy snapshots, + ArtifactRepositoryPolicy releases) { + ArtifactRepositoryPolicy snapshotPolicy = snapshots != null ? snapshots : new ArtifactRepositoryPolicy(); + ArtifactRepositoryPolicy releasePolicy = releases != null ? releases : new ArtifactRepositoryPolicy(); + + if (layout instanceof ArtifactRepositoryLayout2) { + return ((ArtifactRepositoryLayout2) layout) + .newMavenArtifactRepository(id, url, snapshotPolicy, releasePolicy); + } + return new MavenArtifactRepository(id, url, layout, snapshotPolicy, releasePolicy); + } + + /** + * Resolves a mirror's layout id. Maven core wraps an unknown id in a layout that delegates to the default one; + * falling back to the default layout outright computes the same paths. + */ + private ArtifactRepositoryLayout getLayout(String layoutId) { + ArtifactRepositoryLayout layout = repositoryLayouts.get(layoutId); + return layout != null ? layout : repositoryLayouts.get("default"); + } + + private Proxy getProxy(RepositorySystemSession session, ArtifactRepository repository) { + ProxySelector selector = session.getProxySelector(); + if (selector == null) { + return null; + } + + RemoteRepository repo = RepositoryUtils.toRepo(repository); + org.eclipse.aether.repository.Proxy proxy = selector.getProxy(repo); + if (proxy == null) { + return null; + } + + Proxy result = new Proxy(); + result.setHost(proxy.getHost()); + result.setProtocol(proxy.getType()); + result.setPort(proxy.getPort()); + + if (proxy.getAuthentication() != null) { + repo = new RemoteRepository.Builder(repo).setProxy(proxy).build(); + try (AuthenticationContext authCtx = AuthenticationContext.forProxy(session, repo)) { + result.setUserName(authCtx.get(AuthenticationContext.USERNAME)); + result.setPassword(authCtx.get(AuthenticationContext.PASSWORD)); + result.setNtlmDomain(authCtx.get(AuthenticationContext.NTLM_DOMAIN)); + result.setNtlmHost(authCtx.get(AuthenticationContext.NTLM_WORKSTATION)); + } + } + + return result; + } + + private Authentication getAuthentication(RepositorySystemSession session, ArtifactRepository repository) { + AuthenticationSelector selector = session.getAuthenticationSelector(); + if (selector == null) { + return null; + } + + RemoteRepository repo = RepositoryUtils.toRepo(repository); + org.eclipse.aether.repository.Authentication auth = selector.getAuthentication(repo); + if (auth == null) { + return null; + } + + repo = new RemoteRepository.Builder(repo).setAuthentication(auth).build(); + try (AuthenticationContext authCtx = AuthenticationContext.forRepository(session, repo)) { + Authentication result = new Authentication( + authCtx.get(AuthenticationContext.USERNAME), authCtx.get(AuthenticationContext.PASSWORD)); + result.setPrivateKey(authCtx.get(AuthenticationContext.PRIVATE_KEY_PATH)); + result.setPassphrase(authCtx.get(AuthenticationContext.PRIVATE_KEY_PASSPHRASE)); + return result; + } + } +} diff --git a/src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java b/src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java new file mode 100644 index 000000000..13d3eb6ec --- /dev/null +++ b/src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java @@ -0,0 +1,97 @@ +/* + * 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.artifact.repository.metadata; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.repository.ArtifactRepository; + +/** + * Metadata for the artifact version directory of the repository. + *

+ * Class copied verbatim from maven-compat so that the maven-compat dependency can be dropped. It is used by the + * unit tests only, never by the plugin itself, and it is a plain data holder over + * {@link AbstractRepositoryMetadata} (which lives in maven-core), so copying it is enough — there is no component + * implementation left behind in maven-compat. Same technique as maven-plugin-plugin used for + * {@code GroupRepositoryMetadata} in MPLUGIN-384. + * + * @author Brett Porter + */ +public class SnapshotArtifactRepositoryMetadata extends AbstractRepositoryMetadata { + private final Artifact artifact; + + public SnapshotArtifactRepositoryMetadata(Artifact artifact) { + super(createMetadata(artifact, null)); + this.artifact = artifact; + } + + public SnapshotArtifactRepositoryMetadata(Artifact artifact, Snapshot snapshot) { + super(createMetadata(artifact, createVersioning(snapshot))); + this.artifact = artifact; + } + + @Override + public boolean storedInGroupDirectory() { + return false; + } + + @Override + public boolean storedInArtifactVersionDirectory() { + return true; + } + + @Override + public String getGroupId() { + return artifact.getGroupId(); + } + + @Override + public String getArtifactId() { + return artifact.getArtifactId(); + } + + @Override + public String getBaseVersion() { + return artifact.getBaseVersion(); + } + + @Override + public Object getKey() { + return "snapshot " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getBaseVersion(); + } + + @Override + public boolean isSnapshot() { + return artifact.isSnapshot(); + } + + @Override + public int getNature() { + return isSnapshot() ? SNAPSHOT : RELEASE; + } + + @Override + public ArtifactRepository getRepository() { + return artifact.getRepository(); + } + + @Override + public void setRepository(ArtifactRepository remoteRepository) { + artifact.setRepository(remoteRepository); + } +} diff --git a/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java b/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java index 2cdb47831..028500278 100644 --- a/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java +++ b/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java @@ -21,6 +21,8 @@ import javax.inject.Inject; import java.net.InetAddress; +import java.net.URI; +import java.nio.file.Path; import java.util.Collections; import org.apache.maven.api.plugin.testing.Basedir; @@ -31,10 +33,18 @@ import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.DefaultProjectBuildingRequest; +import org.apache.maven.settings.Proxy; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; +import org.eclipse.aether.util.repository.DefaultProxySelector; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.HashLoginService; @@ -46,10 +56,13 @@ import org.eclipse.jetty.util.security.Constraint; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.apache.maven.api.plugin.testing.MojoExtension.getTestPath; import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; @@ -57,9 +70,17 @@ @Basedir("/unit/get-test") class TestGetMojo { + private static final String PROXY_HOST = "proxy.invalid"; + @Inject private MavenSession session; + @Inject + private RepositorySystem repositorySystem; + + @TempDir + private Path isolatedLocalRepository; + @BeforeEach void setUp() { Settings settings = new Settings(); @@ -72,6 +93,57 @@ void setUp() { settings.addServer(server); } + /** + * Mirrors the {@code } and {@code } entries of the session settings into the repository session, + * the way {@code DefaultRepositorySystemSessionFactory} does when Maven builds that session for real. GetMojo + * reads credentials and proxies from the session's selectors rather than from the {@link Settings} object, since + * those selectors are what Resolver itself consults when it performs the transfer, and the test harness has + * already created the repository session by the time {@link #setUp()} declares any of this. + *

+ * The local repository is swapped for an empty one at the same time, so that the tests below depend on the + * transfer actually happening rather than on what an earlier test left in the shared local repository. + */ + private void applySettingsToRepositorySession() { + Settings settings = session.getSettings(); + + DefaultAuthenticationSelector authenticationSelector = new DefaultAuthenticationSelector(); + for (Server server : settings.getServers()) { + authenticationSelector.add( + server.getId(), + new AuthenticationBuilder() + .addUsername(server.getUsername()) + .addPassword(server.getPassword()) + .addPrivateKey(server.getPrivateKey(), server.getPassphrase()) + .build()); + } + + DefaultProxySelector proxySelector = new DefaultProxySelector(); + for (Proxy proxy : settings.getProxies()) { + if (!proxy.isActive()) { + // Maven filters the inactive ones out before they ever reach the selector + continue; + } + proxySelector.add( + new org.eclipse.aether.repository.Proxy( + proxy.getProtocol(), + proxy.getHost(), + proxy.getPort(), + new AuthenticationBuilder() + .addUsername(proxy.getUsername()) + .addPassword(proxy.getPassword()) + .build()), + proxy.getNonProxyHosts()); + } + + DefaultRepositorySystemSession repositorySession = + new DefaultRepositorySystemSession(session.getRepositorySession()); + repositorySession.setAuthenticationSelector(authenticationSelector); + repositorySession.setProxySelector(proxySelector); + repositorySession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager( + repositorySession, new LocalRepository(isolatedLocalRepository.toFile()))); + when(session.getRepositorySession()).thenReturn(repositorySession); + } + /** * Test transitive parameter * @@ -126,15 +198,80 @@ void testRemoteRepositoriesAuthentication(GetMojo mojo) throws Exception { org.eclipse.jetty.server.Server server = createServer(); try { server.start(); - ServerConnector serverConnector = (ServerConnector) server.getConnectors()[0]; - String url = "http://" - + (serverConnector.getHost() == null - ? InetAddress.getLoopbackAddress().getHostName() - : serverConnector.getHost()); - url = url + ":" + serverConnector.getLocalPort() + "/maven"; + + setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + serverUrl(server)); + + applySettingsToRepositorySession(); + + DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); + pbr.setRepositorySession(session.getRepositorySession()); + when(session.getProjectBuildingRequest()).thenReturn(pbr); + + mojo.setGroupId("test"); + mojo.setArtifactId("test"); + mojo.setVersion("1.0"); + + mojo.execute(); + } finally { + server.stop(); + } + } + + /** + * Test that an active proxy from the settings is applied to the repositories given by the + * remoteRepositories parameter. The proxy points at a host that cannot resolve, so a successful + * resolution would mean the proxy was never applied. + * + * @throws Exception in case of errors + */ + @Test + @InjectMojo(goal = "get") + void testRemoteRepositoriesProxy(GetMojo mojo) throws Exception { + org.eclipse.jetty.server.Server server = createServer(); + try { + server.start(); + + setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + serverUrl(server)); + + session.getSettings().addProxy(createProxy(null)); + applySettingsToRepositorySession(); + + DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); + pbr.setRepositorySession(session.getRepositorySession()); + when(session.getProjectBuildingRequest()).thenReturn(pbr); + + mojo.setGroupId("test"); + mojo.setArtifactId("test"); + mojo.setVersion("1.0"); + + MojoExecutionException e = assertThrows(MojoExecutionException.class, mojo::execute); + assertTrue( + mentionsProxyHost(e), + "Expected the resolution to have been attempted through the unreachable proxy, got: " + e); + } finally { + server.stop(); + } + } + + /** + * Test that nonProxyHosts excludes a repository from the proxy that would otherwise match it: the + * same unreachable proxy as above must not be applied, so the resolution has to succeed. + * + * @throws Exception in case of errors + */ + @Test + @InjectMojo(goal = "get") + void testRemoteRepositoriesNonProxyHosts(GetMojo mojo) throws Exception { + org.eclipse.jetty.server.Server server = createServer(); + try { + server.start(); + String url = serverUrl(server); setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + url); + session.getSettings().addProxy(createProxy(URI.create(url).getHost())); + applySettingsToRepositorySession(); + DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); pbr.setRepositorySession(session.getRepositorySession()); when(session.getProjectBuildingRequest()).thenReturn(pbr); @@ -149,6 +286,41 @@ void testRemoteRepositoriesAuthentication(GetMojo mojo) throws Exception { } } + /** + * Whether the failure came from the proxy rather than from, say, a missing artifact or a rejected login — the + * host of {@link #createProxy(String)} only ever appears if the proxy really was applied to the repository. + */ + private boolean mentionsProxyHost(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause()) { + if (cause.getMessage() != null && cause.getMessage().contains(PROXY_HOST)) { + return true; + } + } + return false; + } + + /** + * An active HTTP proxy pointing at a name that is guaranteed not to resolve (RFC 2606 reserved TLD), so that + * applying it is always observable as a failure. + */ + private Proxy createProxy(String nonProxyHosts) { + Proxy proxy = new Proxy(); + proxy.setActive(true); + proxy.setProtocol("http"); + proxy.setHost(PROXY_HOST); + proxy.setPort(3128); + proxy.setNonProxyHosts(nonProxyHosts); + return proxy; + } + + private String serverUrl(org.eclipse.jetty.server.Server server) throws Exception { + ServerConnector serverConnector = (ServerConnector) server.getConnectors()[0]; + String host = serverConnector.getHost() == null + ? InetAddress.getLoopbackAddress().getHostName() + : serverConnector.getHost(); + return "http://" + host + ":" + serverConnector.getLocalPort() + "/maven"; + } + /** * Test parsing of the remote repositories parameter * diff --git a/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java b/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java new file mode 100644 index 000000000..22e7ec1b4 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java @@ -0,0 +1,184 @@ +/* + * 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.plugins.dependency.utils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; +import org.apache.maven.artifact.repository.MavenArtifactRepository; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; +import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; +import org.eclipse.aether.util.repository.DefaultMirrorSelector; +import org.eclipse.aether.util.repository.DefaultProxySelector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Covers the mirror, proxy and authentication selection that {@link RepositorySessionInjector} takes over from + * {@code MavenRepositorySystem}, which a plugin cannot reference. + */ +class RepositorySessionInjectorTest { + + private RepositorySessionInjector injector; + + private DefaultRepositorySystemSession session; + + @BeforeEach + void setUp() { + Map layouts = new HashMap<>(); + layouts.put("default", new DefaultRepositoryLayout()); + injector = new RepositorySessionInjector(layouts); + session = new DefaultRepositorySystemSession(); + } + + private ArtifactRepository repository(String id, String url) { + return new MavenArtifactRepository( + id, url, new DefaultRepositoryLayout(), new ArtifactRepositoryPolicy(), new ArtifactRepositoryPolicy()); + } + + @Test + void nullSessionLeavesRepositoriesAlone() { + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + + injector.inject(null, Collections.singletonList(repository)); + + assertEquals("central", repository.getId()); + assertNull(repository.getAuthentication()); + assertNull(repository.getProxy()); + } + + @Test + void mirrorReplacesIdAndUrlAndKeepsTheOriginal() { + session.setMirrorSelector( + new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, false, "*", null)); + + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertEquals("mirror", repository.getId()); + assertEquals("https://mirror.example.com", repository.getUrl()); + + List mirrored = repository.getMirroredRepositories(); + assertEquals(1, mirrored.size()); + assertEquals("central", mirrored.get(0).getId()); + assertEquals("https://repo.example.com/maven2", mirrored.get(0).getUrl()); + // MavenArtifactRepository stores null policies as null; the mirrored original must not carry any + assertNotNull(mirrored.get(0).getSnapshots()); + assertNotNull(mirrored.get(0).getReleases()); + } + + @Test + void mirrorOfDoesNotMatchEveryRepository() { + session.setMirrorSelector(new DefaultMirrorSelector() + .add("mirror", "https://mirror.example.com", null, false, false, "central", null)); + + ArtifactRepository repository = repository("other", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertEquals("other", repository.getId()); + assertEquals("https://repo.example.com/maven2", repository.getUrl()); + } + + @Test + void blockedMirrorBlocksTheRepository() { + session.setMirrorSelector( + new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, true, "*", null)); + + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertTrue(repository.isBlocked()); + } + + @Test + void credentialsAreSelectedByRepositoryId() { + session.setAuthenticationSelector(new DefaultAuthenticationSelector() + .add( + "myserver", + new AuthenticationBuilder() + .addUsername("foo") + .addPassword("bar") + .build())); + + ArtifactRepository matching = repository("myserver", "https://repo.example.com/maven2"); + ArtifactRepository other = repository("other", "https://repo.example.com/maven2"); + injector.inject(session, java.util.Arrays.asList(matching, other)); + + assertNotNull(matching.getAuthentication()); + assertEquals("foo", matching.getAuthentication().getUsername()); + assertEquals("bar", matching.getAuthentication().getPassword()); + assertNull(other.getAuthentication()); + } + + @Test + void credentialsAreSelectedForTheMirrorNotTheMirroredRepository() { + session.setMirrorSelector( + new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, false, "*", null)); + session.setAuthenticationSelector(new DefaultAuthenticationSelector() + .add("mirror", new AuthenticationBuilder().addUsername("foo").build()) + .add("central", new AuthenticationBuilder().addUsername("wrong").build())); + + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertNotNull(repository.getAuthentication()); + assertEquals("foo", repository.getAuthentication().getUsername()); + } + + @Test + void proxyIsSelectedByProtocol() { + session.setProxySelector(new DefaultProxySelector() + .add(new org.eclipse.aether.repository.Proxy("http", "proxy.example.com", 3128), null)); + + ArtifactRepository http = repository("http-repo", "http://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(http)); + + assertNotNull(http.getProxy()); + assertEquals("proxy.example.com", http.getProxy().getHost()); + assertEquals(3128, http.getProxy().getPort()); + assertEquals("http", http.getProxy().getProtocol()); + } + + @Test + void nonProxyHostsExcludesTheRepository() { + session.setProxySelector(new DefaultProxySelector() + .add( + new org.eclipse.aether.repository.Proxy("http", "proxy.example.com", 3128), + "*.example.com|localhost")); + + ArtifactRepository excluded = repository("repo", "http://repo.example.com/maven2"); + ArtifactRepository proxied = repository("repo", "http://repo.example.org/maven2"); + injector.inject(session, java.util.Arrays.asList(excluded, proxied)); + + assertNull(excluded.getProxy()); + assertNotNull(proxied.getProxy()); + } +}