| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.account; |
| |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.common.collect.Iterables.getOnlyElement; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| import static java.util.stream.Collectors.joining; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.server.account.AccountResolver.Result; |
| import com.google.gerrit.server.account.AccountResolver.Searcher; |
| import com.google.gerrit.server.account.AccountResolver.StringSearcher; |
| import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException; |
| import com.google.gerrit.server.util.time.TimeUtil; |
| import java.util.Arrays; |
| import java.util.function.Predicate; |
| import java.util.function.Supplier; |
| import java.util.stream.Stream; |
| import org.junit.Test; |
| |
| public class AccountResolverTest { |
| private static class TestSearcher extends StringSearcher { |
| private final String pattern; |
| private final boolean shortCircuit; |
| private final ImmutableList<AccountState> accounts; |
| private boolean assumeVisible; |
| private boolean filterInactive; |
| |
| private TestSearcher(String pattern, boolean shortCircuit, AccountState... accounts) { |
| this.pattern = pattern; |
| this.shortCircuit = shortCircuit; |
| this.accounts = ImmutableList.copyOf(accounts); |
| } |
| |
| @Override |
| protected boolean matches(String input) { |
| return input.matches(pattern); |
| } |
| |
| @Override |
| public Stream<AccountState> search(String input) { |
| return accounts.stream(); |
| } |
| |
| @Override |
| public boolean shortCircuitIfNoResults() { |
| return shortCircuit; |
| } |
| |
| @Override |
| public boolean callerMayAssumeCandidatesAreVisible() { |
| return assumeVisible; |
| } |
| |
| void setCallerMayAssumeCandidatesAreVisible() { |
| this.assumeVisible = true; |
| } |
| |
| @Override |
| public boolean callerShouldFilterOutInactiveCandidates() { |
| return filterInactive; |
| } |
| |
| void setCallerShouldFilterOutInactiveCandidates() { |
| this.filterInactive = true; |
| } |
| |
| @Override |
| public String toString() { |
| return accounts.stream() |
| .map(a -> a.account().id().toString()) |
| .collect(joining(",", pattern + "(", ")")); |
| } |
| } |
| |
| @Test |
| public void noShortCircuit() throws Exception { |
| ImmutableList<Searcher<?>> searchers = |
| ImmutableList.of( |
| new TestSearcher("foo", false, newAccount(1)), |
| new TestSearcher("bar", false, newAccount(2), newAccount(3))); |
| |
| Result result = search("foo", searchers, allVisible()); |
| assertThat(result.input()).isEqualTo("foo"); |
| assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1)); |
| |
| result = search("bar", searchers, allVisible()); |
| assertThat(result.input()).isEqualTo("bar"); |
| assertThat(result.asIdSet()).containsExactlyElementsIn(ids(2, 3)); |
| |
| result = search("baz", searchers, allVisible()); |
| assertThat(result.input()).isEqualTo("baz"); |
| assertThat(result.asIdSet()).isEmpty(); |
| } |
| |
| @Test |
| public void shortCircuit() throws Exception { |
| ImmutableList<Searcher<?>> searchers = |
| ImmutableList.of( |
| new TestSearcher("f.*", true), new TestSearcher("foo|bar", false, newAccount(1))); |
| |
| Result result = search("foo", searchers, allVisible()); |
| assertThat(result.input()).isEqualTo("foo"); |
| assertThat(result.asIdSet()).isEmpty(); |
| |
| result = search("bar", searchers, allVisible()); |
| assertThat(result.input()).isEqualTo("bar"); |
| assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1)); |
| } |
| |
| @Test |
| public void filterInvisible() throws Exception { |
| ImmutableList<Searcher<?>> searchers = |
| ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2))); |
| |
| assertThat(search("foo", searchers, allVisible()).asIdSet()) |
| .containsExactlyElementsIn(ids(1, 2)); |
| assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2)); |
| } |
| |
| @Test |
| public void skipVisibilityCheck() throws Exception { |
| TestSearcher searcher = new TestSearcher("foo", false, newAccount(1), newAccount(2)); |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher); |
| |
| assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2)); |
| |
| searcher.setCallerMayAssumeCandidatesAreVisible(); |
| assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(1, 2)); |
| } |
| |
| @Test |
| public void dontFilterInactive() throws Exception { |
| ImmutableList<Searcher<?>> searchers = |
| ImmutableList.of( |
| new TestSearcher("foo", false, newInactiveAccount(1)), |
| new TestSearcher("f.*", false, newInactiveAccount(2))); |
| |
| Result result = search("foo", searchers, allVisible()); |
| // Searchers always short-circuit when finding a non-empty result list, and this one didn't |
| // filter out inactive results, so the second searcher never ran. |
| assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1)); |
| assertThat(getOnlyElement(result.asList()).account().isActive()).isFalse(); |
| assertThat(filteredInactiveIds(result)).isEmpty(); |
| } |
| |
| @Test |
| public void shouldUseCustomAccountActivityPredicate() throws Exception { |
| TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1)); |
| searcher1.setCallerShouldFilterOutInactiveCandidates(); |
| TestSearcher searcher2 = new TestSearcher("f.*", false, newInactiveAccount(2)); |
| searcher2.setCallerShouldFilterOutInactiveCandidates(); |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2); |
| |
| Result result = search("foo", searchers, allVisible(), (a) -> true); |
| // Searchers always short-circuit when finding a non-empty result list, |
| // and this one didn't filter out inactive results, |
| // so the second searcher never ran. |
| assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1)); |
| assertThat(getOnlyElement(result.asList()).account().isActive()).isFalse(); |
| assertThat(filteredInactiveIds(result)).isEmpty(); |
| } |
| |
| @Test |
| public void filterInactiveEventuallyFindingResults() throws Exception { |
| TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1)); |
| searcher1.setCallerShouldFilterOutInactiveCandidates(); |
| TestSearcher searcher2 = new TestSearcher("f.*", false, newAccount(2)); |
| searcher2.setCallerShouldFilterOutInactiveCandidates(); |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2); |
| |
| Result result = search("foo", searchers, allVisible()); |
| assertThat(search("foo", searchers, allVisible()).asIdSet()).containsExactlyElementsIn(ids(2)); |
| // No info about inactive results exposed if there was at least one active result. |
| assertThat(filteredInactiveIds(result)).isEmpty(); |
| } |
| |
| @Test |
| public void filterInactiveEventuallyFindingNoResults() throws Exception { |
| TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1)); |
| searcher1.setCallerShouldFilterOutInactiveCandidates(); |
| TestSearcher searcher2 = new TestSearcher("f.*", false, newInactiveAccount(2)); |
| searcher2.setCallerShouldFilterOutInactiveCandidates(); |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2); |
| |
| Result result = search("foo", searchers, allVisible()); |
| assertThat(result.asIdSet()).isEmpty(); |
| assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(1, 2)); |
| } |
| |
| @Test |
| public void dontShortCircuitAfterFilteringInactiveCandidatesResultsInEmptyList() |
| throws Exception { |
| AccountState account1 = newAccount(1); |
| AccountState account2 = newInactiveAccount(2); |
| TestSearcher searcher1 = new TestSearcher("foo", false, account2); |
| searcher1.setCallerShouldFilterOutInactiveCandidates(); |
| |
| TestSearcher searcher2 = new TestSearcher("foo", false, account1, account2); |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2); |
| |
| // searcher1 matched, but filtered out all candidates because account2 is inactive. Actual |
| // result came from searcher2 instead. |
| Result result = search("foo", searchers, allVisible()); |
| assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1, 2)); |
| } |
| |
| @Test |
| public void shortCircuitAfterFilteringInactiveCandidatesResultsInEmptyList() throws Exception { |
| AccountState account1 = newAccount(1); |
| AccountState account2 = newInactiveAccount(2); |
| TestSearcher searcher1 = new TestSearcher("foo", true, account2); |
| searcher1.setCallerShouldFilterOutInactiveCandidates(); |
| |
| TestSearcher searcher2 = new TestSearcher("foo", false, account1, account2); |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2); |
| |
| // searcher1 matched and then filtered out all candidates because account2 is inactive, but |
| // still short-circuited. |
| Result result = search("foo", searchers, allVisible()); |
| assertThat(result.asIdSet()).isEmpty(); |
| assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(2)); |
| } |
| |
| @Test |
| public void asUniqueWithNoResults() throws Exception { |
| String input = "foo"; |
| ImmutableList<Searcher<?>> searchers = ImmutableList.of(); |
| Supplier<Predicate<AccountState>> visibilitySupplier = allVisible(); |
| UnresolvableAccountException thrown = |
| assertThrows( |
| UnresolvableAccountException.class, |
| () -> search(input, searchers, visibilitySupplier).asUnique()); |
| assertThat(thrown).hasMessageThat().isEqualTo("Account 'foo' not found"); |
| } |
| |
| @Test |
| public void asUniqueWithOneResult() throws Exception { |
| AccountState account = newAccount(1); |
| ImmutableList<Searcher<?>> searchers = |
| ImmutableList.of(new TestSearcher("foo", false, account)); |
| assertThat(search("foo", searchers, allVisible()).asUnique().account().id()) |
| .isEqualTo(account.account().id()); |
| } |
| |
| @Test |
| public void asUniqueWithMultipleResults() throws Exception { |
| ImmutableList<Searcher<?>> searchers = |
| ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2))); |
| UnresolvableAccountException thrown = |
| assertThrows( |
| UnresolvableAccountException.class, |
| () -> search("foo", searchers, allVisible()).asUnique()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo( |
| "Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n2: Anonymous Name (2)"); |
| } |
| |
| @Test |
| public void exceptionMessageNotFound() throws Exception { |
| AccountResolver resolver = newAccountResolver(); |
| assertThat( |
| new UnresolvableAccountException( |
| resolver.new Result("foo", ImmutableList.of(), ImmutableList.of()))) |
| .hasMessageThat() |
| .isEqualTo("Account 'foo' not found"); |
| } |
| |
| @Test |
| public void exceptionMessageSelf() throws Exception { |
| AccountResolver resolver = newAccountResolver(); |
| UnresolvableAccountException e = |
| new UnresolvableAccountException( |
| resolver.new Result("self", ImmutableList.of(), ImmutableList.of())); |
| assertThat(e.isSelf()).isTrue(); |
| assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login"); |
| } |
| |
| @Test |
| public void exceptionMessageMe() throws Exception { |
| AccountResolver resolver = newAccountResolver(); |
| UnresolvableAccountException e = |
| new UnresolvableAccountException( |
| resolver.new Result("me", ImmutableList.of(), ImmutableList.of())); |
| assertThat(e.isSelf()).isTrue(); |
| assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login"); |
| } |
| |
| @Test |
| public void exceptionMessageAmbiguous() throws Exception { |
| AccountResolver resolver = newAccountResolver(); |
| assertThat( |
| new UnresolvableAccountException( |
| resolver |
| .new Result( |
| "foo", ImmutableList.of(newAccount(3), newAccount(1)), ImmutableList.of()))) |
| .hasMessageThat() |
| .isEqualTo( |
| "Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n3: Anonymous Name (3)"); |
| } |
| |
| @Test |
| public void exceptionMessageOnlyInactive() throws Exception { |
| AccountResolver resolver = newAccountResolver(); |
| assertThat( |
| new UnresolvableAccountException( |
| resolver |
| .new Result( |
| "foo", |
| ImmutableList.of(), |
| ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1))))) |
| .hasMessageThat() |
| .isEqualTo( |
| "Account 'foo' only matches inactive accounts. To use an inactive account, retry" |
| + " with one of the following exact account IDs:\n" |
| + "1: Anonymous Name (1)\n" |
| + "3: Anonymous Name (3)"); |
| } |
| |
| private Result search( |
| String input, |
| ImmutableList<Searcher<?>> searchers, |
| Supplier<Predicate<AccountState>> visibilitySupplier) |
| throws Exception { |
| return search(input, searchers, visibilitySupplier, activityPrediate()); |
| } |
| |
| private Result search( |
| String input, |
| ImmutableList<Searcher<?>> searchers, |
| Supplier<Predicate<AccountState>> visibilitySupplier, |
| Predicate<AccountState> activityPredicate) |
| throws Exception { |
| return newAccountResolver().searchImpl(input, searchers, visibilitySupplier, activityPredicate); |
| } |
| |
| private static AccountResolver newAccountResolver() { |
| return new AccountResolver(null, null, null, null, null, null, null, null, "Anonymous Name"); |
| } |
| |
| private AccountState newAccount(int id) { |
| return AccountState.forAccount( |
| Account.builder(Account.id(id), TimeUtil.nowTs()) |
| .setMetaId("1234567812345678123456781234567812345678") |
| .build()); |
| } |
| |
| private AccountState newInactiveAccount(int id) { |
| Account.Builder a = Account.builder(Account.id(id), TimeUtil.nowTs()); |
| a.setActive(false); |
| return AccountState.forAccount(a.build()); |
| } |
| |
| private static ImmutableSet<Account.Id> ids(int... ids) { |
| return Arrays.stream(ids).mapToObj(Account::id).collect(toImmutableSet()); |
| } |
| |
| private static Supplier<Predicate<AccountState>> allVisible() { |
| return () -> a -> true; |
| } |
| |
| private Predicate<AccountState> activityPrediate() { |
| return (AccountState accountState) -> accountState.account().isActive(); |
| } |
| |
| private static Supplier<Predicate<AccountState>> only(int... ids) { |
| ImmutableSet<Account.Id> idSet = |
| Arrays.stream(ids).mapToObj(Account::id).collect(toImmutableSet()); |
| return () -> a -> idSet.contains(a.account().id()); |
| } |
| |
| private static ImmutableSet<Account.Id> filteredInactiveIds(Result result) { |
| return result.filteredInactive().stream().map(a -> a.account().id()).collect(toImmutableSet()); |
| } |
| } |