Allow the rendering of changes imported from other GerritServerIds

Allowing Gerrit to read, index and render the change with NoteDb data
comint from another server with a different serverId.
Add a new test suite for validating the scenario where a Gerrit
setup has two changes coming from different serverIds.

LIMITATION: The imported changes need to have the same or a subset
of the account ids of the hosting Gerrit server, otherwise the review
identities would not match.

Release-Notes: Allow importing changes with different serverIds
Forward-Compatible: checked
Change-Id: I42bac21a746b3935ad6e30daf86442bf6d4cc5d6
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 234389e..365a26e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2543,6 +2543,21 @@
 by the target version of the upgrade. Refer to the release notes and check whether
 the rolling upgrade is possible or not and the associated constraints.
 
+[[gerrit.importedServerId]]gerrit.importedServerId::
++
+ServerId of the repositories imported from other Gerrit servers. Changes coming
+associated with the imported serverIds are indexed and displayed in the UI.
++
+Specify multiple `gerrit.importedServerId` for allowing the import from multiple
+Gerrit servers with different serverIds. The servers associated with the imported
+serverIds need to have the same or a subset of the account Ids of the hosting
+Gerrit server.
++
+If this value is not set, all changes imported from other Gerrit servers will be
+ignored.
++
+By default empty.
+
 [[gerrit.serverId]]gerrit.serverId::
 +
 Used by NoteDb to, amongst other things, identify author identities from
diff --git a/java/com/google/gerrit/server/config/GerritImportedServerIds.java b/java/com/google/gerrit/server/config/GerritImportedServerIds.java
new file mode 100644
index 0000000..c47d3be
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritImportedServerIds.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2022 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/**
+ * List of ServerIds of the Gerrit data imported from other servers.
+ *
+ * <p>This values correspond to the {@code GerritServerId} of other servers.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface GerritImportedServerIds {}
diff --git a/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java b/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
new file mode 100644
index 0000000..f3f7645
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2022 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.config;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.lib.Config;
+
+public class GerritImportedServerIdsProvider implements Provider<ImmutableSet<String>> {
+  public static final String SECTION = "gerrit";
+  public static final String KEY = "importedServerId";
+
+  private final ImmutableSet<String> importedIds;
+
+  @Inject
+  public GerritImportedServerIdsProvider(@GerritServerConfig Config cfg) {
+    importedIds = ImmutableSet.copyOf(cfg.getStringList(SECTION, null, KEY));
+  }
+
+  @Override
+  public ImmutableSet<String> get() {
+    return importedIds;
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 158972f..3757244 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -17,6 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Change;
@@ -24,6 +25,7 @@
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritImportedServerIds;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -51,6 +53,7 @@
     public final AllUsersName allUsers;
     public final NoteDbMetrics metrics;
     public final String serverId;
+    public final ImmutableSet<String> importedServerIds;
 
     // Providers required to avoid dependency cycles.
 
@@ -64,7 +67,8 @@
         ChangeNoteJson changeNoteJson,
         NoteDbMetrics metrics,
         Provider<ChangeNotesCache> cache,
-        @GerritServerId String serverId) {
+        @GerritServerId String serverId,
+        @GerritImportedServerIds ImmutableSet<String> importedServerIds) {
       this.failOnLoadForTest = new AtomicBoolean();
       this.repoManager = repoManager;
       this.allUsers = allUsers;
@@ -72,6 +76,7 @@
       this.metrics = metrics;
       this.cache = cache;
       this.serverId = serverId;
+      this.importedServerIds = importedServerIds;
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 26d5933..31934d5 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -406,6 +406,10 @@
     return state.metaId();
   }
 
+  public String getServerId() {
+    return state.serverId();
+  }
+
   public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
     if (patchSets == null) {
       ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
@@ -655,7 +659,9 @@
      * be to bump the cache version, but that would invalidate all persistent cache entries, what we
      * rather try to avoid.
      */
-    if (!Strings.isNullOrEmpty(stateServerId) && !args.serverId.equals(stateServerId)) {
+    if (!Strings.isNullOrEmpty(stateServerId)
+        && !args.serverId.equals(stateServerId)
+        && !args.importedServerIds.contains(stateServerId)) {
       throw new InvalidServerIdException(args.serverId, stateServerId);
     }
 
diff --git a/java/com/google/gerrit/server/schema/SchemaModule.java b/java/com/google/gerrit/server/schema/SchemaModule.java
index ff2073d..e0e64a3 100644
--- a/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -16,6 +16,7 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -25,9 +26,12 @@
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.GerritImportedServerIds;
+import com.google.gerrit.server.config.GerritImportedServerIdsProvider;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.config.GerritServerIdProvider;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.inject.TypeLiteral;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /** Bindings for low-level Gerrit schema data. */
@@ -51,6 +55,11 @@
         .toProvider(GerritServerIdProvider.class)
         .in(SINGLETON);
 
+    bind(new TypeLiteral<ImmutableSet<String>>() {})
+        .annotatedWith(GerritImportedServerIds.class)
+        .toProvider(GerritImportedServerIdsProvider.class)
+        .in(SINGLETON);
+
     // It feels wrong to have this binding in a seemingly unrelated module, but it's a dependency of
     // SchemaCreatorImpl, so it's needed.
     // TODO(dborowitz): Is there any way to untangle this?
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b00cadb..b1ab6b1 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -19,6 +19,7 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
@@ -61,6 +62,8 @@
 import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.FileBasedGlobalPluginConfigProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritImportedServerIds;
+import com.google.gerrit.server.config.GerritImportedServerIdsProvider;
 import com.google.gerrit.server.config.GerritInstanceIdModule;
 import com.google.gerrit.server.config.GerritInstanceNameModule;
 import com.google.gerrit.server.config.GerritOptions;
@@ -311,6 +314,17 @@
 
   @Provides
   @Singleton
+  @GerritImportedServerIds
+  public ImmutableSet<String> createImportedServerIds() {
+    ImmutableSet<String> serverIds =
+        ImmutableSet.copyOf(
+            cfg.getStringList(
+                GerritServerIdProvider.SECTION, null, GerritImportedServerIdsProvider.KEY));
+    return serverIds;
+  }
+
+  @Provides
+  @Singleton
   @SendEmailExecutor
   public ExecutorService createSendEmailExecutor() {
     return newDirectExecutorService();
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index d7c779e..d17c3cc 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -18,6 +18,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
@@ -48,11 +49,13 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.DefaultUrlFormatter.DefaultUrlFormatterModule;
 import com.google.gerrit.server.config.EnablePeerIPInReflogRecord;
+import com.google.gerrit.server.config.GerritImportedServerIds;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.NullProjectCache;
 import com.google.gerrit.server.project.ProjectCache;
@@ -67,9 +70,11 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
+import com.google.inject.TypeLiteral;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.util.concurrent.ExecutorService;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -84,10 +89,13 @@
 @Ignore
 @RunWith(ConfigSuite.class)
 public abstract class AbstractChangeNotesTest {
+  protected static final String LOCAL_SERVER_ID = "gerrit";
+
   private static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
 
   @ConfigSuite.Parameter public Config testConfig;
 
+  protected Account.Id changeOwnerId;
   protected Account.Id otherUserId;
   protected FakeAccountCache accountCache;
   protected IdentifiedUser changeOwner;
@@ -116,6 +124,14 @@
 
   @Before
   public void setUpTestEnvironment() throws Exception {
+    setupTestPrerequisites();
+
+    injector = createTestInjector(LOCAL_SERVER_ID);
+    createAllUsers(injector);
+    injector.injectMembers(this);
+  }
+
+  protected void setupTestPrerequisites() throws Exception {
     setTimeForTesting();
 
     serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.now(), ZONE_ID);
@@ -134,60 +150,71 @@
     ou.setPreferredEmail("other@account.com");
     accountCache.put(ou.build());
     assertableFanOutExecutor = new AssertableExecutorService();
-
-    injector =
-        Guice.createInjector(
-            new FactoryModule() {
-              @Override
-              public void configure() {
-                install(new GitModule());
-
-                install(new DefaultUrlFormatterModule());
-                install(NoteDbModule.forTest());
-                bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
-                bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
-                bind(GitRepositoryManager.class).toInstance(repoManager);
-                bind(ProjectCache.class).to(NullProjectCache.class);
-                bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
-                bind(String.class)
-                    .annotatedWith(AnonymousCowardName.class)
-                    .toProvider(AnonymousCowardNameProvider.class);
-                bind(String.class)
-                    .annotatedWith(CanonicalWebUrl.class)
-                    .toInstance("http://localhost:8080/");
-                bind(Boolean.class)
-                    .annotatedWith(EnablePeerIPInReflogRecord.class)
-                    .toInstance(Boolean.FALSE);
-                bind(Realm.class).to(FakeRealm.class);
-                bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
-                bind(AccountCache.class).toInstance(accountCache);
-                bind(PersonIdent.class)
-                    .annotatedWith(GerritPersonIdent.class)
-                    .toInstance(serverIdent);
-                bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
-                bind(MetricMaker.class).to(DisabledMetricMaker.class);
-                bind(ExecutorService.class)
-                    .annotatedWith(FanOutExecutor.class)
-                    .toInstance(assertableFanOutExecutor);
-                bind(ServiceUserClassifier.class).to(ServiceUserClassifier.NoOp.class);
-                bind(InternalChangeQuery.class)
-                    .toProvider(
-                        () -> {
-                          throw new UnsupportedOperationException();
-                        });
-                bind(PatchSetApprovalUuidGenerator.class)
-                    .to(TestPatchSetApprovalUuidGenerator.class);
-              }
-            });
-
-    injector.injectMembers(this);
-    repoManager.createRepository(allUsers);
-    changeOwner = userFactory.create(co.id());
-    otherUser = userFactory.create(ou.id());
-    otherUserId = otherUser.getAccountId();
+    changeOwnerId = co.id();
+    otherUserId = ou.id();
     internalUser = new InternalUser();
   }
 
+  protected Injector createTestInjector(String serverId, String... importedServerIds)
+      throws Exception {
+
+    return Guice.createInjector(
+        new FactoryModule() {
+          @Override
+          public void configure() {
+            install(new GitModule());
+
+            install(new DefaultUrlFormatterModule());
+            install(NoteDbModule.forTest());
+            bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
+            bind(String.class).annotatedWith(GerritServerId.class).toInstance(serverId);
+            bind(new TypeLiteral<ImmutableSet<String>>() {})
+                .annotatedWith(GerritImportedServerIds.class)
+                .toInstance(new ImmutableSet.Builder<String>().add(importedServerIds).build());
+            bind(GitRepositoryManager.class).toInstance(repoManager);
+            bind(ProjectCache.class).to(NullProjectCache.class);
+            bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
+            bind(String.class)
+                .annotatedWith(AnonymousCowardName.class)
+                .toProvider(AnonymousCowardNameProvider.class);
+            bind(String.class)
+                .annotatedWith(CanonicalWebUrl.class)
+                .toInstance("http://localhost:8080/");
+            bind(Boolean.class)
+                .annotatedWith(EnablePeerIPInReflogRecord.class)
+                .toInstance(Boolean.FALSE);
+            bind(Realm.class).to(FakeRealm.class);
+            bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+            bind(AccountCache.class).toInstance(accountCache);
+            bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class).toInstance(serverIdent);
+            bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
+            bind(MetricMaker.class).to(DisabledMetricMaker.class);
+            bind(ExecutorService.class)
+                .annotatedWith(FanOutExecutor.class)
+                .toInstance(assertableFanOutExecutor);
+            bind(ServiceUserClassifier.class).to(ServiceUserClassifier.NoOp.class);
+            bind(InternalChangeQuery.class)
+                .toProvider(
+                    () -> {
+                      throw new UnsupportedOperationException();
+                    });
+            bind(PatchSetApprovalUuidGenerator.class).to(TestPatchSetApprovalUuidGenerator.class);
+          }
+        });
+  }
+
+  protected void createAllUsers(Injector injector)
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException {
+    AllUsersName allUsersName = injector.getInstance(AllUsersName.class);
+
+    repoManager.createRepository(allUsersName);
+
+    IdentifiedUser.GenericFactory identifiedUserFactory =
+        injector.getInstance(IdentifiedUser.GenericFactory.class);
+    changeOwner = identifiedUserFactory.create(changeOwnerId);
+    otherUser = identifiedUserFactory.create(otherUserId);
+  }
+
   private void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -200,8 +227,12 @@
   }
 
   protected Change newChange(boolean workInProgress) throws Exception {
+    return newChange(injector, workInProgress);
+  }
+
+  protected Change newChange(Injector injector, boolean workInProgress) throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
-    ChangeUpdate u = newUpdateForNewChange(c, changeOwner);
+    ChangeUpdate u = newUpdate(injector, c, changeOwner, false);
     u.setChangeId(c.getKey().get());
     u.setBranch(c.getDest().branch());
     u.setWorkInProgress(workInProgress);
@@ -218,15 +249,20 @@
   }
 
   protected ChangeUpdate newUpdateForNewChange(Change c, CurrentUser user) throws Exception {
-    return newUpdate(c, user, false);
+    return newUpdate(injector, c, user, false);
   }
 
   protected ChangeUpdate newUpdate(Change c, CurrentUser user) throws Exception {
-    return newUpdate(c, user, true);
+    return newUpdate(injector, c, user, true);
   }
 
   protected ChangeUpdate newUpdate(Change c, CurrentUser user, boolean shouldExist)
       throws Exception {
+    return newUpdate(injector, c, user, shouldExist);
+  }
+
+  protected ChangeUpdate newUpdate(
+      Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
     ChangeUpdate update = TestChanges.newUpdate(injector, c, user, shouldExist);
     update.setPatchSetId(c.currentPatchSetId());
     update.setAllowWriteToNewRef(true);
@@ -237,6 +273,10 @@
     return new ChangeNotes(args, c, true, null).load();
   }
 
+  protected ChangeNotes newNotes(AbstractChangeNotes.Args cArgs, Change c) {
+    return new ChangeNotes(cArgs, c, true, null).load();
+  }
+
   protected static SubmitRecord submitRecord(
       String status, String errorMessage, SubmitRecord.Label... labels) {
     SubmitRecord rec = new SubmitRecord();
diff --git a/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
new file mode 100644
index 0000000..1eb08ce
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2022 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.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ImportedChangeNotesTest extends AbstractChangeNotesTest {
+
+  private static final String FOREIGN_SERVER_ID = "foreign-server-id";
+  private static final String IMPORTED_SERVER_ID = "gerrit-imported-1";
+
+  @Before
+  @Override
+  public void setUpTestEnvironment() throws Exception {
+    setupTestPrerequisites();
+  }
+
+  private void initServerIds(String serverId, String... importedServerIds)
+      throws Exception, RepositoryCaseMismatchException, RepositoryNotFoundException {
+    injector = createTestInjector(serverId, importedServerIds);
+    injector.injectMembers(this);
+    createAllUsers(injector);
+  }
+
+  @Test
+  public void allowChangeFromImportedServerId() throws Exception {
+    initServerIds(LOCAL_SERVER_ID, IMPORTED_SERVER_ID);
+
+    Change importedChange = newChange(createTestInjector(IMPORTED_SERVER_ID), false);
+    Change localChange = newChange();
+
+    assertThat(newNotes(importedChange).getServerId()).isEqualTo(IMPORTED_SERVER_ID);
+    assertThat(newNotes(localChange).getServerId()).isEqualTo(LOCAL_SERVER_ID);
+  }
+
+  @Test
+  public void rejectChangeWithForeignServerId() throws Exception {
+    initServerIds(LOCAL_SERVER_ID);
+    Change foreignChange = newChange(createTestInjector(FOREIGN_SERVER_ID), false);
+
+    InvalidServerIdException invalidServerIdEx =
+        assertThrows(InvalidServerIdException.class, () -> newNotes(foreignChange));
+
+    String invalidServerIdMessage = invalidServerIdEx.getMessage();
+    assertThat(invalidServerIdMessage).contains("expected " + LOCAL_SERVER_ID);
+    assertThat(invalidServerIdMessage).contains("actual: " + FOREIGN_SERVER_ID);
+  }
+}