blob: d27013803716ab3e5c88ba8336bc5dcc62fbb028 [file] [log] [blame]
// Copyright (C) 2019 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.server.account.externalids;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.base.CharMatcher;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.testing.InMemoryRepositoryManager;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class ExternalIDCacheLoaderTest {
private static AllUsersName ALL_USERS = new AllUsersName(AllUsersNameProvider.DEFAULT);
private Cache<ObjectId, AllExternalIds> externalIdCache;
private ExternalIdCacheLoader loader;
private GitRepositoryManager repoManager = new InMemoryRepositoryManager();
private ExternalIdReader externalIdReader;
private ExternalIdReader externalIdReaderSpy;
private ExternalIdFactory externalIdFactory;
@Mock private AuthConfig authConfig;
@Before
public void setUp() throws Exception {
externalIdFactory = new ExternalIdFactory(new ExternalIdKeyFactory(() -> false), authConfig);
externalIdCache = CacheBuilder.newBuilder().build();
repoManager.createRepository(ALL_USERS).close();
externalIdReader =
new ExternalIdReader(
repoManager, ALL_USERS, new DisabledMetricMaker(), externalIdFactory, authConfig);
externalIdReaderSpy = Mockito.spy(externalIdReader);
loader = createLoader();
}
@Test
public void worksOnSingleCommit() throws Exception {
ObjectId firstState = insertExternalId(1, 1);
assertThat(loader.load(firstState)).isEqualTo(allFromGit(firstState));
verify(externalIdReaderSpy, times(1)).all(firstState);
}
@Test
public void reloadsSingleUpdateUsingPartialReload() throws Exception {
ObjectId firstState = insertExternalId(1, 1);
ObjectId head = insertExternalId(2, 2);
externalIdCache.put(firstState, allFromGit(firstState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
@Test
public void loadCacheSuccessfullyWhenInInconsistentState() throws Exception {
int key = 1;
int account = 1;
ExternalId externalId = externalId(key, account);
ExternalId.Key externalIdKey = externalId.key();
Repository repo = repoManager.openRepository(ALL_USERS);
ObjectId newState = insertExternalId(key, account);
TreeWalk tw = new TreeWalk(repo);
tw.reset(new RevWalk(repo).parseCommit(newState).getTree());
tw.next();
HashMap<ObjectId, ObjectId> additions = new HashMap<>();
additions.put(fileNameToObjectId(tw.getPathString()), tw.getObjectId(0));
AllExternalIds oldExternalIds =
AllExternalIds.create(Stream.<ExternalId>builder().add(externalId).build());
AllExternalIds allExternalIds =
loader.buildAllExternalIds(repo, oldExternalIds, additions, new HashSet<>());
assertThat(allExternalIds).isNotNull();
assertThat(allExternalIds.byKey().containsKey(externalIdKey)).isTrue();
assertThat(allExternalIds.byKey().get(externalIdKey)).isEqualTo(externalId);
}
private static ObjectId fileNameToObjectId(String path) {
return ObjectId.fromString(CharMatcher.is('/').removeFrom(path));
}
@Test
public void reloadsMultipleUpdatesUsingPartialReload() throws Exception {
ObjectId firstState = insertExternalId(1, 1);
insertExternalId(2, 2);
insertExternalId(3, 3);
ObjectId head = insertExternalId(4, 4);
externalIdCache.put(firstState, allFromGit(firstState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
@Test
public void reloadsAllExternalIdsWhenNoOldStateIsCached() throws Exception {
insertExternalId(1, 1);
ObjectId head = insertExternalId(2, 2);
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).all(head);
}
@Test
public void fallsBackToFullReloadOnManyUpdatesOnBranch() throws Exception {
insertExternalId(1, 1);
ObjectId head = null;
for (int i = 2; i < 20; i++) {
head = insertExternalId(i, i);
}
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).all(head);
}
@Test
public void doesFullReloadWhenNoCacheStateIsFound() throws Exception {
ObjectId head = insertExternalId(1, 1);
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).all(head);
}
@Test
public void handlesDeletionInPartialReload() throws Exception {
ObjectId firstState = insertExternalId(1, 1);
ObjectId head = deleteExternalId(1, 1);
assertThat(allFromGit(head).byAccount().size()).isEqualTo(0);
externalIdCache.put(firstState, allFromGit(firstState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
@Test
public void handlesModifyInPartialReload() throws Exception {
ObjectId firstState = insertExternalId(1, 1);
ObjectId head =
modifyExternalId(
externalId(1, 1),
externalIdFactory.create(
"fooschema", "bar1", Account.id(1), "foo@bar.com", "password"));
assertThat(allFromGit(head).byAccount().size()).isEqualTo(1);
externalIdCache.put(firstState, allFromGit(firstState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
@Test
public void ignoresInvalidExternalId() throws Exception {
ObjectId firstState = insertExternalId(1, 1);
ObjectId head;
try (Repository repo = repoManager.openRepository(ALL_USERS);
RevWalk rw = new RevWalk(repo)) {
ExternalIdTestUtil.insertExternalIdWithKeyThatDoesntMatchNoteId(
repo, rw, new PersonIdent("foo", "foo@bar.com"), Account.id(2), "test");
head = repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId();
}
externalIdCache.put(firstState, allFromGit(firstState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
@Test
public void handlesTreePrefixesInDifferentialReload() throws Exception {
// Create more than 256 notes (NoteMap's current sharding limit) and check that we really have
// created a situation where NoteNames are sharded.
ObjectId oldState = insertExternalIds(257);
assertAllFilesHaveSlashesInPath();
ObjectId head = insertExternalId(500, 500);
externalIdCache.put(oldState, allFromGit(oldState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
@Test
public void handlesReshard() throws Exception {
// Create 256 notes (NoteMap's current sharding limit) and check that we are not yet sharding
ObjectId oldState = insertExternalIds(256);
assertNoFilesHaveSlashesInPath();
// Create one more external ID and then have the Loader compute the new state
ObjectId head = insertExternalId(500, 500);
assertAllFilesHaveSlashesInPath(); // NoteMap resharded
externalIdCache.put(oldState, allFromGit(oldState));
assertThat(loader.load(head)).isEqualTo(allFromGit(head));
verify(externalIdReaderSpy, times(1)).checkReadEnabled();
verifyNoMoreInteractions(externalIdReaderSpy);
}
private ExternalIdCacheLoader createLoader() {
return new ExternalIdCacheLoader(
repoManager,
ALL_USERS,
externalIdReaderSpy,
externalIdCache,
new DisabledMetricMaker(),
new Config(),
externalIdFactory);
}
private AllExternalIds allFromGit(ObjectId revision) throws Exception {
return AllExternalIds.create(externalIdReader.all(revision).stream());
}
private ObjectId insertExternalIds(int numberOfIdsToInsert) throws Exception {
ObjectId oldState = null;
// Create more than 256 notes (NoteMap's current sharding limit) and check that we really have
// created a situation where NoteNames are sharded.
for (int i = 0; i < numberOfIdsToInsert; i++) {
oldState = insertExternalId(i, i);
}
return oldState;
}
private ObjectId insertExternalId(int key, int accountId) throws Exception {
return performExternalIdUpdate(
u -> {
try {
u.insert(externalId(key, accountId));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
private ObjectId modifyExternalId(ExternalId oldId, ExternalId newId) throws Exception {
return performExternalIdUpdate(
u -> {
try {
u.replace(oldId, newId);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
private ObjectId deleteExternalId(int key, int accountId) throws Exception {
return performExternalIdUpdate(u -> u.delete(externalId(key, accountId)));
}
private ExternalId externalId(int key, int accountId) {
return externalIdFactory.create("fooschema", "bar" + key, Account.id(accountId));
}
private ObjectId performExternalIdUpdate(Consumer<ExternalIdNotes> update) throws Exception {
try (Repository repo = repoManager.openRepository(ALL_USERS)) {
PersonIdent updater = new PersonIdent("Foo bar", "foo@bar.com");
ExternalIdNotes extIdNotes = ExternalIdNotes.load(ALL_USERS, repo, externalIdFactory, false);
update.accept(extIdNotes);
try (MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
metaDataUpdate.getCommitBuilder().setAuthor(updater);
metaDataUpdate.getCommitBuilder().setCommitter(updater);
return extIdNotes.commit(metaDataUpdate).getId();
}
}
}
private void assertAllFilesHaveSlashesInPath() throws Exception {
assertThat(allFilesInExternalIdRef().stream().allMatch(f -> f.contains("/"))).isTrue();
}
private void assertNoFilesHaveSlashesInPath() throws Exception {
assertThat(allFilesInExternalIdRef().stream().noneMatch(f -> f.contains("/"))).isTrue();
}
private ImmutableList<String> allFilesInExternalIdRef() throws Exception {
try (Repository repo = repoManager.openRepository(ALL_USERS);
TreeWalk treeWalk = new TreeWalk(repo);
RevWalk rw = new RevWalk(repo)) {
treeWalk.reset(
rw.parseCommit(repo.exactRef(RefNames.REFS_EXTERNAL_IDS).getObjectId()).getTree());
treeWalk.setRecursive(true);
ImmutableList.Builder<String> allPaths = ImmutableList.builder();
while (treeWalk.next()) {
allPaths.add(treeWalk.getPathString());
}
return allPaths.build();
}
}
}