
* stable-2.15: Elasticsearch: Exclude types from V7 which deprecates them Send an email notification when the HTTP password is deleted or changed Send email notification when SSH key or GPG key is removed Show progress on number of users migrated during schema migration 146 Additional changes done in this merge to adjust to stable-2.16: - Replace slf4j with Flogger. - Edit newly added soy templates to remove 'autoescape' and 'kind' parameters which are no longer needed and cause parsing errors. - Move newly added email sender classes to correct package. Change-Id: I01a804f10c7247d18a0958eb7e0f03cbcf7453c7
3126 lines
118 KiB
Java
3126 lines
118 KiB
Java
// Copyright (C) 2014 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.acceptance.api.accounts;
|
||
|
||
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.common.truth.Truth.assert_;
|
||
import static com.google.common.truth.Truth8.assertThat;
|
||
import static com.google.gerrit.acceptance.GitUtil.deleteRef;
|
||
import static com.google.gerrit.acceptance.GitUtil.fetch;
|
||
import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
|
||
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
|
||
import static com.google.gerrit.gpg.testing.TestKeys.allValidKeys;
|
||
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithExpiration;
|
||
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
|
||
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
|
||
import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
|
||
import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
|
||
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
|
||
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
|
||
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
|
||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||
import static java.util.Objects.requireNonNull;
|
||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||
import static java.util.stream.Collectors.toSet;
|
||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||
|
||
import com.github.rholder.retry.StopStrategies;
|
||
import com.google.common.cache.LoadingCache;
|
||
import com.google.common.collect.FluentIterable;
|
||
import com.google.common.collect.ImmutableList;
|
||
import com.google.common.collect.ImmutableSet;
|
||
import com.google.common.collect.ImmutableSetMultimap;
|
||
import com.google.common.collect.Iterables;
|
||
import com.google.common.io.BaseEncoding;
|
||
import com.google.common.truth.Correspondence;
|
||
import com.google.common.util.concurrent.AtomicLongMap;
|
||
import com.google.common.util.concurrent.Runnables;
|
||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||
import com.google.gerrit.acceptance.GerritConfig;
|
||
import com.google.gerrit.acceptance.PushOneCommit;
|
||
import com.google.gerrit.acceptance.Sandboxed;
|
||
import com.google.gerrit.acceptance.TestAccount;
|
||
import com.google.gerrit.acceptance.UseSsh;
|
||
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
|
||
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
|
||
import com.google.gerrit.common.Nullable;
|
||
import com.google.gerrit.common.data.AccessSection;
|
||
import com.google.gerrit.common.data.GlobalCapability;
|
||
import com.google.gerrit.common.data.GroupReference;
|
||
import com.google.gerrit.common.data.Permission;
|
||
import com.google.gerrit.common.data.PermissionRule.Action;
|
||
import com.google.gerrit.extensions.api.accounts.AccountInput;
|
||
import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
|
||
import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
|
||
import com.google.gerrit.extensions.api.accounts.EmailInput;
|
||
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
|
||
import com.google.gerrit.extensions.api.changes.DraftInput;
|
||
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
||
import com.google.gerrit.extensions.api.changes.StarsInput;
|
||
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
|
||
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
|
||
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
|
||
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput.CheckAccountsInput;
|
||
import com.google.gerrit.extensions.common.AccountDetailInfo;
|
||
import com.google.gerrit.extensions.common.AccountInfo;
|
||
import com.google.gerrit.extensions.common.ChangeInfo;
|
||
import com.google.gerrit.extensions.common.CommentInfo;
|
||
import com.google.gerrit.extensions.common.EmailInfo;
|
||
import com.google.gerrit.extensions.common.GpgKeyInfo;
|
||
import com.google.gerrit.extensions.common.GroupInfo;
|
||
import com.google.gerrit.extensions.common.SshKeyInfo;
|
||
import com.google.gerrit.extensions.events.AccountIndexedListener;
|
||
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
|
||
import com.google.gerrit.extensions.registration.DynamicSet;
|
||
import com.google.gerrit.extensions.registration.RegistrationHandle;
|
||
import com.google.gerrit.extensions.restapi.AuthException;
|
||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
||
import com.google.gerrit.gpg.Fingerprint;
|
||
import com.google.gerrit.gpg.PublicKeyStore;
|
||
import com.google.gerrit.gpg.testing.TestKey;
|
||
import com.google.gerrit.mail.Address;
|
||
import com.google.gerrit.reviewdb.client.Account;
|
||
import com.google.gerrit.reviewdb.client.Branch;
|
||
import com.google.gerrit.reviewdb.client.Change;
|
||
import com.google.gerrit.reviewdb.client.Project;
|
||
import com.google.gerrit.reviewdb.client.RefNames;
|
||
import com.google.gerrit.server.Sequences;
|
||
import com.google.gerrit.server.ServerInitiated;
|
||
import com.google.gerrit.server.account.AccountProperties;
|
||
import com.google.gerrit.server.account.AccountState;
|
||
import com.google.gerrit.server.account.AccountsUpdate;
|
||
import com.google.gerrit.server.account.Emails;
|
||
import com.google.gerrit.server.account.ProjectWatches;
|
||
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
|
||
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
|
||
import com.google.gerrit.server.account.externalids.ExternalId;
|
||
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
|
||
import com.google.gerrit.server.account.externalids.ExternalIds;
|
||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||
import com.google.gerrit.server.git.LockFailureException;
|
||
import com.google.gerrit.server.git.meta.MetaDataUpdate;
|
||
import com.google.gerrit.server.index.account.AccountIndexer;
|
||
import com.google.gerrit.server.index.account.StalenessChecker;
|
||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
|
||
import com.google.gerrit.server.project.ProjectConfig;
|
||
import com.google.gerrit.server.project.RefPattern;
|
||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||
import com.google.gerrit.server.update.RetryHelper;
|
||
import com.google.gerrit.server.util.MagicBranch;
|
||
import com.google.gerrit.server.util.time.TimeUtil;
|
||
import com.google.gerrit.server.validators.AccountActivationValidationListener;
|
||
import com.google.gerrit.server.validators.ValidationException;
|
||
import com.google.gerrit.testing.ConfigSuite;
|
||
import com.google.gerrit.testing.FakeEmailSender.Message;
|
||
import com.google.gerrit.testing.TestTimeUtil;
|
||
import com.google.gwtorm.server.OrmException;
|
||
import com.google.inject.Inject;
|
||
import com.google.inject.Provider;
|
||
import com.google.inject.name.Named;
|
||
import com.jcraft.jsch.KeyPair;
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.IOException;
|
||
import java.util.ArrayList;
|
||
import java.util.Arrays;
|
||
import java.util.EnumSet;
|
||
import java.util.HashMap;
|
||
import java.util.HashSet;
|
||
import java.util.Iterator;
|
||
import java.util.List;
|
||
import java.util.Locale;
|
||
import java.util.Map;
|
||
import java.util.Objects;
|
||
import java.util.Optional;
|
||
import java.util.Set;
|
||
import java.util.concurrent.atomic.AtomicBoolean;
|
||
import java.util.concurrent.atomic.AtomicInteger;
|
||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||
import org.eclipse.jgit.api.errors.TransportException;
|
||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
||
import org.eclipse.jgit.junit.TestRepository;
|
||
import org.eclipse.jgit.lib.CommitBuilder;
|
||
import org.eclipse.jgit.lib.Config;
|
||
import org.eclipse.jgit.lib.ObjectId;
|
||
import org.eclipse.jgit.lib.ObjectInserter;
|
||
import org.eclipse.jgit.lib.ObjectReader;
|
||
import org.eclipse.jgit.lib.PersonIdent;
|
||
import org.eclipse.jgit.lib.Ref;
|
||
import org.eclipse.jgit.lib.RefUpdate;
|
||
import org.eclipse.jgit.lib.Repository;
|
||
import org.eclipse.jgit.revwalk.RevCommit;
|
||
import org.eclipse.jgit.revwalk.RevWalk;
|
||
import org.eclipse.jgit.transport.PushCertificateIdent;
|
||
import org.eclipse.jgit.transport.PushResult;
|
||
import org.eclipse.jgit.transport.RemoteRefUpdate;
|
||
import org.eclipse.jgit.treewalk.TreeWalk;
|
||
import org.junit.After;
|
||
import org.junit.Before;
|
||
import org.junit.Test;
|
||
|
||
public class AccountIT extends AbstractDaemonTest {
|
||
@ConfigSuite.Default
|
||
public static Config enableSignedPushConfig() {
|
||
Config cfg = new Config();
|
||
cfg.setBoolean("receive", null, "enableSignedPush", true);
|
||
|
||
// Disable the staleness checker so that tests that verify the number of expected index events
|
||
// are stable.
|
||
cfg.setBoolean("index", null, "autoReindexIfStale", false);
|
||
|
||
return cfg;
|
||
}
|
||
|
||
@Inject private Provider<PublicKeyStore> publicKeyStoreProvider;
|
||
|
||
@Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
|
||
|
||
@Inject private ExternalIds externalIds;
|
||
|
||
@Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
|
||
|
||
@Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
|
||
|
||
@Inject private Sequences seq;
|
||
|
||
@Inject private Provider<InternalAccountQuery> accountQueryProvider;
|
||
|
||
@Inject protected Emails emails;
|
||
|
||
@Inject private StalenessChecker stalenessChecker;
|
||
|
||
@Inject private AccountIndexer accountIndexer;
|
||
|
||
@Inject private GitReferenceUpdated gitReferenceUpdated;
|
||
|
||
@Inject private RetryHelper.Metrics retryMetrics;
|
||
|
||
@Inject private Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
|
||
|
||
@Inject private ExternalIdNotes.Factory extIdNotesFactory;
|
||
|
||
@Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
|
||
|
||
@Inject
|
||
@Named("accounts")
|
||
private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
|
||
|
||
@Inject private AccountOperations accountOperations;
|
||
|
||
@Inject
|
||
private DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners;
|
||
|
||
private AccountIndexedCounter accountIndexedCounter;
|
||
private RegistrationHandle accountIndexEventCounterHandle;
|
||
private RefUpdateCounter refUpdateCounter;
|
||
private RegistrationHandle refUpdateCounterHandle;
|
||
|
||
@Before
|
||
public void addAccountIndexEventCounter() {
|
||
accountIndexedCounter = new AccountIndexedCounter();
|
||
accountIndexEventCounterHandle = accountIndexedListeners.add("gerrit", accountIndexedCounter);
|
||
}
|
||
|
||
@After
|
||
public void removeAccountIndexEventCounter() {
|
||
if (accountIndexEventCounterHandle != null) {
|
||
accountIndexEventCounterHandle.remove();
|
||
}
|
||
}
|
||
|
||
@Before
|
||
public void addRefUpdateCounter() {
|
||
refUpdateCounter = new RefUpdateCounter();
|
||
refUpdateCounterHandle = refUpdateListeners.add("gerrit", refUpdateCounter);
|
||
}
|
||
|
||
@After
|
||
public void removeRefUpdateCounter() {
|
||
if (refUpdateCounterHandle != null) {
|
||
refUpdateCounterHandle.remove();
|
||
}
|
||
}
|
||
|
||
@After
|
||
public void clearPublicKeyStore() throws Exception {
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
Ref ref = repo.exactRef(REFS_GPG_KEYS);
|
||
if (ref != null) {
|
||
RefUpdate ru = repo.updateRef(REFS_GPG_KEYS);
|
||
ru.setForceUpdate(true);
|
||
assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
|
||
}
|
||
}
|
||
}
|
||
|
||
@After
|
||
public void deleteGpgKeys() throws Exception {
|
||
String ref = REFS_GPG_KEYS;
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
if (repo.getRefDatabase().exactRef(ref) != null) {
|
||
RefUpdate ru = repo.updateRef(ref);
|
||
ru.setForceUpdate(true);
|
||
assertWithMessage("Failed to delete " + ref)
|
||
.that(ru.delete())
|
||
.isEqualTo(RefUpdate.Result.FORCED);
|
||
}
|
||
}
|
||
}
|
||
|
||
protected void assertLabelPermission(
|
||
Project.NameKey project,
|
||
GroupReference groupReference,
|
||
String ref,
|
||
boolean exclusive,
|
||
String labelName,
|
||
int min,
|
||
int max)
|
||
throws IOException {
|
||
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
|
||
AccessSection accessSection = cfg.getAccessSection(ref);
|
||
assertThat(accessSection).isNotNull();
|
||
|
||
String permissionName = Permission.LABEL + labelName;
|
||
Permission permission = accessSection.getPermission(permissionName);
|
||
assertPermission(permission, permissionName, exclusive, labelName);
|
||
assertPermissionRule(
|
||
permission.getRule(groupReference), groupReference, Action.ALLOW, false, min, max);
|
||
}
|
||
|
||
@Test
|
||
public void createByAccountCreator() throws Exception {
|
||
Account.Id accountId = createByAccountCreator(1);
|
||
refUpdateCounter.assertRefUpdateFor(
|
||
RefUpdateCounter.projectRef(allUsers, RefNames.refsUsers(accountId)),
|
||
RefUpdateCounter.projectRef(allUsers, RefNames.REFS_EXTERNAL_IDS),
|
||
RefUpdateCounter.projectRef(allUsers, RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS));
|
||
}
|
||
|
||
private Account.Id createByAccountCreator(int expectedAccountReindexCalls) throws Exception {
|
||
String name = "foo";
|
||
TestAccount foo = accountCreator.create(name);
|
||
AccountInfo info = gApi.accounts().id(foo.id.get()).get();
|
||
assertThat(info.username).isEqualTo(name);
|
||
assertThat(info.name).isEqualTo(name);
|
||
accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
|
||
assertUserBranch(foo.getId(), name, null);
|
||
return foo.getId();
|
||
}
|
||
|
||
@Test
|
||
public void createAnonymousCowardByAccountCreator() throws Exception {
|
||
TestAccount anonymousCoward = accountCreator.create();
|
||
accountIndexedCounter.assertReindexOf(anonymousCoward);
|
||
assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
|
||
}
|
||
|
||
@Test
|
||
public void create() throws Exception {
|
||
AccountInput input = new AccountInput();
|
||
input.username = "foo";
|
||
input.name = "Foo";
|
||
input.email = "foo@example.com";
|
||
AccountInfo accountInfo = gApi.accounts().create(input).get();
|
||
assertThat(accountInfo._accountId).isNotNull();
|
||
assertThat(accountInfo.username).isEqualTo(input.username);
|
||
assertThat(accountInfo.name).isEqualTo(input.name);
|
||
assertThat(accountInfo.email).isEqualTo(input.email);
|
||
assertThat(accountInfo.status).isNull();
|
||
|
||
Account.Id accountId = new Account.Id(accountInfo._accountId);
|
||
accountIndexedCounter.assertReindexOf(accountId, 1);
|
||
assertThat(externalIds.byAccount(accountId))
|
||
.containsExactly(
|
||
ExternalId.createUsername(input.username, accountId, null),
|
||
ExternalId.createEmail(accountId, input.email));
|
||
}
|
||
|
||
@Test
|
||
public void createAccountUsernameAlreadyTaken() throws Exception {
|
||
AccountInput input = new AccountInput();
|
||
input.username = admin.username;
|
||
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage("username '" + admin.username + "' already exists");
|
||
gApi.accounts().create(input);
|
||
}
|
||
|
||
@Test
|
||
public void createAccountEmailAlreadyTaken() throws Exception {
|
||
AccountInput input = new AccountInput();
|
||
input.username = "foo";
|
||
input.email = admin.email;
|
||
|
||
exception.expect(UnprocessableEntityException.class);
|
||
exception.expectMessage("email '" + admin.email + "' already exists");
|
||
gApi.accounts().create(input);
|
||
}
|
||
|
||
@Test
|
||
public void commitMessageOnAccountUpdates() throws Exception {
|
||
AccountsUpdate au = accountsUpdateProvider.get();
|
||
Account.Id accountId = new Account.Id(seq.nextAccountId());
|
||
au.insert("Create Test Account", accountId, u -> {});
|
||
assertLastCommitMessageOfUserBranch(accountId, "Create Test Account");
|
||
|
||
au.update("Set Status", accountId, u -> u.setStatus("Foo"));
|
||
assertLastCommitMessageOfUserBranch(accountId, "Set Status");
|
||
}
|
||
|
||
private void assertLastCommitMessageOfUserBranch(Account.Id accountId, String expectedMessage)
|
||
throws Exception {
|
||
try (Repository repo = repoManager.openRepository(allUsers);
|
||
RevWalk rw = new RevWalk(repo)) {
|
||
Ref exactRef = repo.exactRef(RefNames.refsUsers(accountId));
|
||
assertThat(rw.parseCommit(exactRef.getObjectId()).getShortMessage())
|
||
.isEqualTo(expectedMessage);
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void createAtomically() throws Exception {
|
||
TestTimeUtil.resetWithClockStep(1, SECONDS);
|
||
try {
|
||
Account.Id accountId = new Account.Id(seq.nextAccountId());
|
||
String fullName = "Foo";
|
||
ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
|
||
AccountState accountState =
|
||
accountsUpdateProvider
|
||
.get()
|
||
.insert(
|
||
"Create Account Atomically",
|
||
accountId,
|
||
u -> u.setFullName(fullName).addExternalId(extId));
|
||
assertThat(accountState.getAccount().getFullName()).isEqualTo(fullName);
|
||
|
||
AccountInfo info = gApi.accounts().id(accountId.get()).get();
|
||
assertThat(info.name).isEqualTo(fullName);
|
||
|
||
List<EmailInfo> emails = gApi.accounts().id(accountId.get()).getEmails();
|
||
assertThat(emails.stream().map(e -> e.email).collect(toSet())).containsExactly(extId.email());
|
||
|
||
RevCommit commitUserBranch = getRemoteHead(allUsers, RefNames.refsUsers(accountId));
|
||
RevCommit commitRefsMetaExternalIds = getRemoteHead(allUsers, RefNames.REFS_EXTERNAL_IDS);
|
||
assertThat(commitUserBranch.getCommitTime())
|
||
.isEqualTo(commitRefsMetaExternalIds.getCommitTime());
|
||
} finally {
|
||
TestTimeUtil.useSystemTime();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void updateNonExistingAccount() throws Exception {
|
||
Account.Id nonExistingAccountId = new Account.Id(999999);
|
||
AtomicBoolean consumerCalled = new AtomicBoolean();
|
||
Optional<AccountState> accountState =
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Update Non-Existing Account", nonExistingAccountId, a -> consumerCalled.set(true));
|
||
assertThat(accountState).isEmpty();
|
||
assertThat(consumerCalled.get()).isFalse();
|
||
}
|
||
|
||
@Test
|
||
public void updateAccountWithoutAccountConfigNoteDb() throws Exception {
|
||
TestAccount anonymousCoward = accountCreator.create();
|
||
assertUserBranchWithoutAccountConfig(anonymousCoward.getId());
|
||
|
||
String status = "OOO";
|
||
Optional<AccountState> accountState =
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update("Set status", anonymousCoward.getId(), u -> u.setStatus(status));
|
||
assertThat(accountState).isPresent();
|
||
Account account = accountState.get().getAccount();
|
||
assertThat(account.getFullName()).isNull();
|
||
assertThat(account.getStatus()).isEqualTo(status);
|
||
assertUserBranch(anonymousCoward.getId(), null, status);
|
||
}
|
||
|
||
private void assertUserBranchWithoutAccountConfig(Account.Id accountId) throws Exception {
|
||
assertUserBranch(accountId, null, null);
|
||
}
|
||
|
||
private void assertUserBranch(
|
||
Account.Id accountId, @Nullable String name, @Nullable String status) throws Exception {
|
||
try (Repository repo = repoManager.openRepository(allUsers);
|
||
RevWalk rw = new RevWalk(repo);
|
||
ObjectReader or = repo.newObjectReader()) {
|
||
Ref ref = repo.exactRef(RefNames.refsUsers(accountId));
|
||
assertThat(ref).isNotNull();
|
||
RevCommit c = rw.parseCommit(ref.getObjectId());
|
||
long timestampDiffMs =
|
||
Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).getRegisteredOn().getTime());
|
||
assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
|
||
|
||
// Check the 'account.config' file.
|
||
try (TreeWalk tw = TreeWalk.forPath(or, AccountProperties.ACCOUNT_CONFIG, c.getTree())) {
|
||
if (name != null || status != null) {
|
||
assertThat(tw).isNotNull();
|
||
Config cfg = new Config();
|
||
cfg.fromText(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8));
|
||
assertThat(
|
||
cfg.getString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_FULL_NAME))
|
||
.isEqualTo(name);
|
||
assertThat(cfg.getString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS))
|
||
.isEqualTo(status);
|
||
} else {
|
||
// No account properties were set, hence an 'account.config' file was not created.
|
||
assertThat(tw).isNull();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void get() throws Exception {
|
||
AccountInfo info = gApi.accounts().id("admin").get();
|
||
assertThat(info.name).isEqualTo("Administrator");
|
||
assertThat(info.email).isEqualTo("admin@example.com");
|
||
assertThat(info.username).isEqualTo("admin");
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void getByIntId() throws Exception {
|
||
AccountInfo info = gApi.accounts().id("admin").get();
|
||
AccountInfo infoByIntId = gApi.accounts().id(info._accountId).get();
|
||
assertThat(info.name).isEqualTo(infoByIntId.name);
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void self() throws Exception {
|
||
AccountInfo info = gApi.accounts().self().get();
|
||
assertUser(info, admin);
|
||
|
||
info = gApi.accounts().id("self").get();
|
||
assertUser(info, admin);
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void active() throws Exception {
|
||
assertThat(gApi.accounts().id("user").getActive()).isTrue();
|
||
gApi.accounts().id("user").setActive(false);
|
||
assertThat(gApi.accounts().id("user").getActive()).isFalse();
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
|
||
gApi.accounts().id("user").setActive(true);
|
||
assertThat(gApi.accounts().id("user").getActive()).isTrue();
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
}
|
||
|
||
@Test
|
||
public void validateAccountActivation() throws Exception {
|
||
Account.Id activatableAccountId =
|
||
accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
|
||
Account.Id deactivatableAccountId =
|
||
accountOperations.newAccount().preferredEmail("foo@deactivatable.com").create();
|
||
RegistrationHandle registrationHandle =
|
||
accountActivationValidationListeners.add(
|
||
"gerrit",
|
||
new AccountActivationValidationListener() {
|
||
@Override
|
||
public void validateActivation(AccountState account) throws ValidationException {
|
||
String preferredEmail = account.getAccount().getPreferredEmail();
|
||
if (preferredEmail == null || !preferredEmail.endsWith("@activatable.com")) {
|
||
throw new ValidationException("not allowed to active account");
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void validateDeactivation(AccountState account) throws ValidationException {
|
||
String preferredEmail = account.getAccount().getPreferredEmail();
|
||
if (preferredEmail == null || !preferredEmail.endsWith("@deactivatable.com")) {
|
||
throw new ValidationException("not allowed to deactive account");
|
||
}
|
||
}
|
||
});
|
||
try {
|
||
/* Test account that can be activated, but not deactivated */
|
||
// Deactivate account that is already inactive
|
||
try {
|
||
gApi.accounts().id(activatableAccountId.get()).setActive(false);
|
||
fail("Expected exception");
|
||
} catch (ResourceConflictException e) {
|
||
assertThat(e.getMessage()).isEqualTo("account not active");
|
||
}
|
||
assertThat(accountOperations.account(activatableAccountId).get().active()).isFalse();
|
||
|
||
// Activate account that can be activated
|
||
gApi.accounts().id(activatableAccountId.get()).setActive(true);
|
||
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
|
||
|
||
// Activate account that is already active
|
||
gApi.accounts().id(activatableAccountId.get()).setActive(true);
|
||
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
|
||
|
||
// Try deactivating account that cannot be deactivated
|
||
try {
|
||
gApi.accounts().id(activatableAccountId.get()).setActive(false);
|
||
fail("Expected exception");
|
||
} catch (ResourceConflictException e) {
|
||
assertThat(e.getMessage()).isEqualTo("not allowed to deactive account");
|
||
}
|
||
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
|
||
|
||
/* Test account that can be deactivated, but not activated */
|
||
// Activate account that is already inactive
|
||
gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
|
||
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isTrue();
|
||
|
||
// Deactivate account that can be deactivated
|
||
gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
|
||
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
|
||
|
||
// Deactivate account that is already inactive
|
||
try {
|
||
gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
|
||
fail("Expected exception");
|
||
} catch (ResourceConflictException e) {
|
||
assertThat(e.getMessage()).isEqualTo("account not active");
|
||
}
|
||
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
|
||
|
||
// Try activating account that cannot be activated
|
||
try {
|
||
gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
|
||
fail("Expected exception");
|
||
} catch (ResourceConflictException e) {
|
||
assertThat(e.getMessage()).isEqualTo("not allowed to active account");
|
||
}
|
||
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
|
||
} finally {
|
||
registrationHandle.remove();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void deactivateSelf() throws Exception {
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage("cannot deactivate own account");
|
||
gApi.accounts().self().setActive(false);
|
||
}
|
||
|
||
@Test
|
||
public void deactivateNotActive() throws Exception {
|
||
assertThat(gApi.accounts().id("user").getActive()).isTrue();
|
||
gApi.accounts().id("user").setActive(false);
|
||
assertThat(gApi.accounts().id("user").getActive()).isFalse();
|
||
try {
|
||
gApi.accounts().id("user").setActive(false);
|
||
fail("Expected exception");
|
||
} catch (ResourceConflictException e) {
|
||
assertThat(e.getMessage()).isEqualTo("account not active");
|
||
}
|
||
gApi.accounts().id("user").setActive(true);
|
||
}
|
||
|
||
@Test
|
||
public void starUnstarChange() throws Exception {
|
||
PushOneCommit.Result r = createChange();
|
||
String triplet = project.get() + "~master~" + r.getChangeId();
|
||
refUpdateCounter.clear();
|
||
|
||
gApi.accounts().self().starChange(triplet);
|
||
ChangeInfo change = info(triplet);
|
||
assertThat(change.starred).isTrue();
|
||
assertThat(change.stars).contains(DEFAULT_LABEL);
|
||
refUpdateCounter.assertRefUpdateFor(
|
||
RefUpdateCounter.projectRef(
|
||
allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
|
||
|
||
gApi.accounts().self().unstarChange(triplet);
|
||
change = info(triplet);
|
||
assertThat(change.starred).isNull();
|
||
assertThat(change.stars).isNull();
|
||
refUpdateCounter.assertRefUpdateFor(
|
||
RefUpdateCounter.projectRef(
|
||
allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
|
||
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void starUnstarChangeWithLabels() throws Exception {
|
||
PushOneCommit.Result r = createChange();
|
||
String triplet = project.get() + "~master~" + r.getChangeId();
|
||
refUpdateCounter.clear();
|
||
|
||
assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
|
||
assertThat(gApi.accounts().self().getStarredChanges()).isEmpty();
|
||
|
||
gApi.accounts()
|
||
.self()
|
||
.setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "red", "blue")));
|
||
ChangeInfo change = info(triplet);
|
||
assertThat(change.starred).isTrue();
|
||
assertThat(change.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
|
||
assertThat(gApi.accounts().self().getStars(triplet))
|
||
.containsExactly("blue", "red", DEFAULT_LABEL)
|
||
.inOrder();
|
||
List<ChangeInfo> starredChanges = gApi.accounts().self().getStarredChanges();
|
||
assertThat(starredChanges).hasSize(1);
|
||
ChangeInfo starredChange = starredChanges.get(0);
|
||
assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
|
||
assertThat(starredChange.starred).isTrue();
|
||
assertThat(starredChange.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
|
||
refUpdateCounter.assertRefUpdateFor(
|
||
RefUpdateCounter.projectRef(
|
||
allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
|
||
|
||
gApi.accounts()
|
||
.self()
|
||
.setStars(
|
||
triplet,
|
||
new StarsInput(ImmutableSet.of("yellow"), ImmutableSet.of(DEFAULT_LABEL, "blue")));
|
||
change = info(triplet);
|
||
assertThat(change.starred).isNull();
|
||
assertThat(change.stars).containsExactly("red", "yellow").inOrder();
|
||
assertThat(gApi.accounts().self().getStars(triplet)).containsExactly("red", "yellow").inOrder();
|
||
starredChanges = gApi.accounts().self().getStarredChanges();
|
||
assertThat(starredChanges).hasSize(1);
|
||
starredChange = starredChanges.get(0);
|
||
assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
|
||
assertThat(starredChange.starred).isNull();
|
||
assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
|
||
refUpdateCounter.assertRefUpdateFor(
|
||
RefUpdateCounter.projectRef(
|
||
allUsers, RefNames.refsStarredChanges(new Change.Id(change._number), admin.id)));
|
||
|
||
accountIndexedCounter.assertNoReindex();
|
||
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
exception.expectMessage("not allowed to get stars of another account");
|
||
gApi.accounts().id(Integer.toString((admin.id.get()))).getStars(triplet);
|
||
}
|
||
|
||
@Test
|
||
public void starWithInvalidLabels() throws Exception {
|
||
PushOneCommit.Result r = createChange();
|
||
String triplet = project.get() + "~master~" + r.getChangeId();
|
||
exception.expect(BadRequestException.class);
|
||
exception.expectMessage("invalid labels: another invalid label, invalid label");
|
||
gApi.accounts()
|
||
.self()
|
||
.setStars(
|
||
triplet,
|
||
new StarsInput(
|
||
ImmutableSet.of(DEFAULT_LABEL, "invalid label", "blue", "another invalid label")));
|
||
}
|
||
|
||
@Test
|
||
public void deleteStarLabelsFromChangeWithoutStarLabels() throws Exception {
|
||
PushOneCommit.Result r = createChange();
|
||
String triplet = project.get() + "~master~" + r.getChangeId();
|
||
assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
|
||
|
||
gApi.accounts().self().setStars(triplet, new StarsInput());
|
||
|
||
assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
|
||
}
|
||
|
||
@Test
|
||
public void starWithDefaultAndIgnoreLabel() throws Exception {
|
||
PushOneCommit.Result r = createChange();
|
||
String triplet = project.get() + "~master~" + r.getChangeId();
|
||
exception.expect(BadRequestException.class);
|
||
exception.expectMessage(
|
||
"The labels "
|
||
+ DEFAULT_LABEL
|
||
+ " and "
|
||
+ IGNORE_LABEL
|
||
+ " are mutually exclusive."
|
||
+ " Only one of them can be set.");
|
||
gApi.accounts()
|
||
.self()
|
||
.setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "blue", IGNORE_LABEL)));
|
||
}
|
||
|
||
@Test
|
||
public void ignoreChangeBySetStars() throws Exception {
|
||
TestAccount user2 = accountCreator.user2();
|
||
accountIndexedCounter.clear();
|
||
|
||
PushOneCommit.Result r = createChange();
|
||
|
||
AddReviewerInput in = new AddReviewerInput();
|
||
in.reviewer = user.email;
|
||
gApi.changes().id(r.getChangeId()).addReviewer(in);
|
||
|
||
in = new AddReviewerInput();
|
||
in.reviewer = user2.email;
|
||
gApi.changes().id(r.getChangeId()).addReviewer(in);
|
||
|
||
setApiUser(user);
|
||
gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
|
||
|
||
sender.clear();
|
||
setApiUser(admin);
|
||
gApi.changes().id(r.getChangeId()).abandon();
|
||
List<Message> messages = sender.getMessages();
|
||
assertThat(messages).hasSize(1);
|
||
assertThat(messages.get(0).rcpt()).containsExactly(user2.emailAddress);
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void addReviewerToIgnoredChange() throws Exception {
|
||
PushOneCommit.Result r = createChange();
|
||
|
||
setApiUser(user);
|
||
gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
|
||
|
||
sender.clear();
|
||
setApiUser(admin);
|
||
|
||
AddReviewerInput in = new AddReviewerInput();
|
||
in.reviewer = user.email;
|
||
gApi.changes().id(r.getChangeId()).addReviewer(in);
|
||
List<Message> messages = sender.getMessages();
|
||
assertThat(messages).hasSize(1);
|
||
Message message = messages.get(0);
|
||
assertThat(message.rcpt()).containsExactly(user.emailAddress);
|
||
assertMailReplyTo(message, admin.email);
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void suggestAccounts() throws Exception {
|
||
String adminUsername = "admin";
|
||
List<AccountInfo> result = gApi.accounts().suggestAccounts().withQuery(adminUsername).get();
|
||
assertThat(result).hasSize(1);
|
||
assertThat(result.get(0).username).isEqualTo(adminUsername);
|
||
|
||
List<AccountInfo> resultShortcutApi = gApi.accounts().suggestAccounts(adminUsername).get();
|
||
assertThat(resultShortcutApi).hasSize(result.size());
|
||
|
||
List<AccountInfo> emptyResult = gApi.accounts().suggestAccounts("unknown").get();
|
||
assertThat(emptyResult).isEmpty();
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void getOwnDetail() throws Exception {
|
||
String email = "preferred@example.com";
|
||
String name = "Foo";
|
||
String username = name("foo");
|
||
TestAccount foo = accountCreator.create(username, email, name);
|
||
String secondaryEmail = "secondary@example.com";
|
||
EmailInput input = newEmailInput(secondaryEmail);
|
||
gApi.accounts().id(foo.id.get()).addEmail(input);
|
||
|
||
String status = "OOO";
|
||
gApi.accounts().id(foo.id.get()).setStatus(status);
|
||
|
||
setApiUser(foo);
|
||
AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
|
||
assertThat(detail._accountId).isEqualTo(foo.id.get());
|
||
assertThat(detail.name).isEqualTo(name);
|
||
assertThat(detail.username).isEqualTo(username);
|
||
assertThat(detail.email).isEqualTo(email);
|
||
assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
|
||
assertThat(detail.status).isEqualTo(status);
|
||
assertThat(detail.registeredOn).isEqualTo(getAccount(foo.getId()).getRegisteredOn());
|
||
assertThat(detail.inactive).isNull();
|
||
assertThat(detail._moreAccounts).isNull();
|
||
}
|
||
|
||
@Test
|
||
public void detailOfOtherAccountDoesntIncludeSecondaryEmailsWithoutModifyAccount()
|
||
throws Exception {
|
||
String email = "preferred@example.com";
|
||
TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
|
||
String secondaryEmail = "secondary@example.com";
|
||
EmailInput input = newEmailInput(secondaryEmail);
|
||
gApi.accounts().id(foo.id.get()).addEmail(input);
|
||
|
||
setApiUser(user);
|
||
AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
|
||
assertThat(detail.secondaryEmails).isNull();
|
||
}
|
||
|
||
@Test
|
||
public void detailOfOtherAccountIncludeSecondaryEmailsWithModifyAccount() throws Exception {
|
||
String email = "preferred@example.com";
|
||
TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
|
||
String secondaryEmail = "secondary@example.com";
|
||
EmailInput input = newEmailInput(secondaryEmail);
|
||
gApi.accounts().id(foo.id.get()).addEmail(input);
|
||
|
||
AccountDetailInfo detail = gApi.accounts().id(foo.id.get()).detail();
|
||
assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
|
||
}
|
||
|
||
@Test
|
||
public void getOwnEmails() throws Exception {
|
||
String email = "preferred@example.com";
|
||
TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
|
||
|
||
setApiUser(foo);
|
||
assertThat(getEmails()).containsExactly(email);
|
||
|
||
setApiUser(admin);
|
||
String secondaryEmail = "secondary@example.com";
|
||
EmailInput input = newEmailInput(secondaryEmail);
|
||
gApi.accounts().id(foo.id.hashCode()).addEmail(input);
|
||
|
||
setApiUser(foo);
|
||
assertThat(getEmails()).containsExactly(email, secondaryEmail);
|
||
}
|
||
|
||
@Test
|
||
public void cannotGetEmailsOfOtherAccountWithoutModifyAccount() throws Exception {
|
||
String email = "preferred2@example.com";
|
||
TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
|
||
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
exception.expectMessage("modify account not permitted");
|
||
gApi.accounts().id(foo.id.get()).getEmails();
|
||
}
|
||
|
||
@Test
|
||
public void getEmailsOfOtherAccount() throws Exception {
|
||
String email = "preferred3@example.com";
|
||
String secondaryEmail = "secondary3@example.com";
|
||
TestAccount foo = accountCreator.create(name("foo"), email, "Foo");
|
||
EmailInput input = newEmailInput(secondaryEmail);
|
||
gApi.accounts().id(foo.id.hashCode()).addEmail(input);
|
||
|
||
assertThat(
|
||
gApi.accounts().id(foo.id.get()).getEmails().stream()
|
||
.map(e -> e.email)
|
||
.collect(toSet()))
|
||
.containsExactly(email, secondaryEmail);
|
||
}
|
||
|
||
@Test
|
||
public void addEmail() throws Exception {
|
||
List<String> emails = ImmutableList.of("new.email@example.com", "new.email@example.systems");
|
||
Set<String> currentEmails = getEmails();
|
||
for (String email : emails) {
|
||
assertThat(currentEmails).doesNotContain(email);
|
||
EmailInput input = newEmailInput(email);
|
||
gApi.accounts().self().addEmail(input);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
resetCurrentApiUser();
|
||
assertThat(getEmails()).containsAllIn(emails);
|
||
}
|
||
|
||
@Test
|
||
public void addInvalidEmail() throws Exception {
|
||
List<String> emails =
|
||
ImmutableList.of(
|
||
// Missing domain part
|
||
"new.email",
|
||
|
||
// Missing domain part
|
||
"new.email@",
|
||
|
||
// Missing user part
|
||
"@example.com",
|
||
|
||
// Non-supported TLD (see tlds-alpha-by-domain.txt)
|
||
"new.email@example.africa");
|
||
for (String email : emails) {
|
||
EmailInput input = newEmailInput(email);
|
||
try {
|
||
gApi.accounts().self().addEmail(input);
|
||
fail("Expected BadRequestException for invalid email address: " + email);
|
||
} catch (BadRequestException e) {
|
||
assertThat(e).hasMessageThat().isEqualTo("invalid email address");
|
||
}
|
||
}
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
|
||
TestAccount account = accountCreator.create(name("user"));
|
||
EmailInput input = newEmailInput("test@test.com");
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
gApi.accounts().id(account.username).addEmail(input);
|
||
}
|
||
|
||
@Test
|
||
public void cannotAddEmailAddressUsedByAnotherAccount() throws Exception {
|
||
String email = "new.email@example.com";
|
||
EmailInput input = newEmailInput(email);
|
||
gApi.accounts().self().addEmail(input);
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage("Identity 'mailto:" + email + "' in use by another account");
|
||
gApi.accounts().id(user.username).addEmail(input);
|
||
}
|
||
|
||
@Test
|
||
@GerritConfig(
|
||
name = "auth.registerEmailPrivateKey",
|
||
value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
|
||
public void addEmailSendsConfirmationEmail() throws Exception {
|
||
String email = "new.email@example.com";
|
||
EmailInput input = newEmailInput(email, false);
|
||
gApi.accounts().self().addEmail(input);
|
||
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
Message m = sender.getMessages().get(0);
|
||
assertThat(m.rcpt()).containsExactly(new Address(email));
|
||
}
|
||
|
||
@Test
|
||
@GerritConfig(
|
||
name = "auth.registerEmailPrivateKey",
|
||
value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
|
||
public void addEmailToBeConfirmedToOwnAccount() throws Exception {
|
||
TestAccount user = accountCreator.create();
|
||
setApiUser(user);
|
||
|
||
String email = "self@example.com";
|
||
EmailInput input = newEmailInput(email, false);
|
||
gApi.accounts().self().addEmail(input);
|
||
}
|
||
|
||
@Test
|
||
public void cannotAddEmailToBeConfirmedToOtherAccountWithoutModifyAccountPermission()
|
||
throws Exception {
|
||
TestAccount user = accountCreator.create();
|
||
setApiUser(user);
|
||
|
||
exception.expect(AuthException.class);
|
||
exception.expectMessage("modify account not permitted");
|
||
gApi.accounts().id(admin.id.get()).addEmail(newEmailInput("foo@example.com", false));
|
||
}
|
||
|
||
@Test
|
||
@GerritConfig(
|
||
name = "auth.registerEmailPrivateKey",
|
||
value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
|
||
public void addEmailToBeConfirmedToOtherAccount() throws Exception {
|
||
TestAccount user = accountCreator.create();
|
||
String email = "me@example.com";
|
||
gApi.accounts().id(user.id.get()).addEmail(newEmailInput(email, false));
|
||
}
|
||
|
||
@Test
|
||
public void deleteEmail() throws Exception {
|
||
String email = "foo.bar@example.com";
|
||
EmailInput input = newEmailInput(email);
|
||
gApi.accounts().self().addEmail(input);
|
||
|
||
resetCurrentApiUser();
|
||
assertThat(getEmails()).contains(email);
|
||
|
||
accountIndexedCounter.clear();
|
||
gApi.accounts().self().deleteEmail(input.email);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
resetCurrentApiUser();
|
||
assertThat(getEmails()).doesNotContain(email);
|
||
}
|
||
|
||
@Test
|
||
public void deleteEmailFromCustomExternalIdSchemes() throws Exception {
|
||
String email = "foo.bar@example.com";
|
||
String extId1 = "foo:bar";
|
||
String extId2 = "foo:baz";
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Add External IDs",
|
||
admin.id,
|
||
u ->
|
||
u.addExternalId(
|
||
ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email))
|
||
.addExternalId(
|
||
ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email)));
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
assertThat(
|
||
gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
|
||
.containsAllOf(extId1, extId2);
|
||
|
||
resetCurrentApiUser();
|
||
assertThat(getEmails()).contains(email);
|
||
|
||
gApi.accounts().self().deleteEmail(email);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
resetCurrentApiUser();
|
||
assertThat(getEmails()).doesNotContain(email);
|
||
assertThat(
|
||
gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
|
||
.containsNoneOf(extId1, extId2);
|
||
}
|
||
|
||
@Test
|
||
public void deleteEmailOfOtherUser() throws Exception {
|
||
String email = "foo.bar@example.com";
|
||
EmailInput input = new EmailInput();
|
||
input.email = email;
|
||
input.noConfirmation = true;
|
||
gApi.accounts().id(user.id.get()).addEmail(input);
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
|
||
setApiUser(user);
|
||
assertThat(getEmails()).contains(email);
|
||
|
||
// admin can delete email of user
|
||
setApiUser(admin);
|
||
gApi.accounts().id(user.id.get()).deleteEmail(email);
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
|
||
setApiUser(user);
|
||
assertThat(getEmails()).doesNotContain(email);
|
||
|
||
// user cannot delete email of admin
|
||
exception.expect(AuthException.class);
|
||
exception.expectMessage("modify account not permitted");
|
||
gApi.accounts().id(admin.id.get()).deleteEmail(admin.email);
|
||
}
|
||
|
||
@Test
|
||
public void lookUpByEmail() throws Exception {
|
||
// exact match with scheme "mailto:"
|
||
assertEmail(emails.getAccountFor(admin.email), admin);
|
||
|
||
// exact match with other scheme
|
||
String email = "foo.bar@example.com";
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Add Email",
|
||
admin.id,
|
||
u ->
|
||
u.addExternalId(
|
||
ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email)));
|
||
assertEmail(emails.getAccountFor(email), admin);
|
||
|
||
// wrong case doesn't match
|
||
assertThat(emails.getAccountFor(admin.email.toUpperCase(Locale.US))).isEmpty();
|
||
|
||
// prefix doesn't match
|
||
assertThat(emails.getAccountFor(admin.email.substring(0, admin.email.indexOf('@')))).isEmpty();
|
||
|
||
// non-existing doesn't match
|
||
assertThat(emails.getAccountFor("non-existing@example.com")).isEmpty();
|
||
|
||
// lookup several accounts by email at once
|
||
ImmutableSetMultimap<String, Account.Id> byEmails =
|
||
emails.getAccountsFor(admin.email, user.email);
|
||
assertEmail(byEmails.get(admin.email), admin);
|
||
assertEmail(byEmails.get(user.email), user);
|
||
}
|
||
|
||
@Test
|
||
public void lookUpByPreferredEmail() throws Exception {
|
||
// create an inconsistent account that has a preferred email without external ID
|
||
String prefix = "foo.preferred";
|
||
String prefEmail = prefix + "@example.com";
|
||
TestAccount foo = accountCreator.create(name("foo"));
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update("Set Preferred Email", foo.id, u -> u.setPreferredEmail(prefEmail));
|
||
|
||
// verify that the account is still found when using the preferred email to lookup the account
|
||
ImmutableSet<Account.Id> accountsByPrefEmail = emails.getAccountFor(prefEmail);
|
||
assertThat(accountsByPrefEmail).hasSize(1);
|
||
assertThat(Iterables.getOnlyElement(accountsByPrefEmail)).isEqualTo(foo.id);
|
||
|
||
// look up by email prefix doesn't find the account
|
||
accountsByPrefEmail = emails.getAccountFor(prefix);
|
||
assertThat(accountsByPrefEmail).isEmpty();
|
||
|
||
// look up by other case doesn't find the account
|
||
accountsByPrefEmail = emails.getAccountFor(prefEmail.toUpperCase(Locale.US));
|
||
assertThat(accountsByPrefEmail).isEmpty();
|
||
}
|
||
|
||
@Test
|
||
public void putStatus() throws Exception {
|
||
List<String> statuses = ImmutableList.of("OOO", "Busy");
|
||
AccountInfo info;
|
||
for (String status : statuses) {
|
||
gApi.accounts().self().setStatus(status);
|
||
info = gApi.accounts().self().get();
|
||
assertUser(info, admin, status);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
gApi.accounts().self().setStatus(null);
|
||
info = gApi.accounts().self().get();
|
||
assertUser(info, admin);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
@Test
|
||
public void setName() throws Exception {
|
||
gApi.accounts().self().setName("Admin McAdminface");
|
||
assertThat(gApi.accounts().self().get().name).isEqualTo("Admin McAdminface");
|
||
}
|
||
|
||
@Test
|
||
public void adminCanSetNameOfOtherUser() throws Exception {
|
||
gApi.accounts().id(user.username).setName("User McUserface");
|
||
assertThat(gApi.accounts().id(user.username).get().name).isEqualTo("User McUserface");
|
||
}
|
||
|
||
@Test
|
||
public void userCannotSetNameOfOtherUser() throws Exception {
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
gApi.accounts().id(admin.username).setName("Admin McAdminface");
|
||
}
|
||
|
||
@Test
|
||
@Sandboxed
|
||
public void userCanSetNameOfOtherUserWithModifyAccountPermission() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.MODIFY_ACCOUNT);
|
||
gApi.accounts().id(admin.username).setName("Admin McAdminface");
|
||
assertThat(gApi.accounts().id(admin.username).get().name).isEqualTo("Admin McAdminface");
|
||
}
|
||
|
||
@Test
|
||
public void fetchUserBranch() throws Exception {
|
||
setApiUser(user);
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
|
||
String userRefName = RefNames.refsUsers(user.id);
|
||
|
||
// remove default READ permissions
|
||
try (ProjectConfigUpdate u = updateProject(allUsers)) {
|
||
u.getConfig()
|
||
.getAccessSection(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true)
|
||
.remove(new Permission(Permission.READ));
|
||
u.save();
|
||
}
|
||
|
||
// deny READ permission that is inherited from All-Projects
|
||
deny(allUsers, RefNames.REFS + "*", Permission.READ, ANONYMOUS_USERS);
|
||
|
||
// fetching user branch without READ permission fails
|
||
try {
|
||
fetch(allUsersRepo, userRefName + ":userRef");
|
||
fail("user branch is visible although no READ permission is granted");
|
||
} catch (TransportException e) {
|
||
// expected because no READ granted on user branch
|
||
}
|
||
|
||
// allow each user to read its own user branch
|
||
grant(
|
||
allUsers,
|
||
RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
|
||
Permission.READ,
|
||
false,
|
||
REGISTERED_USERS);
|
||
|
||
// fetch user branch using refs/users/YY/XXXXXXX
|
||
fetch(allUsersRepo, userRefName + ":userRef");
|
||
Ref userRef = allUsersRepo.getRepository().exactRef("userRef");
|
||
assertThat(userRef).isNotNull();
|
||
|
||
// fetch user branch using refs/users/self
|
||
fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userSelfRef");
|
||
Ref userSelfRef = allUsersRepo.getRepository().getRefDatabase().exactRef("userSelfRef");
|
||
assertThat(userSelfRef).isNotNull();
|
||
assertThat(userSelfRef.getObjectId()).isEqualTo(userRef.getObjectId());
|
||
|
||
accountIndexedCounter.assertNoReindex();
|
||
|
||
// fetching user branch of another user fails
|
||
String otherUserRefName = RefNames.refsUsers(admin.id);
|
||
exception.expect(TransportException.class);
|
||
exception.expectMessage("Remote does not have " + otherUserRefName + " available for fetch.");
|
||
fetch(allUsersRepo, otherUserRefName + ":otherUserRef");
|
||
}
|
||
|
||
@Test
|
||
public void pushToUserBranch() throws Exception {
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
|
||
push.to(RefNames.refsUsers(admin.id)).assertOkStatus();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
|
||
push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
@Test
|
||
public void pushToUserBranchForReview() throws Exception {
|
||
String userRefName = RefNames.refsUsers(admin.id);
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRefName + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
|
||
PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
|
||
r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName);
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchForReviewAndSubmit() throws Exception {
|
||
String userRef = RefNames.refsUsers(admin.id);
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(MagicBranch.NEW_CHANGE + userRef);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
|
||
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
AccountInfo info = gApi.accounts().self().get();
|
||
assertThat(info.email).isEqualTo(admin.email);
|
||
assertThat(info.name).isEqualTo(admin.fullName);
|
||
assertThat(info.status).isEqualTo("out-of-office");
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
|
||
throws Exception {
|
||
TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
|
||
String userRef = RefNames.refsUsers(foo.id);
|
||
accountIndexedCounter.clear();
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
String email = "some.email@example.com";
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
foo.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(MagicBranch.NEW_CHANGE + userRef);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
|
||
|
||
setApiUser(foo);
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
|
||
accountIndexedCounter.assertReindexOf(foo);
|
||
|
||
AccountInfo info = gApi.accounts().self().get();
|
||
assertThat(info.email).isEqualTo(email);
|
||
assertThat(info.name).isEqualTo(foo.fullName);
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfConfigIsInvalid()
|
||
throws Exception {
|
||
String userRef = RefNames.refsUsers(admin.id);
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
"invalid config")
|
||
.to(MagicBranch.NEW_CHANGE + userRef);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
|
||
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage(
|
||
String.format(
|
||
"invalid account configuration: commit '%s' has an invalid '%s' file for account '%s':"
|
||
+ " Invalid config file %s in commit %s",
|
||
r.getCommit().name(),
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
admin.id,
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
r.getCommit().name()));
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfPreferredEmailIsInvalid()
|
||
throws Exception {
|
||
String userRef = RefNames.refsUsers(admin.id);
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
String noEmail = "no.email";
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(MagicBranch.NEW_CHANGE + userRef);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
|
||
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage(
|
||
String.format(
|
||
"invalid account configuration: invalid preferred email '%s' for account '%s'",
|
||
noEmail, admin.id));
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmitIfOwnAccountIsDeactivated()
|
||
throws Exception {
|
||
String userRef = RefNames.refsUsers(admin.id);
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(MagicBranch.NEW_CHANGE + userRef);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
|
||
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage("invalid account configuration: cannot deactivate own account");
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
|
||
TestAccount foo = accountCreator.create(name("foo"));
|
||
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
|
||
String userRef = RefNames.refsUsers(foo.id);
|
||
accountIndexedCounter.clear();
|
||
|
||
grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
|
||
grantLabel("Code-Review", -2, 2, allUsers, userRef, false, adminGroupUuid(), false);
|
||
grant(allUsers, userRef, Permission.SUBMIT, false, adminGroupUuid());
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(MagicBranch.NEW_CHANGE + userRef);
|
||
r.assertOkStatus();
|
||
accountIndexedCounter.assertNoReindex();
|
||
assertThat(r.getChange().change().getDest().get()).isEqualTo(userRef);
|
||
|
||
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
|
||
gApi.changes().id(r.getChangeId()).current().submit();
|
||
accountIndexedCounter.assertReindexOf(foo);
|
||
|
||
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isFalse();
|
||
}
|
||
|
||
@Test
|
||
public void pushWatchConfigToUserBranch() throws Exception {
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config wc = new Config();
|
||
wc.setString(
|
||
ProjectWatches.PROJECT,
|
||
project.get(),
|
||
ProjectWatches.KEY_NOTIFY,
|
||
ProjectWatches.NotifyValue.create(null, EnumSet.of(NotifyType.ALL_COMMENTS)).toString());
|
||
PushOneCommit push =
|
||
pushFactory.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Add project watch",
|
||
ProjectWatches.WATCH_CONFIG,
|
||
wc.toText());
|
||
push.to(RefNames.REFS_USERS_SELF).assertOkStatus();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
String invalidNotifyValue = "]invalid[";
|
||
wc.setString(
|
||
ProjectWatches.PROJECT, project.get(), ProjectWatches.KEY_NOTIFY, invalidNotifyValue);
|
||
push =
|
||
pushFactory.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Add invalid project watch",
|
||
ProjectWatches.WATCH_CONFIG,
|
||
wc.toText());
|
||
PushOneCommit.Result r = push.to(RefNames.REFS_USERS_SELF);
|
||
r.assertErrorStatus("invalid account configuration");
|
||
r.assertMessage(
|
||
String.format(
|
||
"%s: Invalid project watch of account %d for project %s: %s",
|
||
ProjectWatches.WATCH_CONFIG, admin.getId().get(), project.get(), invalidNotifyValue));
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranch() throws Exception {
|
||
TestAccount oooUser = accountCreator.create("away", "away@mail.invalid", "Ambrose Way");
|
||
setApiUser(oooUser);
|
||
|
||
// Must clone as oooUser to ensure the push is allowed.
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, oooUser);
|
||
fetch(allUsersRepo, RefNames.refsUsers(oooUser.id) + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, "out-of-office");
|
||
|
||
accountIndexedCounter.clear();
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
oooUser.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(RefNames.refsUsers(oooUser.id))
|
||
.assertOkStatus();
|
||
|
||
accountIndexedCounter.assertReindexOf(oooUser);
|
||
|
||
AccountInfo info = gApi.accounts().self().get();
|
||
assertThat(info.email).isEqualTo(oooUser.email);
|
||
assertThat(info.name).isEqualTo(oooUser.fullName);
|
||
assertThat(info.status).isEqualTo("out-of-office");
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchIsRejectedIfConfigIsInvalid() throws Exception {
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
"invalid config")
|
||
.to(RefNames.REFS_USERS_SELF);
|
||
r.assertErrorStatus("invalid account configuration");
|
||
r.assertMessage(
|
||
String.format(
|
||
"commit '%s' has an invalid '%s' file for account '%s':"
|
||
+ " Invalid config file %s in commit %s",
|
||
r.getCommit().name(),
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
admin.id,
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
r.getCommit().name()));
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchIsRejectedIfPreferredEmailIsInvalid() throws Exception {
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
String noEmail = "no.email";
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, noEmail);
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(RefNames.REFS_USERS_SELF);
|
||
r.assertErrorStatus("invalid account configuration");
|
||
r.assertMessage(
|
||
String.format("invalid preferred email '%s' for account '%s'", noEmail, admin.id));
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
|
||
TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
|
||
String userRef = RefNames.refsUsers(foo.id);
|
||
|
||
String noEmail = "no.email";
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update("Set Preferred Email", foo.id, u -> u.setPreferredEmail(noEmail));
|
||
accountIndexedCounter.clear();
|
||
|
||
grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
String status = "in vacation";
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_STATUS, status);
|
||
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
foo.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(userRef)
|
||
.assertOkStatus();
|
||
accountIndexedCounter.assertReindexOf(foo);
|
||
|
||
AccountInfo info = gApi.accounts().id(foo.id.get()).get();
|
||
assertThat(info.email).isEqualTo(noEmail);
|
||
assertThat(info.name).isEqualTo(foo.fullName);
|
||
assertThat(info.status).isEqualTo(status);
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
|
||
TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
|
||
String userRef = RefNames.refsUsers(foo.id);
|
||
accountIndexedCounter.clear();
|
||
|
||
grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
String email = "some.email@example.com";
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setString(AccountProperties.ACCOUNT, null, AccountProperties.KEY_PREFERRED_EMAIL, email);
|
||
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
foo.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(userRef)
|
||
.assertOkStatus();
|
||
accountIndexedCounter.assertReindexOf(foo);
|
||
|
||
AccountInfo info = gApi.accounts().id(foo.id.get()).get();
|
||
assertThat(info.email).isEqualTo(email);
|
||
assertThat(info.name).isEqualTo(foo.fullName);
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchIsRejectedIfOwnAccountIsDeactivated() throws Exception {
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
|
||
|
||
PushOneCommit.Result r =
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(RefNames.REFS_USERS_SELF);
|
||
r.assertErrorStatus("invalid account configuration");
|
||
r.assertMessage("cannot deactivate own account");
|
||
accountIndexedCounter.assertNoReindex();
|
||
}
|
||
|
||
@Test
|
||
public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
|
||
TestAccount foo = accountCreator.create(name("foo"));
|
||
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
|
||
String userRef = RefNames.refsUsers(foo.id);
|
||
accountIndexedCounter.clear();
|
||
|
||
grant(allUsers, userRef, Permission.PUSH, false, adminGroupUuid());
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
fetch(allUsersRepo, userRef + ":userRef");
|
||
allUsersRepo.reset("userRef");
|
||
|
||
Config ac = getAccountConfig(allUsersRepo);
|
||
ac.setBoolean(AccountProperties.ACCOUNT, null, AccountProperties.KEY_ACTIVE, false);
|
||
|
||
pushFactory
|
||
.create(
|
||
db,
|
||
admin.getIdent(),
|
||
allUsersRepo,
|
||
"Update account config",
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
ac.toText())
|
||
.to(userRef)
|
||
.assertOkStatus();
|
||
accountIndexedCounter.assertReindexOf(foo);
|
||
|
||
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isFalse();
|
||
}
|
||
|
||
@Test
|
||
public void cannotCreateUserBranch() throws Exception {
|
||
grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
|
||
grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
|
||
|
||
String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef);
|
||
r.assertErrorStatus();
|
||
assertThat(r.getMessage()).contains("Not allowed to create user branch.");
|
||
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(userRef)).isNull();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void createUserBranchWithAccessDatabaseCapability() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
|
||
grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
|
||
|
||
String userRef = RefNames.refsUsers(new Account.Id(seq.nextAccountId()));
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef).assertOkStatus();
|
||
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(userRef)).isNotNull();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability()
|
||
throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE);
|
||
grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH);
|
||
|
||
String userRef = RefNames.REFS_USERS + "foo";
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef);
|
||
r.assertErrorStatus();
|
||
assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/.");
|
||
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(userRef)).isNull();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void createDefaultUserBranch() throws Exception {
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNull();
|
||
}
|
||
|
||
grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.CREATE);
|
||
grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.PUSH);
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
pushFactory
|
||
.create(db, admin.getIdent(), allUsersRepo)
|
||
.to(RefNames.REFS_USERS_DEFAULT)
|
||
.assertOkStatus();
|
||
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNotNull();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void cannotDeleteUserBranch() throws Exception {
|
||
grant(
|
||
allUsers,
|
||
RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
|
||
Permission.DELETE,
|
||
true,
|
||
REGISTERED_USERS);
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
String userRef = RefNames.refsUsers(admin.id);
|
||
PushResult r = deleteRef(allUsersRepo, userRef);
|
||
RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
|
||
assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
|
||
assertThat(refUpdate.getMessage()).contains("Not allowed to delete user branch.");
|
||
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(userRef)).isNotNull();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void deleteUserBranchWithAccessDatabaseCapability() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
grant(
|
||
allUsers,
|
||
RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
|
||
Permission.DELETE,
|
||
true,
|
||
REGISTERED_USERS);
|
||
|
||
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
||
String userRef = RefNames.refsUsers(admin.id);
|
||
PushResult r = deleteRef(allUsersRepo, userRef);
|
||
RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
|
||
assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
|
||
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
assertThat(repo.exactRef(userRef)).isNull();
|
||
}
|
||
|
||
assertThat(accountCache.get(admin.id)).isEmpty();
|
||
assertThat(accountQueryProvider.get().byDefault(admin.id.toString())).isEmpty();
|
||
}
|
||
|
||
@Test
|
||
public void addGpgKey() throws Exception {
|
||
TestKey key = validKeyWithoutExpiration();
|
||
String id = key.getKeyIdString();
|
||
addExternalIdEmail(admin, "test1@example.com");
|
||
|
||
sender.clear();
|
||
assertKeyMapContains(key, addGpgKey(key.getPublicKeyArmored()));
|
||
assertKeys(key);
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
|
||
|
||
setApiUser(user);
|
||
exception.expect(ResourceNotFoundException.class);
|
||
exception.expectMessage(id);
|
||
gApi.accounts().self().gpgKey(id).get();
|
||
}
|
||
|
||
@Test
|
||
public void reAddExistingGpgKey() throws Exception {
|
||
addExternalIdEmail(admin, "test5@example.com");
|
||
TestKey key = validKeyWithSecondUserId();
|
||
String id = key.getKeyIdString();
|
||
PGPPublicKey pk = key.getPublicKey();
|
||
|
||
sender.clear();
|
||
GpgKeyInfo info = addGpgKey(armor(pk)).get(id);
|
||
assertThat(info.userIds).hasSize(2);
|
||
assertIteratorSize(2, getOnlyKeyFromStore(key).getUserIDs());
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
|
||
|
||
pk = PGPPublicKey.removeCertification(pk, "foo:myId");
|
||
sender.clear();
|
||
info = addGpgKeyNoReindex(armor(pk)).get(id);
|
||
assertThat(info.userIds).hasSize(1);
|
||
assertIteratorSize(1, getOnlyKeyFromStore(key).getUserIDs());
|
||
// TODO: Issue 10769: Adding an already existing key should not result in a notification email
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
|
||
}
|
||
|
||
@Test
|
||
public void addOtherUsersGpgKey_Conflict() throws Exception {
|
||
// Both users have a matching external ID for this key.
|
||
addExternalIdEmail(admin, "test5@example.com");
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Add External ID",
|
||
user.getId(),
|
||
u -> u.addExternalId(ExternalId.create("foo", "myId", user.getId())));
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
|
||
TestKey key = validKeyWithSecondUserId();
|
||
addGpgKey(key.getPublicKeyArmored());
|
||
setApiUser(user);
|
||
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage("GPG key already associated with another account");
|
||
addGpgKey(key.getPublicKeyArmored());
|
||
}
|
||
|
||
@Test
|
||
public void listGpgKeys() throws Exception {
|
||
List<TestKey> keys = allValidKeys();
|
||
List<String> toAdd = new ArrayList<>(keys.size());
|
||
for (TestKey key : keys) {
|
||
addExternalIdEmail(admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
|
||
toAdd.add(key.getPublicKeyArmored());
|
||
}
|
||
gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.<String>of());
|
||
assertKeys(keys);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
@Test
|
||
public void deleteGpgKey() throws Exception {
|
||
TestKey key = validKeyWithoutExpiration();
|
||
String id = key.getKeyIdString();
|
||
addExternalIdEmail(admin, "test1@example.com");
|
||
addGpgKey(key.getPublicKeyArmored());
|
||
assertKeys(key);
|
||
|
||
sender.clear();
|
||
gApi.accounts().self().gpgKey(id).delete();
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
assertKeys();
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("GPG keys have been deleted");
|
||
|
||
exception.expect(ResourceNotFoundException.class);
|
||
exception.expectMessage(id);
|
||
gApi.accounts().self().gpgKey(id).get();
|
||
}
|
||
|
||
@Test
|
||
public void addAndRemoveGpgKeys() throws Exception {
|
||
for (TestKey key : allValidKeys()) {
|
||
addExternalIdEmail(admin, PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
|
||
}
|
||
TestKey key1 = validKeyWithoutExpiration();
|
||
TestKey key2 = validKeyWithExpiration();
|
||
TestKey key5 = validKeyWithSecondUserId();
|
||
|
||
Map<String, GpgKeyInfo> infos =
|
||
gApi.accounts()
|
||
.self()
|
||
.putGpgKeys(
|
||
ImmutableList.of(key1.getPublicKeyArmored(), key2.getPublicKeyArmored()),
|
||
ImmutableList.of(key5.getKeyIdString()));
|
||
assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key2.getKeyIdString());
|
||
assertKeys(key1, key2);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
infos =
|
||
gApi.accounts()
|
||
.self()
|
||
.putGpgKeys(
|
||
ImmutableList.of(key5.getPublicKeyArmored()),
|
||
ImmutableList.of(key1.getKeyIdString()));
|
||
assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key5.getKeyIdString());
|
||
assertKeyMapContains(key5, infos);
|
||
assertThat(infos.get(key1.getKeyIdString()).key).isNull();
|
||
assertKeys(key2, key5);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
exception.expect(BadRequestException.class);
|
||
exception.expectMessage("Cannot both add and delete key: " + keyToString(key2.getPublicKey()));
|
||
gApi.accounts()
|
||
.self()
|
||
.putGpgKeys(
|
||
ImmutableList.of(key2.getPublicKeyArmored()), ImmutableList.of(key2.getKeyIdString()));
|
||
}
|
||
|
||
@Test
|
||
public void addMalformedGpgKey() throws Exception {
|
||
String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\ntest\n-----END PGP PUBLIC KEY BLOCK-----";
|
||
exception.expect(BadRequestException.class);
|
||
exception.expectMessage("Failed to parse GPG keys");
|
||
addGpgKey(key);
|
||
}
|
||
|
||
@Test
|
||
@UseSsh
|
||
public void sshKeys() throws Exception {
|
||
// The test account should initially have exactly one ssh key
|
||
List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
|
||
assertThat(info).hasSize(1);
|
||
assertSequenceNumbers(info);
|
||
SshKeyInfo key = info.get(0);
|
||
KeyPair keyPair = sshKeys.getKeyPair(admin);
|
||
String initial = TestSshKeys.publicKey(keyPair, admin.email);
|
||
assertThat(key.sshPublicKey).isEqualTo(initial);
|
||
accountIndexedCounter.assertNoReindex();
|
||
|
||
// Add a new key
|
||
sender.clear();
|
||
String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
|
||
gApi.accounts().self().addSshKey(newKey);
|
||
info = gApi.accounts().self().listSshKeys();
|
||
assertThat(info).hasSize(2);
|
||
assertSequenceNumbers(info);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
|
||
|
||
// Add an existing key (the request succeeds, but the key isn't added again)
|
||
sender.clear();
|
||
gApi.accounts().self().addSshKey(initial);
|
||
info = gApi.accounts().self().listSshKeys();
|
||
assertThat(info).hasSize(2);
|
||
assertSequenceNumbers(info);
|
||
accountIndexedCounter.assertNoReindex();
|
||
// TODO: Issue 10769: Adding an already existing key should not result in a notification email
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
|
||
|
||
// Add another new key
|
||
sender.clear();
|
||
String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
|
||
gApi.accounts().self().addSshKey(newKey2);
|
||
info = gApi.accounts().self().listSshKeys();
|
||
assertThat(info).hasSize(3);
|
||
assertSequenceNumbers(info);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
|
||
|
||
// Delete second key
|
||
sender.clear();
|
||
gApi.accounts().self().deleteSshKey(2);
|
||
info = gApi.accounts().self().listSshKeys();
|
||
assertThat(info).hasSize(2);
|
||
assertThat(info.get(0).seq).isEqualTo(1);
|
||
assertThat(info.get(1).seq).isEqualTo(3);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("SSH keys have been deleted");
|
||
|
||
// Mark first key as invalid
|
||
assertThat(info.get(0).valid).isTrue();
|
||
authorizedKeys.markKeyInvalid(admin.id, 1);
|
||
info = gApi.accounts().self().listSshKeys();
|
||
assertThat(info).hasSize(2);
|
||
assertThat(info.get(0).seq).isEqualTo(1);
|
||
assertThat(info.get(0).valid).isFalse();
|
||
assertThat(info.get(1).seq).isEqualTo(3);
|
||
accountIndexedCounter.assertReindexOf(admin);
|
||
}
|
||
|
||
// reindex is tested by {@link AbstractQueryAccountsTest#reindex}
|
||
@Test
|
||
public void reindexPermissions() throws Exception {
|
||
// admin can reindex any account
|
||
setApiUser(admin);
|
||
gApi.accounts().id(user.username).index();
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
|
||
// user can reindex own account
|
||
setApiUser(user);
|
||
gApi.accounts().self().index();
|
||
accountIndexedCounter.assertReindexOf(user);
|
||
|
||
// user cannot reindex any account
|
||
exception.expect(AuthException.class);
|
||
exception.expectMessage("modify account not permitted");
|
||
gApi.accounts().id(admin.username).index();
|
||
}
|
||
|
||
@Test
|
||
public void checkConsistency() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
resetCurrentApiUser();
|
||
|
||
// Create an account with a preferred email.
|
||
String username = name("foo");
|
||
String email = username + "@example.com";
|
||
TestAccount account = accountCreator.create(username, email, "Foo Bar");
|
||
|
||
ConsistencyCheckInput input = new ConsistencyCheckInput();
|
||
input.checkAccounts = new CheckAccountsInput();
|
||
ConsistencyCheckInfo checkInfo = gApi.config().server().checkConsistency(input);
|
||
assertThat(checkInfo.checkAccountsResult.problems).isEmpty();
|
||
|
||
Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
|
||
|
||
// Delete the external ID for the preferred email. This makes the account inconsistent since it
|
||
// now doesn't have an external ID for its preferred email.
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Delete External ID",
|
||
account.getId(),
|
||
u -> u.deleteExternalId(ExternalId.createEmail(account.getId(), email)));
|
||
expectedProblems.add(
|
||
new ConsistencyProblemInfo(
|
||
ConsistencyProblemInfo.Status.ERROR,
|
||
"Account '"
|
||
+ account.getId().get()
|
||
+ "' has no external ID for its preferred email '"
|
||
+ email
|
||
+ "'"));
|
||
|
||
checkInfo = gApi.config().server().checkConsistency(input);
|
||
assertThat(checkInfo.checkAccountsResult.problems).hasSize(expectedProblems.size());
|
||
assertThat(checkInfo.checkAccountsResult.problems).containsExactlyElementsIn(expectedProblems);
|
||
}
|
||
|
||
@Test
|
||
public void internalQueryFindActiveAndInactiveAccounts() throws Exception {
|
||
String name = name("foo");
|
||
assertThat(accountQueryProvider.get().byDefault(name)).isEmpty();
|
||
|
||
TestAccount foo1 = accountCreator.create(name + "-1");
|
||
assertThat(gApi.accounts().id(foo1.username).getActive()).isTrue();
|
||
|
||
TestAccount foo2 = accountCreator.create(name + "-2");
|
||
gApi.accounts().id(foo2.username).setActive(false);
|
||
assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
|
||
|
||
assertThat(accountQueryProvider.get().byDefault(name)).hasSize(2);
|
||
}
|
||
|
||
@Test
|
||
public void checkMetaId() throws Exception {
|
||
// metaId is set when account is loaded
|
||
assertThat(accounts.get(admin.getId()).get().getAccount().getMetaId())
|
||
.isEqualTo(getMetaId(admin.getId()));
|
||
|
||
// metaId is set when account is created
|
||
AccountsUpdate au = accountsUpdateProvider.get();
|
||
Account.Id accountId = new Account.Id(seq.nextAccountId());
|
||
AccountState accountState = au.insert("Create Test Account", accountId, u -> {});
|
||
assertThat(accountState.getAccount().getMetaId()).isEqualTo(getMetaId(accountId));
|
||
|
||
// metaId is set when account is updated
|
||
Optional<AccountState> updatedAccountState =
|
||
au.update("Set Full Name", accountId, u -> u.setFullName("foo"));
|
||
assertThat(updatedAccountState).isPresent();
|
||
Account updatedAccount = updatedAccountState.get().getAccount();
|
||
assertThat(accountState.getAccount().getMetaId()).isNotEqualTo(updatedAccount.getMetaId());
|
||
assertThat(updatedAccount.getMetaId()).isEqualTo(getMetaId(accountId));
|
||
}
|
||
|
||
private EmailInput newEmailInput(String email, boolean noConfirmation) {
|
||
EmailInput input = new EmailInput();
|
||
input.email = email;
|
||
input.noConfirmation = noConfirmation;
|
||
return input;
|
||
}
|
||
|
||
private EmailInput newEmailInput(String email) {
|
||
return newEmailInput(email, true);
|
||
}
|
||
|
||
private String getMetaId(Account.Id accountId) throws IOException {
|
||
try (Repository repo = repoManager.openRepository(allUsers);
|
||
RevWalk rw = new RevWalk(repo);
|
||
ObjectReader or = repo.newObjectReader()) {
|
||
Ref ref = repo.exactRef(RefNames.refsUsers(accountId));
|
||
return ref != null ? ref.getObjectId().name() : null;
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void allGroupsForAnAdminAccountCanBeRetrieved() throws Exception {
|
||
List<GroupInfo> groups = gApi.accounts().id(admin.username).getGroups();
|
||
assertThat(groups)
|
||
.comparingElementsUsing(getGroupToNameCorrespondence())
|
||
.containsExactly("Anonymous Users", "Registered Users", "Administrators");
|
||
}
|
||
|
||
@Test
|
||
public void createUserWithValidUsername() throws Exception {
|
||
ImmutableList<String> names =
|
||
ImmutableList.of(
|
||
"user@domain",
|
||
"user-name",
|
||
"user_name",
|
||
"1234",
|
||
"user1234",
|
||
"1234@domain",
|
||
"user!+alias{*}#$%&’^=~|@domain");
|
||
for (String name : names) {
|
||
gApi.accounts().create(name);
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void createUserWithInvalidUsername() throws Exception {
|
||
ImmutableList<String> invalidNames =
|
||
ImmutableList.of(
|
||
"@", "@foo", "-", "-foo", "_", "_foo", "!", "+", "{", "}", "*", "%", "#", "$", "&", "’",
|
||
"^", "=", "~");
|
||
for (String name : invalidNames) {
|
||
try {
|
||
gApi.accounts().create(name);
|
||
fail(String.format("Expected BadRequestException for username [%s]", name));
|
||
} catch (BadRequestException e) {
|
||
assertThat(e).hasMessageThat().isEqualTo(String.format("Invalid username '%s'", name));
|
||
}
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void allGroupsForAUserAccountCanBeRetrieved() throws Exception {
|
||
String username = name("user1");
|
||
accountOperations.newAccount().username(username).create();
|
||
String group = createGroup("group");
|
||
gApi.groups().id(group).addMembers(username);
|
||
|
||
List<GroupInfo> allGroups = gApi.accounts().id(username).getGroups();
|
||
assertThat(allGroups)
|
||
.comparingElementsUsing(getGroupToNameCorrespondence())
|
||
.containsExactly("Anonymous Users", "Registered Users", group);
|
||
}
|
||
|
||
@Test
|
||
public void defaultPermissionsOnUserBranches() throws Exception {
|
||
String userRef = RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}";
|
||
assertPermissions(
|
||
allUsers,
|
||
groupRef(REGISTERED_USERS),
|
||
userRef,
|
||
true,
|
||
Permission.READ,
|
||
Permission.PUSH,
|
||
Permission.SUBMIT);
|
||
|
||
assertLabelPermission(
|
||
allUsers, groupRef(REGISTERED_USERS), userRef, true, "Code-Review", -2, 2);
|
||
|
||
assertPermissions(
|
||
allUsers,
|
||
adminGroupRef(),
|
||
RefNames.REFS_USERS_DEFAULT,
|
||
true,
|
||
Permission.READ,
|
||
Permission.PUSH,
|
||
Permission.CREATE);
|
||
}
|
||
|
||
@Test
|
||
public void retryOnLockFailure() throws Exception {
|
||
String status = "happy";
|
||
String fullName = "Foo";
|
||
AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
|
||
PersonIdent ident = serverIdent.get();
|
||
AccountsUpdate update =
|
||
new AccountsUpdate(
|
||
repoManager,
|
||
gitReferenceUpdated,
|
||
null,
|
||
allUsers,
|
||
externalIds,
|
||
metaDataUpdateInternalFactory,
|
||
new RetryHelper(
|
||
cfg,
|
||
retryMetrics,
|
||
null,
|
||
null,
|
||
null,
|
||
r -> r.withBlockStrategy(noSleepBlockStrategy)),
|
||
extIdNotesFactory,
|
||
ident,
|
||
ident,
|
||
() -> {
|
||
if (!doneBgUpdate.getAndSet(true)) {
|
||
try {
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update("Set Status", admin.id, u -> u.setStatus(status));
|
||
} catch (IOException | ConfigInvalidException | OrmException e) {
|
||
// Ignore, the successful update of the account is asserted later
|
||
}
|
||
}
|
||
},
|
||
Runnables.doNothing());
|
||
assertThat(doneBgUpdate.get()).isFalse();
|
||
AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
|
||
assertThat(accountInfo.status).isNull();
|
||
assertThat(accountInfo.name).isNotEqualTo(fullName);
|
||
|
||
Optional<AccountState> updatedAccountState =
|
||
update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
|
||
assertThat(doneBgUpdate.get()).isTrue();
|
||
|
||
assertThat(updatedAccountState).isPresent();
|
||
Account updatedAccount = updatedAccountState.get().getAccount();
|
||
assertThat(updatedAccount.getStatus()).isEqualTo(status);
|
||
assertThat(updatedAccount.getFullName()).isEqualTo(fullName);
|
||
|
||
accountInfo = gApi.accounts().id(admin.id.get()).get();
|
||
assertThat(accountInfo.status).isEqualTo(status);
|
||
assertThat(accountInfo.name).isEqualTo(fullName);
|
||
}
|
||
|
||
@Test
|
||
public void failAfterRetryerGivesUp() throws Exception {
|
||
List<String> status = ImmutableList.of("foo", "bar", "baz");
|
||
String fullName = "Foo";
|
||
AtomicInteger bgCounter = new AtomicInteger(0);
|
||
PersonIdent ident = serverIdent.get();
|
||
AccountsUpdate update =
|
||
new AccountsUpdate(
|
||
repoManager,
|
||
gitReferenceUpdated,
|
||
null,
|
||
allUsers,
|
||
externalIds,
|
||
metaDataUpdateInternalFactory,
|
||
new RetryHelper(
|
||
cfg,
|
||
retryMetrics,
|
||
null,
|
||
null,
|
||
null,
|
||
r ->
|
||
r.withStopStrategy(StopStrategies.stopAfterAttempt(status.size()))
|
||
.withBlockStrategy(noSleepBlockStrategy)),
|
||
extIdNotesFactory,
|
||
ident,
|
||
ident,
|
||
() -> {
|
||
try {
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Set Status",
|
||
admin.id,
|
||
u -> u.setStatus(status.get(bgCounter.getAndAdd(1))));
|
||
} catch (IOException | ConfigInvalidException | OrmException e) {
|
||
// Ignore, the expected exception is asserted later
|
||
}
|
||
},
|
||
Runnables.doNothing());
|
||
assertThat(bgCounter.get()).isEqualTo(0);
|
||
AccountInfo accountInfo = gApi.accounts().id(admin.id.get()).get();
|
||
assertThat(accountInfo.status).isNull();
|
||
assertThat(accountInfo.name).isNotEqualTo(fullName);
|
||
|
||
try {
|
||
update.update("Set Full Name", admin.id, u -> u.setFullName(fullName));
|
||
fail("expected LockFailureException");
|
||
} catch (LockFailureException e) {
|
||
// Ignore, expected
|
||
}
|
||
assertThat(bgCounter.get()).isEqualTo(status.size());
|
||
|
||
Account updatedAccount = accounts.get(admin.id).get().getAccount();
|
||
assertThat(updatedAccount.getStatus()).isEqualTo(Iterables.getLast(status));
|
||
assertThat(updatedAccount.getFullName()).isEqualTo(admin.fullName);
|
||
|
||
accountInfo = gApi.accounts().id(admin.id.get()).get();
|
||
assertThat(accountInfo.status).isEqualTo(Iterables.getLast(status));
|
||
assertThat(accountInfo.name).isEqualTo(admin.fullName);
|
||
}
|
||
|
||
@Test
|
||
public void atomicReadMofifyWrite() throws Exception {
|
||
gApi.accounts().id(admin.id.get()).setStatus("A-1");
|
||
|
||
AtomicInteger bgCounterA1 = new AtomicInteger(0);
|
||
AtomicInteger bgCounterA2 = new AtomicInteger(0);
|
||
PersonIdent ident = serverIdent.get();
|
||
AccountsUpdate update =
|
||
new AccountsUpdate(
|
||
repoManager,
|
||
gitReferenceUpdated,
|
||
null,
|
||
allUsers,
|
||
externalIds,
|
||
metaDataUpdateInternalFactory,
|
||
new RetryHelper(
|
||
cfg,
|
||
retryMetrics,
|
||
null,
|
||
null,
|
||
null,
|
||
r -> r.withBlockStrategy(noSleepBlockStrategy)),
|
||
extIdNotesFactory,
|
||
ident,
|
||
ident,
|
||
Runnables.doNothing(),
|
||
() -> {
|
||
try {
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update("Set Status", admin.id, u -> u.setStatus("A-2"));
|
||
} catch (IOException | ConfigInvalidException | OrmException e) {
|
||
// Ignore, the expected exception is asserted later
|
||
}
|
||
});
|
||
assertThat(bgCounterA1.get()).isEqualTo(0);
|
||
assertThat(bgCounterA2.get()).isEqualTo(0);
|
||
assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("A-1");
|
||
|
||
Optional<AccountState> updatedAccountState =
|
||
update.update(
|
||
"Set Status",
|
||
admin.id,
|
||
(a, u) -> {
|
||
if ("A-1".equals(a.getAccount().getStatus())) {
|
||
bgCounterA1.getAndIncrement();
|
||
u.setStatus("B-1");
|
||
}
|
||
|
||
if ("A-2".equals(a.getAccount().getStatus())) {
|
||
bgCounterA2.getAndIncrement();
|
||
u.setStatus("B-2");
|
||
}
|
||
});
|
||
|
||
assertThat(bgCounterA1.get()).isEqualTo(1);
|
||
assertThat(bgCounterA2.get()).isEqualTo(1);
|
||
|
||
assertThat(updatedAccountState).isPresent();
|
||
assertThat(updatedAccountState.get().getAccount().getStatus()).isEqualTo("B-2");
|
||
assertThat(accounts.get(admin.id).get().getAccount().getStatus()).isEqualTo("B-2");
|
||
assertThat(gApi.accounts().id(admin.id.get()).get().status).isEqualTo("B-2");
|
||
}
|
||
|
||
@Test
|
||
public void atomicReadMofifyWriteExternalIds() throws Exception {
|
||
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
||
|
||
Account.Id accountId = new Account.Id(seq.nextAccountId());
|
||
ExternalId extIdA1 = ExternalId.create("foo", "A-1", accountId);
|
||
accountsUpdateProvider
|
||
.get()
|
||
.insert("Create Test Account", accountId, u -> u.addExternalId(extIdA1));
|
||
|
||
AtomicInteger bgCounterA1 = new AtomicInteger(0);
|
||
AtomicInteger bgCounterA2 = new AtomicInteger(0);
|
||
PersonIdent ident = serverIdent.get();
|
||
ExternalId extIdA2 = ExternalId.create("foo", "A-2", accountId);
|
||
AccountsUpdate update =
|
||
new AccountsUpdate(
|
||
repoManager,
|
||
gitReferenceUpdated,
|
||
null,
|
||
allUsers,
|
||
externalIds,
|
||
metaDataUpdateInternalFactory,
|
||
new RetryHelper(
|
||
cfg,
|
||
retryMetrics,
|
||
null,
|
||
null,
|
||
null,
|
||
r -> r.withBlockStrategy(noSleepBlockStrategy)),
|
||
extIdNotesFactory,
|
||
ident,
|
||
ident,
|
||
Runnables.doNothing(),
|
||
() -> {
|
||
try {
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Update External ID",
|
||
accountId,
|
||
u -> u.replaceExternalId(extIdA1, extIdA2));
|
||
} catch (IOException | ConfigInvalidException | OrmException e) {
|
||
// Ignore, the expected exception is asserted later
|
||
}
|
||
});
|
||
assertThat(bgCounterA1.get()).isEqualTo(0);
|
||
assertThat(bgCounterA2.get()).isEqualTo(0);
|
||
assertThat(
|
||
gApi.accounts().id(accountId.get()).getExternalIds().stream()
|
||
.map(i -> i.identity)
|
||
.collect(toSet()))
|
||
.containsExactly(extIdA1.key().get());
|
||
|
||
ExternalId extIdB1 = ExternalId.create("foo", "B-1", accountId);
|
||
ExternalId extIdB2 = ExternalId.create("foo", "B-2", accountId);
|
||
Optional<AccountState> updatedAccount =
|
||
update.update(
|
||
"Update External ID",
|
||
accountId,
|
||
(a, u) -> {
|
||
if (a.getExternalIds().contains(extIdA1)) {
|
||
bgCounterA1.getAndIncrement();
|
||
u.replaceExternalId(extIdA1, extIdB1);
|
||
}
|
||
|
||
if (a.getExternalIds().contains(extIdA2)) {
|
||
bgCounterA2.getAndIncrement();
|
||
u.replaceExternalId(extIdA2, extIdB2);
|
||
}
|
||
});
|
||
|
||
assertThat(bgCounterA1.get()).isEqualTo(1);
|
||
assertThat(bgCounterA2.get()).isEqualTo(1);
|
||
|
||
assertThat(updatedAccount).isPresent();
|
||
assertThat(updatedAccount.get().getExternalIds()).containsExactly(extIdB2);
|
||
assertThat(accounts.get(accountId).get().getExternalIds()).containsExactly(extIdB2);
|
||
assertThat(
|
||
gApi.accounts().id(accountId.get()).getExternalIds().stream()
|
||
.map(i -> i.identity)
|
||
.collect(toSet()))
|
||
.containsExactly(extIdB2.key().get());
|
||
}
|
||
|
||
@Test
|
||
public void stalenessChecker() throws Exception {
|
||
// Newly created account is not stale.
|
||
AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
|
||
Account.Id accountId = new Account.Id(accountInfo._accountId);
|
||
assertThat(stalenessChecker.isStale(accountId)).isFalse();
|
||
|
||
// Manually updating the user ref makes the index document stale.
|
||
String userRef = RefNames.refsUsers(accountId);
|
||
try (Repository repo = repoManager.openRepository(allUsers);
|
||
ObjectInserter oi = repo.newObjectInserter();
|
||
RevWalk rw = new RevWalk(repo)) {
|
||
RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
|
||
|
||
PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
|
||
CommitBuilder cb = new CommitBuilder();
|
||
cb.setTreeId(commit.getTree());
|
||
cb.setCommitter(ident);
|
||
cb.setAuthor(ident);
|
||
cb.setMessage(commit.getFullMessage());
|
||
ObjectId emptyCommit = oi.insert(cb);
|
||
oi.flush();
|
||
|
||
RefUpdate updateRef = repo.updateRef(userRef);
|
||
updateRef.setExpectedOldObjectId(commit.toObjectId());
|
||
updateRef.setNewObjectId(emptyCommit);
|
||
assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
|
||
}
|
||
assertStaleAccountAndReindex(accountId);
|
||
|
||
// Manually inserting/updating/deleting an external ID of the user makes the index document
|
||
// stale.
|
||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
||
ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo);
|
||
|
||
ExternalId.Key key = ExternalId.Key.create("foo", "foo");
|
||
extIdNotes.insert(ExternalId.create(key, accountId));
|
||
try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
|
||
extIdNotes.commit(update);
|
||
}
|
||
assertStaleAccountAndReindex(accountId);
|
||
|
||
extIdNotes.upsert(ExternalId.createWithEmail(key, accountId, "foo@example.com"));
|
||
try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
|
||
extIdNotes.commit(update);
|
||
}
|
||
assertStaleAccountAndReindex(accountId);
|
||
|
||
extIdNotes.delete(accountId, key);
|
||
try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
|
||
extIdNotes.commit(update);
|
||
}
|
||
assertStaleAccountAndReindex(accountId);
|
||
}
|
||
|
||
// Manually delete account
|
||
try (Repository repo = repoManager.openRepository(allUsers);
|
||
RevWalk rw = new RevWalk(repo)) {
|
||
RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
|
||
RefUpdate updateRef = repo.updateRef(userRef);
|
||
updateRef.setExpectedOldObjectId(commit.toObjectId());
|
||
updateRef.setNewObjectId(ObjectId.zeroId());
|
||
updateRef.setForceUpdate(true);
|
||
assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
|
||
}
|
||
assertStaleAccountAndReindex(accountId);
|
||
}
|
||
|
||
private void assertStaleAccountAndReindex(Account.Id accountId) throws IOException {
|
||
// Evict account from cache to be sure that we use the index state for staleness checks. This
|
||
// has to happen directly on the accounts cache because AccountCacheImpl triggers a reindex for
|
||
// the account.
|
||
accountsCache.invalidate(accountId);
|
||
assertThat(stalenessChecker.isStale(accountId)).isTrue();
|
||
|
||
// Reindex fixes staleness
|
||
accountIndexer.index(accountId);
|
||
assertThat(stalenessChecker.isStale(accountId)).isFalse();
|
||
}
|
||
|
||
@Test
|
||
public void deleteAllDraftComments() throws Exception {
|
||
try {
|
||
TestTimeUtil.resetWithClockStep(1, SECONDS);
|
||
Project.NameKey project2 = createProject("project2");
|
||
PushOneCommit.Result r1 = createChange();
|
||
|
||
TestRepository<?> tr2 = cloneProject(project2);
|
||
PushOneCommit.Result r2 =
|
||
createChange(
|
||
tr2,
|
||
"refs/heads/master",
|
||
"Change in project2",
|
||
PushOneCommit.FILE_NAME,
|
||
"content2",
|
||
null);
|
||
|
||
// Create 2 drafts each on both changes for user.
|
||
setApiUser(user);
|
||
createDraft(r1, PushOneCommit.FILE_NAME, "draft 1a");
|
||
createDraft(r1, PushOneCommit.FILE_NAME, "draft 1b");
|
||
createDraft(r2, PushOneCommit.FILE_NAME, "draft 2a");
|
||
createDraft(r2, PushOneCommit.FILE_NAME, "draft 2b");
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(2);
|
||
assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(2);
|
||
|
||
// Create 1 draft on first change for admin.
|
||
setApiUser(admin);
|
||
createDraft(r1, PushOneCommit.FILE_NAME, "admin draft");
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
|
||
// Delete user's draft comments; leave admin's alone.
|
||
setApiUser(user);
|
||
List<DeletedDraftCommentInfo> result =
|
||
gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
|
||
|
||
// Results are ordered according to the change search, most recently updated first.
|
||
assertThat(result).hasSize(2);
|
||
DeletedDraftCommentInfo del2 = result.get(0);
|
||
assertThat(del2.change.changeId).isEqualTo(r2.getChangeId());
|
||
assertThat(del2.deleted.stream().map(c -> c.message)).containsExactly("draft 2a", "draft 2b");
|
||
DeletedDraftCommentInfo del1 = result.get(1);
|
||
assertThat(del1.change.changeId).isEqualTo(r1.getChangeId());
|
||
assertThat(del1.deleted.stream().map(c -> c.message)).containsExactly("draft 1a", "draft 1b");
|
||
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
|
||
assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).isEmpty();
|
||
|
||
setApiUser(admin);
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
} finally {
|
||
cleanUpDrafts();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void deleteDraftCommentsByQuery() throws Exception {
|
||
try {
|
||
PushOneCommit.Result r1 = createChange();
|
||
PushOneCommit.Result r2 = createChange();
|
||
|
||
createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
|
||
createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
|
||
List<DeletedDraftCommentInfo> result =
|
||
gApi.accounts()
|
||
.self()
|
||
.deleteDraftComments(new DeleteDraftCommentsInput("change:" + r1.getChangeId()));
|
||
assertThat(result).hasSize(1);
|
||
assertThat(result.get(0).change.changeId).isEqualTo(r1.getChangeId());
|
||
assertThat(result.get(0).deleted.stream().map(c -> c.message)).containsExactly("draft a");
|
||
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
|
||
assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
} finally {
|
||
cleanUpDrafts();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void deleteOtherUsersDraftCommentsDisallowed() throws Exception {
|
||
try {
|
||
PushOneCommit.Result r = createChange();
|
||
setApiUser(user);
|
||
createDraft(r, PushOneCommit.FILE_NAME, "draft");
|
||
setApiUser(admin);
|
||
try {
|
||
gApi.accounts().id(user.id.get()).deleteDraftComments(new DeleteDraftCommentsInput());
|
||
assert_().fail("expected AuthException");
|
||
} catch (AuthException e) {
|
||
assertThat(e).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
|
||
}
|
||
} finally {
|
||
cleanUpDrafts();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void deleteDraftCommentsSkipsInvisibleChanges() throws Exception {
|
||
try {
|
||
createBranch(new Branch.NameKey(project, "secret"));
|
||
PushOneCommit.Result r1 = createChange();
|
||
PushOneCommit.Result r2 = createChange("refs/for/secret");
|
||
|
||
setApiUser(user);
|
||
createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
|
||
createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
|
||
block(project, "refs/heads/secret", Permission.READ, REGISTERED_USERS);
|
||
List<DeletedDraftCommentInfo> result =
|
||
gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
|
||
assertThat(result).hasSize(1);
|
||
assertThat(result.get(0).change.changeId).isEqualTo(r1.getChangeId());
|
||
assertThat(result.get(0).deleted.stream().map(c -> c.message)).containsExactly("draft a");
|
||
|
||
removePermission(project, "refs/heads/secret", Permission.READ);
|
||
assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
|
||
// Draft still exists since change wasn't visible when drafts where deleted.
|
||
assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
|
||
} finally {
|
||
cleanUpDrafts();
|
||
}
|
||
}
|
||
|
||
@Test
|
||
public void userCanGenerateNewHttpPassword() throws Exception {
|
||
sender.clear();
|
||
String newPassword = gApi.accounts().self().generateHttpPassword();
|
||
assertThat(newPassword).isNotNull();
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
|
||
}
|
||
|
||
@Test
|
||
public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
|
||
setApiUser(admin);
|
||
sender.clear();
|
||
String newPassword = gApi.accounts().id(user.username).generateHttpPassword();
|
||
assertThat(newPassword).isNotNull();
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
|
||
}
|
||
|
||
@Test
|
||
public void userCannotGenerateNewHttpPasswordForOtherUser() throws Exception {
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
gApi.accounts().id(admin.username).generateHttpPassword();
|
||
}
|
||
|
||
@Test
|
||
public void userCannotExplicitlySetHttpPassword() throws Exception {
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
gApi.accounts().self().setHttpPassword("my-new-password");
|
||
}
|
||
|
||
@Test
|
||
public void userCannotExplicitlySetHttpPasswordForOtherUser() throws Exception {
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
gApi.accounts().id(admin.username).setHttpPassword("my-new-password");
|
||
}
|
||
|
||
@Test
|
||
public void userCanRemoveHttpPassword() throws Exception {
|
||
setApiUser(user);
|
||
sender.clear();
|
||
assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
|
||
}
|
||
|
||
@Test
|
||
public void userCannotRemoveHttpPasswordForOtherUser() throws Exception {
|
||
setApiUser(user);
|
||
exception.expect(AuthException.class);
|
||
gApi.accounts().id(admin.username).setHttpPassword(null);
|
||
}
|
||
|
||
@Test
|
||
public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
|
||
setApiUser(admin);
|
||
String httpPassword = "new-password-for-user";
|
||
sender.clear();
|
||
assertThat(gApi.accounts().id(user.username).setHttpPassword(httpPassword))
|
||
.isEqualTo(httpPassword);
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
|
||
}
|
||
|
||
@Test
|
||
public void adminCanRemoveHttpPasswordForUser() throws Exception {
|
||
setApiUser(admin);
|
||
sender.clear();
|
||
assertThat(gApi.accounts().id(user.username).setHttpPassword(null)).isNull();
|
||
assertThat(sender.getMessages()).hasSize(1);
|
||
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
|
||
}
|
||
|
||
@Test
|
||
public void cannotGenerateHttpPasswordWhenUsernameIsNotSet() throws Exception {
|
||
setApiUser(admin);
|
||
int userId = accountCreator.create().id.get();
|
||
assertThat(gApi.accounts().id(userId).get().username).isNull();
|
||
exception.expect(ResourceConflictException.class);
|
||
exception.expectMessage("username");
|
||
gApi.accounts().id(userId).generateHttpPassword();
|
||
}
|
||
|
||
private void createDraft(PushOneCommit.Result r, String path, String message) throws Exception {
|
||
DraftInput in = new DraftInput();
|
||
in.path = path;
|
||
in.line = 1;
|
||
in.message = message;
|
||
gApi.changes().id(r.getChangeId()).current().createDraft(in);
|
||
}
|
||
|
||
private void cleanUpDrafts() throws Exception {
|
||
for (TestAccount testAccount : accountCreator.getAll()) {
|
||
setApiUser(testAccount);
|
||
for (ChangeInfo changeInfo : gApi.changes().query("has:draft").get()) {
|
||
for (CommentInfo c :
|
||
gApi.changes().id(changeInfo.id).drafts().values().stream()
|
||
.flatMap(List::stream)
|
||
.collect(toImmutableList())) {
|
||
gApi.changes().id(changeInfo.id).revision(c.patchSet).draft(c.id).delete();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private static Correspondence<GroupInfo, String> getGroupToNameCorrespondence() {
|
||
return new Correspondence<GroupInfo, String>() {
|
||
@Override
|
||
public boolean compare(GroupInfo actualGroup, String expectedName) {
|
||
String groupName = actualGroup == null ? null : actualGroup.name;
|
||
return Objects.equals(groupName, expectedName);
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
return "has name";
|
||
}
|
||
};
|
||
}
|
||
|
||
private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
|
||
int seq = 1;
|
||
for (SshKeyInfo key : sshKeys) {
|
||
assertThat(key.seq).isEqualTo(seq++);
|
||
}
|
||
}
|
||
|
||
private PGPPublicKey getOnlyKeyFromStore(TestKey key) throws Exception {
|
||
try (PublicKeyStore store = publicKeyStoreProvider.get()) {
|
||
Iterable<PGPPublicKeyRing> keys = store.get(key.getKeyId());
|
||
assertThat(keys).hasSize(1);
|
||
return keys.iterator().next().getPublicKey();
|
||
}
|
||
}
|
||
|
||
private static String armor(PGPPublicKey key) throws Exception {
|
||
ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
|
||
try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
|
||
key.encode(aout);
|
||
}
|
||
return new String(out.toByteArray(), UTF_8);
|
||
}
|
||
|
||
private static void assertIteratorSize(int size, Iterator<?> it) {
|
||
List<?> lst = ImmutableList.copyOf(it);
|
||
assertThat(lst).hasSize(size);
|
||
}
|
||
|
||
private static void assertKeyMapContains(TestKey expected, Map<String, GpgKeyInfo> actualMap) {
|
||
GpgKeyInfo actual = actualMap.get(expected.getKeyIdString());
|
||
assertThat(actual).isNotNull();
|
||
assertThat(actual.id).isNull();
|
||
actual.id = expected.getKeyIdString();
|
||
assertKeyEquals(expected, actual);
|
||
}
|
||
|
||
private void assertKeys(TestKey... expectedKeys) throws Exception {
|
||
assertKeys(Arrays.asList(expectedKeys));
|
||
}
|
||
|
||
private void assertKeys(Iterable<TestKey> expectedKeys) throws Exception {
|
||
// Check via API.
|
||
FluentIterable<TestKey> expected = FluentIterable.from(expectedKeys);
|
||
Map<String, GpgKeyInfo> keyMap = gApi.accounts().self().listGpgKeys();
|
||
assertThat(keyMap.keySet())
|
||
.named("keys returned by listGpgKeys()")
|
||
.containsExactlyElementsIn(expected.transform(TestKey::getKeyIdString));
|
||
|
||
for (TestKey key : expected) {
|
||
assertKeyEquals(key, gApi.accounts().self().gpgKey(key.getKeyIdString()).get());
|
||
assertKeyEquals(
|
||
key,
|
||
gApi.accounts()
|
||
.self()
|
||
.gpgKey(Fingerprint.toString(key.getPublicKey().getFingerprint()))
|
||
.get());
|
||
assertKeyMapContains(key, keyMap);
|
||
}
|
||
|
||
// Check raw external IDs.
|
||
Account.Id currAccountId = atrScope.get().getUser().getAccountId();
|
||
Iterable<String> expectedFps =
|
||
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
|
||
Iterable<String> actualFps =
|
||
externalIds.byAccount(currAccountId, SCHEME_GPGKEY).stream()
|
||
.map(e -> e.key().id())
|
||
.collect(toSet());
|
||
assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
|
||
|
||
// Check raw stored keys.
|
||
for (TestKey key : expected) {
|
||
getOnlyKeyFromStore(key);
|
||
}
|
||
}
|
||
|
||
private static void assertKeyEquals(TestKey expected, GpgKeyInfo actual) {
|
||
String id = expected.getKeyIdString();
|
||
assertThat(actual.id).named(id).isEqualTo(id);
|
||
assertThat(actual.fingerprint)
|
||
.named(id)
|
||
.isEqualTo(Fingerprint.toString(expected.getPublicKey().getFingerprint()));
|
||
List<String> userIds = ImmutableList.copyOf(expected.getPublicKey().getUserIDs());
|
||
assertThat(actual.userIds).named(id).containsExactlyElementsIn(userIds);
|
||
String key = actual.key;
|
||
assertThat(key).named(id).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
|
||
assertThat(key).named(id).endsWith("-----END PGP PUBLIC KEY BLOCK-----\n");
|
||
assertThat(actual.status).isEqualTo(GpgKeyInfo.Status.TRUSTED);
|
||
assertThat(actual.problems).isEmpty();
|
||
}
|
||
|
||
private void addExternalIdEmail(TestAccount account, String email) throws Exception {
|
||
requireNonNull(email);
|
||
accountsUpdateProvider
|
||
.get()
|
||
.update(
|
||
"Add Email",
|
||
account.getId(),
|
||
u ->
|
||
u.addExternalId(
|
||
ExternalId.createWithEmail(name("test"), email, account.getId(), email)));
|
||
accountIndexedCounter.assertReindexOf(account);
|
||
setApiUser(account);
|
||
}
|
||
|
||
private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception {
|
||
Map<String, GpgKeyInfo> gpgKeys =
|
||
gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
|
||
accountIndexedCounter.assertReindexOf(gApi.accounts().self().get());
|
||
return gpgKeys;
|
||
}
|
||
|
||
private Map<String, GpgKeyInfo> addGpgKeyNoReindex(String armored) throws Exception {
|
||
return gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
|
||
}
|
||
|
||
private void assertUser(AccountInfo info, TestAccount account) throws Exception {
|
||
assertUser(info, account, null);
|
||
}
|
||
|
||
private void assertUser(AccountInfo info, TestAccount account, @Nullable String expectedStatus)
|
||
throws Exception {
|
||
assertThat(info.name).isEqualTo(account.fullName);
|
||
assertThat(info.email).isEqualTo(account.email);
|
||
assertThat(info.username).isEqualTo(account.username);
|
||
assertThat(info.status).isEqualTo(expectedStatus);
|
||
}
|
||
|
||
private Set<String> getEmails() throws RestApiException {
|
||
return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet());
|
||
}
|
||
|
||
private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) {
|
||
assertThat(accounts).hasSize(1);
|
||
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());
|
||
}
|
||
|
||
private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception {
|
||
Config ac = new Config();
|
||
try (TreeWalk tw =
|
||
TreeWalk.forPath(
|
||
allUsersRepo.getRepository(),
|
||
AccountProperties.ACCOUNT_CONFIG,
|
||
getHead(allUsersRepo.getRepository()).getTree())) {
|
||
assertThat(tw).isNotNull();
|
||
ac.fromText(
|
||
new String(
|
||
allUsersRepo
|
||
.getRevWalk()
|
||
.getObjectReader()
|
||
.open(tw.getObjectId(0), OBJ_BLOB)
|
||
.getBytes(),
|
||
UTF_8));
|
||
}
|
||
return ac;
|
||
}
|
||
|
||
/** Checks if an account is indexed the correct number of times. */
|
||
private static class AccountIndexedCounter implements AccountIndexedListener {
|
||
private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create();
|
||
|
||
@Override
|
||
public void onAccountIndexed(int id) {
|
||
countsByAccount.incrementAndGet(id);
|
||
}
|
||
|
||
void clear() {
|
||
countsByAccount.clear();
|
||
}
|
||
|
||
long getCount(Account.Id accountId) {
|
||
return countsByAccount.get(accountId.get());
|
||
}
|
||
|
||
void assertReindexOf(TestAccount testAccount) {
|
||
assertReindexOf(testAccount, 1);
|
||
}
|
||
|
||
void assertReindexOf(AccountInfo accountInfo) {
|
||
assertReindexOf(new Account.Id(accountInfo._accountId), 1);
|
||
}
|
||
|
||
void assertReindexOf(TestAccount testAccount, int expectedCount) {
|
||
assertThat(getCount(testAccount.id)).isEqualTo(expectedCount);
|
||
assertThat(countsByAccount).hasSize(1);
|
||
clear();
|
||
}
|
||
|
||
void assertReindexOf(Account.Id accountId, int expectedCount) {
|
||
assertThat(getCount(accountId)).isEqualTo(expectedCount);
|
||
countsByAccount.remove(accountId.get());
|
||
}
|
||
|
||
void assertNoReindex() {
|
||
assertThat(countsByAccount).isEmpty();
|
||
}
|
||
}
|
||
|
||
private static class RefUpdateCounter implements GitReferenceUpdatedListener {
|
||
private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create();
|
||
|
||
static String projectRef(Project.NameKey project, String ref) {
|
||
return projectRef(project.get(), ref);
|
||
}
|
||
|
||
static String projectRef(String project, String ref) {
|
||
return project + ":" + ref;
|
||
}
|
||
|
||
@Override
|
||
public void onGitReferenceUpdated(Event event) {
|
||
countsByProjectRefs.incrementAndGet(projectRef(event.getProjectName(), event.getRefName()));
|
||
}
|
||
|
||
void clear() {
|
||
countsByProjectRefs.clear();
|
||
}
|
||
|
||
long getCount(String projectRef) {
|
||
return countsByProjectRefs.get(projectRef);
|
||
}
|
||
|
||
void assertRefUpdateFor(String... projectRefs) {
|
||
Map<String, Integer> expectedRefUpdateCounts = new HashMap<>();
|
||
for (String projectRef : projectRefs) {
|
||
expectedRefUpdateCounts.put(projectRef, 1);
|
||
}
|
||
assertRefUpdateFor(expectedRefUpdateCounts);
|
||
}
|
||
|
||
void assertRefUpdateFor(Map<String, Integer> expectedProjectRefUpdateCounts) {
|
||
for (Map.Entry<String, Integer> e : expectedProjectRefUpdateCounts.entrySet()) {
|
||
assertThat(getCount(e.getKey())).isEqualTo(e.getValue());
|
||
}
|
||
assertThat(countsByProjectRefs).hasSize(expectedProjectRefUpdateCounts.size());
|
||
clear();
|
||
}
|
||
}
|
||
}
|