|  | // 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(); | 
|  | } | 
|  | } | 
|  | } |