AccountResolver: Fix StringIndexOutOfBoundsException

If a change query is done for an account with the 'Full Name <email>'
format, Gerrit first finds accounts that match the email. If there are
multiple accounts that match the email, Gerrit only returns the matches
where also the full name matches. This logic to match on the full name
run into a StringIndexOutOfBoundsException when the input string was an
email surrounded by '<' and '>', e.g. "<foo.bar@example.com>". Such an
input string is deteced as 'Full Name <email>' format but then parsing
the full name in front of '<' failed because it is not present in the
input string.

Stacktrace:
java.lang.StringIndexOutOfBoundsException: String index out of range: -1
  at java.lang.String.substring(String.java:1969)
  at com.google.gerrit.server.account.AccountResolver$ByNameAndEmail.search(AccountResolver.java:349)
  at com.google.gerrit.server.account.AccountResolver$ByNameAndEmail.search(AccountResolver.java:328)
  at com.google.gerrit.server.account.AccountResolver$Searcher.trySearch(AccountResolver.java:227)
  at com.google.gerrit.server.account.AccountResolver.searchImpl(AccountResolver.java:595)
  at com.google.gerrit.server.account.AccountResolver.resolve(AccountResolver.java:519)
  at com.google.gerrit.server.query.change.ChangeQueryBuilder.parseAccount(ChangeQueryBuilder.java:1394)
  at com.google.gerrit.server.query.change.ChangeQueryBuilder.ownerDefaultField(ChangeQueryBuilder.java:1034)
  at com.google.gerrit.server.query.change.ChangeQueryBuilder.defaultField(ChangeQueryBuilder.java:1320)
  at com.google.gerrit.index.query.QueryBuilder.toPredicate(QueryBuilder.java:258)
  at com.google.gerrit.index.query.QueryBuilder.children(QueryBuilder.java:331)
  at com.google.gerrit.index.query.QueryBuilder.toPredicate(QueryBuilder.java:251)
  at com.google.gerrit.index.query.QueryBuilder.parse(QueryBuilder.java:224)
  at com.google.gerrit.index.query.QueryBuilder.parse(QueryBuilder.java:243)
  at com.google.gerrit.server.restapi.change.QueryChanges.query(QueryChanges.java:166)
  at com.google.gerrit.server.restapi.change.QueryChanges.apply(QueryChanges.java:130)
  at com.google.gerrit.server.restapi.change.QueryChanges.apply(QueryChanges.java:45)
  ...

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Id03b16fedd6b31ac2666d165a0627a46c163f3c7
This commit is contained in:
Edwin Kempin
2020-02-18 10:06:51 +01:00
parent d31ad03da9
commit e9f5e658ae
5 changed files with 130 additions and 0 deletions

View File

@@ -95,5 +95,25 @@ public interface AccountOperations {
* @return a builder to update the account
*/
TestAccountUpdate.Builder forUpdate();
/**
* Starts the fluent chain to invalidate an account. The returned builder can be used to specify
* how the account should be made invalid. To invalidate the account for real, {@link
* TestAccountInvalidation.Builder#invalidate()} must be called.
*
* <p>Example:
*
* <pre>
* accountOperations.forInvalidation()
* .preferredEmailWithoutExternalId("foo.bar@example.com")
* .invalidate();
* </pre>
*
* <p><strong>Note:</strong> The invalidation will fail with an exception if the account to
* invalidate doesn't exist.
*
* @return a builder to invalidate the account
*/
TestAccountInvalidation.Builder forInvalidation();
}
}

View File

@@ -168,5 +168,23 @@ public class AccountOperationsImpl implements AccountOperations {
accountUpdate.status().ifPresent(builder::setStatus);
accountUpdate.active().ifPresent(builder::setActive);
}
@Override
public TestAccountInvalidation.Builder forInvalidation() {
return TestAccountInvalidation.builder(this::invalidateAccount);
}
private void invalidateAccount(TestAccountInvalidation testAccountInvalidation)
throws Exception {
Optional<AccountState> accountState = getAccountState(accountId);
checkState(accountState.isPresent(), "Tried to invalidate a non-existing test account");
if (testAccountInvalidation.preferredEmailWithoutExternalId().isPresent()) {
updateAccount(
(account, updateBuilder) ->
updateBuilder.setPreferredEmail(
testAccountInvalidation.preferredEmailWithoutExternalId().get()));
}
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance.testsuite.account;
import com.google.auto.value.AutoValue;
import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
import java.util.Optional;
/**
* API to invalidate accounts in tests.
*
* <p>This allows to test Gerrit behavior when there is invalid account data in NoteDb (e.g.
* accounts with duplicate emails).
*/
@AutoValue
public abstract class TestAccountInvalidation {
/**
* Sets a preferred email for the account for which the account doesn't have an external ID.
*
* <p>This allows to set the same preferred email for multiple accounts so that the email becomes
* ambiguous.
*/
public abstract Optional<String> preferredEmailWithoutExternalId();
abstract ThrowingConsumer<TestAccountInvalidation> accountInvalidator();
public static Builder builder(ThrowingConsumer<TestAccountInvalidation> accountInvalidator) {
return new AutoValue_TestAccountInvalidation.Builder().accountInvalidator(accountInvalidator);
}
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder preferredEmailWithoutExternalId(String preferredEmail);
abstract Builder accountInvalidator(
ThrowingConsumer<TestAccountInvalidation> accountInvalidator);
abstract TestAccountInvalidation autoBuild();
/** Executes the account invalidation as specified. */
public void invalidate() {
TestAccountInvalidation accountInvalidation = autoBuild();
accountInvalidation.accountInvalidator().acceptAndThrowSilently(accountInvalidation);
}
}
}