RequestScopeOperations: Implement additional setter methods

The implementations of scope setting methods in AbstractDaemonTest now
delegate directly to RequestScopeOperations, and can be inlined in a
future change.

Change-Id: I50a4096cb51a671ce5be89be32334b0b0094b350
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 699b2f9..46672d3 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
@@ -82,7 +83,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -277,17 +277,17 @@
   protected boolean testRequiresSsh;
   protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
 
-  @Inject private ChangeIndexCollection changeIndexes;
-  @Inject private AccountIndexCollection accountIndexes;
-  @Inject private ProjectIndexCollection projectIndexes;
-  @Inject private EventRecorder.Factory eventRecorderFactory;
-  @Inject private InProcessProtocol inProcessProtocol;
-  @Inject private Provider<AnonymousUser> anonymousUser;
-  @Inject private AccountIndexer accountIndexer;
-  @Inject private Groups groups;
-  @Inject private GroupIndexer groupIndexer;
   @Inject private AbstractChangeNotes.Args changeNotesArgs;
+  @Inject private AccountIndexCollection accountIndexes;
+  @Inject private AccountIndexer accountIndexer;
+  @Inject private ChangeIndexCollection changeIndexes;
+  @Inject private EventRecorder.Factory eventRecorderFactory;
+  @Inject private GroupIndexer groupIndexer;
+  @Inject private Groups groups;
+  @Inject private InProcessProtocol inProcessProtocol;
+  @Inject private ProjectIndexCollection projectIndexes;
   @Inject private ProjectOperations projectOperations;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private ProjectResetter resetter;
   private List<Repository> toClose;
@@ -785,8 +785,8 @@
   }
 
   private Context newRequestContext(TestAccount account) {
-    return atrScope.newContext(
-        new SshSession(sshKeys, server, account), identifiedUserFactory.create(account.getId()));
+    requestScopeOperations.setApiUser(account.getId());
+    return atrScope.get();
   }
 
   /**
@@ -796,15 +796,15 @@
    * reloaded (e.g. the email addresses of the user).
    */
   protected Context resetCurrentApiUser() {
-    return atrScope.set(newRequestContext(atrScope.get().getSession().getAccount()));
+    return requestScopeOperations.resetCurrentApiUser();
   }
 
   protected Context setApiUser(TestAccount account) {
-    return atrScope.set(newRequestContext(account));
+    return requestScopeOperations.setApiUser(account.getId());
   }
 
   protected Context setApiUserAnonymous() {
-    return atrScope.set(atrScope.newContext(null, anonymousUser.get()));
+    return requestScopeOperations.setApiUserAnonymous();
   }
 
   protected Account getAccount(Account.Id accountId) {
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 5abb0c0..8420ff2 100644
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -55,7 +55,7 @@
       finished = p.finished;
     }
 
-    SshSession getSession() {
+    public SshSession getSession() {
       return session;
     }
 
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index e7017b8..f829842 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.Objects.requireNonNull;
 import static org.apache.log4j.Logger.getLogger;
 
@@ -50,10 +51,14 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.SshMode;
 import com.google.inject.AbstractModule;
+import com.google.inject.BindingAnnotation;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -89,6 +94,11 @@
     }
   }
 
+  /** Marker on {@link InetSocketAddress} for test SSH server. */
+  @Retention(RUNTIME)
+  @BindingAnnotation
+  public @interface TestSshServerAddress {}
+
   @AutoValue
   public abstract static class Description {
     public static Description forTestClass(
@@ -511,6 +521,18 @@
             install(new AsyncReceiveCommits.Module());
             factory(ProjectResetter.Builder.Factory.class);
           }
+
+          @Provides
+          @Singleton
+          @Nullable
+          @TestSshServerAddress
+          InetSocketAddress getSshAddress(@GerritServerConfig Config cfg) {
+            String addr = cfg.getString("sshd", null, "listenAddress");
+            // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
+            return !"off".equalsIgnoreCase(addr)
+                ? SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0)
+                : null;
+          }
         };
     return sysInjector.createChildInjector(module);
   }
@@ -535,7 +557,6 @@
   private ExecutorService daemonService;
   private Injector testInjector;
   private String url;
-  private InetSocketAddress sshdAddress;
   private InetSocketAddress httpAddress;
 
   private GerritServer(
@@ -553,12 +574,6 @@
     Config cfg = testInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     url = cfg.getString("gerrit", null, "canonicalWebUrl");
     URI uri = URI.create(url);
-
-    String addr = cfg.getString("sshd", null, "listenAddress");
-    // We do not use InitSshd.isOff to avoid coupling GerritServer to the SSH code.
-    if (!"off".equalsIgnoreCase(addr)) {
-      sshdAddress = SocketUtil.resolve(cfg.getString("sshd", null, "listenAddress"), 0);
-    }
     httpAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
   }
 
@@ -566,10 +581,6 @@
     return url;
   }
 
-  InetSocketAddress getSshdAddress() {
-    return sshdAddress;
-  }
-
   InetSocketAddress getHttpAddress() {
     return httpAddress;
   }
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 52d7f28..fa0bc90 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.jcraft.jsch.ChannelExec;
 import com.jcraft.jsch.JSch;
@@ -35,9 +36,9 @@
   private Session session;
   private String error;
 
-  public SshSession(TestSshKeys sshKeys, GerritServer server, TestAccount account) {
+  public SshSession(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
     this.sshKeys = sshKeys;
-    this.addr = server.getSshdAddress();
+    this.addr = addr;
     this.account = account;
   }
 
@@ -112,8 +113,14 @@
       JSch jsch = new JSch();
       jsch.addIdentity(
           "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
-      session =
-          jsch.getSession(account.username, addr.getAddress().getHostAddress(), addr.getPort());
+      String username =
+          account
+              .username()
+              .orElseThrow(
+                  () ->
+                      new IllegalStateException(
+                          "account " + account.accountId() + " must have a username to use SSH"));
+      session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
       session.setConfig("StrictHostKeyChecking", "no");
       session.connect();
     }
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
index 578b895..a63d28a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.testsuite.request;
 
-import static java.util.Objects.requireNonNull;
-
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.reviewdb.client.Account;
@@ -29,7 +27,10 @@
   /**
    * Sets the Guice request scope to the given account.
    *
-   * <p>The resulting scope has no SSH session attached.
+   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
+   * by {@link AcceptanceTestRequestScope.Context#getSession()}, SSH must be enabled in the test and
+   * the account must have a username set. However, these are not requirements simply to call this
+   * method.
    *
    * @param accountId account ID. Must exist; throws an unchecked exception otherwise.
    * @return the previous request scope.
@@ -39,12 +40,32 @@
   /**
    * Sets the Guice request scope to the given account.
    *
-   * <p>The resulting scope has no SSH session attached.
+   * <p>The resulting context has an SSH session attached. In order to use the SSH session returned
+   * by {@link AcceptanceTestRequestScope.Context#getSession()}, SSH must be enabled in the test and
+   * the account must have a username set. However, these are not requirements simply to call this
+   * method.
    *
    * @param testAccount test account from {@code AccountOperations}.
    * @return the previous request scope.
    */
-  default AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
-    return setApiUser(requireNonNull(testAccount).accountId());
-  }
+  AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount);
+
+  /**
+   * Enforces a new request context for the current API user.
+   *
+   * <p>This recreates the {@code IdentifiedUser}, hence everything which is cached in the {@code
+   * IdentifiedUser} is reloaded (e.g. the email addresses of the user).
+   *
+   * <p>The current user must be an identified user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context resetCurrentApiUser();
+
+  /**
+   * Sets the Guice request scope to the anonymous user.
+   *
+   * @return the previous request scope.
+   */
+  AcceptanceTestRequestScope.Context setApiUserAnonymous();
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index d60700a..27b71b9 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -14,16 +14,26 @@
 
 package com.google.gerrit.acceptance.testsuite.request;
 
+import static com.google.common.base.Preconditions.checkState;
 import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountState;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.net.InetSocketAddress;
 
 /**
  * The implementation of {@code RequestScopeOperations}.
@@ -35,25 +45,61 @@
 public class RequestScopeOperationsImpl implements RequestScopeOperations {
   private final AcceptanceTestRequestScope atrScope;
   private final AccountCache accountCache;
+  private final AccountOperations accountOperations;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<AnonymousUser> anonymousUserProvider;
+  private final InetSocketAddress sshAddress;
+  private final TestSshKeys testSshKeys;
 
   @Inject
   RequestScopeOperationsImpl(
       AcceptanceTestRequestScope atrScope,
       AccountCache accountCache,
-      IdentifiedUser.GenericFactory userFactory) {
+      AccountOperations accountOperations,
+      GenericFactory userFactory,
+      Provider<AnonymousUser> anonymousUserProvider,
+      @Nullable @TestSshServerAddress InetSocketAddress sshAddress,
+      TestSshKeys testSshKeys) {
     this.atrScope = atrScope;
     this.accountCache = accountCache;
+    this.accountOperations = accountOperations;
     this.userFactory = userFactory;
+    this.anonymousUserProvider = anonymousUserProvider;
+    this.sshAddress = sshAddress;
+    this.testSshKeys = testSshKeys;
   }
 
   @Override
-  public Context setApiUser(Account.Id accountId) {
-    AccountState accountState =
+  public AcceptanceTestRequestScope.Context setApiUser(Account.Id accountId) {
+    return setApiUser(accountOperations.account(accountId).get());
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
+    return atrScope.set(
+        atrScope.newContext(
+            new SshSession(testSshKeys, sshAddress, testAccount),
+            createIdentifiedUser(testAccount.accountId())));
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context resetCurrentApiUser() {
+    CurrentUser user = atrScope.get().getUser();
+    // More special cases for anonymous users etc. can be added as needed.
+    checkState(user.isIdentifiedUser(), "can only reset IdentifiedUser, not %s", user);
+    return setApiUser(user.getAccountId());
+  }
+
+  @Override
+  public AcceptanceTestRequestScope.Context setApiUserAnonymous() {
+    return atrScope.set(atrScope.newContext(null, anonymousUserProvider.get()));
+  }
+
+  private IdentifiedUser createIdentifiedUser(Account.Id accountId) {
+    return userFactory.create(
         accountCache
             .get(requireNonNull(accountId))
             .orElseThrow(
-                () -> new IllegalArgumentException("account does not exist: " + accountId));
-    return atrScope.set(atrScope.newContext(null, userFactory.create(accountState)));
+                () -> new IllegalArgumentException("account does not exist: " + accountId)));
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
index a22b57d..9b498011 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImplTest.java
@@ -16,63 +16,100 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth8.assertThat;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.UseSsh;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.CurrentUser.PropertyKey;
 import com.google.gerrit.server.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import org.junit.Test;
 
+@UseSsh
 public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
+  private static final AtomicInteger changeCounter = new AtomicInteger();
+
   @Inject private AccountOperations accountOperations;
-  @Inject private Provider<IdentifiedUser> userProvider;
+  @Inject private Provider<CurrentUser> userProvider;
   @Inject private RequestScopeOperationsImpl requestScopeOperations;
   @Inject private Sequences sequences;
 
   @Test
   public void setApiUserToExistingUserById() throws Exception {
-    checkApiUser(admin.getId());
+    fastCheckCurrentUser(admin.getId());
     AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(user.getId());
     assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
-    checkApiUser(user.getId());
+    checkCurrentUser(user.getId());
   }
 
   @Test
   public void setApiUserToExistingUserByTestAccount() throws Exception {
-    checkApiUser(admin.getId());
+    fastCheckCurrentUser(admin.getId());
     TestAccount testAccount =
-        accountOperations.account(accountOperations.newAccount().create()).get();
+        accountOperations.account(accountOperations.newAccount().username("tester").create()).get();
     AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.setApiUser(testAccount);
     assertThat(oldCtx.getUser().getAccountId()).isEqualTo(admin.getId());
-    checkApiUser(testAccount.accountId());
+    checkCurrentUser(testAccount.accountId());
   }
 
   @Test
   public void setApiUserToNonExistingUser() throws Exception {
-    checkApiUser(admin.getId());
+    fastCheckCurrentUser(admin.getId());
     try {
       requestScopeOperations.setApiUser(new Account.Id(sequences.nextAccountId()));
       assert_().fail("expected RuntimeException");
     } catch (RuntimeException e) {
       // Expected.
     }
-    checkApiUser(admin.getId());
+    checkCurrentUser(admin.getId());
   }
 
-  private void checkApiUser(Account.Id expected) throws Exception {
-    // Test all supported ways that an acceptance test might query the active user.
-    assertThat(gApi.accounts().self().get()._accountId)
-        .named("user from GerritApi")
-        .isEqualTo(expected.get());
+  @Test
+  public void resetCurrentApiUserClearsCachedState() throws Exception {
+    requestScopeOperations.setApiUser(user.getId());
+    PropertyKey<String> key = PropertyKey.create();
+    atrScope.get().getUser().put(key, "foo");
+    assertThat(atrScope.get().getUser().get(key)).hasValue("foo");
+
+    AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.resetCurrentApiUser();
+    checkCurrentUser(user.getId());
+    assertThat(atrScope.get().getUser().get(key)).isEmpty();
+    assertThat(oldCtx.getUser().get(key)).hasValue("foo");
+  }
+
+  @Test
+  public void setApiUserAnonymousSetsAnonymousUser() throws Exception {
+    fastCheckCurrentUser(admin.getId());
+    requestScopeOperations.setApiUserAnonymous();
+    assertThat(userProvider.get()).isInstanceOf(AnonymousUser.class);
+  }
+
+  private void fastCheckCurrentUser(Account.Id expected) {
+    // Check current user quickly, since the full check requires creating changes and is quite slow.
     assertThat(userProvider.get().isIdentifiedUser())
         .named("user from provider is an IdentifiedUser")
         .isTrue();
     assertThat(userProvider.get().getAccountId()).named("user from provider").isEqualTo(expected);
+  }
+
+  private void checkCurrentUser(Account.Id expected) throws Exception {
+    // Test all supported ways that an acceptance test might query the active user.
+    fastCheckCurrentUser(expected);
+    assertThat(gApi.accounts().self().get()._accountId)
+        .named("user from GerritApi")
+        .isEqualTo(expected.get());
     AcceptanceTestRequestScope.Context ctx = atrScope.get();
     assertThat(ctx.getUser().isIdentifiedUser())
         .named("user from AcceptanceTestRequestScope.Context is an IdentifiedUser")
@@ -80,5 +117,31 @@
     assertThat(ctx.getUser().getAccountId())
         .named("user from AcceptanceTestRequestScope.Context")
         .isEqualTo(expected);
+    checkSshUser(expected);
+  }
+
+  private void checkSshUser(Account.Id expected) throws Exception {
+    // No "gerrit whoami" command, so the simplest way to check who the user is over SSH is to query
+    // for owner:self.
+    ChangeInput cin = new ChangeInput();
+    cin.project = project.get();
+    cin.branch = "master";
+    cin.subject = "Test change " + changeCounter.incrementAndGet();
+    String changeId = gApi.changes().create(cin).get().changeId;
+    assertThat(gApi.changes().id(changeId).get().owner._accountId).isEqualTo(expected.get());
+    String queryResults =
+        atrScope.get().getSession().exec("gerrit query owner:self change:" + changeId);
+    assertThat(findDistinct(queryResults, "I[0-9a-f]{40}"))
+        .named("Change-Ids in query results:\n%s", queryResults)
+        .containsExactly(changeId);
+  }
+
+  private static ImmutableSet<String> findDistinct(String input, String pattern) {
+    Matcher m = Pattern.compile(pattern).matcher(input);
+    ImmutableSet.Builder<String> b = ImmutableSet.builder();
+    while (m.find()) {
+      b.add(m.group(0));
+    }
+    return b.build();
   }
 }