Gerrit 2.0.24.2

Change-Id: I6dea4945fdaa03b554e8e014715535d71777c23e
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/pom.xml b/pom.xml
index 910db5a..606826d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
   <groupId>gerrit</groupId>
   <artifactId>gerrit</artifactId>
   <packaging>war</packaging>
-  <version>2.0.24.1</version>
+  <version>2.0.24.2</version>
   <name>gerrit</name>
   <description>Gerrit - Web Based Code Review</description>
   <url>http://android.git.kernel.org/?p=tools/gerrit.git</url>
@@ -632,6 +632,13 @@
     </dependency>
 
     <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <version>1.2.122</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
       <groupId>org.easymock</groupId>
       <artifactId>easymock</artifactId>
       <version>2.5.1</version>
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/AccountGroup.java b/src/main/java/com/google/gerrit/client/reviewdb/AccountGroup.java
index 79a35a5..eb38ef2 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/AccountGroup.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/AccountGroup.java
@@ -105,13 +105,38 @@
   }
 
   public static enum Type {
-    /** System defined and managed group, e.g. anonymous users. */
+    /**
+     * System defined and managed group, e.g. anonymous users.
+     * <p>
+     * These groups must be explicitly named by {@link SystemConfig} and are
+     * specially handled throughout the code. In UI contexts their membership is
+     * not displayed. When computing effective group membership for any given
+     * user account, these groups are automatically handled using specialized
+     * branch conditions.
+     */
     SYSTEM,
 
-    /** Group defined within our database. */
+    /**
+     * Group defined within our database.
+     * <p>
+     * An internal group has its membership fully enumerated in the database.
+     * The membership can be viewed and edited through the web UI by any user
+     * who is a member of the owner group. These groups are not treated special
+     * in the code.
+     */
     INTERNAL,
 
-    /** Group defined by external LDAP database. */
+    /**
+     * Group defined by external LDAP database.
+     * <p>
+     * A group whose membership is determined by the LDAP directory that we
+     * connect to for user and group information. In UI contexts the membership
+     * of the group is not displayed, as it may be exceedingly large, or might
+     * contain users who have never logged into this server before (and thus
+     * have no matching account record). Adding or removing users from an LDAP
+     * group requires making edits through the LDAP directory, and cannot be
+     * done through our UI.
+     */
     LDAP;
   }
 
@@ -151,6 +176,7 @@
     name = newName;
     groupId = newId;
     ownerGroupId = groupId;
+    setType(Type.INTERNAL);
   }
 
   public AccountGroup.Id getId() {
diff --git a/src/main/java/com/google/gerrit/server/config/SystemConfigProvider.java b/src/main/java/com/google/gerrit/server/config/SystemConfigProvider.java
index 15c9ecb..ab5a293 100644
--- a/src/main/java/com/google/gerrit/server/config/SystemConfigProvider.java
+++ b/src/main/java/com/google/gerrit/server/config/SystemConfigProvider.java
@@ -38,13 +38,13 @@
 import java.util.List;
 
 /** Loads the {@link SystemConfig} from the database. */
-class SystemConfigProvider implements Provider<SystemConfig> {
+public class SystemConfigProvider implements Provider<SystemConfig> {
   private static final Project.NameKey DEFAULT_WILD_NAME =
       new Project.NameKey("-- All Projects --");
   private final SchemaFactory<ReviewDb> schema;
 
   @Inject
-  SystemConfigProvider(final SchemaFactory<ReviewDb> sf) {
+  public SystemConfigProvider(final SchemaFactory<ReviewDb> sf) {
     schema = sf;
   }
 
@@ -133,6 +133,7 @@
         new AccountGroup(new AccountGroup.NameKey("Administrators"),
             new AccountGroup.Id(c.nextAccountGroupId()));
     admin.setDescription("Gerrit Site Administrators");
+    admin.setType(AccountGroup.Type.INTERNAL);
     c.accountGroups().insert(Collections.singleton(admin));
 
     final AccountGroup anonymous =
@@ -148,7 +149,7 @@
             new AccountGroup.Id(c.nextAccountGroupId()));
     registered.setDescription("Any signed-in user");
     registered.setOwnerGroupId(admin.getId());
-    anonymous.setType(AccountGroup.Type.SYSTEM);
+    registered.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(registered));
 
     File sitePath = new File(".").getAbsoluteFile();
@@ -255,6 +256,14 @@
       final ProjectRight read =
           new ProjectRight(new ProjectRight.Key(DEFAULT_WILD_NAME, cat.getId(),
               sConfig.anonymousGroupId));
+      read.setMaxValue((short) 1);
+      read.setMinValue((short) 1);
+      c.projectRights().insert(Collections.singleton(read));
+    }
+    {
+      final ProjectRight read =
+          new ProjectRight(new ProjectRight.Key(DEFAULT_WILD_NAME, cat.getId(),
+              sConfig.registeredGroupId));
       read.setMaxValue((short) 2);
       read.setMinValue((short) 1);
       c.projectRights().insert(Collections.singleton(read));
diff --git a/src/test/java/com/google/gerrit/server/config/SystemConfigProviderTest.java b/src/test/java/com/google/gerrit/server/config/SystemConfigProviderTest.java
new file mode 100644
index 0000000..c1fce7b
--- /dev/null
+++ b/src/test/java/com/google/gerrit/server/config/SystemConfigProviderTest.java
@@ -0,0 +1,362 @@
+// Copyright (C) 2009 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.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.client.reviewdb.ApprovalCategory;
+import com.google.gerrit.client.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.client.reviewdb.Project;
+import com.google.gerrit.client.reviewdb.ProjectRight;
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.client.reviewdb.SchemaVersion;
+import com.google.gerrit.client.reviewdb.SystemConfig;
+import com.google.gerrit.server.workflow.NoOpFunction;
+import com.google.gerrit.server.workflow.SubmitFunction;
+import com.google.gerrit.testutil.TestDatabase;
+import com.google.gwtorm.client.OrmException;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.sql.SQLException;
+import java.util.HashSet;
+
+public class SystemConfigProviderTest extends TestCase {
+  private ApprovalCategory.Id codeReview = new ApprovalCategory.Id("CRVW");
+  private TestDatabase db;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    db = new TestDatabase();
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    TestDatabase.drop(db);
+    super.tearDown();
+  }
+
+  public void testGetCauses_CreateSchema() throws OrmException {
+    // Initially the schema should be empty.
+    //
+    try {
+      getSchemaVersion();
+      fail("Brand new test database has schema_version table");
+    } catch (OrmException e) {
+      final Throwable cause = e.getCause();
+      assertTrue(cause instanceof SQLException);
+
+      final String msg = cause.getMessage();
+      assertEquals("Table SCHEMA_VERSION not found", msg.split(";")[0]);
+    }
+
+    // Create the schema using the current schema version.
+    //
+    final SystemConfig config = getSystemConfig();
+    final SchemaVersion version = getSchemaVersion();
+    assertNotNull(version);
+    assertEquals(ReviewDb.VERSION, version.versionNbr);
+
+    assertNotNull(config);
+    assertNotNull(config.adminGroupId);
+    assertNotNull(config.anonymousGroupId);
+    assertNotNull(config.registeredGroupId);
+
+    // By default sitePath is set to the current working directory.
+    //
+    File sitePath = new File(".").getAbsoluteFile();
+    if (sitePath.getName().equals(".")) {
+      sitePath = sitePath.getParentFile();
+    }
+    assertEquals(sitePath.getAbsolutePath(), config.sitePath);
+
+    // This is randomly generated and should be at least 20 bytes long.
+    //
+    assertNotNull(config.registerEmailPrivateKey);
+    assertTrue(20 < config.registerEmailPrivateKey.length());
+  }
+
+  public void testSubsequentGetReads() {
+    final SystemConfig exp = getSystemConfig();
+    final SystemConfig act = getSystemConfig();
+
+    assertNotSame(exp, act);
+    assertEquals(exp.adminGroupId, act.adminGroupId);
+    assertEquals(exp.anonymousGroupId, act.anonymousGroupId);
+    assertEquals(exp.registeredGroupId, act.registeredGroupId);
+    assertEquals(exp.sitePath, act.sitePath);
+    assertEquals(exp.registerEmailPrivateKey, act.registerEmailPrivateKey);
+  }
+
+  public void testCreateSchema_Group_Administrators() throws OrmException {
+    final SystemConfig config = getSystemConfig();
+    final ReviewDb c = db.open();
+    try {
+      final AccountGroup admin = c.accountGroups().get(config.adminGroupId);
+      assertNotNull(admin);
+      assertEquals(config.adminGroupId, admin.getId());
+      assertEquals("Administrators", admin.getName());
+      assertSame(AccountGroup.Type.INTERNAL, admin.getType());
+    } finally {
+      c.close();
+    }
+  }
+
+  public void testCreateSchema_Group_AnonymousUsers() throws OrmException {
+    final SystemConfig config = getSystemConfig();
+    final ReviewDb c = db.open();
+    try {
+      final AccountGroup anon = c.accountGroups().get(config.anonymousGroupId);
+      assertNotNull(anon);
+      assertEquals(config.anonymousGroupId, anon.getId());
+      assertEquals("Anonymous Users", anon.getName());
+      assertSame(AccountGroup.Type.SYSTEM, anon.getType());
+    } finally {
+      c.close();
+    }
+  }
+
+  public void testCreateSchema_Group_RegisteredUsers() throws OrmException {
+    final SystemConfig config = getSystemConfig();
+    final ReviewDb c = db.open();
+    try {
+      final AccountGroup reg = c.accountGroups().get(config.registeredGroupId);
+      assertNotNull(reg);
+      assertEquals(config.registeredGroupId, reg.getId());
+      assertEquals("Registered Users", reg.getName());
+      assertSame(AccountGroup.Type.SYSTEM, reg.getType());
+    } finally {
+      c.close();
+    }
+  }
+
+  public void testCreateSchema_WildCardProject() throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final Project all;
+
+      all = c.projects().get(WildProjectNameProvider.WILD_PROJECT_ID);
+      assertNotNull(all);
+      assertEquals("-- All Projects --", all.getName());
+      assertEquals(new Project.Id(0), all.getId());
+      assertFalse(all.isUseContributorAgreements());
+      assertFalse(all.isUseSignedOffBy());
+    } finally {
+      c.close();
+    }
+  }
+
+  public void testCreateSchema_ApprovalCategory_CodeReview()
+      throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final ApprovalCategory cat;
+
+      cat = c.approvalCategories().get(codeReview);
+      assertNotNull(cat);
+      assertEquals(codeReview, cat.getId());
+      assertEquals("Code Review", cat.getName());
+      assertEquals("R", cat.getAbbreviatedName());
+      assertEquals("MaxWithBlock", cat.getFunctionName());
+      assertTrue(cat.isCopyMinScore());
+      assertFalse(cat.isAction());
+      assertTrue(0 <= cat.getPosition());
+    } finally {
+      c.close();
+    }
+    assertValueRange(codeReview, -2, -1, 0, 1, 2);
+  }
+
+  public void testCreateSchema_ApprovalCategory_Read() throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final ApprovalCategory cat;
+
+      cat = c.approvalCategories().get(ApprovalCategory.READ);
+      assertNotNull(cat);
+      assertEquals(ApprovalCategory.READ, cat.getId());
+      assertEquals("Read Access", cat.getName());
+      assertNull(cat.getAbbreviatedName());
+      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
+      assertTrue(cat.isAction());
+    } finally {
+      c.close();
+    }
+    assertValueRange(ApprovalCategory.READ, -1, 1, 2);
+  }
+
+  public void testCreateSchema_ApprovalCategory_Submit() throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final ApprovalCategory cat;
+
+      cat = c.approvalCategories().get(ApprovalCategory.SUBMIT);
+      assertNotNull(cat);
+      assertEquals(ApprovalCategory.SUBMIT, cat.getId());
+      assertEquals("Submit", cat.getName());
+      assertNull(cat.getAbbreviatedName());
+      assertEquals(SubmitFunction.NAME, cat.getFunctionName());
+      assertTrue(cat.isAction());
+    } finally {
+      c.close();
+    }
+    assertValueRange(ApprovalCategory.SUBMIT, 1);
+  }
+
+  public void testCreateSchema_ApprovalCategory_PushTag() throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final ApprovalCategory cat;
+
+      cat = c.approvalCategories().get(ApprovalCategory.PUSH_TAG);
+      assertNotNull(cat);
+      assertEquals(ApprovalCategory.PUSH_TAG, cat.getId());
+      assertEquals("Push Annotated Tag", cat.getName());
+      assertNull(cat.getAbbreviatedName());
+      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
+      assertTrue(cat.isAction());
+    } finally {
+      c.close();
+    }
+    assertValueRange(ApprovalCategory.PUSH_TAG, //
+        ApprovalCategory.PUSH_TAG_SIGNED, //
+        ApprovalCategory.PUSH_TAG_ANNOTATED, //
+        ApprovalCategory.PUSH_TAG_ANY);
+  }
+
+  public void testCreateSchema_ApprovalCategory_PushHead() throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final ApprovalCategory cat;
+
+      cat = c.approvalCategories().get(ApprovalCategory.PUSH_HEAD);
+      assertNotNull(cat);
+      assertEquals(ApprovalCategory.PUSH_HEAD, cat.getId());
+      assertEquals("Push Branch", cat.getName());
+      assertNull(cat.getAbbreviatedName());
+      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
+      assertTrue(cat.isAction());
+    } finally {
+      c.close();
+    }
+    assertValueRange(ApprovalCategory.PUSH_HEAD, //
+        ApprovalCategory.PUSH_HEAD_UPDATE, //
+        ApprovalCategory.PUSH_HEAD_CREATE, //
+        ApprovalCategory.PUSH_HEAD_REPLACE);
+  }
+
+  public void testCreateSchema_ApprovalCategory_Owner() throws OrmException {
+    final ReviewDb c = db.create().open();
+    try {
+      final ApprovalCategory cat;
+
+      cat = c.approvalCategories().get(ApprovalCategory.OWN);
+      assertNotNull(cat);
+      assertEquals(ApprovalCategory.OWN, cat.getId());
+      assertEquals("Owner", cat.getName());
+      assertNull(cat.getAbbreviatedName());
+      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
+      assertTrue(cat.isAction());
+    } finally {
+      c.close();
+    }
+    assertValueRange(ApprovalCategory.OWN, 1);
+  }
+
+  private void assertValueRange(ApprovalCategory.Id cat, int... range)
+      throws OrmException {
+    final HashSet<ApprovalCategoryValue.Id> act =
+        new HashSet<ApprovalCategoryValue.Id>();
+    final ReviewDb c = db.open();
+    try {
+      for (ApprovalCategoryValue v : c.approvalCategoryValues().byCategory(cat)) {
+        assertNotNull(v.getId());
+        assertNotNull(v.getName());
+        assertEquals(cat, v.getCategoryId());
+        assertFalse(v.getName().isEmpty());
+
+        act.add(v.getId());
+      }
+    } finally {
+      c.close();
+    }
+
+    for (int value : range) {
+      final ApprovalCategoryValue.Id exp =
+          new ApprovalCategoryValue.Id(cat, (short) value);
+      if (!act.remove(exp)) {
+        fail("Category " + cat + " lacks value " + value);
+      }
+    }
+    if (!act.isEmpty()) {
+      fail("Category " + cat + " has additional values: " + act);
+    }
+  }
+
+  public void testCreateSchema_DefaultAccess_AnonymousUsers()
+      throws OrmException {
+    final SystemConfig config = getSystemConfig();
+    assertDefaultRight(config.anonymousGroupId, ApprovalCategory.READ, 1, 1);
+  }
+
+  public void testCreateSchema_DefaultAccess_RegisteredUsers()
+      throws OrmException {
+    final SystemConfig config = getSystemConfig();
+    assertDefaultRight(config.registeredGroupId, ApprovalCategory.READ, 1, 2);
+    assertDefaultRight(config.registeredGroupId, codeReview, -1, 1);
+  }
+
+  public void testCreateSchema_DefaultAccess_Administrators()
+      throws OrmException {
+    final SystemConfig config = getSystemConfig();
+    assertDefaultRight(config.adminGroupId, ApprovalCategory.READ, 1, 1);
+  }
+
+  private void assertDefaultRight(final AccountGroup.Id group,
+      final ApprovalCategory.Id category, int min, int max) throws OrmException {
+    final ReviewDb c = db.open();
+    try {
+      final Project all;
+      final ProjectRight right;
+
+      all = c.projects().get(WildProjectNameProvider.WILD_PROJECT_ID);
+      right = c.projectRights().get( //
+          new ProjectRight.Key(all.getNameKey(), category, group));
+
+      assertNotNull(right);
+      assertEquals(all.getNameKey(), right.getProjectNameKey());
+      assertEquals(group, right.getAccountGroupId());
+      assertEquals(category, right.getApprovalCategoryId());
+      assertEquals(min, right.getMinValue());
+      assertEquals(max, right.getMaxValue());
+    } finally {
+      c.close();
+    }
+  }
+
+  private SystemConfig getSystemConfig() {
+    return new SystemConfigProvider(db).get();
+  }
+
+  private SchemaVersion getSchemaVersion() throws OrmException {
+    final ReviewDb c = db.open();
+    try {
+      return c.schemaVersion().get(new SchemaVersion.Key());
+    } finally {
+      c.close();
+    }
+  }
+}
diff --git a/src/test/java/com/google/gerrit/testutil/TestDatabase.java b/src/test/java/com/google/gerrit/testutil/TestDatabase.java
new file mode 100644
index 0000000..d628403
--- /dev/null
+++ b/src/test/java/com/google/gerrit/testutil/TestDatabase.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2009 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.testutil;
+
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.server.config.SystemConfigProvider;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.jdbc.SimpleDataSource;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+/**
+ * An in-memory test instance of {@link ReviewDb} database.
+ * <p>
+ * Test classes should create one instance of this class for each unique test
+ * database they want to use. When the tests needing this instance are complete,
+ * ensure that {@link #drop(TestDatabase)} is called to free the resources so
+ * the JVM running the unit tests doesn't run out of heap space.
+ */
+public class TestDatabase implements SchemaFactory<ReviewDb> {
+  private static int dbCnt;
+
+  private static synchronized DataSource newDataSource() throws SQLException {
+    final Properties p = new Properties();
+    p.setProperty("driver", org.h2.Driver.class.getName());
+    p.setProperty("url", "jdbc:h2:mem:" + "Test_" + (++dbCnt));
+    final DataSource dataSource = new SimpleDataSource(p);
+    return dataSource;
+  }
+
+  /** Drop the database from memory; does nothing if the instance was null. */
+  public static void drop(final TestDatabase db) {
+    if (db != null) {
+      db.drop();
+    }
+  }
+
+  private Connection openHandle;
+  private Database<ReviewDb> database;
+
+  public TestDatabase() throws OrmException {
+    try {
+      final DataSource dataSource = newDataSource();
+
+      // Open one connection. This will peg the database into memory
+      // until someone calls drop on us, allowing subsequent connections
+      // opened against the same URL to go to the same set of tables.
+      //
+      openHandle = dataSource.getConnection();
+
+      // Build the access layer around the connection factory.
+      //
+      database = new Database<ReviewDb>(dataSource, ReviewDb.class);
+    } catch (SQLException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  public Database<ReviewDb> getDatabase() {
+    return database;
+  }
+
+  @Override
+  public ReviewDb open() throws OrmException {
+    return getDatabase().open();
+  }
+
+  /** Ensure the database schema has been created and initialized. */
+  public TestDatabase create() {
+    new SystemConfigProvider(this).get();
+    return this;
+  }
+
+  /** Drop this database from memory so it no longer exists. */
+  public void drop() {
+    if (openHandle != null) {
+      try {
+        openHandle.close();
+      } catch (SQLException e) {
+        System.err.println("WARNING: Cannot close database connection");
+        e.printStackTrace(System.err);
+      }
+      openHandle = null;
+      database = null;
+    }
+  }
+}