]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12366 Create WS to read/write New Code Periods
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Fri, 2 Aug 2019 19:14:47 +0000 (14:14 -0500)
committerSonarTech <sonartech@sonarsource.com>
Tue, 24 Sep 2019 18:21:11 +0000 (20:21 +0200)
server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodParser.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/setting/ws/GetNewCodePeriodAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/setting/ws/SetNewCodePeriodAction.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/setting/ws/SetNewCodePeriodActionTest.java [new file with mode: 0644]

diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodParser.java b/server/sonar-db-dao/src/main/java/org/sonar/db/newcodeperiod/NewCodePeriodParser.java
new file mode 100644 (file)
index 0000000..f0c2304
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.db.newcodeperiod;
+
+import java.time.LocalDate;
+
+public class NewCodePeriodParser {
+  public static LocalDate parseDate(String value) {
+    return LocalDate.parse(value);
+  }
+
+  public static int parseDays(String value) {
+    return Integer.parseInt(value);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/setting/ws/GetNewCodePeriodAction.java b/server/sonar-server/src/main/java/org/sonar/server/setting/ws/GetNewCodePeriodAction.java
new file mode 100644 (file)
index 0000000..47d00dd
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.newcodeperiod.NewCodePeriodDao;
+import org.sonar.server.user.UserSession;
+
+public class GetNewCodePeriodAction implements SettingsWsAction {
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final NewCodePeriodDao newCodePeriodDao;
+
+  public GetNewCodePeriodAction(DbClient dbClient, UserSession userSession, NewCodePeriodDao newCodePeriodDao) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.newCodePeriodDao = newCodePeriodDao;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    context.createAction("get_new_code_period")
+      .setDescription("Updates the setting for the New Code Period.<br>" +
+        "Requires one of the following permissions: " +
+        "<ul>" +
+        "<li>'Administer System'</li>" +
+        "<li>'Administer' rights on the specified component</li>" +
+        "</ul>")
+      .setSince("8.0")
+      .setResponseExample(getClass().getResource("generate_secret_key-example.json"))
+      .setHandler(this);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    userSession.checkIsSystemAdministrator();
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      // TODO should it fall back branch->project->global?
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/setting/ws/SetNewCodePeriodAction.java b/server/sonar-server/src/main/java/org/sonar/server/setting/ws/SetNewCodePeriodAction.java
new file mode 100644 (file)
index 0000000..bc8259c
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting.ws;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import java.util.Locale;
+import java.util.Set;
+import javax.annotation.Nullable;
+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.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodDao;
+import org.sonar.db.newcodeperiod.NewCodePeriodDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodParser;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.DATE;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
+import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS;
+import static org.sonar.server.component.ComponentFinder.ParamNames.PROJECT_ID_AND_KEY;
+
+public class SetNewCodePeriodAction implements SettingsWsAction {
+  private static final String PARAM_BRANCH = "branchKey";
+  private static final String PARAM_PROJECT = "projectKey";
+  private static final String PARAM_TYPE = "type";
+  private static final String PARAM_VALUE = "value";
+  private static final Set<NewCodePeriodType> OVERALL_TYPES = ImmutableSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS);
+  private static final Set<NewCodePeriodType> PROJECT_TYPES = ImmutableSet.of(DATE, PREVIOUS_VERSION, NUMBER_OF_DAYS);
+  private static final Set<NewCodePeriodType> BRANCH_TYPES = ImmutableSet.of(DATE, PREVIOUS_VERSION, NUMBER_OF_DAYS, SPECIFIC_ANALYSIS);
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final ComponentFinder componentFinder;
+  private final NewCodePeriodDao newCodePeriodDao;
+
+  public SetNewCodePeriodAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, NewCodePeriodDao newCodePeriodDao) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.componentFinder = componentFinder;
+    this.newCodePeriodDao = newCodePeriodDao;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction("set_new_code_period")
+      .setDescription("Updates the setting for the New Code Period.<br>" +
+        "Requires one of the following permissions: " +
+        "<ul>" +
+        "<li>'Administer System'</li>" +
+        "<li>'Administer' rights on the specified component</li>" +
+        "</ul>")
+      .setSince("8.0")
+      .setResponseExample(getClass().getResource("generate_secret_key-example.json"))
+      .setHandler(this);
+
+    action.createParam(PARAM_PROJECT)
+      .setDescription("Project key");
+    action.createParam(PARAM_BRANCH)
+      .setDescription("Branch key");
+    action.createParam(PARAM_TYPE)
+      .setRequired(true)
+      .setDescription("Type");
+    action.createParam(PARAM_VALUE)
+      .setDescription("Value");
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String projectStr = request.getParam(PARAM_PROJECT).emptyAsNull().or(() -> null);
+    String branchStr = request.getParam(PARAM_BRANCH).emptyAsNull().or(() -> null);
+
+    if (projectStr == null && branchStr != null) {
+      throw new IllegalArgumentException("If branch key is specified, project key needs to be specified too");
+    }
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      String typeStr = request.mandatoryParam(PARAM_TYPE);
+      String valueStr = request.getParam(PARAM_VALUE).emptyAsNull().or(() -> null);
+      NewCodePeriodType type = validateType(typeStr, projectStr == null, branchStr != null);
+
+      NewCodePeriodDto dto = new NewCodePeriodDto();
+      dto.setType(type);
+
+      ComponentDto projectBranch = null;
+      if (projectStr != null) {
+        projectBranch = getProject(dbSession, projectStr, branchStr);
+        userSession.checkComponentPermission(UserRole.ADMIN, projectBranch);
+        dto.setProjectUuid(projectBranch.projectUuid());
+      } else {
+        userSession.checkIsSystemAdministrator();
+      }
+
+      setValue(dbSession, dto, type, projectBranch, branchStr, valueStr);
+
+      // TODO upsert?
+      newCodePeriodDao.insert(dbSession, dto);
+    }
+  }
+
+  private void setValue(DbSession dbSession, NewCodePeriodDto dto, NewCodePeriodType type, @Nullable ComponentDto projectBranch,
+    @Nullable String branch, @Nullable String value) {
+    switch (type) {
+      case PREVIOUS_VERSION:
+        Preconditions.checkArgument(value == null, "Unexpected value for type '%s'", type);
+        break;
+      case NUMBER_OF_DAYS:
+        requireValue(type, value);
+        try {
+          dto.setValue(Integer.toString(NewCodePeriodParser.parseDays(value)));
+        } catch (Exception e) {
+          throw new IllegalArgumentException("Failed to parse number of days: " + value);
+        }
+        break;
+      case DATE:
+        requireValue(type, value);
+        try {
+          dto.setValue(NewCodePeriodParser.parseDate(value).toString());
+        } catch (Exception e) {
+          throw new IllegalArgumentException("Failed to parse date: " + value);
+        }
+        break;
+      case SPECIFIC_ANALYSIS:
+        requireValue(type, value);
+        SnapshotDto analysis = getAnalysis(dbSession, value, projectBranch, branch);
+        dto.setValue(analysis.getUuid());
+        break;
+      default:
+        throw new IllegalStateException("Unexpected type: " + type);
+    }
+  }
+
+  private void requireValue(NewCodePeriodType type, @Nullable String value) {
+    Preconditions.checkArgument(value != null, "New Code Period type '%s' requires a value", type);
+  }
+
+  private ComponentDto getProject(DbSession dbSession, String projectKey, @Nullable String branchKey) {
+    if (branchKey == null) {
+      return componentFinder.getByUuidOrKey(dbSession, null, projectKey, PROJECT_ID_AND_KEY);
+    }
+    ComponentDto project = componentFinder.getByKeyAndBranch(dbSession, projectKey, branchKey);
+
+    BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, project.uuid())
+      .orElseThrow(() -> new NotFoundException(format("Branch '%s' is not found", branchKey)));
+
+    checkArgument(branchDto.getBranchType() == BranchType.LONG,
+      "Not a long-living branch: '%s'", branchKey);
+
+    return project;
+  }
+
+  private NewCodePeriodType validateType(String typeStr, boolean isOverall, boolean isBranch) {
+    NewCodePeriodType type;
+    try {
+      type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US));
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException("Invalid type: " + typeStr);
+    }
+
+    if (isOverall) {
+      checkType("Overall setting", OVERALL_TYPES, type);
+    } else if (isBranch) {
+      checkType("Branches", BRANCH_TYPES, type);
+    } else {
+      checkType("Projects", PROJECT_TYPES, type);
+    }
+    return type;
+  }
+
+  private SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid, ComponentDto projectBranch, @Nullable String branchKey) {
+    SnapshotDto snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid)
+      .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid)));
+    checkAnalysis(dbSession, projectBranch, branchKey, snapshotDto);
+    return snapshotDto;
+  }
+
+  private void checkAnalysis(DbSession dbSession, ComponentDto projectBranch, @Nullable String branchKey, SnapshotDto analysis) {
+    ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, analysis.getComponentUuid()).orElse(null);
+
+    boolean analysisMatchesProjectBranch = project != null && projectBranch.uuid().equals(project.uuid());
+    if (branchKey != null) {
+      checkArgument(analysisMatchesProjectBranch,
+        "Analysis '%s' does not belong to branch '%s' of project '%s'",
+        analysis.getUuid(), branchKey, projectBranch.getKey());
+    } else {
+      checkArgument(analysisMatchesProjectBranch,
+        "Analysis '%s' does not belong to project '%s'",
+        analysis.getUuid(), projectBranch.getKey());
+    }
+  }
+
+  private static void checkType(String name, Set<NewCodePeriodType> validTypes, NewCodePeriodType type) {
+    if (!validTypes.contains(type)) {
+      throw new IllegalArgumentException(String.format("Invalid type '%s'. %s can only be set with types: %s", type, name, validTypes));
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/setting/ws/SetNewCodePeriodActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/setting/ws/SetNewCodePeriodActionTest.java
new file mode 100644 (file)
index 0000000..3d3b1a7
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.setting.ws;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodDao;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class SetNewCodePeriodActionTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+
+  private ComponentDbTester componentDb = new ComponentDbTester(db);
+  private DbClient dbClient = db.getDbClient();
+  private DbSession dbSession = db.getSession();
+  private ComponentFinder componentFinder = TestComponentFinder.from(db);
+  private NewCodePeriodDao dao = mock(NewCodePeriodDao.class);
+
+  private SetNewCodePeriodAction underTest = new SetNewCodePeriodAction(dbClient, userSession, componentFinder, dao);
+  private WsActionTester ws = new WsActionTester(underTest);
+
+  @Test
+  public void test_definition() {
+    WebService.Action definition = ws.getDef();
+
+    assertThat(definition.key()).isEqualTo("set_new_code_period");
+    assertThat(definition.isInternal()).isFalse();
+    //assertThat(definition.responseExampleAsString()).isNotEmpty();
+    assertThat(definition.since()).isEqualTo("8.0");
+    assertThat(definition.isPost()).isFalse();
+
+    assertThat(definition.params()).extracting(WebService.Param::key).containsOnly("value", "type", "projectKey", "branchKey");
+    assertThat(definition.param("value").isRequired()).isFalse();
+    assertThat(definition.param("type").isRequired()).isTrue();
+    assertThat(definition.param("projectKey").isRequired()).isFalse();
+    assertThat(definition.param("branchKey").isRequired()).isFalse();
+
+  }
+
+  // validation of type
+  @Test
+  public void throw_IAE_if_no_type_specified() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'type' parameter is missing");
+
+    ws.newRequest()
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_type_is_invalid() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Invalid type: unknown");
+
+    ws.newRequest()
+      .setParam("type", "unknown")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_type_is_invalid_for_global() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Invalid type 'DATE'. Overall setting can only be set with types: [PREVIOUS_VERSION, DAYS]");
+
+    ws.newRequest()
+      .setParam("type", "date")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_type_is_invalid_for_project() {
+    ComponentDto project = componentDb.insertPublicProject();
+    logInAsProjectAdministrator(project);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Invalid type 'ANALYSIS'. Projects can only be set with types: [DATE, PREVIOUS_VERSION, DAYS]");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "analysis")
+      .execute();
+  }
+
+  // validation of value
+  @Test
+  public void throw_IAE_if_no_value_for_date() {
+    ComponentDto project = componentDb.insertPublicProject();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("New Code Period type 'DATE' requires a value");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "date")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_no_value_for_days() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("New Code Period type 'DAYS' requires a value");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("branchKey", "master")
+      .setParam("type", "days")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_no_value_for_analysis() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("New Code Period type 'ANALYSIS' requires a value");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "analysis")
+      .setParam("branchKey", "master")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_date_is_invalid() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Failed to parse date: unknown");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "date")
+      .setParam("branchKey", "master")
+      .setParam("value", "unknown")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_days_is_invalid() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Failed to parse number of days: unknown");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "days")
+      .setParam("branchKey", "master")
+      .setParam("value", "unknown")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_analysis_is_not_found() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Analysis 'unknown' is not found");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "analysis")
+      .setParam("branchKey", "master")
+      .setParam("value", "unknown")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_analysis_doesnt_belong_to_branch() {
+    ComponentDto project = componentDb.insertMainBranch();
+    ComponentDto branch = componentDb.insertProjectBranch(project, b -> b.setKey("branch"));
+
+    SnapshotDto analysisMaster = db.components().insertSnapshot(project);
+    SnapshotDto analysisBranch = db.components().insertSnapshot(branch);
+
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Analysis '" + analysisBranch.getUuid() + "' does not belong to branch 'master' of project '" + project.getKey() + "'");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "analysis")
+      .setParam("branchKey", "master")
+      .setParam("value", analysisBranch.getUuid())
+      .execute();
+  }
+
+  // validation of project/branch
+  @Test
+  public void throw_IAE_if_branch_is_specified_without_project() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("If branch key is specified, project key needs to be specified too");
+
+    ws.newRequest()
+      .setParam("branchKey", "branch")
+      .execute();
+  }
+
+  @Test
+  public void throw_NFE_if_project_not_found() {
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Component key 'unknown' not found");
+
+    ws.newRequest()
+      .setParam("type", "previous_version")
+      .setParam("projectKey", "unknown")
+      .execute();
+  }
+
+  @Test
+  public void throw_NFE_if_branch_not_found() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Component '" + project.getKey() + "' on branch 'unknown' not found");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "previous_version")
+      .setParam("branchKey", "unknown")
+      .execute();
+  }
+
+  @Test
+  public void throw_IAE_if_branch_is_a_SLB() {
+    ComponentDto project = componentDb.insertMainBranch();
+    ComponentDto branch = componentDb.insertProjectBranch(project, b -> b.setKey("branch").setBranchType(BranchType.SHORT));
+    logInAsProjectAdministrator(project);
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Not a long-living branch: 'branch'");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "previous_version")
+      .setParam("branchKey", "branch")
+      .execute();
+  }
+
+  // permission
+  @Test
+  public void throw_NFE_if_no_project_permission() {
+    ComponentDto project = componentDb.insertMainBranch();
+    expectedException.expect(ForbiddenException.class);
+    expectedException.expectMessage("Insufficient privileges");
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "previous_version")
+      .execute();
+  }
+
+  @Test
+  public void throw_NFE_if_no_system_permission() {
+    expectedException.expect(ForbiddenException.class);
+    expectedException.expectMessage("Insufficient privileges");
+
+    ws.newRequest()
+      .setParam("type", "previous_version")
+      .execute();
+  }
+
+  // success cases
+  @Test
+  public void set_global_period_to_previous_version() {
+    logInAsSystemAdministrator();
+    ws.newRequest()
+      .setParam("type", "previous_version")
+      .execute();
+    // TODO
+
+  }
+
+  @Test
+  public void set_project_period_to_number_of_days() {
+    ComponentDto project = componentDb.insertMainBranch();
+    logInAsProjectAdministrator(project);
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "days")
+      .setParam("value", "5")
+      .execute();
+    // TODO
+
+  }
+
+  @Test
+  public void set_branch_period_to_analysis() {
+    ComponentDto project = componentDb.insertMainBranch();
+    ComponentDto branch = componentDb.insertProjectBranch(project, b -> b.setKey("branch"));
+
+    SnapshotDto analysisMaster = db.components().insertSnapshot(project);
+    SnapshotDto analysisBranch = db.components().insertSnapshot(branch);
+
+    logInAsProjectAdministrator(project);
+
+    ws.newRequest()
+      .setParam("projectKey", project.getKey())
+      .setParam("type", "analysis")
+      .setParam("branchKey", "branch")
+      .setParam("value", analysisBranch.getUuid())
+      .execute();
+    // TODO
+  }
+
+  private void logInAsProjectAdministrator(ComponentDto project) {
+    userSession.logIn().addProjectPermission(UserRole.ADMIN, project);
+  }
+
+  private void logInAsSystemAdministrator() {
+    userSession.logIn().setSystemAdministrator();
+  }
+}