blob: f8b21aa111eac70037fb42d6a272bc0c2f2ccf7c [file] [log] [blame]
// Copyright (C) 2021 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.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status.NOT_APPLICABLE;
import static com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status.SATISFIED;
import static com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status.UNSATISFIED;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.stream.Collectors.toList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.Streams;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerApprovalHasOperand;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
@Sandboxed
public class CodeOwnerHasOperandsIT extends AbstractCodeOwnersIT {
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ChangeQueryBuilder changeQueryBuilder;
private CodeOwnerApprovalHasOperand codeOwnerApprovalHasOperand;
@Before
public void setup() throws Exception {
codeOwnerApprovalHasOperand =
plugin.getSysInjector().getInstance(CodeOwnerApprovalHasOperand.class);
configSubmitRequirement(
project,
SubmitRequirement.builder()
.setName("Code-Owner-Approval")
.setApplicabilityExpression(SubmitRequirementExpression.of("has:enabled_code-owners"))
.setSubmittabilityExpression(
SubmitRequirementExpression.create("has:approval_code-owners"))
.setAllowOverrideInChildProjects(false)
.build());
}
@Test
public void hasApproval_notSupportedInSearchQueries() throws Exception {
Exception thrown =
assertThrows(BadRequestException.class, () -> assertQuery("has:approval_code-owners"));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Operator 'has:approval_code-owners' cannot be used in queries");
}
@Test
public void hasEnabled_notSupportedInSearchQueries() throws Exception {
Exception thrown =
assertThrows(BadRequestException.class, () -> assertQuery("has:enabled_code-owners"));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Operator 'has:enabled_code-owners' cannot be used in queries");
}
@Test
public void hasApproval_satisfied() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String path = "foo/bar.baz";
ChangeData changeData = createChange("Change Adding A File", path, "file content").getChange();
int changeId = changeData.change().getChangeId();
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", UNSATISFIED);
// Add a Code-Review+1 from a code owner (by default this counts as code owner approval).
requestScopeOperations.setApiUser(user.id());
recommend(changeData.change().getKey().get());
changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", SATISFIED);
}
@Test
public void hasApproval_unsatisfiedIfChangeIsClosed() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String path = "foo/bar.baz";
ChangeData changeData = createChange("Change Adding A File", path, "file content").getChange();
int changeId = changeData.change().getChangeId();
// Add a Code-Review+1 from a code owner (by default this counts as code owner approval).
requestScopeOperations.setApiUser(user.id());
recommend(changeData.change().getKey().get());
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", SATISFIED);
// Approve and submit.
requestScopeOperations.setApiUser(admin.id());
approve(changeData.change().getKey().get());
gApi.changes().id(changeId).current().submit();
changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
// When the change is merged, submit requirement results are persisted in NoteDb. Later lookups
// return the persisted snapshot.
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", SATISFIED);
}
@Test
public void hasApproval_internalServerError() throws Exception {
ChangeData changeData = createChange().getChange();
// Create a ChangeData without change notes to trigger an error.
// Set change and current patch set, so that this info can be included into the error message.
ChangeData changeDataWithoutChangeNotes = mock(ChangeData.class);
when(changeDataWithoutChangeNotes.change()).thenReturn(changeData.change());
when(changeDataWithoutChangeNotes.currentPatchSet()).thenReturn(changeData.currentPatchSet());
CodeOwnersInternalServerErrorException exception =
assertThrows(
CodeOwnersInternalServerErrorException.class,
() ->
codeOwnerApprovalHasOperand
.create(changeQueryBuilder)
.asMatchable()
.match(changeDataWithoutChangeNotes));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Failed to evaluate code owner statuses for patch set %d of change %d.",
changeData.change().currentPatchSetId().get(), changeData.change().getId().get()));
}
@Test
public void hasApproval_ruleErrorForNonParsableCodeOwnerConfig() throws Exception {
String nameOfInvalidCodeOwnerConfigFile = getCodeOwnerConfigFileName();
createNonParseableCodeOwnerConfig(nameOfInvalidCodeOwnerConfigFile);
ChangeData changeData = createChange().getChange();
ChangeInfo changeInfo =
gApi.changes()
.id(changeData.change().getChangeId())
.get(ListChangesOption.SUBMIT_REQUIREMENTS);
// Requirement is unsatisfied if a relevant code owner config file is not parseable and hence
// the submit rule cannot be evaluated.
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", UNSATISFIED);
}
@Test
@GerritConfig(name = "plugin.code-owners.disabled", value = "true")
public void hasEnabled_notMatchingWhenCodeOwnersIsDisabledForTheChange() throws Exception {
Change change =
createChange("Change Adding A File", "foo/bar.baz", "file content").getChange().change();
String changeId = change.getKey().get();
ChangeInfo changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", NOT_APPLICABLE);
requestScopeOperations.setApiUser(user.id());
recommend(changeId);
changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
assertSubmitRequirement(changeInfo.submitRequirements, "Code-Owner-Approval", NOT_APPLICABLE);
}
private List<ChangeInfo> assertQuery(Object query, Change... changes) throws Exception {
QueryRequest queryRequest = newQuery(query);
Change.Id[] changeArray = Arrays.stream(changes).map(Change::getId).toArray(Change.Id[]::new);
List<ChangeInfo> result = queryRequest.get();
Iterable<Change.Id> ids = ids(result);
assertWithMessage(format(queryRequest.getQuery(), ids, changeArray))
.that(ids)
.containsExactlyElementsIn(Arrays.asList(changeArray))
.inOrder();
return result;
}
private static Iterable<Change.Id> ids(Iterable<ChangeInfo> changes) {
return Streams.stream(changes).map(c -> Change.id(c._number)).collect(toList());
}
private QueryRequest newQuery(Object query) {
return gApi.changes().query(query.toString());
}
private void assertSubmitRequirement(
Collection<SubmitRequirementResultInfo> requirements, String name, Status status) {
for (SubmitRequirementResultInfo requirement : requirements) {
if (requirement.name.equals(name) && requirement.status == status) {
return;
}
}
throw new AssertionError(
String.format(
"Could not find submit requirement %s with status %s (results = %s)",
name,
status,
requirements.stream()
.map(r -> String.format("%s=%s", r.name, r.status))
.collect(toImmutableList())));
}
private String format(String query, Iterable<Change.Id> actualIds, Change.Id... expectedChanges)
throws RestApiException {
return "query '"
+ query
+ "' with expected changes "
+ format(Arrays.asList(expectedChanges))
+ " and result "
+ format(actualIds);
}
private String format(Iterable<Change.Id> changeIds) throws RestApiException {
Iterator<Change.Id> changeIdsItr = changeIds.iterator();
StringBuilder b = new StringBuilder();
b.append("[");
while (changeIdsItr.hasNext()) {
Change.Id id = changeIdsItr.next();
ChangeInfo c = gApi.changes().id(id.get()).get();
b.append("{")
.append(id)
.append(" (")
.append(c.changeId)
.append("), ")
.append("dest=")
.append(BranchNameKey.create(Project.nameKey(c.project), c.branch))
.append(", ")
.append("status=")
.append(c.status)
.append(", ")
.append("lastUpdated=")
.append(c.updated.getTime())
.append("}");
if (changeIdsItr.hasNext()) {
b.append(", ");
}
}
b.append("]");
return b.toString();
}
}