DefaultDependencyResolverTest.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.impl;

import java.util.List;
import java.util.Optional;

import org.apache.maven.api.DependencyCoordinates;
import org.apache.maven.api.Node;
import org.apache.maven.api.Project;
import org.apache.maven.api.services.DependencyResolverRequest;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.graph.DefaultDependencyNode;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.Exclusion;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class DefaultDependencyResolverTest {

    @Test
    void collectAppliesProjectManagementToCallerSuppliedDependencies() throws Exception {
        InternalSession session = mock(InternalSession.class);
        RepositorySystem repositorySystem = mock(RepositorySystem.class);
        RepositorySystemSession repositorySession = mock(RepositorySystemSession.class);
        DependencyResolverRequest request = mock(DependencyResolverRequest.class);
        Project project = mock(Project.class);
        DependencyCoordinates projectDependency = mock(DependencyCoordinates.class);
        DependencyCoordinates callerDependency = mock(DependencyCoordinates.class);
        DependencyCoordinates managedDependency = mock(DependencyCoordinates.class);
        List<DependencyCoordinates> projectDependencies = List.of(projectDependency);
        List<DependencyCoordinates> callerDependencies = List.of(callerDependency);
        List<DependencyCoordinates> managedDependencies = List.of(managedDependency);
        Dependency resolverDependency =
                new Dependency(new DefaultArtifact("org.example", "processor", "", "jar", ""), "", false);
        Exclusion exclusion = new Exclusion("org.example", "excluded", "*", "*");
        Dependency resolverManagedDependency = new Dependency(
                new DefaultArtifact("org.example:processor:jar:1.2.3"), "runtime", false, List.of(exclusion));

        when(request.getSession()).thenReturn(session);
        when(request.getProject()).thenReturn(Optional.of(project));
        when(request.getDependencies()).thenReturn(callerDependencies);
        when(request.getRepositories()).thenReturn(List.of());
        when(project.getDependencies()).thenReturn(projectDependencies);
        when(project.getManagedDependencies()).thenReturn(managedDependencies);
        when(session.toDependencies(callerDependencies, false)).thenReturn(List.of(resolverDependency));
        when(session.toDependencies(managedDependencies, true)).thenReturn(List.of(resolverManagedDependency));
        when(session.toResolvingRepositories(List.of())).thenReturn(List.of());
        when(session.getRepositorySystem()).thenReturn(repositorySystem);
        when(session.getSession()).thenReturn(repositorySession);
        when(repositorySystem.collectDependencies(eq(repositorySession), any(CollectRequest.class)))
                .thenAnswer(invocation -> new CollectResult(invocation.getArgument(1))
                        .setRoot(new DefaultDependencyNode(resolverDependency)));
        when(session.getNode(any(), eq(false))).thenReturn(mock(Node.class));

        new DefaultDependencyResolver().collect(request);

        ArgumentCaptor<CollectRequest> requestCaptor = ArgumentCaptor.forClass(CollectRequest.class);
        verify(repositorySystem).collectDependencies(eq(repositorySession), requestCaptor.capture());
        Dependency collectedDependency =
                requestCaptor.getValue().getDependencies().get(0);
        assertEquals("1.2.3", collectedDependency.getArtifact().getVersion());
        assertEquals("runtime", collectedDependency.getScope());
        assertIterableEquals(List.of(exclusion), collectedDependency.getExclusions());
        verify(session).toDependencies(callerDependencies, false);
        verify(session, never()).toDependencies(projectDependencies, false);
    }

    @Test
    void collectWithProjectButNoCallerDepsSkipsManagementApplication() throws Exception {
        // When a Project is present but request.getDependencies() is empty, the guard
        // `request.getProject().isPresent() && !request.getDependencies().isEmpty()` is false,
        // so applyDependencyManagement must NOT be called: the project's own deps go through
        // as-is (no managed version/scope/exclusion merge).
        InternalSession session = mock(InternalSession.class);
        RepositorySystem repositorySystem = mock(RepositorySystem.class);
        RepositorySystemSession repositorySession = mock(RepositorySystemSession.class);
        DependencyResolverRequest request = mock(DependencyResolverRequest.class);
        Project project = mock(Project.class);
        DependencyCoordinates projectDependency = mock(DependencyCoordinates.class);
        DependencyCoordinates managedDependency = mock(DependencyCoordinates.class);
        List<DependencyCoordinates> projectDependencies = List.of(projectDependency);
        List<DependencyCoordinates> managedDependencies = List.of(managedDependency);
        // The resolver-level dependency has no version ��� management would set "1.2.3", but must NOT
        Dependency resolverProjectDependency =
                new Dependency(new DefaultArtifact("org.example", "processor", "", "jar", ""), "", false);
        Dependency resolverManagedDependency =
                new Dependency(new DefaultArtifact("org.example:processor:jar:1.2.3"), "runtime");

        when(request.getSession()).thenReturn(session);
        when(request.getProject()).thenReturn(Optional.of(project));
        when(request.getDependencies()).thenReturn(List.of()); // no caller-supplied deps
        when(request.getRepositories()).thenReturn(List.of());
        when(project.getDependencies()).thenReturn(projectDependencies);
        when(project.getManagedDependencies()).thenReturn(managedDependencies);
        when(session.toDependencies(projectDependencies, false)).thenReturn(List.of(resolverProjectDependency));
        when(session.toDependencies(managedDependencies, true)).thenReturn(List.of(resolverManagedDependency));
        when(session.toResolvingRepositories(List.of())).thenReturn(List.of());
        when(session.getRepositorySystem()).thenReturn(repositorySystem);
        when(session.getSession()).thenReturn(repositorySession);
        when(repositorySystem.collectDependencies(eq(repositorySession), any(CollectRequest.class)))
                .thenAnswer(invocation -> new CollectResult(invocation.getArgument(1))
                        .setRoot(new DefaultDependencyNode(resolverProjectDependency)));
        when(session.getNode(any(), eq(false))).thenReturn(mock(Node.class));

        new DefaultDependencyResolver().collect(request);

        ArgumentCaptor<CollectRequest> requestCaptor = ArgumentCaptor.forClass(CollectRequest.class);
        verify(repositorySystem).collectDependencies(eq(repositorySession), requestCaptor.capture());
        Dependency collectedDependency =
                requestCaptor.getValue().getDependencies().get(0);
        // version must remain empty ��� management was NOT applied
        assertEquals("", collectedDependency.getArtifact().getVersion());
        assertEquals("", collectedDependency.getScope());
        // project's own dependencies were used (not caller-supplied, since there were none)
        verify(session).toDependencies(projectDependencies, false);
    }

    @Test
    void appliesProjectManagementToPluginDependency() {
        Dependency dependency =
                new Dependency(new DefaultArtifact("org.example", "processor", "", "jar", ""), "", false);
        Exclusion exclusion = new Exclusion("org.example", "excluded", "*", "*");
        Dependency managed = new Dependency(
                new DefaultArtifact("org.example:processor:jar:1.2.3"), "runtime", false, List.of(exclusion));

        Dependency result = DefaultDependencyResolver.applyDependencyManagement(List.of(dependency), List.of(managed))
                .get(0);

        assertEquals("1.2.3", result.getArtifact().getVersion());
        assertEquals("runtime", result.getScope());
        assertIterableEquals(List.of(exclusion), result.getExclusions());
    }

    @Test
    void keepsValuesDeclaredByPluginDependency() {
        Exclusion declaredExclusion = new Exclusion("org.example", "declared", "*", "*");
        Dependency dependency = new Dependency(
                new DefaultArtifact("org.example:processor:jar:2.0.0"), "test", false, List.of(declaredExclusion));
        Exclusion managedExclusion = new Exclusion("org.example", "managed", "*", "*");
        Dependency managed = new Dependency(
                new DefaultArtifact("org.example:processor:jar:1.2.3"), "runtime", false, List.of(managedExclusion));

        Dependency result = DefaultDependencyResolver.applyDependencyManagement(List.of(dependency), List.of(managed))
                .get(0);

        assertEquals("2.0.0", result.getArtifact().getVersion());
        assertEquals("test", result.getScope());
        assertIterableEquals(List.of(declaredExclusion), result.getExclusions());
    }

    @Test
    void applyingDependencyManagementToEmptyDependenciesIsNoOp() {
        Dependency managed = new Dependency(new DefaultArtifact("org.example:processor:jar:1.2.3"), "runtime");

        assertTrue(DefaultDependencyResolver.applyDependencyManagement(List.of(), List.of(managed))
                .isEmpty());
    }

    @Test
    void appliesDependencyManagementOnlyToMatchingDependencies() {
        Dependency matching = new Dependency(new DefaultArtifact("org.example", "processor", "", "jar", ""), "", false);
        Dependency unmatched = new Dependency(new DefaultArtifact("org.example:standalone:jar:2.0.0"), "compile");
        Dependency managed = new Dependency(new DefaultArtifact("org.example:processor:jar:1.2.3"), "runtime");

        List<Dependency> result =
                DefaultDependencyResolver.applyDependencyManagement(List.of(matching, unmatched), List.of(managed));

        assertEquals("1.2.3", result.get(0).getArtifact().getVersion());
        assertSame(unmatched, result.get(1));
    }
}