blob: 18fff7f10dd80fd443ea2004f1db67b0344665fe [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.backend;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
import java.nio.file.Paths;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
/** Tests for {@link CodeOwnerConfigScanner}. */
public class CodeOwnerConfigScannerTest extends AbstractCodeOwnersTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Mock private CodeOwnerConfigVisitor visitor;
@Mock private InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback;
private CodeOwnerConfigOperations codeOwnerConfigOperations;
private CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory;
@Before
public void setUpCodeOwnersPlugin() throws Exception {
codeOwnerConfigOperations =
plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class);
codeOwnerConfigScannerFactory =
plugin.getSysInjector().getInstance(CodeOwnerConfigScanner.Factory.class);
}
@Test
public void cannotVisitCodeOwnerConfigsForNullBranch() throws Exception {
NullPointerException npe =
assertThrows(
NullPointerException.class,
() ->
codeOwnerConfigScannerFactory
.create()
.visit(/* branchNameKey= */ null, visitor, invalidCodeOwnerConfigCallback));
assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
}
@Test
public void cannotVisitCodeOwnerConfigsWithNullVisitor() throws Exception {
BranchNameKey branchNameKey = BranchNameKey.create(project, "master");
NullPointerException npe =
assertThrows(
NullPointerException.class,
() ->
codeOwnerConfigScannerFactory
.create()
.visit(
branchNameKey,
/* codeOwnerConfigVisitor= */ null,
invalidCodeOwnerConfigCallback));
assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigVisitor");
}
@Test
public void cannotVisitCodeOwnerConfigsWithNullCallback() throws Exception {
BranchNameKey branchNameKey = BranchNameKey.create(project, "master");
NullPointerException npe =
assertThrows(
NullPointerException.class,
() ->
codeOwnerConfigScannerFactory
.create()
.visit(branchNameKey, visitor, /* invalidCodeOwnerConfigCallback= */ null));
assertThat(npe).hasMessageThat().isEqualTo("invalidCodeOwnerConfigCallback");
}
@Test
public void cannotVisitCodeOwnerConfigsForNonExistingBranch() throws Exception {
BranchNameKey branchNameKey = BranchNameKey.create(project, "non-existing");
IllegalStateException exception =
assertThrows(
IllegalStateException.class,
() ->
codeOwnerConfigScannerFactory
.create()
.visit(branchNameKey, visitor, invalidCodeOwnerConfigCallback));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"branch %s of project %s not found", branchNameKey.branch(), project.get()));
}
@Test
public void visitorNotInvokedIfNoCodeOwnerConfigFilesExists() throws Exception {
visit();
verifyNoInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorNotInvokedForNonCodeOwnerConfigFiles() throws Exception {
// Create some non code owner config files.
try (TestRepository<Repository> testRepo =
new TestRepository<>(repoManager.openRepository(project))) {
Ref ref = testRepo.getRepository().exactRef("refs/heads/master");
RevCommit head = testRepo.getRevWalk().parseCommit(ref.getObjectId());
testRepo.update(
"refs/heads/master",
testRepo
.commit()
.parent(head)
.message("Add some non code owner config files")
.add("owners.txt", "some content")
.add("owners", "some content")
.add("foo/bar/owners.txt", "some content")
.add("foo/bar/owners", "some content"));
}
visit();
verifyNoInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorNotInvokedForInvalidCodeOwnerConfigFiles() throws Exception {
createNonParseableCodeOwnerConfig("/OWNERS");
visit();
verifyNoInteractions(visitor);
// Verify that we received the expected callbacks for the invalid code onwer config.
Mockito.verify(invalidCodeOwnerConfigCallback)
.onInvalidCodeOwnerConfig(
eq(Paths.get("/OWNERS")), any(InvalidCodeOwnerConfigException.class));
verifyNoMoreInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorInvokedForValidCodeOwnerConfigFilesEvenIfInvalidCodeOwnerConfigFileExist()
throws Exception {
createNonParseableCodeOwnerConfig("/OWNERS");
// Create a valid code owner config file.
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
visit();
// Verify that we received the expected callbacks.
Mockito.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
// Verify that we received the expected callbacks for the invalid code onwer config.
Mockito.verify(invalidCodeOwnerConfigCallback)
.onInvalidCodeOwnerConfig(
eq(Paths.get("/OWNERS")), any(InvalidCodeOwnerConfigException.class));
verifyNoMoreInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorInvokedForCodeOwnerConfigFileAtRoot() throws Exception {
testVisitorInvoked("/", "OWNERS");
}
@Test
public void visitorInvokedForCodeOwnerConfigFileWithPostFixAtRoot() throws Exception {
testVisitorInvoked("/", "OWNERS_post_fix");
}
@Test
public void visitorInvokedForCodeOwnerConfigFileWithPreFixAtRoot() throws Exception {
testVisitorInvoked("/", "pre_fix_OWNERS");
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void visitorInvokedForCodeOwnerConfigFileWithExtensionAtRoot() throws Exception {
testVisitorInvoked("/", "OWNERS.foo");
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void visitorInvokedForCodeOwnerConfigFileWithPostFixAndExtensionAtRoot() throws Exception {
testVisitorInvoked("/", "OWNERS_post_fix.foo");
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void visitorInvokedForCodeOwnerConfigFileWithPreFixAndExtensionAtRoot() throws Exception {
testVisitorInvoked("/", "pre_fix_OWNERS.foo");
}
@Test
public void visitorInvokedForCodeOwnerConfigFileInSubfolder() throws Exception {
testVisitorInvoked("/foo/bar/", "OWNERS");
}
@Test
public void visitorInvokedForCodeOwnerConfigFileWithPostFixInSubfolder() throws Exception {
testVisitorInvoked("/foo/bar/", "OWNERS_post_fix");
}
@Test
public void visitorInvokedForCodeOwnerConfigFileWithPreFixInSubfolder() throws Exception {
testVisitorInvoked("/foo/bar/", "pre_fix_OWNERS");
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void visitorInvokedForCodeOwnerConfigFileWithExtensionInSubfolder() throws Exception {
testVisitorInvoked("/foo/bar/", "OWNERS.foo");
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void visitorInvokedForCodeOwnerConfigFileWithPostFixAndExtensionInSubfolder()
throws Exception {
testVisitorInvoked("/foo/bar/", "OWNERS_post_fix.foo");
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void visitorInvokedForCodeOwnerConfigFileWithPreFixAndExtensionInSubfolder()
throws Exception {
testVisitorInvoked("/foo/bar/", "pre_fix_OWNERS.foo");
}
private void testVisitorInvoked(String folderPath, String fileName) throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath(folderPath)
.fileName(fileName)
.addCodeOwnerEmail(admin.email())
.create();
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
visit();
// Verify that we received the expected callbacks.
Mockito.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorInvokedForDefaultCodeOwnerConfigFileInRefsMetaConfig() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(RefNames.REFS_CONFIG)
.folderPath("/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
visit();
// Verify that we received the expected callback.
Mockito.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorNotInvokedForDefaulCodeOwnerConfigFileInRefsMetaConfigIfSkipped()
throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(RefNames.REFS_CONFIG)
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.create();
visit(
/** includeDefaultCodeOwnerConfig */
false);
// Verify that we did not receive any callback.
verifyNoInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorIsInvokedForAllCodeOwnerConfigFiles() throws Exception {
CodeOwnerConfig.Key metaCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(RefNames.REFS_CONFIG)
.folderPath("/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfig.Key rootCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfig.Key fooCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfig.Key fooBarCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
visit();
// Verify that we received the expected callbacks.
InOrder orderVerifier = Mockito.inOrder(visitor);
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(metaCodeOwnerConfigKey).get());
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(fooCodeOwnerConfigKey).get());
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(fooBarCodeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void skipDefaultCodeOwnerConfigFile() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(RefNames.REFS_CONFIG)
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfig.Key rootCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfig.Key fooCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
visit(
/** includeDefaultCodeOwnerConfig */
false);
// Verify that we received only the expected callbacks (e.g. no callback for the default code
// owner config in refs/meta/config).
InOrder orderVerifier = Mockito.inOrder(visitor);
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(fooCodeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorCanStopTheIterationOverCodeOwnerConfigsByReturningFalse() throws Exception {
CodeOwnerConfig.Key rootCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfig.Key fooCodeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
// Return true for the first time the visitor is invoked, and false for all further invocations.
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true).thenReturn(false);
visit();
// Verify that we received the callbacks in the right order, starting from the folder of the
// given path up to the root folder. We expect only 2 callbacks, since the visitor returns false
// for the second invocation.
InOrder orderVerifier = Mockito.inOrder(visitor);
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(rootCodeOwnerConfigKey).get());
orderVerifier
.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(fooCodeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
@Test
public void visitorIsOnlyInvokedOnceForDefaultCodeOnwerConfigFileIfRefsMetaConfigIsScanned()
throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(RefNames.REFS_CONFIG)
.folderPath("/")
.fileName("OWNERS")
.addCodeOwnerEmail(admin.email())
.create();
when(visitor.visit(any(CodeOwnerConfig.class))).thenReturn(true);
codeOwnerConfigScannerFactory
.create()
.includeDefaultCodeOwnerConfig(true)
.visit(
BranchNameKey.create(project, RefNames.REFS_CONFIG),
visitor,
invalidCodeOwnerConfigCallback);
// Verify that we received the callback for the code owner config only once.
Mockito.verify(visitor)
.visit(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
verifyNoMoreInteractions(visitor);
verifyNoInteractions(invalidCodeOwnerConfigCallback);
}
private void visit() {
visit(
/** includeDefaultCodeOwnerConfig */
true);
}
private void visit(boolean includeDefaultCodeOwnerConfig) {
codeOwnerConfigScannerFactory
.create()
.includeDefaultCodeOwnerConfig(includeDefaultCodeOwnerConfig)
.visit(BranchNameKey.create(project, "master"), visitor, invalidCodeOwnerConfigCallback);
}
}