diff options
Diffstat (limited to 'javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java')
-rw-r--r-- | javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java | 1561 |
1 files changed, 1561 insertions, 0 deletions
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java new file mode 100644 index 0000000000..e2d390e6fd --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java @@ -0,0 +1,1561 @@ +// Copyright (C) 2015 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.api.group; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.gerrit.acceptance.GitUtil.deleteRef; +import static com.google.gerrit.acceptance.GitUtil.fetch; +import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo; +import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos; +import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.truth.Correspondence; +import com.google.common.util.concurrent.AtomicLongMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.GitUtil; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.ProjectResetter; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.Sandboxed; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.testsuite.account.AccountOperations; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.common.data.GroupReference; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.groups.GroupApi; +import com.google.gerrit.extensions.api.groups.GroupInput; +import com.google.gerrit.extensions.api.groups.Groups.ListRequest; +import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.common.GroupAuditEventInfo; +import com.google.gerrit.extensions.common.GroupAuditEventInfo.GroupMemberAuditEventInfo; +import com.google.gerrit.extensions.common.GroupAuditEventInfo.Type; +import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo; +import com.google.gerrit.extensions.common.GroupInfo; +import com.google.gerrit.extensions.common.GroupOptionsInfo; +import com.google.gerrit.extensions.events.GroupIndexedListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.extensions.restapi.Url; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.Sequences; +import com.google.gerrit.server.ServerInitiated; +import com.google.gerrit.server.account.GroupIncludeCache; +import com.google.gerrit.server.group.InternalGroup; +import com.google.gerrit.server.group.PeriodicGroupIndexer; +import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.gerrit.server.group.db.Groups; +import com.google.gerrit.server.group.db.GroupsConsistencyChecker; +import com.google.gerrit.server.group.db.GroupsUpdate; +import com.google.gerrit.server.group.db.InternalGroupCreation; +import com.google.gerrit.server.group.db.InternalGroupUpdate; +import com.google.gerrit.server.index.group.GroupIndexer; +import com.google.gerrit.server.index.group.StalenessChecker; +import com.google.gerrit.server.util.MagicBranch; +import com.google.gerrit.server.util.time.TimeUtil; +import com.google.gerrit.testing.TestTimeUtil; +import com.google.inject.Inject; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +@NoHttpd +public class GroupsIT extends AbstractDaemonTest { + @Inject private Groups groups; + @Inject @ServerInitiated private GroupsUpdate groupsUpdate; + @Inject private GroupIncludeCache groupIncludeCache; + @Inject private StalenessChecker stalenessChecker; + @Inject private GroupIndexer groupIndexer; + @Inject private GroupsConsistencyChecker consistencyChecker; + @Inject private PeriodicGroupIndexer slaveGroupIndexer; + @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners; + @Inject private Sequences seq; + @Inject private AccountOperations accountOperations; + + @Before + public void setTimeForTesting() { + TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS); + } + + @After + public void resetTime() { + TestTimeUtil.useSystemTime(); + } + + @After + public void consistencyCheck() throws Exception { + if (description.getAnnotation(IgnoreGroupInconsistencies.class) == null) { + assertThat(consistencyChecker.check()).isEmpty(); + } + } + + @Override + protected ProjectResetter.Config resetProjects() { + // Don't reset All-Users since deleting users makes groups inconsistent (e.g. groups would + // contain members that no longer exist) and as result of this the group consistency checker + // that is executed after each test would fail. + return new ProjectResetter.Config().reset(allProjects, RefNames.REFS_CONFIG); + } + + @Test + public void systemGroupCanBeRetrievedFromIndex() throws Exception { + List<GroupInfo> groupInfos = gApi.groups().query("name:Administrators").get(); + assertThat(groupInfos).isNotEmpty(); + } + + @Test + public void addToNonExistingGroup_NotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.groups().id("non-existing").addMembers("admin"); + } + + @Test + public void removeFromNonExistingGroup_NotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.groups().id("non-existing").removeMembers("admin"); + } + + @Test + public void addRemoveMember() throws Exception { + String g = createGroup("users"); + gApi.groups().id(g).addMembers("user"); + assertMembers(g, user); + + gApi.groups().id(g).removeMembers("user"); + assertNoMembers(g); + } + + @Test + public void cachedGroupsForMemberAreUpdatedOnMemberAdditionAndRemoval() throws Exception { + String username = name("user"); + Account.Id accountId = accountOperations.newAccount().username(username).create(); + + // Fill the cache for the observed account. + groupIncludeCache.getGroupsWithMember(accountId); + String groupName = createGroup("users"); + AccountGroup.UUID groupUuid = new AccountGroup.UUID(gApi.groups().id(groupName).get().id); + + gApi.groups().id(groupName).addMembers(username); + + Collection<AccountGroup.UUID> groupsWithMemberAfterAddition = + groupIncludeCache.getGroupsWithMember(accountId); + assertThat(groupsWithMemberAfterAddition).contains(groupUuid); + + gApi.groups().id(groupName).removeMembers(username); + + Collection<AccountGroup.UUID> groupsWithMemberAfterRemoval = + groupIncludeCache.getGroupsWithMember(accountId); + assertThat(groupsWithMemberAfterRemoval).doesNotContain(groupUuid); + } + + @Test + public void cachedGroupByNameIsUpdatedOnCreation() throws Exception { + String newGroupName = name("newGroup"); + AccountGroup.NameKey nameKey = new AccountGroup.NameKey(newGroupName); + assertThat(groupCache.get(nameKey)).isEmpty(); + gApi.groups().create(newGroupName); + assertThat(groupCache.get(nameKey)).isPresent(); + } + + @Test + public void addExistingMember_OK() throws Exception { + String g = "Administrators"; + assertMembers(g, admin); + gApi.groups().id("Administrators").addMembers("admin"); + assertMembers(g, admin); + } + + @Test + public void addNonExistingMember_UnprocessableEntity() throws Exception { + exception.expect(UnprocessableEntityException.class); + gApi.groups().id("Administrators").addMembers("non-existing"); + } + + @Test + public void addMultipleMembers() throws Exception { + String g = createGroup("users"); + + String u1 = name("u1"); + accountOperations.newAccount().username(u1).create(); + String u2 = name("u2"); + accountOperations.newAccount().username(u2).create(); + + gApi.groups().id(g).addMembers(u1, u2); + + List<AccountInfo> members = gApi.groups().id(g).members(); + assertThat(members) + .comparingElementsUsing(getAccountToUsernameCorrespondence()) + .containsExactly(u1, u2); + } + + @Test + public void membersWithAtSignInUsernameCanBeAdded() throws Exception { + String g = createGroup("users"); + String usernameWithAt = name("u1@something"); + accountOperations.newAccount().username(usernameWithAt).create(); + + gApi.groups().id(g).addMembers(usernameWithAt); + + List<AccountInfo> members = gApi.groups().id(g).members(); + assertThat(members) + .comparingElementsUsing(getAccountToUsernameCorrespondence()) + .containsExactly(usernameWithAt); + } + + @Test + public void membersWithAtSignInUsernameAreNotConfusedWithSimilarUsernames() throws Exception { + String g = createGroup("users"); + String usernameWithAt = name("u1@something"); + accountOperations.newAccount().username(usernameWithAt).create(); + String usernameWithoutAt = name("u1something"); + accountOperations.newAccount().username(usernameWithoutAt).create(); + String usernameOnlyPrefix = name("u1"); + accountOperations.newAccount().username(usernameOnlyPrefix).create(); + String usernameOnlySuffix = name("something"); + accountOperations.newAccount().username(usernameOnlySuffix).create(); + + gApi.groups() + .id(g) + .addMembers(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix); + + List<AccountInfo> members = gApi.groups().id(g).members(); + assertThat(members) + .comparingElementsUsing(getAccountToUsernameCorrespondence()) + .containsExactly(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix); + } + + @Test + public void includeRemoveGroup() throws Exception { + String p = createGroup("parent"); + String g = createGroup("newGroup"); + gApi.groups().id(p).addGroups(g); + assertIncludes(p, g); + + gApi.groups().id(p).removeGroups(g); + assertNoIncludes(p); + } + + @Test + public void includeExternalGroup() throws Exception { + String g = createGroup("group"); + String subgroupUuid = SystemGroupBackend.REGISTERED_USERS.get(); + gApi.groups().id(g).addGroups(subgroupUuid); + + List<GroupInfo> subgroups = gApi.groups().id(g).includedGroups(); + assertThat(subgroups).hasSize(1); + assertThat(subgroups.get(0).id).isEqualTo(subgroupUuid.replace(":", "%3A")); + assertThat(subgroups.get(0).name).isEqualTo("Registered Users"); + assertThat(subgroups.get(0).groupId).isNull(); + + List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(g).auditLog(); + assertThat(auditEvents).hasSize(1); + assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, "Registered Users"); + } + + @Test + public void includeExistingGroup_OK() throws Exception { + String p = createGroup("parent"); + String g = createGroup("newGroup"); + gApi.groups().id(p).addGroups(g); + assertIncludes(p, g); + gApi.groups().id(p).addGroups(g); + assertIncludes(p, g); + } + + @Test + public void addMultipleIncludes() throws Exception { + String p = createGroup("parent"); + String g1 = createGroup("newGroup1"); + String g2 = createGroup("newGroup2"); + List<String> groups = new ArrayList<>(); + groups.add(g1); + groups.add(g2); + gApi.groups().id(p).addGroups(g1, g2); + assertIncludes(p, g1, g2); + } + + @Test + public void createGroup() throws Exception { + String newGroupName = name("newGroup"); + GroupInfo g = gApi.groups().create(newGroupName).get(); + assertGroupInfo(group(newGroupName), g); + } + + @Test + public void createDuplicateInternalGroupCaseSensitiveName_Conflict() throws Exception { + String dupGroupName = name("dupGroup"); + gApi.groups().create(dupGroupName); + exception.expect(ResourceConflictException.class); + exception.expectMessage("group '" + dupGroupName + "' already exists"); + gApi.groups().create(dupGroupName); + } + + @Test + public void createDuplicateInternalGroupCaseInsensitiveName() throws Exception { + String dupGroupName = name("dupGroupA"); + String dupGroupNameLowerCase = name("dupGroupA").toLowerCase(); + gApi.groups().create(dupGroupName); + gApi.groups().create(dupGroupNameLowerCase); + assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupName); + assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupNameLowerCase); + } + + @Test + public void createDuplicateSystemGroupCaseSensitiveName_Conflict() throws Exception { + String newGroupName = "Registered Users"; + exception.expect(ResourceConflictException.class); + exception.expectMessage("group 'Registered Users' already exists"); + gApi.groups().create(newGroupName); + } + + @Test + public void createDuplicateSystemGroupCaseInsensitiveName_Conflict() throws Exception { + String newGroupName = "registered users"; + exception.expect(ResourceConflictException.class); + exception.expectMessage("group 'Registered Users' already exists"); + gApi.groups().create(newGroupName); + } + + @Test + @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users") + public void createGroupWithConfiguredNameOfSystemGroup_Conflict() throws Exception { + exception.expect(ResourceConflictException.class); + exception.expectMessage("group 'All Users' already exists"); + gApi.groups().create("all users"); + } + + @Test + @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users") + public void createGroupWithDefaultNameOfSystemGroup_Conflict() throws Exception { + exception.expect(ResourceConflictException.class); + exception.expectMessage("group name 'Anonymous Users' is reserved"); + gApi.groups().create("anonymous users"); + } + + @Test + public void createGroupWithProperties() throws Exception { + GroupInput in = new GroupInput(); + in.name = name("newGroup"); + in.description = "Test description"; + in.visibleToAll = true; + in.ownerId = adminGroupUuid().get(); + GroupInfo g = gApi.groups().create(in).detail(); + assertThat(g.description).isEqualTo(in.description); + assertThat(g.options.visibleToAll).isEqualTo(in.visibleToAll); + assertThat(g.ownerId).isEqualTo(in.ownerId); + } + + @Test + public void createGroupWithoutCapability_Forbidden() throws Exception { + setApiUser(user); + exception.expect(AuthException.class); + gApi.groups().create(name("newGroup")); + } + + @Test + public void createdOnFieldIsPopulatedForNewGroup() throws Exception { + // NoteDb allows only second precision. + Timestamp testStartTime = TimeUtil.truncateToSecond(TimeUtil.nowTs()); + String newGroupName = name("newGroup"); + GroupInfo group = gApi.groups().create(newGroupName).get(); + + assertThat(group.createdOn).isAtLeast(testStartTime); + } + + @Test + public void cachedGroupsForMemberAreUpdatedOnGroupCreation() throws Exception { + Account.Id accountId = accountOperations.newAccount().create(); + + // Fill the cache for the observed account. + groupIncludeCache.getGroupsWithMember(accountId); + + GroupInput groupInput = new GroupInput(); + groupInput.name = name("Users"); + groupInput.members = ImmutableList.of(String.valueOf(accountId.get())); + GroupInfo group = gApi.groups().create(groupInput).get(); + + Collection<AccountGroup.UUID> groups = groupIncludeCache.getGroupsWithMember(accountId); + assertThat(groups).containsExactly(new AccountGroup.UUID(group.id)); + } + + @Test + public void getGroup() throws Exception { + InternalGroup adminGroup = adminGroup(); + testGetGroup(adminGroup.getGroupUUID().get(), adminGroup); + testGetGroup(adminGroup.getName(), adminGroup); + testGetGroup(adminGroup.getId().get(), adminGroup); + } + + private void testGetGroup(Object id, InternalGroup expectedGroup) throws Exception { + GroupInfo group = gApi.groups().id(id.toString()).get(); + assertGroupInfo(expectedGroup, group); + } + + @Test + @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users") + public void getSystemGroupByConfiguredName() throws Exception { + GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS); + assertThat(anonymousUsersGroup.getName()).isEqualTo("All Users"); + + GroupInfo group = gApi.groups().id(anonymousUsersGroup.getUUID().get()).get(); + assertThat(group.name).isEqualTo(anonymousUsersGroup.getName()); + + group = gApi.groups().id(anonymousUsersGroup.getName()).get(); + assertThat(group.id).isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get()))); + } + + @Test + public void getSystemGroupByDefaultName() throws Exception { + GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS); + GroupInfo group = gApi.groups().id("Anonymous Users").get(); + assertThat(group.name).isEqualTo(anonymousUsersGroup.getName()); + assertThat(group.id).isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get()))); + } + + @Test + @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users") + public void getSystemGroupByDefaultName_NotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.groups().id("Anonymous-Users").get(); + } + + @Test + public void groupIsCreatedForSpecifiedName() throws Exception { + String name = name("Users"); + gApi.groups().create(name); + + assertThat(gApi.groups().id(name).name()).isEqualTo(name); + } + + @Test + public void groupCannotBeCreatedWithNameOfAnotherGroup() throws Exception { + String name = name("Users"); + gApi.groups().create(name).get(); + + exception.expect(ResourceConflictException.class); + gApi.groups().create(name); + } + + @Test + public void groupCanBeRenamed() throws Exception { + String name = name("Name1"); + GroupInfo group = gApi.groups().create(name).get(); + + String newName = name("Name2"); + gApi.groups().id(name).name(newName); + assertThat(gApi.groups().id(group.id).name()).isEqualTo(newName); + } + + @Test + public void groupCanBeRenamedToItsCurrentName() throws Exception { + String name = name("Users"); + GroupInfo group = gApi.groups().create(name).get(); + + gApi.groups().id(group.id).name(name); + assertThat(gApi.groups().id(group.id).name()).isEqualTo(name); + } + + @Test + public void groupCannotBeRenamedToNameOfAnotherGroup() throws Exception { + String name1 = name("Name1"); + GroupInfo group1 = gApi.groups().create(name1).get(); + + String name2 = name("Name2"); + gApi.groups().create(name2); + + exception.expect(ResourceConflictException.class); + gApi.groups().id(group1.id).name(name2); + } + + @Test + public void renamedGroupCanBeLookedUpByNewName() throws Exception { + String name = name("Name1"); + GroupInfo group = gApi.groups().create(name).get(); + + String newName = name("Name2"); + gApi.groups().id(group.id).name(newName); + + GroupInfo foundGroup = gApi.groups().id(newName).get(); + assertThat(foundGroup.id).isEqualTo(group.id); + } + + @Test + public void oldNameOfRenamedGroupIsNotAccessibleAnymore() throws Exception { + String name = name("Name1"); + GroupInfo group = gApi.groups().create(name).get(); + + String newName = name("Name2"); + gApi.groups().id(group.id).name(newName); + + assertGroupDoesNotExist(name); + exception.expect(ResourceNotFoundException.class); + gApi.groups().id(name).get(); + } + + @Test + public void oldNameOfRenamedGroupIsFreeForUseAgain() throws Exception { + String name = name("Name1"); + GroupInfo group1 = gApi.groups().create(name).get(); + + String newName = name("Name2"); + gApi.groups().id(group1.id).name(newName); + + GroupInfo group2 = gApi.groups().create(name).get(); + assertThat(group2.id).isNotEqualTo(group1.id); + } + + @Test + public void groupDescription() throws Exception { + String name = name("group"); + gApi.groups().create(name); + + // get description + assertThat(gApi.groups().id(name).description()).isEmpty(); + + // set description + String desc = "New description for the group."; + gApi.groups().id(name).description(desc); + assertThat(gApi.groups().id(name).description()).isEqualTo(desc); + + // set description to null + gApi.groups().id(name).description(null); + assertThat(gApi.groups().id(name).description()).isEmpty(); + + // set description to empty string + gApi.groups().id(name).description(""); + assertThat(gApi.groups().id(name).description()).isEmpty(); + } + + @Test + public void groupOptions() throws Exception { + String name = name("group"); + gApi.groups().create(name); + + // get options + assertThat(gApi.groups().id(name).options().visibleToAll).isNull(); + + // set options + GroupOptionsInfo options = new GroupOptionsInfo(); + options.visibleToAll = true; + gApi.groups().id(name).options(options); + assertThat(gApi.groups().id(name).options().visibleToAll).isTrue(); + } + + @Test + public void groupOwner() throws Exception { + String name = name("group"); + GroupInfo info = gApi.groups().create(name).get(); + String adminUUID = adminGroupUuid().get(); + String registeredUUID = SystemGroupBackend.REGISTERED_USERS.get(); + + // get owner + assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(info.id); + + // set owner by name + gApi.groups().id(name).owner("Registered Users"); + assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(registeredUUID); + + // set owner by UUID + gApi.groups().id(name).owner(adminUUID); + assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(adminUUID); + + // set non existing owner + exception.expect(UnprocessableEntityException.class); + gApi.groups().id(name).owner("Non-Existing Group"); + } + + @Test + public void listNonExistingGroupIncludes_NotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.groups().id("non-existing").includedGroups(); + } + + @Test + public void listEmptyGroupIncludes() throws Exception { + String gx = createGroup("gx"); + assertThat(gApi.groups().id(gx).includedGroups()).isEmpty(); + } + + @Test + public void includeNonExistingGroup() throws Exception { + String gx = createGroup("gx"); + exception.expect(UnprocessableEntityException.class); + gApi.groups().id(gx).addGroups("non-existing"); + } + + @Test + public void listNonEmptyGroupIncludes() throws Exception { + String gx = createGroup("gx"); + String gy = createGroup("gy"); + String gz = createGroup("gz"); + gApi.groups().id(gx).addGroups(gy); + gApi.groups().id(gx).addGroups(gz); + assertIncludes(gApi.groups().id(gx).includedGroups(), gy, gz); + } + + @Test + public void listOneIncludeMember() throws Exception { + String gx = createGroup("gx"); + String gy = createGroup("gy"); + gApi.groups().id(gx).addGroups(gy); + assertIncludes(gApi.groups().id(gx).includedGroups(), gy); + } + + @Test + public void listNonExistingGroupMembers_NotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.groups().id("non-existing").members(); + } + + @Test + public void listEmptyGroupMembers() throws Exception { + String group = createGroup("empty"); + assertThat(gApi.groups().id(group).members()).isEmpty(); + } + + @Test + public void listNonEmptyGroupMembers() throws Exception { + String group = createGroup("group"); + String user1 = name("user1"); + accountOperations.newAccount().username(user1).create(); + String user2 = name("user2"); + accountOperations.newAccount().username(user2).create(); + gApi.groups().id(group).addMembers(user1, user2); + + assertMembers(gApi.groups().id(group).members(), user1, user2); + } + + @Test + public void listOneGroupMember() throws Exception { + String group = createGroup("group"); + String user = name("user1"); + accountOperations.newAccount().username(user).create(); + gApi.groups().id(group).addMembers(user); + + assertMembers(gApi.groups().id(group).members(), user); + } + + @Test + public void listGroupMembersRecursively() throws Exception { + String gx = createGroup("gx"); + String ux = name("ux"); + accountOperations.newAccount().username(ux).create(); + gApi.groups().id(gx).addMembers(ux); + + String gy = createGroup("gy"); + String uy = name("uy"); + accountOperations.newAccount().username(uy).create(); + gApi.groups().id(gy).addMembers(uy); + + String gz = createGroup("gz"); + String uz = name("uz"); + accountOperations.newAccount().username(uz).create(); + gApi.groups().id(gz).addMembers(uz); + + gApi.groups().id(gx).addGroups(gy); + gApi.groups().id(gy).addGroups(gz); + assertMembers(gApi.groups().id(gx).members(), ux); + assertMembers(gApi.groups().id(gx).members(true), ux, uy, uz); + } + + @Test + public void usersSeeTheirDirectMembershipWhenListingMembersRecursively() throws Exception { + String group = createGroup("group"); + gApi.groups().id(group).addMembers(user.username); + + setApiUser(user); + assertMembers(gApi.groups().id(group).members(true), user.fullName); + } + + @Test + public void usersDoNotSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception { + String group1 = createGroup("group1"); + String group2 = createGroup("group2"); + gApi.groups().id(group1).addGroups(group2); + gApi.groups().id(group2).addMembers(user.username); + + setApiUser(user); + List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true); + + assertMembers(listedMembers); + } + + @Test + public void adminsSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception { + String ownerGroup = createGroup("ownerGroup", null); + String group1 = createGroup("group1", ownerGroup); + String group2 = createGroup("group2", ownerGroup); + gApi.groups().id(group1).addGroups(group2); + gApi.groups().id(group2).addMembers(admin.username); + + List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true); + + assertMembers(listedMembers, admin.fullName); + } + + @Test + public void ownersSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception { + String ownerGroup = createGroup("ownerGroup", null); + String group1 = createGroup("group1", ownerGroup); + String group2 = createGroup("group2", ownerGroup); + gApi.groups().id(group1).addGroups(group2); + gApi.groups().id(ownerGroup).addMembers(user.username); + gApi.groups().id(group2).addMembers(user.username); + + setApiUser(user); + List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true); + + assertMembers(listedMembers, user.fullName); + } + + @Test + public void defaultGroupsCreated() throws Exception { + Iterable<String> names = gApi.groups().list().getAsMap().keySet(); + assertThat(names).containsAllOf("Administrators", "Non-Interactive Users").inOrder(); + } + + @Test + public void listAllGroups() throws Exception { + List<String> expectedGroups = + groups.getAllGroupReferences().map(GroupReference::getName).sorted().collect(toList()); + assertThat(expectedGroups.size()).isAtLeast(2); + assertThat(gApi.groups().list().getAsMap().keySet()) + .containsExactlyElementsIn(expectedGroups) + .inOrder(); + } + + @Test + public void getGroupsByOwner() throws Exception { + String parent = createGroup("test-parent"); + List<String> children = + Arrays.asList(createGroup("test-child1", parent), createGroup("test-child2", parent)); + + // By UUID + List<GroupInfo> owned = gApi.groups().list().withOwnedBy(groupUuid(parent).get()).get(); + assertThat(owned.stream().map(g -> g.name).collect(toList())) + .containsExactlyElementsIn(children); + + // By name + owned = gApi.groups().list().withOwnedBy(parent).get(); + assertThat(owned.stream().map(g -> g.name).collect(toList())) + .containsExactlyElementsIn(children); + + // By group that does not own any others + owned = gApi.groups().list().withOwnedBy(owned.get(0).id).get(); + assertThat(owned).isEmpty(); + + // By non-existing group + exception.expect(UnprocessableEntityException.class); + exception.expectMessage("Group Not Found: does-not-exist"); + gApi.groups().list().withOwnedBy("does-not-exist").get(); + } + + @Test + public void onlyVisibleGroupsReturned() throws Exception { + String newGroupName = name("newGroup"); + GroupInput in = new GroupInput(); + in.name = newGroupName; + in.description = "a hidden group"; + in.visibleToAll = false; + in.ownerId = adminGroupUuid().get(); + gApi.groups().create(in); + + setApiUser(user); + assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName); + + setApiUser(admin); + gApi.groups().id(newGroupName).addMembers(user.username); + + setApiUser(user); + assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName); + } + + @Test + public void suggestGroup() throws Exception { + Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap(); + assertThat(groups).containsKey("Administrators"); + assertThat(groups).hasSize(1); + assertBadRequest(gApi.groups().list().withSuggest("adm").withSubstring("foo")); + assertBadRequest(gApi.groups().list().withSuggest("adm").withRegex("foo.*")); + assertBadRequest(gApi.groups().list().withSuggest("adm").withUser("user")); + assertBadRequest(gApi.groups().list().withSuggest("adm").withOwned(true)); + assertBadRequest(gApi.groups().list().withSuggest("adm").withVisibleToAll(true)); + assertBadRequest(gApi.groups().list().withSuggest("adm").withStart(1)); + } + + @Test + public void withSubstring() throws Exception { + String group = name("Abcdefghijklmnop"); + gApi.groups().create(group); + + // Choose a substring which isn't part of any group or test method within this class. + String substring = "efghijk"; + Map<String, GroupInfo> groups = gApi.groups().list().withSubstring(substring).getAsMap(); + assertThat(groups).containsKey(group); + assertThat(groups).hasSize(1); + + groups = gApi.groups().list().withSubstring("abcdefghi").getAsMap(); + assertThat(groups).containsKey(group); + assertThat(groups).hasSize(1); + + String otherGroup = name("Abcdefghijklmnop2"); + gApi.groups().create(otherGroup); + groups = gApi.groups().list().withSubstring(substring).getAsMap(); + assertThat(groups).hasSize(2); + assertThat(groups).containsKey(group); + assertThat(groups).containsKey(otherGroup); + + groups = gApi.groups().list().withSubstring("non-existing-substring").getAsMap(); + assertThat(groups).isEmpty(); + } + + @Test + public void withRegex() throws Exception { + Map<String, GroupInfo> groups = gApi.groups().list().withRegex("Admin.*").getAsMap(); + assertThat(groups).containsKey("Administrators"); + assertThat(groups).hasSize(1); + + groups = gApi.groups().list().withRegex("admin.*").getAsMap(); + assertThat(groups).isEmpty(); + + groups = gApi.groups().list().withRegex(".*istrators").getAsMap(); + assertThat(groups).containsKey("Administrators"); + assertThat(groups).hasSize(1); + + assertBadRequest(gApi.groups().list().withRegex(".*istrators").withSubstring("s")); + } + + @Test + public void allGroupInfoFieldsSetCorrectly() throws Exception { + InternalGroup adminGroup = adminGroup(); + Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap(); + assertThat(groups).hasSize(1); + assertThat(groups).containsKey("Administrators"); + assertGroupInfo(adminGroup, Iterables.getOnlyElement(groups.values())); + } + + @Test + public void getAuditLog() throws Exception { + GroupApi g = gApi.groups().create(name("group")); + List<? extends GroupAuditEventInfo> auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(1); + assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id); + + g.addMembers(user.username); + auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(2); + assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id); + + g.removeMembers(user.username); + auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(3); + assertMemberAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id); + + String otherGroup = name("otherGroup"); + gApi.groups().create(otherGroup); + g.addGroups(otherGroup); + auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(4); + assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup); + + g.removeGroups(otherGroup); + auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(5); + assertSubgroupAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup); + + // Add a removed member back again. + g.addMembers(user.username); + auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(6); + assertMemberAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id); + + // Add a removed group back again. + g.addGroups(otherGroup); + auditEvents = g.auditLog(); + assertThat(auditEvents).hasSize(7); + assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup); + + Timestamp lastDate = null; + for (GroupAuditEventInfo auditEvent : auditEvents) { + if (lastDate != null) { + assertThat(lastDate).isAtLeast(auditEvent.date); + } + lastDate = auditEvent.date; + } + } + + /** + * @Sandboxed is used by this test because it deletes a group reference which introduces an + * inconsistency for the group storage. Once group deletion is supported, this test should be + * updated to use the API instead. + */ + @Test + @Sandboxed + @IgnoreGroupInconsistencies + public void getAuditLogAfterDeletingASubgroup() throws Exception { + GroupInfo parentGroup = gApi.groups().create(name("parent-group")).get(); + + // Creates a subgroup and adds it to "parent-group" as a subgroup. + GroupInfo subgroup = gApi.groups().create(name("sub-group")).get(); + gApi.groups().id(parentGroup.id).addGroups(subgroup.id); + + // Deletes the subgroup. + deleteGroupRef(subgroup.id); + + List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(parentGroup.id).auditLog(); + assertThat(auditEvents).hasSize(2); + // Verify the unavailable subgroup's name is null. + assertSubgroupAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, null); + } + + private void deleteGroupRef(String groupId) throws Exception { + AccountGroup.UUID uuid = new AccountGroup.UUID(groupId); + try (Repository repo = repoManager.openRepository(allUsers)) { + RefUpdate ru = repo.updateRef(RefNames.refsGroups(uuid)); + ru.setForceUpdate(true); + ru.setNewObjectId(ObjectId.zeroId()); + assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); + } + + // Reindex the group. + gApi.groups().id(uuid.get()).index(); + + // Verify "sub-group" has been deleted. + try { + gApi.groups().id(uuid.get()).get(); + fail("expected ResourceNotFoundException"); + } catch (ResourceNotFoundException e) { + } + } + + // reindex is tested by {@link AbstractQueryGroupsTest#reindex} + @Test + public void reindexPermissions() throws Exception { + TestAccount groupOwner = accountCreator.user2(); + GroupInput in = new GroupInput(); + in.name = name("group"); + in.members = + Collections.singleton(groupOwner).stream().map(u -> u.id.toString()).collect(toList()); + in.visibleToAll = true; + GroupInfo group = gApi.groups().create(in).get(); + + // admin can reindex any group + setApiUser(admin); + gApi.groups().id(group.id).index(); + + // group owner can reindex own group (group is owned by itself) + setApiUser(groupOwner); + gApi.groups().id(group.id).index(); + + // user cannot reindex any group + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to index group"); + gApi.groups().id(group.id).index(); + } + + @Test + public void pushToGroupBranchIsRejectedForAllUsersRepo() throws Exception { + assertPushToGroupBranch( + allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed"); + } + + @Test + public void pushToDeletedGroupBranchIsRejectedForAllUsersRepo() throws Exception { + String groupRef = + RefNames.refsDeletedGroups( + new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id)); + createBranch(allUsers, groupRef); + assertPushToGroupBranch(allUsers, groupRef, "group update not allowed"); + } + + @Test + public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception { + // refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, "group update not allowed"); + } + + @Test + public void pushToGroupsBranchForNonAllUsersRepo() throws Exception { + assertCreateGroupBranch(project, null); + String groupRef = + RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id)); + createBranch(project, groupRef); + assertPushToGroupBranch(project, groupRef, null); + } + + @Test + public void pushToDeletedGroupsBranchForNonAllUsersRepo() throws Exception { + assertCreateGroupBranch(project, null); + String groupRef = + RefNames.refsDeletedGroups( + new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id)); + createBranch(project, groupRef); + assertPushToGroupBranch(project, groupRef, null); + } + + @Test + public void pushToGroupNamesBranchForNonAllUsersRepo() throws Exception { + createBranch(project, RefNames.REFS_GROUPNAMES); + assertPushToGroupBranch(project, RefNames.REFS_GROUPNAMES, null); + } + + private void assertPushToGroupBranch( + Project.NameKey project, String groupRefName, String expectedErrorOnUpdate) throws Exception { + grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS); + grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS); + grant(project, RefNames.REFS_DELETED_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS); + grant(project, RefNames.REFS_DELETED_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS); + grant(project, RefNames.REFS_GROUPNAMES, Permission.PUSH, false, REGISTERED_USERS); + + TestRepository<InMemoryRepository> repo = cloneProject(project); + + // update existing branch + fetch(repo, groupRefName + ":groupRef"); + repo.reset("groupRef"); + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content") + .to(groupRefName); + if (expectedErrorOnUpdate != null) { + r.assertErrorStatus(expectedErrorOnUpdate); + } else { + r.assertOkStatus(); + } + } + + private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate) + throws Exception { + grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS); + grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS); + TestRepository<InMemoryRepository> repo = cloneProject(project); + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content") + .setParents(ImmutableList.of()) + .to(RefNames.REFS_GROUPS + name("bar")); + if (expectedErrorOnCreate != null) { + r.assertErrorStatus(expectedErrorOnCreate); + } else { + r.assertOkStatus(); + } + } + + @Test + public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Exception { + pushToGroupBranchForReviewAndSubmit( + allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed"); + } + + @Test + public void pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit() throws Exception { + String groupRef = RefNames.refsGroups(adminGroupUuid()); + createBranch(project, groupRef); + pushToGroupBranchForReviewAndSubmit(project, groupRef, null); + } + + @Test + public void pushCustomInheritanceForAllUsersFails() throws Exception { + TestRepository<InMemoryRepository> repo = cloneProject(allUsers); + GitUtil.fetch(repo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG); + repo.reset(RefNames.REFS_CONFIG); + String config = + gApi.projects() + .name(allUsers.get()) + .branch(RefNames.REFS_CONFIG) + .file("project.config") + .asString(); + + Config cfg = new Config(); + cfg.fromText(config); + cfg.setString("access", null, "inheritFrom", project.get()); + config = cfg.toText(); + + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), repo, "Subject", "project.config", config) + .to(RefNames.REFS_CONFIG); + r.assertErrorStatus("invalid project configuration"); + r.assertMessage("All-Users must inherit from All-Projects"); + } + + @Test + public void cannotCreateGroupBranch() throws Exception { + testCannotCreateGroupBranch( + RefNames.REFS_GROUPS + "*", RefNames.refsGroups(new AccountGroup.UUID(name("foo")))); + } + + @Test + public void cannotCreateDeletedGroupBranch() throws Exception { + testCannotCreateGroupBranch( + RefNames.REFS_DELETED_GROUPS + "*", + RefNames.refsDeletedGroups(new AccountGroup.UUID(name("foo")))); + } + + @Test + @IgnoreGroupInconsistencies + public void cannotCreateGroupNamesBranch() throws Exception { + // Use ProjectResetter to restore the group names ref + try (ProjectResetter resetter = + projectResetter + .builder() + .build(new ProjectResetter.Config().reset(allUsers, RefNames.REFS_GROUPNAMES))) { + // Manually delete group names ref + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo)) { + RevCommit commit = rw.parseCommit(repo.exactRef(RefNames.REFS_GROUPNAMES).getObjectId()); + RefUpdate updateRef = repo.updateRef(RefNames.REFS_GROUPNAMES); + updateRef.setExpectedOldObjectId(commit.toObjectId()); + updateRef.setNewObjectId(ObjectId.zeroId()); + updateRef.setForceUpdate(true); + assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED); + } + + // refs/meta/group-names is only visible with ACCESS_DATABASE + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + + testCannotCreateGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES); + } + } + + private void testCannotCreateGroupBranch(String refPattern, String groupRef) throws Exception { + grant(allUsers, refPattern, Permission.CREATE); + grant(allUsers, refPattern, Permission.PUSH); + + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(groupRef); + r.assertErrorStatus(); + assertThat(r.getMessage()).contains("Not allowed to create group branch."); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(groupRef)).isNull(); + } + } + + @Test + public void cannotDeleteGroupBranch() throws Exception { + testCannotDeleteGroupBranch(RefNames.REFS_GROUPS + "*", RefNames.refsGroups(adminGroupUuid())); + } + + @Test + public void cannotDeleteDeletedGroupBranch() throws Exception { + String groupRef = RefNames.refsDeletedGroups(new AccountGroup.UUID(name("foo"))); + createBranch(allUsers, groupRef); + testCannotDeleteGroupBranch(RefNames.REFS_DELETED_GROUPS + "*", groupRef); + } + + @Test + public void cannotDeleteGroupNamesBranch() throws Exception { + // refs/meta/group-names is only visible with ACCESS_DATABASE + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + + testCannotDeleteGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES); + } + + private void testCannotDeleteGroupBranch(String refPattern, String groupRef) throws Exception { + grant(allUsers, refPattern, Permission.DELETE, true, REGISTERED_USERS); + + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + PushResult r = deleteRef(allUsersRepo, groupRef); + RemoteRefUpdate refUpdate = r.getRemoteUpdate(groupRef); + assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON); + assertThat(refUpdate.getMessage()).contains("Not allowed to delete group branch."); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(groupRef)).isNotNull(); + } + } + + @Test + public void defaultPermissionsOnGroupBranches() throws Exception { + assertPermissions( + allUsers, groupRef(REGISTERED_USERS), RefNames.REFS_GROUPS + "*", true, Permission.READ); + } + + @Test + @IgnoreGroupInconsistencies + public void stalenessChecker() throws Exception { + // Newly created group is not stale + GroupInfo groupInfo = gApi.groups().create(name("foo")).get(); + AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupInfo.id); + assertThat(stalenessChecker.isStale(groupUuid)).isFalse(); + + // Manual update makes index document stale + String groupRef = RefNames.refsGroups(groupUuid); + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo)) { + RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId()); + ObjectId emptyCommit = createCommit(repo, commit.getFullMessage(), commit.getTree()); + RefUpdate updateRef = repo.updateRef(groupRef); + updateRef.setExpectedOldObjectId(commit.toObjectId()); + updateRef.setNewObjectId(emptyCommit); + assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED); + } + assertStaleGroupAndReindex(groupUuid); + + // Manually delete group + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo)) { + RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId()); + RefUpdate updateRef = repo.updateRef(groupRef); + updateRef.setExpectedOldObjectId(commit.toObjectId()); + updateRef.setNewObjectId(ObjectId.zeroId()); + updateRef.setForceUpdate(true); + assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED); + } + assertStaleGroupAndReindex(groupUuid); + } + + @Test + public void groupNamesWithLeadingAndTrailingWhitespace() throws Exception { + for (String leading : ImmutableList.of("", " ", " ")) { + for (String trailing : ImmutableList.of("", " ", " ")) { + String name = leading + name("group") + trailing; + GroupInfo g = gApi.groups().create(name).get(); + assertThat(g.name).isEqualTo(name); + } + } + } + + @Test + @Sandboxed + public void groupsOfUserCanBeListedInSlaveMode() throws Exception { + GroupInput groupInput = new GroupInput(); + groupInput.name = name("contributors"); + groupInput.members = ImmutableList.of(user.username); + gApi.groups().create(groupInput).get(); + restartAsSlave(); + + setApiUser(user); + List<GroupInfo> groups = gApi.groups().list().withUser(user.username).get(); + ImmutableList<String> groupNames = + groups.stream().map(group -> group.name).collect(toImmutableList()); + assertThat(groupNames).contains(groupInput.name); + } + + @Test + @Sandboxed + @GerritConfig(name = "index.scheduledIndexer.enabled", value = "false") + @GerritConfig(name = "index.autoReindexIfStale", value = "false") + @IgnoreGroupInconsistencies + public void reindexGroupsInSlaveMode() throws Exception { + List<AccountGroup.UUID> expectedGroups = + groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toList()); + assertThat(expectedGroups.size()).isAtLeast(2); + + // Restart the server as slave, on startup of the slave all groups are indexed. + restartAsSlave(); + + GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter(); + RegistrationHandle groupIndexEventCounterHandle = + groupIndexedListeners.add("gerrit", groupIndexedCounter); + try { + // Running the reindexer right after startup should not need to reindex any group since + // reindexing was already done on startup. + slaveGroupIndexer.run(); + groupIndexedCounter.assertNoReindex(); + + // Create a group without updating the cache or index, + // then run the reindexer -> only the new group is reindexed. + String groupName = "foo"; + AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupName + "-UUID"); + groupsUpdate.createGroupInNoteDb( + InternalGroupCreation.builder() + .setGroupUUID(groupUuid) + .setNameKey(new AccountGroup.NameKey(groupName)) + .setId(new AccountGroup.Id(seq.nextGroupId())) + .build(), + InternalGroupUpdate.builder().build()); + slaveGroupIndexer.run(); + groupIndexedCounter.assertReindexOf(groupUuid); + + // Update a group without updating the cache or index, + // then run the reindexer -> only the updated group is reindexed. + groupsUpdate.updateGroupInNoteDb( + groupUuid, InternalGroupUpdate.builder().setDescription("bar").build()); + slaveGroupIndexer.run(); + groupIndexedCounter.assertReindexOf(groupUuid); + + // Delete a group without updating the cache or index, + // then run the reindexer -> only the deleted group is reindexed. + try (Repository repo = repoManager.openRepository(allUsers)) { + RefUpdate u = repo.updateRef(RefNames.refsGroups(groupUuid)); + u.setForceUpdate(true); + assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED); + } + slaveGroupIndexer.run(); + groupIndexedCounter.assertReindexOf(groupUuid); + } finally { + groupIndexEventCounterHandle.remove(); + } + } + + @Test + @Sandboxed + @GerritConfig(name = "index.scheduledIndexer.runOnStartup", value = "false") + @GerritConfig(name = "index.scheduledIndexer.enabled", value = "false") + @GerritConfig(name = "index.autoReindexIfStale", value = "false") + @IgnoreGroupInconsistencies + public void disabledReindexGroupsOnStartupSlaveMode() throws Exception { + List<AccountGroup.UUID> expectedGroups = + groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toList()); + assertThat(expectedGroups.size()).isAtLeast(2); + + restartAsSlave(); + + GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter(); + RegistrationHandle groupIndexEventCounterHandle = + groupIndexedListeners.add("gerrit", groupIndexedCounter); + try { + // No group indexing happened on startup. All groups should be reindexed now. + slaveGroupIndexer.run(); + groupIndexedCounter.assertReindexOf(expectedGroups); + } finally { + groupIndexEventCounterHandle.remove(); + } + } + + private static Correspondence<AccountInfo, String> getAccountToUsernameCorrespondence() { + return new Correspondence<AccountInfo, String>() { + @Override + public boolean compare(AccountInfo actualAccount, String expectedName) { + String username = actualAccount == null ? null : actualAccount.username; + return Objects.equals(username, expectedName); + } + + @Override + public String toString() { + return "has username"; + } + }; + } + + private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException { + // Evict group from cache to be sure that we use the index state for staleness checks. + groupCache.evict(groupUuid); + assertThat(stalenessChecker.isStale(groupUuid)).isTrue(); + + // Reindex fixes staleness + groupIndexer.index(groupUuid); + assertThat(stalenessChecker.isStale(groupUuid)).isFalse(); + } + + private void pushToGroupBranchForReviewAndSubmit( + Project.NameKey project, String groupRef, String expectedError) throws Exception { + grantLabel( + "Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", false, REGISTERED_USERS, false); + grant(project, RefNames.REFS_GROUPS + "*", Permission.SUBMIT, false, REGISTERED_USERS); + + TestRepository<InMemoryRepository> repo = cloneProject(project); + fetch(repo, groupRef + ":groupRef"); + repo.reset("groupRef"); + + PushOneCommit.Result r = + pushFactory + .create( + db, admin.getIdent(), repo, "Update group config", "group.config", "some content") + .to(MagicBranch.NEW_CHANGE + groupRef); + r.assertOkStatus(); + assertThat(r.getChange().change().getDest().get()).isEqualTo(groupRef); + gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); + + if (expectedError != null) { + exception.expect(ResourceConflictException.class); + exception.expectMessage("group update not allowed"); + } + gApi.changes().id(r.getChangeId()).current().submit(); + } + + private void createBranch(Project.NameKey project, String ref) throws IOException { + try (Repository r = repoManager.openRepository(project); + ObjectInserter oi = r.newObjectInserter(); + RevWalk rw = new RevWalk(r)) { + ObjectId emptyCommit = createCommit(r, "Test change"); + RefUpdate updateRef = r.updateRef(ref); + updateRef.setExpectedOldObjectId(ObjectId.zeroId()); + updateRef.setNewObjectId(emptyCommit); + assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW); + } + } + + private ObjectId createCommit(Repository repo, String commitMessage) throws IOException { + return createCommit(repo, commitMessage, null); + } + + private ObjectId createCommit(Repository repo, String commitMessage, @Nullable ObjectId treeId) + throws IOException { + try (ObjectInserter oi = repo.newObjectInserter()) { + if (treeId == null) { + treeId = oi.insert(Constants.OBJ_TREE, new byte[] {}); + } + + PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs()); + CommitBuilder cb = new CommitBuilder(); + cb.setTreeId(treeId); + cb.setCommitter(ident); + cb.setAuthor(ident); + cb.setMessage(commitMessage); + + ObjectId commit = oi.insert(cb); + oi.flush(); + return commit; + } + } + + private void assertMemberAuditEvent( + GroupAuditEventInfo info, + Type expectedType, + Account.Id expectedUser, + Account.Id expectedMember) { + assertThat(info.user._accountId).isEqualTo(expectedUser.get()); + assertThat(info.type).isEqualTo(expectedType); + assertThat(info).isInstanceOf(UserMemberAuditEventInfo.class); + assertThat(((UserMemberAuditEventInfo) info).member._accountId).isEqualTo(expectedMember.get()); + } + + private void assertSubgroupAuditEvent( + GroupAuditEventInfo info, + Type expectedType, + Account.Id expectedUser, + String expectedMemberGroupName) { + assertThat(info.user._accountId).isEqualTo(expectedUser.get()); + assertThat(info.type).isEqualTo(expectedType); + assertThat(info).isInstanceOf(GroupMemberAuditEventInfo.class); + assertThat(((GroupMemberAuditEventInfo) info).member.name).isEqualTo(expectedMemberGroupName); + } + + private void assertMembers(String group, TestAccount... expectedMembers) throws Exception { + assertMembers( + gApi.groups().id(group).members(), + TestAccount.names(expectedMembers).stream().toArray(String[]::new)); + assertAccountInfos(Arrays.asList(expectedMembers), gApi.groups().id(group).members()); + } + + private void assertMembers(Iterable<AccountInfo> members, String... expectedNames) { + assertThat(Iterables.transform(members, i -> i.name)) + .containsExactlyElementsIn(Arrays.asList(expectedNames)) + .inOrder(); + } + + private void assertNoMembers(String group) throws Exception { + assertThat(gApi.groups().id(group).members()).isEmpty(); + } + + private void assertIncludes(String group, String... expectedNames) throws Exception { + assertIncludes(gApi.groups().id(group).includedGroups(), expectedNames); + } + + private static void assertIncludes(Iterable<GroupInfo> includes, String... expectedNames) { + assertThat(Iterables.transform(includes, i -> i.name)) + .containsExactlyElementsIn(Arrays.asList(expectedNames)) + .inOrder(); + } + + private void assertNoIncludes(String group) throws Exception { + assertThat(gApi.groups().id(group).includedGroups()).isEmpty(); + } + + private void assertBadRequest(ListRequest req) throws Exception { + try { + req.get(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { + // Expected + } + } + + @Target({METHOD}) + @Retention(RUNTIME) + private @interface IgnoreGroupInconsistencies {} + + /** Checks if a group is indexed the correct number of times. */ + private static class GroupIndexedCounter implements GroupIndexedListener { + private final AtomicLongMap<String> countsByGroup = AtomicLongMap.create(); + + @Override + public void onGroupIndexed(String uuid) { + countsByGroup.incrementAndGet(uuid); + } + + void clear() { + countsByGroup.clear(); + } + + long getCount(AccountGroup.UUID groupUuid) { + return countsByGroup.get(groupUuid.get()); + } + + void assertReindexOf(AccountGroup.UUID groupUuid) { + assertReindexOf(ImmutableList.of(groupUuid)); + } + + void assertReindexOf(List<AccountGroup.UUID> groupUuids) { + for (AccountGroup.UUID groupUuid : groupUuids) { + assertThat(getCount(groupUuid)).named(groupUuid.get()).isEqualTo(1); + } + assertThat(countsByGroup).hasSize(groupUuids.size()); + clear(); + } + + void assertNoReindex() { + assertThat(countsByGroup).isEmpty(); + } + } +} |