]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8751 factor organization creation and attr validation
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 9 Feb 2017 10:42:58 +0000 (11:42 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 10 Feb 2017 17:21:45 +0000 (18:21 +0100)
into dedicated classes: OrganizationValidation and OrganizationCreation

13 files changed:
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreation.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationValidation.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationValidationImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/ws/CreateAction.java
server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsSupport.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationValidationImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/organization/ws/CreateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/organization/ws/UpdateActionTest.java

diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreation.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreation.java
new file mode 100644 (file)
index 0000000..0fc54a7
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.organization;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.OrganizationDto;
+
+import static java.util.Objects.requireNonNull;
+
+public interface OrganizationCreation {
+  String OWNERS_GROUP_NAME = "Owners";
+  String OWNERS_GROUP_DESCRIPTION_PATTERN = "Owners of organization %s";
+  String PERM_TEMPLATE_DESCRIPTION_PATTERN = "Default permission template of organization %s";
+
+  /**
+   * Create a new Organization with the specified properties and of which the specified user will assign Administer
+   * Organization permission.
+   * <p>
+   * This method does several operations at once:
+   * <ol>
+   *   <li>create an ungarded organization with the specified details</li>
+   *   <li>create a group called {@link #OWNERS_GROUP_NAME Owners} with Administer Organization permission</li>
+   *   <li>make the specified user a member of this group</li>
+   *   <li>create a default template for the organization (which name and description will follow patterns
+   *       {@link #OWNERS_GROUP_NAME} and {@link #OWNERS_GROUP_DESCRIPTION_PATTERN} based on the organization name)</li>
+   *   <li>this group defines the specified permissions (which effectively makes projects public):
+   *     <ul>
+   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ADMIN ADMIN}</li>
+   *       <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ISSUE_ADMIN ISSUE_ADMIN}</li>
+   *       <li>any one : {@link UserRole#USER USER}</li>
+   *       <li>any one : {@link UserRole#CODEVIEWER CODEVIEWER}</li>
+   *     </ul>
+   *   </li>
+   * </ol>
+   * </p>
+   *
+   * @return the created organization
+   *
+   * @throws KeyConflictException if an organization with the specified key already exists
+   * @throws IllegalArgumentException if any field of {@code newOrganization} is invalid according to {@link OrganizationValidation}
+   */
+  OrganizationDto create(DbSession dbSession, long createUserId, NewOrganization newOrganization) throws KeyConflictException;
+
+  final class KeyConflictException extends Exception {
+    public KeyConflictException(String message) {
+      super(message);
+    }
+  }
+
+  final class NewOrganization {
+    private final String key;
+    private final String name;
+    @CheckForNull
+    private final String description;
+    @CheckForNull
+    private final String url;
+    @CheckForNull
+    private final String avatar;
+
+    private NewOrganization(Builder builder) {
+      this.key = builder.key;
+      this.name = builder.name;
+      this.description = builder.description;
+      this.url = builder.url;
+      this.avatar = builder.avatarUrl;
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    @CheckForNull
+    public String getDescription() {
+      return description;
+    }
+
+    @CheckForNull
+    public String getUrl() {
+      return url;
+    }
+
+    @CheckForNull
+    public String getAvatar() {
+      return avatar;
+    }
+
+    public static NewOrganization.Builder newOrganizationBuilder() {
+      return new Builder();
+    }
+
+    public static final class Builder {
+      private String key;
+      private String name;
+      private String description;
+      private String url;
+      private String avatarUrl;
+
+      private Builder() {
+        // use factory method
+      }
+
+      public Builder setKey(String key) {
+        this.key = requireNonNull(key, "key can't be null");
+        return this;
+      }
+
+      public Builder setName(String name) {
+        this.name = requireNonNull(name, "name can't be null");
+        return this;
+      }
+
+      public Builder setDescription(@Nullable String description) {
+        this.description = description;
+        return this;
+      }
+
+      public Builder setUrl(@Nullable String url) {
+        this.url = url;
+        return this;
+      }
+
+      public Builder setAvatarUrl(@Nullable String avatarUrl) {
+        this.avatarUrl = avatarUrl;
+        return this;
+      }
+
+      public NewOrganization build() {
+        requireNonNull(key, "key can't be null");
+        requireNonNull(name, "name can't be null");
+        return new NewOrganization(this);
+      }
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java
new file mode 100644 (file)
index 0000000..a802de4
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.organization;
+
+import java.util.Date;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.DefaultTemplates;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.permission.GroupPermissionDto;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserGroupDto;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public class OrganizationCreationImpl implements OrganizationCreation {
+  private final DbClient dbClient;
+  private final System2 system2;
+  private final UuidFactory uuidFactory;
+  private final OrganizationValidation organizationValidation;
+
+  public OrganizationCreationImpl(DbClient dbClient, System2 system2, UuidFactory uuidFactory,
+    OrganizationValidation organizationValidation) {
+    this.dbClient = dbClient;
+    this.system2 = system2;
+    this.uuidFactory = uuidFactory;
+    this.organizationValidation = organizationValidation;
+  }
+
+  @Override
+  public OrganizationDto create(DbSession dbSession, long creatorUserId, NewOrganization newOrganization) throws KeyConflictException {
+    validate(newOrganization);
+    String key = newOrganization.getKey();
+    if (organizationKeyIsUsed(dbSession, key)) {
+      throw new KeyConflictException(format("Organization key '%s' is already used", key));
+    }
+
+    OrganizationDto organization = insertOrganization(dbSession, newOrganization);
+    GroupDto group = insertOwnersGroup(dbSession, organization);
+    insertDefaultTemplate(dbSession, organization, group);
+    addCurrentUserToGroup(dbSession, group, creatorUserId);
+
+    dbSession.commit();
+
+    return organization;
+  }
+
+  private void validate(NewOrganization newOrganization) {
+    requireNonNull(newOrganization, "newOrganization can't be null");
+    organizationValidation.checkName(newOrganization.getName());
+    organizationValidation.checkKey(newOrganization.getKey());
+    organizationValidation.checkDescription(newOrganization.getDescription());
+    organizationValidation.checkUrl(newOrganization.getUrl());
+    organizationValidation.checkAvatar(newOrganization.getAvatar());
+  }
+
+  private OrganizationDto insertOrganization(DbSession dbSession, NewOrganization newOrganization) {
+    OrganizationDto res = new OrganizationDto()
+      .setUuid(uuidFactory.create())
+      .setName(newOrganization.getName())
+      .setKey(newOrganization.getKey())
+      .setDescription(newOrganization.getDescription())
+      .setUrl(newOrganization.getUrl())
+      .setAvatarUrl(newOrganization.getAvatar());
+    dbClient.organizationDao().insert(dbSession, res);
+    return res;
+  }
+
+  private boolean organizationKeyIsUsed(DbSession dbSession, String key) {
+    return dbClient.organizationDao().selectByKey(dbSession, key).isPresent();
+  }
+
+  private void insertDefaultTemplate(DbSession dbSession, OrganizationDto organizationDto, GroupDto group) {
+    Date now = new Date(system2.now());
+    PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert(
+      dbSession,
+      new PermissionTemplateDto()
+        .setOrganizationUuid(organizationDto.getUuid())
+        .setUuid(uuidFactory.create())
+        .setName("Default template")
+        .setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName()))
+        .setCreatedAt(now)
+        .setUpdatedAt(now));
+
+    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.ADMIN, group);
+    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.ISSUE_ADMIN, group);
+    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.USER, null);
+    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.CODEVIEWER, null);
+
+    dbClient.organizationDao().setDefaultTemplates(
+      dbSession,
+      organizationDto.getUuid(),
+      new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid()));
+  }
+
+  private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, @Nullable GroupDto group) {
+    dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getId(), group == null ? null : group.getId(), permission);
+  }
+
+  /**
+   * Owners group has an hard coded name, a description based on the organization's name and has all global permissions.
+   */
+  private GroupDto insertOwnersGroup(DbSession dbSession, OrganizationDto organization) {
+    GroupDto group = dbClient.groupDao().insert(dbSession, new GroupDto()
+      .setOrganizationUuid(organization.getUuid())
+      .setName(OWNERS_GROUP_NAME)
+      .setDescription(format(OWNERS_GROUP_DESCRIPTION_PATTERN, organization.getName())));
+    GlobalPermissions.ALL.forEach(permission -> addPermissionToGroup(dbSession, group, permission));
+    return group;
+  }
+
+  private void addPermissionToGroup(DbSession dbSession, GroupDto group, String permission) {
+    dbClient.groupPermissionDao().insert(
+      dbSession,
+      new GroupPermissionDto()
+        .setOrganizationUuid(group.getOrganizationUuid())
+        .setGroupId(group.getId())
+        .setRole(permission));
+  }
+
+  private void addCurrentUserToGroup(DbSession dbSession, GroupDto group, long createUserId) {
+    dbClient.userGroupDao().insert(
+      dbSession,
+      new UserGroupDto().setGroupId(group.getId()).setUserId(createUserId));
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationValidation.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationValidation.java
new file mode 100644 (file)
index 0000000..964b371
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.organization;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public interface OrganizationValidation {
+  int KEY_MIN_LENGTH = 2;
+  int KEY_MAX_LENGTH = 32;
+  int NAME_MIN_LENGTH = 2;
+  int NAME_MAX_LENGTH = 64;
+  int DESCRIPTION_MAX_LENGTH = 256;
+  int URL_MAX_LENGTH = 256;
+
+  /**
+   * Ensures the specified argument is a valid key by failing with an exception if it is not so.
+   * <p>
+   * A valid key is non null and its length is between {@link #KEY_MIN_LENGTH 2} and {@link #KEY_MAX_LENGTH 32}.
+   * </p>
+   *
+   * @return the argument
+   *
+   * @throws NullPointerException if argument is {@code null}.
+   * @throws IllegalArgumentException if argument is not a valid key.
+   */
+  String checkKey(String keyCandidate);
+
+  /**
+   * Ensures the specified argument is a valid name by failing with an exception if it is not so.
+   * <p>
+   * A valid name is non null and its length is between {@link #NAME_MIN_LENGTH 2} and {@link #NAME_MAX_LENGTH 64}.
+   * </p>
+   *
+   * @return the argument
+   *
+   * @throws NullPointerException if argument is {@code null}.
+   * @throws IllegalArgumentException if argument is not a valid name.
+   */
+  String checkName(String nameCandidate);
+
+  /**
+   * Ensures the specified argument is either {@code null}, empty or a valid description by failing with an exception
+   * if it is not so.
+   * <p>
+   * The length of a valid url can't be more than {@link #DESCRIPTION_MAX_LENGTH 256}.
+   * </p>
+   *
+   * @return the argument
+   *
+   * @throws IllegalArgumentException if argument is not a valid description.
+   */
+  @CheckForNull
+  String checkDescription(@Nullable String descriptionCandidate);
+
+  /**
+   * Ensures the specified argument is either {@code null}, empty or a valid URL by failing with an exception if it is
+   * not so.
+   * <p>
+   * The length of a valid URL can't be more than {@link #URL_MAX_LENGTH 256}.
+   * </p>
+   *
+   * @return the argument
+   *
+   * @throws IllegalArgumentException if argument is not a valid url.
+   */
+  @CheckForNull
+  String checkUrl(@Nullable String urlCandidate);
+
+  /**
+   * Ensures the specified argument is either {@code null}, empty or a valid avatar URL by failing with an exception if
+   * it is not so.
+   * <p>
+   * The length of a valid avatar URL can't be more than {@link #URL_MAX_LENGTH 256}.
+   * </p>
+   *
+   * @return the argument
+   *
+   * @throws IllegalArgumentException if argument is not a valid avatar url.
+   */
+  @CheckForNull
+  String checkAvatar(@Nullable String avatarCandidate);
+
+  /**
+   * Transforms the specified string into a valid key.
+   *
+   * @see #checkKey(String)
+   */
+  String generateKeyFrom(String source);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationValidationImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationValidationImpl.java
new file mode 100644 (file)
index 0000000..1942166
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.organization;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.Math.min;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.core.util.Slug.slugify;
+
+public class OrganizationValidationImpl implements OrganizationValidation {
+
+  @Override
+  public String checkKey(String keyCandidate) {
+    requireNonNull(keyCandidate, "key can't be null");
+    checkArgument(keyCandidate.length() >= KEY_MIN_LENGTH, "Key '%s' must be at least %s chars long", keyCandidate, KEY_MIN_LENGTH);
+    checkArgument(keyCandidate.length() <= KEY_MAX_LENGTH, "Key '%s' must be at most %s chars long", keyCandidate, KEY_MAX_LENGTH);
+    checkArgument(slugify(keyCandidate).equals(keyCandidate), "Key '%s' contains at least one invalid char", keyCandidate);
+
+    return keyCandidate;
+  }
+
+  @Override
+  public String checkName(String nameCandidate) {
+    requireNonNull(nameCandidate, "name can't be null");
+
+    checkArgument(nameCandidate.length() >= NAME_MIN_LENGTH, "Name '%s' must be at least %s chars long", nameCandidate, NAME_MIN_LENGTH);
+    checkArgument(nameCandidate.length() <= NAME_MAX_LENGTH, "Name '%s' must be at most %s chars long", nameCandidate, NAME_MAX_LENGTH);
+
+    return nameCandidate;
+  }
+
+  @Override
+  public String checkDescription(@Nullable String descriptionCandidate) {
+    checkParamMaxLength(descriptionCandidate, "Description", DESCRIPTION_MAX_LENGTH);
+
+    return descriptionCandidate;
+  }
+
+  @Override
+  public String checkUrl(@Nullable String urlCandidate) {
+    checkParamMaxLength(urlCandidate, "Url", URL_MAX_LENGTH);
+
+    return urlCandidate;
+  }
+
+  @Override
+  public String checkAvatar(@Nullable String avatarCandidate) {
+    checkParamMaxLength(avatarCandidate, "Avatar", URL_MAX_LENGTH);
+
+    return avatarCandidate;
+  }
+
+  @CheckForNull
+  private static void checkParamMaxLength(@Nullable String value, String label, int maxLength) {
+    if (value != null) {
+      checkArgument(value.length() <= maxLength, "%s '%s' must be at most %s chars long", label, value, maxLength);
+    }
+  }
+
+  @Override
+  public String generateKeyFrom(String source) {
+    return slugify(source.substring(0, min(source.length(), KEY_MAX_LENGTH)));
+  }
+}
index 901c53cbf61dbf24c8ab57bc60ae5cbe463ea1b1..57dc62b0a8dd44f0815d254117cdb9edeac4b197 100644 (file)
  */
 package org.sonar.server.organization.ws;
 
-import java.util.Date;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.config.Settings;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.utils.System2;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.config.CorePropertyDefinitions;
-import org.sonar.core.permission.GlobalPermissions;
-import org.sonar.core.util.UuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
-import org.sonar.db.organization.DefaultTemplates;
 import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.permission.GroupPermissionDto;
-import org.sonar.db.permission.template.PermissionTemplateDto;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserGroupDto;
+import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationValidation;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Organizations.CreateWsResponse;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static java.lang.Math.min;
-import static java.lang.String.format;
-import static org.sonar.core.util.Slug.slugify;
-import static org.sonar.server.organization.ws.OrganizationsWsSupport.KEY_MAX_LENGTH;
-import static org.sonar.server.organization.ws.OrganizationsWsSupport.KEY_MIN_LENGTH;
-import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_AVATAR_URL;
-import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_DESCRIPTION;
+import static org.sonar.server.organization.OrganizationCreation.NewOrganization.newOrganizationBuilder;
 import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_KEY;
-import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_URL;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class CreateAction implements OrganizationsAction {
   private static final String ACTION = "create";
-  private static final String OWNERS_GROUP_NAME = "Owners";
-  private static final String OWNERS_GROUP_DESCRIPTION_PATTERN = "Owners of organization %s";
-  private static final String PERM_TEMPLATE_DESCRIPTION_PATTERN = "Default permission template of organization %s";
 
   private final Settings settings;
   private final UserSession userSession;
   private final DbClient dbClient;
-  private final UuidFactory uuidFactory;
   private final OrganizationsWsSupport wsSupport;
-  private final System2 system2;
+  private final OrganizationValidation organizationValidation;
+  private final OrganizationCreation organizationCreation;
 
-  public CreateAction(Settings settings, UserSession userSession, DbClient dbClient, UuidFactory uuidFactory,
-    OrganizationsWsSupport wsSupport, System2 system2) {
+  public CreateAction(Settings settings, UserSession userSession, DbClient dbClient, OrganizationsWsSupport wsSupport,
+    OrganizationValidation organizationValidation, OrganizationCreation organizationCreation) {
     this.settings = settings;
     this.userSession = userSession;
     this.dbClient = dbClient;
-    this.uuidFactory = uuidFactory;
     this.wsSupport = wsSupport;
-    this.system2 = system2;
+    this.organizationValidation = organizationValidation;
+    this.organizationCreation = organizationCreation;
   }
 
   @Override
@@ -110,118 +92,45 @@ public class CreateAction implements OrganizationsAction {
     String name = wsSupport.getAndCheckMandatoryName(request);
     String requestKey = getAndCheckKey(request);
     String key = useOrGenerateKey(requestKey, name);
-    wsSupport.getAndCheckDescription(request);
-    wsSupport.getAndCheckUrl(request);
-    wsSupport.getAndCheckAvatar(request);
+    String description = wsSupport.getAndCheckDescription(request);
+    String url = wsSupport.getAndCheckUrl(request);
+    String avatar = wsSupport.getAndCheckAvatar(request);
 
     try (DbSession dbSession = dbClient.openSession(false)) {
-      checkKeyIsNotUsed(dbSession, key, requestKey, name);
-
-      OrganizationDto organization = createOrganizationDto(dbSession, request, name, key);
-      GroupDto group = createOwnersGroup(dbSession, organization);
-      createDefaultTemplate(dbSession, organization, group);
-      addCurrentUserToGroup(dbSession, group);
-
-      dbSession.commit();
+      OrganizationDto organization = organizationCreation.create(
+        dbSession,
+        userSession.getUserId().longValue(),
+        newOrganizationBuilder()
+          .setName(name)
+          .setKey(key)
+          .setDescription(description)
+          .setUrl(url)
+          .setAvatarUrl(avatar)
+          .build());
 
       writeResponse(request, response, organization);
+    } catch (OrganizationCreation.KeyConflictException e) {
+      checkArgument(requestKey == null, "Key '%s' is already used. Specify another one.", key);
+      checkArgument(requestKey != null, "Key '%s' generated from name '%s' is already used. Specify one.", key, name);
     }
   }
 
-  private OrganizationDto createOrganizationDto(DbSession dbSession, Request request, String name, String key) {
-    OrganizationDto res = new OrganizationDto()
-      .setUuid(uuidFactory.create())
-      .setName(name)
-      .setKey(key)
-      .setDescription(request.param(PARAM_DESCRIPTION))
-      .setUrl(request.param(PARAM_URL))
-      .setAvatarUrl(request.param(PARAM_AVATAR_URL));
-    dbClient.organizationDao().insert(dbSession, res);
-    return res;
-  }
-
-  private void createDefaultTemplate(DbSession dbSession, OrganizationDto organizationDto, GroupDto group) {
-    Date now = new Date(system2.now());
-    PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert(
-      dbSession,
-      new PermissionTemplateDto()
-        .setOrganizationUuid(organizationDto.getUuid())
-        .setUuid(uuidFactory.create())
-        .setName("Default template")
-        .setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName()))
-        .setCreatedAt(now)
-        .setUpdatedAt(now));
-
-    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.ADMIN, group);
-    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.ISSUE_ADMIN, group);
-    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.USER, null);
-    insertGroupPermission(dbSession, permissionTemplateDto, UserRole.CODEVIEWER, null);
-
-    dbClient.organizationDao().setDefaultTemplates(
-      dbSession,
-      organizationDto.getUuid(),
-      new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid()));
-  }
-
-  private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, @Nullable GroupDto group) {
-    dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getId(), group == null ? null : group.getId(), permission);
-  }
-
-  /**
-   * Owners group has an hard coded name, a description based on the organization's name and has all global permissions.
-   */
-  private GroupDto createOwnersGroup(DbSession dbSession, OrganizationDto organization) {
-    GroupDto group = dbClient.groupDao().insert(dbSession, new GroupDto()
-      .setOrganizationUuid(organization.getUuid())
-      .setName(OWNERS_GROUP_NAME)
-      .setDescription(format(OWNERS_GROUP_DESCRIPTION_PATTERN, organization.getName())));
-    GlobalPermissions.ALL.forEach(permission -> addPermissionToGroup(dbSession, group, permission));
-    return group;
-  }
-
-  private void addPermissionToGroup(DbSession dbSession, GroupDto group, String permission) {
-    dbClient.groupPermissionDao().insert(
-      dbSession,
-      new GroupPermissionDto()
-        .setOrganizationUuid(group.getOrganizationUuid())
-        .setGroupId(group.getId())
-        .setRole(permission));
-  }
-
-  private void addCurrentUserToGroup(DbSession dbSession, GroupDto group) {
-    dbClient.userGroupDao().insert(
-      dbSession,
-      new UserGroupDto().setGroupId(group.getId()).setUserId(userSession.getUserId().longValue()));
-  }
-
   @CheckForNull
-  private static String getAndCheckKey(Request request) {
+  private String getAndCheckKey(Request request) {
     String rqstKey = request.param(PARAM_KEY);
     if (rqstKey != null) {
-      checkArgument(rqstKey.length() >= KEY_MIN_LENGTH, "Key '%s' must be at least %s chars long", rqstKey, KEY_MIN_LENGTH);
-      checkArgument(rqstKey.length() <= KEY_MAX_LENGTH, "Key '%s' must be at most %s chars long", rqstKey, KEY_MAX_LENGTH);
-      checkArgument(slugify(rqstKey).equals(rqstKey), "Key '%s' contains at least one invalid char", rqstKey);
+      return organizationValidation.checkKey(rqstKey);
     }
     return rqstKey;
   }
 
-  private static String useOrGenerateKey(@Nullable String key, String name) {
+  private String useOrGenerateKey(@Nullable String key, String name) {
     if (key == null) {
-      return slugify(name.substring(0, min(name.length(), KEY_MAX_LENGTH)));
+      return organizationValidation.generateKeyFrom(name);
     }
     return key;
   }
 
-  private void checkKeyIsNotUsed(DbSession dbSession, String key, @Nullable String requestKey, String name) {
-    boolean isUsed = checkKeyIsUsed(dbSession, key);
-    checkArgument(requestKey == null || !isUsed, "Key '%s' is already used. Specify another one.", key);
-    checkArgument(requestKey != null || !isUsed, "Key '%s' generated from name '%s' is already used. Specify one.", key, name);
-  }
-
-  private boolean checkKeyIsUsed(DbSession dbSession, String key) {
-    return dbClient.organizationDao().selectByKey(dbSession, key).isPresent();
-  }
-
   private void writeResponse(Request request, Response response, OrganizationDto dto) {
     writeProtobuf(
       CreateWsResponse.newBuilder().setOrganization(wsSupport.toOrganization(dto)).build(),
index 2a1c3667604c07f91c7f9cfc7b851e05d5f1886c..42019093cd604a752ec677d10f1db62a67294c84 100644 (file)
@@ -23,9 +23,9 @@ import javax.annotation.CheckForNull;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.organization.OrganizationValidation;
 import org.sonarqube.ws.Organizations;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static org.sonar.core.util.Protobuf.setNullable;
 
 /**
@@ -37,16 +37,16 @@ public class OrganizationsWsSupport {
   static final String PARAM_DESCRIPTION = "description";
   static final String PARAM_URL = "url";
   static final String PARAM_AVATAR_URL = "avatar";
-  static final int KEY_MIN_LENGTH = 2;
-  static final int KEY_MAX_LENGTH = 32;
-  static final int NAME_MIN_LENGTH = 2;
-  static final int NAME_MAX_LENGTH = 64;
-  static final int DESCRIPTION_MAX_LENGTH = 256;
-  static final int URL_MAX_LENGTH = 256;
+
+  private final OrganizationValidation organizationValidation;
+
+  public OrganizationsWsSupport(OrganizationValidation organizationValidation) {
+    this.organizationValidation = organizationValidation;
+  }
 
   String getAndCheckMandatoryName(Request request) {
     String name = request.mandatoryParam(PARAM_NAME);
-    checkName(name);
+    organizationValidation.checkName(name);
     return name;
   }
 
@@ -54,38 +54,24 @@ public class OrganizationsWsSupport {
   String getAndCheckName(Request request) {
     String name = request.param(PARAM_NAME);
     if (name != null) {
-      checkName(name);
+      organizationValidation.checkName(name);
     }
     return name;
   }
 
-  private static void checkName(String name) {
-    checkArgument(name.length() >= NAME_MIN_LENGTH, "Name '%s' must be at least %s chars long", name, NAME_MIN_LENGTH);
-    checkArgument(name.length() <= NAME_MAX_LENGTH, "Name '%s' must be at most %s chars long", name, NAME_MAX_LENGTH);
-  }
-
   @CheckForNull
   String getAndCheckAvatar(Request request) {
-    return getAndCheckParamMaxLength(request, PARAM_AVATAR_URL, URL_MAX_LENGTH);
+    return organizationValidation.checkAvatar(request.param(PARAM_AVATAR_URL));
   }
 
   @CheckForNull
   String getAndCheckUrl(Request request) {
-    return getAndCheckParamMaxLength(request, PARAM_URL, URL_MAX_LENGTH);
+    return organizationValidation.checkUrl(request.param(PARAM_URL));
   }
 
   @CheckForNull
   String getAndCheckDescription(Request request) {
-    return getAndCheckParamMaxLength(request, PARAM_DESCRIPTION, DESCRIPTION_MAX_LENGTH);
-  }
-
-  @CheckForNull
-  private static String getAndCheckParamMaxLength(Request request, String key, int maxLength) {
-    String value = request.param(key);
-    if (value != null) {
-      checkArgument(value.length() <= maxLength, "%s '%s' must be at most %s chars long", key, value, maxLength);
-    }
-    return value;
+    return organizationValidation.checkDescription(request.param(PARAM_DESCRIPTION));
   }
 
   void addOrganizationDetailsParams(WebService.NewAction action, boolean isNameRequired) {
index 7d34fafd99265fd3eba19f50dd6b39cc775b4df5..ed03c036ad37250c6e395e7369f75ae1a5e3b589 100644 (file)
@@ -82,6 +82,8 @@ import org.sonar.server.metric.CoreCustomMetrics;
 import org.sonar.server.metric.DefaultMetricFinder;
 import org.sonar.server.metric.ws.MetricsWsModule;
 import org.sonar.server.notification.NotificationModule;
+import org.sonar.server.organization.OrganizationCreationImpl;
+import org.sonar.server.organization.OrganizationValidationImpl;
 import org.sonar.server.organization.ws.OrganizationsWsModule;
 import org.sonar.server.permission.GroupPermissionChanger;
 import org.sonar.server.permission.PermissionTemplateService;
@@ -247,6 +249,8 @@ public class PlatformLevel4 extends PlatformLevel {
       UpdateCenterModule.class,
 
       // organizations
+      OrganizationValidationImpl.class,
+      OrganizationCreationImpl.class,
       OrganizationsWsModule.class,
 
       // quality profile
index c9d838768c7106296e79594e3e349d13f299aca4..be7ffabefe5a278b5fb7c02f42e1fe198ff0b947 100644 (file)
@@ -57,7 +57,7 @@ import static org.sonar.server.ws.WsUtils.checkFound;
 @ServerSide
 public class UserUpdater {
 
-  public static final String SQ_AUTHORITY = "sonarqube";
+  private static final String SQ_AUTHORITY = "sonarqube";
 
   private static final String LOGIN_PARAM = "Login";
   private static final String PASSWORD_PARAM = "Password";
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java
new file mode 100644 (file)
index 0000000..33e817d
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.organization;
+
+import java.util.List;
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.DefaultTemplates;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.permission.template.PermissionTemplateDto;
+import org.sonar.db.permission.template.PermissionTemplateGroupDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserMembershipDto;
+import org.sonar.db.user.UserMembershipQuery;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.organization.OrganizationCreation.NewOrganization.newOrganizationBuilder;
+
+public class OrganizationCreationImplTest {
+  private static final long SOME_USER_ID = 1L;
+  private static final String SOME_UUID = "org-uuid";
+  private static final long SOME_DATE = 12893434L;
+  private OrganizationCreation.NewOrganization FULL_POPULATED_NEW_ORGANIZATION = newOrganizationBuilder()
+    .setName("a-name")
+    .setKey("a-key")
+    .setDescription("a-description")
+    .setUrl("a-url")
+    .setAvatarUrl("a-avatar")
+    .build();
+
+  private System2 system2 = mock(System2.class);
+
+  @Rule
+  public DbTester dbTester = DbTester.create(system2);
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private DbSession dbSession = dbTester.getSession();
+
+  private IllegalArgumentException exceptionThrownByOrganizationValidation = new IllegalArgumentException("simulate IAE thrown by OrganizationValidation");
+  private DbClient dbClient = dbTester.getDbClient();
+  private UuidFactory uuidFactory = mock(UuidFactory.class);
+  private OrganizationValidation organizationValidation = mock(OrganizationValidation.class);
+
+  private OrganizationCreationImpl underTest = new OrganizationCreationImpl(dbClient, system2, uuidFactory, organizationValidation);
+
+  @Test
+  public void create_throws_NPE_if_NewOrganization_arg_is_null() throws OrganizationCreation.KeyConflictException {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("newOrganization can't be null");
+
+    underTest.create(dbSession, SOME_USER_ID, null);
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidKey() throws OrganizationCreation.KeyConflictException {
+    when(organizationValidation.checkKey(FULL_POPULATED_NEW_ORGANIZATION.getKey()))
+      .thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation();
+  }
+
+  private void createThrowsExceptionThrownByOrganizationValidation() throws OrganizationCreation.KeyConflictException {
+    try {
+      underTest.create(dbSession, SOME_USER_ID, FULL_POPULATED_NEW_ORGANIZATION);
+      fail(exceptionThrownByOrganizationValidation + " should have been thrown");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).isSameAs(exceptionThrownByOrganizationValidation);
+    }
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidDescription() throws OrganizationCreation.KeyConflictException {
+    when(organizationValidation.checkDescription(FULL_POPULATED_NEW_ORGANIZATION.getDescription())).thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation();
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidUrl() throws OrganizationCreation.KeyConflictException {
+    when(organizationValidation.checkUrl(FULL_POPULATED_NEW_ORGANIZATION.getUrl())).thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation();
+  }
+
+  @Test
+  public void create_throws_exception_thrown_by_checkValidAvatar() throws OrganizationCreation.KeyConflictException {
+    when(organizationValidation.checkAvatar(FULL_POPULATED_NEW_ORGANIZATION.getAvatar())).thenThrow(exceptionThrownByOrganizationValidation);
+
+    createThrowsExceptionThrownByOrganizationValidation();
+  }
+
+  @Test
+  public void create_fails_with_KeyConflictException_if_org_with_key_in_NewOrganization_arg_already_exists_in_db() throws OrganizationCreation.KeyConflictException {
+    dbTester.organizations().insertForKey(FULL_POPULATED_NEW_ORGANIZATION.getKey());
+
+    expectedException.expect(OrganizationCreation.KeyConflictException.class);
+    expectedException.expectMessage("Organization key '" + FULL_POPULATED_NEW_ORGANIZATION.getKey() + "' is already used");
+
+    underTest.create(dbSession, SOME_USER_ID, FULL_POPULATED_NEW_ORGANIZATION);
+  }
+
+  @Test
+  public void create_creates_unguarded_organization_with_properties_from_NewOrganization_arg() throws OrganizationCreation.KeyConflictException {
+    mockForSuccessfulInsert(SOME_UUID, SOME_DATE);
+
+    underTest.create(dbSession, SOME_USER_ID, FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    assertThat(organization.getUuid()).isEqualTo(SOME_UUID);
+    assertThat(organization.getKey()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getKey());
+    assertThat(organization.getName()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getName());
+    assertThat(organization.getDescription()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getDescription());
+    assertThat(organization.getUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getUrl());
+    assertThat(organization.getAvatarUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getAvatar());
+    assertThat(organization.getCreatedAt()).isEqualTo(SOME_DATE);
+    assertThat(organization.getUpdatedAt()).isEqualTo(SOME_DATE);
+  }
+
+  @Test
+  public void create_creates_owners_group_with_all_permissions_for_new_organization_and_add_current_user_to_it() throws OrganizationCreation.KeyConflictException {
+    UserDto user = dbTester.users().insertUser();
+
+    mockForSuccessfulInsert(SOME_UUID, SOME_DATE);
+
+    underTest.create(dbSession, user.getId(), FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    Optional<GroupDto> groupDtoOptional = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Owners");
+    assertThat(groupDtoOptional).isNotEmpty();
+    GroupDto groupDto = groupDtoOptional.get();
+    assertThat(groupDto.getDescription()).isEqualTo("Owners of organization " + FULL_POPULATED_NEW_ORGANIZATION.getName());
+    assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, groupDto.getOrganizationUuid(), groupDto.getId()))
+      .containsOnly(GlobalPermissions.ALL.toArray(new String[GlobalPermissions.ALL.size()]));
+    List<UserMembershipDto> members = dbClient.groupMembershipDao().selectMembers(
+      dbSession,
+      UserMembershipQuery.builder().groupId(groupDto.getId()).membership(UserMembershipQuery.IN).build(), 0, Integer.MAX_VALUE);
+    assertThat(members)
+      .extracting(UserMembershipDto::getLogin)
+      .containsOnly(user.getLogin());
+  }
+
+  @Test
+  public void create_does_not_require_description_url_and_avatar_to_be_non_null() throws OrganizationCreation.KeyConflictException {
+    mockForSuccessfulInsert(SOME_UUID, SOME_DATE);
+
+    underTest.create(dbSession, SOME_USER_ID, newOrganizationBuilder()
+      .setKey("key")
+      .setName("name")
+      .build());
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, "key").get();
+    assertThat(organization.getKey()).isEqualTo("key");
+    assertThat(organization.getName()).isEqualTo("name");
+    assertThat(organization.getDescription()).isNull();
+    assertThat(organization.getUrl()).isNull();
+    assertThat(organization.getAvatarUrl()).isNull();
+  }
+
+  @Test
+  public void create_creates_default_template_for_new_organization() throws OrganizationCreation.KeyConflictException {
+    mockForSuccessfulInsert(SOME_UUID, SOME_DATE);
+
+    underTest.create(dbSession, SOME_USER_ID, FULL_POPULATED_NEW_ORGANIZATION);
+
+    OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get();
+    GroupDto ownersGroup = dbClient.groupDao().selectByName(dbSession, organization.getUuid(), "Owners").get();
+    PermissionTemplateDto defaultTemplate = dbClient.permissionTemplateDao().selectByName(dbSession, organization.getUuid(), "default template");
+    assertThat(defaultTemplate.getName()).isEqualTo("Default template");
+    assertThat(defaultTemplate.getDescription()).isEqualTo("Default permission template of organization " + FULL_POPULATED_NEW_ORGANIZATION.getName());
+    DefaultTemplates defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, organization.getUuid()).get();
+    assertThat(defaultTemplates.getProjectUuid()).isEqualTo(defaultTemplate.getUuid());
+    assertThat(defaultTemplates.getViewUuid()).isNull();
+    assertThat(dbClient.permissionTemplateDao().selectGroupPermissionsByTemplateId(dbSession, defaultTemplate.getId()))
+      .extracting(PermissionTemplateGroupDto::getGroupId, PermissionTemplateGroupDto::getPermission)
+      .containsOnly(
+        tuple(ownersGroup.getId(), UserRole.ADMIN), tuple(ownersGroup.getId(), UserRole.ISSUE_ADMIN),
+        tuple(0L, UserRole.USER), tuple(0L, UserRole.CODEVIEWER));
+  }
+
+  private void mockForSuccessfulInsert(String orgUuid, long orgDate) {
+    when(uuidFactory.create()).thenReturn(orgUuid);
+    when(system2.now()).thenReturn(orgDate);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationValidationImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationValidationImplTest.java
new file mode 100644 (file)
index 0000000..a23a0dc
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.organization;
+
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class OrganizationValidationImplTest {
+  private static final String STRING_32_CHARS = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+  private static final String STRING_64_CHARS = STRING_32_CHARS + STRING_32_CHARS;
+  private static final String STRING_256_CHARS = STRING_64_CHARS + STRING_64_CHARS + STRING_64_CHARS + STRING_64_CHARS;
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private OrganizationValidationImpl underTest = new OrganizationValidationImpl();
+
+  @Test
+  public void checkValidKey_throws_NPE_if_arg_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("key can't be null");
+
+    underTest.checkKey(null);
+  }
+
+  @Test
+  public void checkValidKey_throws_IAE_if_arg_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Key '' must be at least 2 chars long");
+
+    underTest.checkKey("");
+  }
+
+  @Test
+  public void checkValidKey_throws_IAE_if_arg_is_1_char_long() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Key 'a' must be at least 2 chars long");
+
+    underTest.checkKey("a");
+  }
+
+  @Test
+  public void checkValidKey_does_not_fail_if_arg_is_2_to_32_chars_long() {
+    String str = "aa";
+    for (int i = 0; i < 31; i++) {
+      underTest.checkKey(str);
+      str += "a";
+    }
+  }
+
+  @Test
+  public void checkValidKey_throws_IAE_if_arg_is_33_or_more_chars_long() {
+    String str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+    underTest.checkKey(str);
+    for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) {
+      str += "c";
+      try {
+        underTest.checkKey(str);
+        fail("A IllegalArgumentException should have been thrown");
+      } catch (IllegalArgumentException e) {
+        assertThat(e).hasMessage("Key '" + str + "' must be at most 32 chars long");
+      }
+    }
+  }
+
+  @Test
+  public void checkValidKey_throws_IAE_if_arg_contains_invalid_chars() {
+    char[] invalidChars = {'é', '<', '@'};
+
+    for (char invalidChar : invalidChars) {
+      String str = "aa" + invalidChar;
+      try {
+        underTest.checkKey(str);
+        fail("A IllegalArgumentException should have been thrown");
+      } catch (IllegalArgumentException e) {
+        assertThat(e).hasMessage("Key '" + str + "' contains at least one invalid char");
+      }
+    }
+  }
+
+  @Test
+  public void checkValidName_throws_NPE_if_arg_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("name can't be null");
+
+    underTest.checkName(null);
+  }
+
+  @Test
+  public void checkValidName_throws_IAE_if_arg_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Name '' must be at least 2 chars long");
+
+    underTest.checkName("");
+  }
+
+  @Test
+  public void checkValidName_throws_IAE_if_arg_is_1_char_long() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Name 'a' must be at least 2 chars long");
+
+    underTest.checkName("a");
+  }
+
+  @Test
+  public void checkValidName_does_not_fail_if_arg_is_2_to_32_chars_long() {
+    String str = "aa";
+    for (int i = 0; i < 63; i++) {
+      underTest.checkName(str);
+      str += "a";
+    }
+  }
+
+  @Test
+  public void checkValidName_throws_IAE_if_arg_is_65_or_more_chars_long() {
+    String str = STRING_64_CHARS;
+    underTest.checkName(str);
+    for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) {
+      str += "c";
+      try {
+        underTest.checkName(str);
+        fail("A IllegalArgumentException should have been thrown");
+      } catch (IllegalArgumentException e) {
+        assertThat(e).hasMessage("Name '" + str + "' must be at most 64 chars long");
+      }
+    }
+  }
+
+  @Test
+  public void checkValidDescription_does_not_fail_if_arg_is_null() {
+    underTest.checkDescription(null);
+  }
+
+  @Test
+  public void checkValidDescription_does_not_fail_if_arg_is_empty() {
+    underTest.checkDescription("");
+  }
+
+  @Test
+  public void checkValidDescription_does_not_fail_if_arg_is_1_to_256_chars_long() {
+    String str = "1";
+    for (int i = 0; i < 256; i++) {
+      underTest.checkDescription(str);
+      str += "a";
+    }
+  }
+
+  @Test
+  public void checkValidDescription_throws_IAE_if_arg_is_more_than_256_chars_long() {
+    String str = STRING_256_CHARS;
+    underTest.checkDescription(str);
+    for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) {
+      str += "c";
+      try {
+        underTest.checkDescription(str);
+        fail("A IllegalArgumentException should have been thrown");
+      } catch (IllegalArgumentException e) {
+        assertThat(e).hasMessage("Description '" + str + "' must be at most 256 chars long");
+      }
+    }
+  }
+
+  @Test
+  public void checkValidUrl_does_not_fail_if_arg_is_null() {
+    underTest.checkUrl(null);
+  }
+
+  @Test
+  public void checkValidUrl_does_not_fail_if_arg_is_1_to_256_chars_long() {
+    String str = "1";
+    for (int i = 0; i < 256; i++) {
+      underTest.checkUrl(str);
+      str += "a";
+    }
+  }
+
+  @Test
+  public void checkValidUrl_throws_IAE_if_arg_is_more_than_256_chars_long() {
+    String str = STRING_256_CHARS;
+    underTest.checkUrl(str);
+    for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) {
+      str += "c";
+      try {
+        underTest.checkUrl(str);
+        fail("A IllegalArgumentException should have been thrown");
+      } catch (IllegalArgumentException e) {
+        assertThat(e).hasMessage("Url '" + str + "' must be at most 256 chars long");
+      }
+    }
+  }
+
+  @Test
+  public void checkValidAvatar_does_not_fail_if_arg_is_null() {
+    underTest.checkAvatar(null);
+  }
+
+  @Test
+  public void checkValidAvatar_does_not_fail_if_arg_is_1_to_256_chars_long() {
+    String str = "1";
+    for (int i = 0; i < 256; i++) {
+      underTest.checkAvatar(str);
+      str += "a";
+    }
+  }
+
+  @Test
+  public void checkValidAvatar_throws_IAE_if_arg_is_more_than_256_chars_long() {
+    String str = STRING_256_CHARS;
+    underTest.checkAvatar(str);
+    for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) {
+      str += "c";
+      try {
+        underTest.checkAvatar(str);
+        fail("A IllegalArgumentException should have been thrown");
+      } catch (IllegalArgumentException e) {
+        assertThat(e).hasMessage("Avatar '" + str + "' must be at most 256 chars long");
+      }
+    }
+  }
+
+  @Test
+  public void generateKeyFrom_returns_slug_of_arg() {
+    assertThat(underTest.generateKeyFrom("foo")).isEqualTo("foo");
+    assertThat(underTest.generateKeyFrom("  FOO ")).isEqualTo("foo");
+    assertThat(underTest.generateKeyFrom("he's here")).isEqualTo("he-s-here");
+    assertThat(underTest.generateKeyFrom("foo-bar")).isEqualTo("foo-bar");
+    assertThat(underTest.generateKeyFrom("foo_bar")).isEqualTo("foo_bar");
+    assertThat(underTest.generateKeyFrom("accents éà")).isEqualTo("accents-ea");
+    assertThat(underTest.generateKeyFrom("<foo>")).isEqualTo("foo");
+    assertThat(underTest.generateKeyFrom("<\"foo:\">")).isEqualTo("foo");
+  }
+
+  @Test
+  public void generateKeyFrom_truncate_arg_to_32_chars() {
+    assertThat(underTest.generateKeyFrom(STRING_64_CHARS))
+      .isEqualTo(underTest.generateKeyFrom(STRING_256_CHARS))
+      .isEqualTo(underTest.generateKeyFrom(STRING_32_CHARS));
+  }
+}
index e3bd50af1ac1ab2f056abaf99f54b3b284352683..a4466e02a202993426fba1e658e896cfebc0252f 100644 (file)
@@ -32,7 +32,6 @@ import org.sonar.api.config.MapSettings;
 import org.sonar.api.config.Settings;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
-import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.util.UuidFactory;
@@ -50,6 +49,10 @@ import org.sonar.db.user.UserMembershipDto;
 import org.sonar.db.user.UserMembershipQuery;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.organization.OrganizationCreation;
+import org.sonar.server.organization.OrganizationCreationImpl;
+import org.sonar.server.organization.OrganizationValidation;
+import org.sonar.server.organization.OrganizationValidationImpl;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
@@ -85,7 +88,9 @@ public class CreateActionTest {
   private Settings settings = new MapSettings()
     .setProperty(ORGANIZATIONS_ANYONE_CAN_CREATE, false);
   private UuidFactory uuidFactory = mock(UuidFactory.class);
-  private CreateAction underTest = new CreateAction(settings, userSession, dbClient, uuidFactory, new OrganizationsWsSupport(), new AlwaysIncreasingSystem2());
+  private OrganizationValidation organizationValidation = new OrganizationValidationImpl();
+  private OrganizationCreation organizationCreation = new OrganizationCreationImpl(dbClient, system2, uuidFactory, organizationValidation);
+  private CreateAction underTest = new CreateAction(settings, userSession, dbClient, new OrganizationsWsSupport(organizationValidation), organizationValidation, organizationCreation);
   private WsActionTester wsTester = new WsActionTester(underTest);
 
   @Test
@@ -380,7 +385,7 @@ public class CreateActionTest {
     makeUserRoot();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("description '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
+    expectedException.expectMessage("Description '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
 
     executeRequest("foo", "bar", STRING_257_CHARS_LONG, null, null);
   }
@@ -400,7 +405,7 @@ public class CreateActionTest {
     makeUserRoot();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("url '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
+    expectedException.expectMessage("Url '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
 
     executeRequest("foo", "bar", null, STRING_257_CHARS_LONG, null);
   }
@@ -420,7 +425,7 @@ public class CreateActionTest {
     makeUserRoot();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("avatar '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
+    expectedException.expectMessage("Avatar '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
 
     executeRequest("foo", "bar", null, null, STRING_257_CHARS_LONG);
   }
index 3183b3eecdcd9521c7488b8cf89b8c363858fb0b..b2a6977848b947676ebc6385f58a1890bc75c893 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.core.util.Uuids;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.organization.OrganizationValidationImpl;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.MediaTypes;
@@ -67,7 +68,7 @@ public class SearchActionTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  private SearchAction underTest = new SearchAction(dbTester.getDbClient(), new OrganizationsWsSupport());
+  private SearchAction underTest = new SearchAction(dbTester.getDbClient(), new OrganizationsWsSupport(new OrganizationValidationImpl()));
   private WsActionTester wsTester = new WsActionTester(underTest);
 
   @Test
index 509da6a99f3492256522eeed05dad9140f358466..01c90b27fd2b0c5f543b11df587c9b4cc7669c45 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.db.DbTester;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.organization.OrganizationValidationImpl;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
@@ -58,7 +59,7 @@ public class UpdateActionTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  private UpdateAction underTest = new UpdateAction(userSession, new OrganizationsWsSupport(), dbTester.getDbClient());
+  private UpdateAction underTest = new UpdateAction(userSession, new OrganizationsWsSupport(new OrganizationValidationImpl()), dbTester.getDbClient());
   private WsActionTester wsTester = new WsActionTester(underTest);
 
   @Test
@@ -207,7 +208,7 @@ public class UpdateActionTest {
     userSession.logIn();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("description '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
+    expectedException.expectMessage("Description '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
 
     executeKeyRequest(SOME_KEY, "bar", STRING_257_CHARS_LONG, null, null);
   }
@@ -227,7 +228,7 @@ public class UpdateActionTest {
     userSession.logIn();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("url '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
+    expectedException.expectMessage("Url '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
 
     executeKeyRequest(SOME_KEY, "bar", null, STRING_257_CHARS_LONG, null);
   }
@@ -247,7 +248,7 @@ public class UpdateActionTest {
     userSession.logIn();
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("avatar '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
+    expectedException.expectMessage("Avatar '" + STRING_257_CHARS_LONG + "' must be at most 256 chars long");
 
     executeKeyRequest(SOME_KEY, "bar", null, null, STRING_257_CHARS_LONG);
   }