blob: a78d420d8af65630633e20d7adbe8b48f176476e [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.plugins.codeowners.acceptance.api;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId;
import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.fail;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.plugins.codeowners.api.CodeOwners;
import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotations;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSetModification;
import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
/**
* Acceptance test for the {@link
* com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch} REST endpoint.
*
* <p>Further tests for the {@link
* com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch} REST endpoint that
* require using the REST API are implemented in {@link
* com.google.gerrit.plugins.codeowners.acceptance.restapi.GetCodeOwnersForPathInBranchRestIT}.
*/
public class GetCodeOwnersForPathInBranchIT extends AbstractGetCodeOwnersForPathIT {
@Inject private ProjectOperations projectOperations;
@Inject private GroupOperations groupOperations;
@Override
protected CodeOwners getCodeOwnersApi() throws RestApiException {
return codeOwnersApiFactory.branch(project, "master");
}
@Test
public void getCodeOwnersForNonExistingBranch() throws Exception {
RestResponse response =
adminRestSession.get(
String.format("/projects/%s/branches/non-existing/code_owners/path", project.get()));
response.assertNotFound();
}
@Test
public void getCodeOwnersFromSymbolicRefPointingToAnUnbornBranch() throws Exception {
try (Repository repo = repoManager.openRepository(project)) {
repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing");
}
RestResponse response =
adminRestSession.get(
String.format("/projects/%s/branches/HEAD/code_owners/path", project.get()));
response.assertNotFound();
}
@Test
public void getCodeOwnersOrderNotDefinedIfCodeOwnersHaveTheSameScoring() throws Exception {
TestAccount user2 = accountCreator.user2();
TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
TestAccount user4 = accountCreator.create("user4", "user4@example.com", "User4", null);
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.addCodeOwnerEmail(user2.email())
.addCodeOwnerEmail(user3.email())
.addCodeOwnerEmail(user4.email())
.create();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar.md");
assertThat(codeOwnersInfo)
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(admin.id(), user.id(), user2.id(), user3.id(), user4.id());
// The first code owner in the result should be user as user has the best distance score.
assertThat(codeOwnersInfo)
.hasCodeOwnersThat()
.element(0)
.hasAccountIdThat()
.isEqualTo(user.id());
// The order of the other code owners is random since they have the same score.
// Check that the order of the code owners with the same score is different for further requests
// at least once.
List<Account.Id> accountIdsInRetrievedOrder1 =
codeOwnersInfo.codeOwners.stream()
.map(info -> Account.id(info.account._accountId))
.collect(toList());
boolean foundOtherOrder = false;
for (int i = 0; i < 10; i++) {
codeOwnersInfo = queryCodeOwners("/foo/bar.md");
List<Account.Id> accountIdsInRetrievedOrder2 =
codeOwnersInfo.codeOwners.stream()
.map(info -> Account.id(info.account._accountId))
.collect(toList());
if (!accountIdsInRetrievedOrder1.equals(accountIdsInRetrievedOrder2)) {
foundOtherOrder = true;
break;
}
}
if (!foundOtherOrder) {
fail(
String.format(
"expected different order, but order was always %s", accountIdsInRetrievedOrder1));
}
}
@Test
public void getCodeOwnersForRevision() throws Exception {
// Create an initial code owner config that only has 'admin' as code owner.
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.addCodeOwnerEmail(admin.email())
.create();
RevCommit revision1 = projectOperations.project(project).getHead("master");
// Update the code owner config so that also 'user' is a code owner now.
codeOwnerConfigOperations
.codeOwnerConfig(codeOwnerConfigKey)
.forUpdate()
.codeOwnerSetsModification(CodeOwnerSetModification.addToOnlySet(user.email()))
.update();
RevCommit revision2 = projectOperations.project(project).getHead("master");
assertThat(revision1).isNotEqualTo(revision2);
// For the first revision we expect that only 'admin' is returned as code owner.
CodeOwnersInfo codeOwnersInfo =
queryCodeOwners(
getCodeOwnersApi().query().forRevision(revision1.name()), "/foo/bar/baz.md");
assertThat(codeOwnersInfo)
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(admin.id());
// For the second revision we expect that 'admin' and 'user' are returned as code owners.
codeOwnersInfo =
queryCodeOwners(
getCodeOwnersApi().query().forRevision(revision2.name()), "/foo/bar/baz.md");
assertThat(codeOwnersInfo)
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(admin.id(), user.id());
}
@Test
public void cannotGetCodeOwnersForInvalidRevision() throws Exception {
BadRequestException exception =
assertThrows(
BadRequestException.class,
() ->
queryCodeOwners(
getCodeOwnersApi().query().forRevision("INVALID"), "/foo/bar/baz.md"));
assertThat(exception).hasMessageThat().isEqualTo("invalid revision");
}
@Test
public void cannotGetCodeOwnersForUnknownRevision() throws Exception {
BadRequestException exception =
assertThrows(
BadRequestException.class,
() ->
queryCodeOwners(
getCodeOwnersApi()
.query()
.forRevision("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
"/foo/bar/baz.md"));
assertThat(exception).hasMessageThat().isEqualTo("unknown revision");
}
@Test
public void cannotGetCodeOwnersForRevisionOfOtherBranch() throws Exception {
RevCommit revision = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
BadRequestException exception =
assertThrows(
BadRequestException.class,
() ->
queryCodeOwners(
getCodeOwnersApi().query().forRevision(revision.name()), "/foo/bar/baz.md"));
assertThat(exception).hasMessageThat().isEqualTo("unknown revision");
}
@Test
public void codeOwnersThatAreServiceUsersAreIncluded() throws Exception {
TestAccount serviceUser =
accountCreator.create("serviceUser", "service.user@example.com", "Service User", null);
// Make 'serviceUser' a service user.
groupOperations
.group(groupCache.get(AccountGroup.nameKey("Service Users")).get().getGroupUUID())
.forUpdate()
.addMember(serviceUser.id())
.update();
// Create a code owner config with 2 code owners.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.addCodeOwnerEmail(serviceUser.email())
.create();
// Expect that 'serviceUser' is included.
assertThat(queryCodeOwners("/foo/bar/baz.md"))
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(admin.id(), serviceUser.id());
}
@Test
@GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "service.user@example.com")
public void globalCodeOwnersThatAreServiceUsersAreIncluded() throws Exception {
TestAccount serviceUser =
accountCreator.create("serviceUser", "service.user@example.com", "Service User", null);
groupOperations
.group(groupCache.get(AccountGroup.nameKey("Service Users")).get().getGroupUUID())
.forUpdate()
.addMember(serviceUser.id())
.update();
assertThat(queryCodeOwners("/foo/bar/baz.md"))
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(serviceUser.id());
}
@Test
public void codeOwnersWithLastResortSuggestionAnnotationAreIncluded() throws Exception {
skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
TestAccount user2 = accountCreator.user2();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addCodeOwnerEmail(admin.email())
.addAnnotation(
admin.email(), CodeOwnerAnnotations.LAST_RESORT_SUGGESTION_ANNOTATION)
.addCodeOwnerEmail(user.email())
.addCodeOwnerEmail(user2.email())
.build())
.create();
// Expectation: admin is included because GetCodeOwnersForPathInBranch ignores the
// LAST_RESORT_SUGGESTION annotation (GetCodeOwnersForPathInBranch is for listing code owners,
// but this annotation is only relevant for GetCodeOwnersForPathInChange which is for suggesting
// code owners)
CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
assertThat(codeOwnersInfo)
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(admin.id(), user.id(), user2.id());
}
@Test
public void perFileCodeOwnersWithLastResortSuggestionAnnotationAreIncluded() throws Exception {
skipTestIfAnnotationsNotSupportedByCodeOwnersBackend();
TestAccount user2 = accountCreator.user2();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression(testPathExpressions.matchFileType("md"))
.addCodeOwnerEmail(admin.email())
.addAnnotation(
admin.email(), CodeOwnerAnnotations.LAST_RESORT_SUGGESTION_ANNOTATION)
.addCodeOwnerEmail(user.email())
.addCodeOwnerEmail(user2.email())
.build())
.create();
// Expectation: admin is included because GetCodeOwnersForPathInBranch ignores the
// LAST_RESORT_SUGGESTION annotation (GetCodeOwnersForPathInBranch is for listing code owners,
// but this annotation is only relevant for GetCodeOwnersForPathInChange which is for suggesting
// code owners)
CodeOwnersInfo codeOwnersInfo = queryCodeOwners("foo/bar/baz.md");
assertThat(codeOwnersInfo)
.hasCodeOwnersThat()
.comparingElementsUsing(hasAccountId())
.containsExactly(admin.id(), user.id(), user2.id());
}
}