Browse Source

SONAR-11626 Add new WS project_analyses/set_baseline

tags/7.7
Janos Gyerik 5 years ago
parent
commit
dced64bda1

+ 3
- 1
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ProjectAnalysisModule.java View File

@@ -25,6 +25,7 @@ import org.sonar.server.projectanalysis.ws.DeleteAction;
import org.sonar.server.projectanalysis.ws.DeleteEventAction;
import org.sonar.server.projectanalysis.ws.ProjectAnalysesWs;
import org.sonar.server.projectanalysis.ws.SearchAction;
import org.sonar.server.projectanalysis.ws.SetBaselineAction;
import org.sonar.server.projectanalysis.ws.UpdateEventAction;

public class ProjectAnalysisModule extends Module {
@@ -38,7 +39,8 @@ public class ProjectAnalysisModule extends Module {
UpdateEventAction.class,
DeleteEventAction.class,
DeleteAction.class,
SearchAction.class);
SearchAction.class,
SetBaselineAction.class);
}

}

+ 148
- 0
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SetBaselineAction.java View File

@@ -0,0 +1,148 @@
/*
* 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.projectanalysis.ws;

import com.google.protobuf.Empty;
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.core.util.Uuids;
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.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.server.component.ComponentFinder.ParamNames.PROJECT_ID_AND_KEY;
import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_ANALYSIS;
import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_BRANCH;
import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PROJECT;
import static org.sonar.server.ws.WsUtils.writeProtobuf;

public class SetBaselineAction implements ProjectAnalysesWsAction {
private final DbClient dbClient;
private final UserSession userSession;
private final ComponentFinder componentFinder;

public SetBaselineAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder) {
this.dbClient = dbClient;
this.userSession = userSession;
this.componentFinder = componentFinder;
}

@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("set_baseline")
.setDescription("Set an analysis as the baseline of the New Code Period on a project or a long-lived branch.<br/>" +
"This manually set baseline overrides the `sonar.leak.period` setting.<br/>" +
"Requires one of the following permissions:" +
"<ul>" +
" <li>'Administer System'</li>" +
" <li>'Administer' rights on the specified project</li>" +
"</ul>")
.setSince("7.7")
.setPost(true)
.setHandler(this);

action.createParam(PARAM_PROJECT)
.setDescription("Project key")
.setRequired(true);

action.createParam(PARAM_BRANCH)
.setDescription("Branch key");

action.createParam(PARAM_ANALYSIS)
.setDescription("Analysis key")
.setExampleValue(Uuids.UUID_EXAMPLE_01)
.setRequired(true);
}

@Override
public void handle(Request httpRequest, Response httpResponse) throws Exception {
doHandle(httpRequest);

writeProtobuf(Empty.newBuilder().build(), httpRequest, httpResponse);
}

private void doHandle(Request request) {
String projectKey = mandatoryNonEmptyParam(request, PARAM_PROJECT);
String branchKey = request.param(PARAM_BRANCH);
checkArgument(branchKey == null || !branchKey.isEmpty(), "The '%s' parameter must not be empty", PARAM_BRANCH);
String analysisUuid = mandatoryNonEmptyParam(request, PARAM_ANALYSIS);

try (DbSession dbSession = dbClient.openSession(false)) {
ComponentDto projectBranch = getProjectBranch(dbSession, projectKey, branchKey);
SnapshotDto analysis = getAnalysis(dbSession, analysisUuid);
checkRequest(dbSession, projectBranch, branchKey, analysis);
dbClient.branchDao().updateManualBaseline(dbSession, projectBranch.uuid(), analysis.getUuid());
dbSession.commit();
}
}

private static String mandatoryNonEmptyParam(Request request, String param) {
String value = request.mandatoryParam(param);
checkArgument(!value.isEmpty(), "The '%s' parameter must not be empty", param);
return value;
}

private ComponentDto getProjectBranch(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 SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid) {
return dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid)
.orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid)));
}

private void checkRequest(DbSession dbSession, ComponentDto projectBranch, @Nullable String branchKey, SnapshotDto analysis) {
userSession.checkComponentPermission(UserRole.ADMIN, projectBranch);
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());
}
}
}

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ProjectAnalysisModuleTest.java View File

@@ -30,6 +30,6 @@ public class ProjectAnalysisModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new ProjectAnalysisModule().configure(container);
assertThat(container.size()).isEqualTo(2 + 6);
assertThat(container.size()).isEqualTo(2 + 7);
}
}

+ 307
- 0
server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SetBaselineActionTest.java View File

@@ -0,0 +1,307 @@
/*
* 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.projectanalysis.ws;

import com.google.common.collect.ImmutableMap;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
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.BranchDto;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.component.SnapshotDto;
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.TestRequest;
import org.sonar.server.ws.WsActionTester;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_ANALYSIS;
import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_BRANCH;
import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PROJECT;
import static org.sonar.test.Matchers.regexMatcher;
import static org.sonarqube.ws.client.WsRequest.Method.POST;

@RunWith(DataProviderRunner.class)
public class SetBaselineActionTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

@Rule
public DbTester db = DbTester.create(System2.INSTANCE);
private DbClient dbClient = db.getDbClient();
private DbSession dbSession = db.getSession();

private WsActionTester ws = new WsActionTester(new SetBaselineAction(dbClient, userSession, TestComponentFinder.from(db)));

@Test
public void set_baseline_on_main_branch() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = new BranchDto()
.setBranchType(BranchType.LONG)
.setProjectUuid(project.uuid())
.setUuid(project.uuid())
.setKey("master");
db.components().insertComponent(project);
db.getDbClient().branchDao().insert(dbSession, branch);
SnapshotDto analysis = db.components().insertSnapshot(project);
logInAsProjectAdministrator(project);

call(ImmutableMap.of(PARAM_PROJECT, project.getKey(), PARAM_ANALYSIS, analysis.getUuid()));

BranchDto loaded = dbClient.branchDao().selectByUuid(dbSession, branch.getUuid()).get();
assertThat(loaded.getManualBaseline()).isEqualTo(analysis.getUuid());
}

@Test
public void set_baseline_on_long_living_branch() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG);
db.components().insertProjectBranch(project, branch);
ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch);
SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto);
logInAsProjectAdministrator(project);

call(project.getKey(), branch.getKey(), analysis.getUuid());

BranchDto loaded = dbClient.branchDao().selectByUuid(dbSession, branch.getUuid()).get();
assertThat(loaded.getManualBaseline()).isEqualTo(analysis.getUuid());
}

@Test
public void fail_when_user_is_not_admin() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG);
db.components().insertProjectBranch(project, branch);
ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch);
SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto);

expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privileges");

call(project.getKey(), branch.getKey(), analysis.getUuid());
}

@Test
@UseDataProvider("missingOrEmptyParamsAndFailureMessage")
public void fail_with_IAE_when_required_param_missing_or_empty(Map<String, String> params, String message) {
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(message);

call(params);
}

@DataProvider
public static Object[][] missingOrEmptyParamsAndFailureMessage() {
MapBuilder builder = new MapBuilder()
.put(PARAM_PROJECT, "project key")
.put(PARAM_BRANCH, "branch key")
.put(PARAM_ANALYSIS, "analysis uuid");

return new Object[][] {
{builder.put(PARAM_PROJECT, null).map, "The 'project' parameter is missing"},
{builder.put(PARAM_PROJECT, "").map, "The 'project' parameter must not be empty"},
{builder.put(PARAM_BRANCH, "").map, "The 'branch' parameter must not be empty"},
{builder.put(PARAM_ANALYSIS, null).map, "The 'analysis' parameter is missing"},
{builder.put(PARAM_ANALYSIS, "").map, "The 'analysis' parameter must not be empty"},
};
}

@Test
@UseDataProvider("nonexistentParamsAndFailureMessage")
public void fail_with_IAE_when_required_param_nonexistent(Map<String, String> nonexistentParams, String regex) {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG);
db.components().insertProjectBranch(project, branch);
ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch);
SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto);
logInAsProjectAdministrator(project);

Map<String, String> params = new HashMap<>();
params.put(PARAM_PROJECT, project.getKey());
params.put(PARAM_BRANCH, branch.getKey());
params.put(PARAM_ANALYSIS, analysis.getUuid());
params.putAll(nonexistentParams);

expectedException.expect(NotFoundException.class);
expectedException.expectMessage(regexMatcher(regex));

call(params);
}

@DataProvider
public static Object[][] nonexistentParamsAndFailureMessage() {
MapBuilder builder = new MapBuilder();

return new Object[][] {
{builder.put(PARAM_PROJECT, "nonexistent").map, "Component 'nonexistent' on branch .* not found"},
{builder.put(PARAM_BRANCH, "nonexistent").map, "Component .* on branch 'nonexistent' not found"},
{builder.put(PARAM_ANALYSIS, "nonexistent").map, "Analysis 'nonexistent' is not found"},
};
}

@Test
public void fail_when_branch_does_not_belong_to_project() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG);
db.components().insertProjectBranch(project, branch);
ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch);
SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto);
logInAsProjectAdministrator(project);

ComponentDto otherProject = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto otherBranch = ComponentTesting.newBranchDto(otherProject.projectUuid(), BranchType.LONG);
db.components().insertProjectBranch(otherProject, otherBranch);
ComponentTesting.newProjectBranch(otherProject, otherBranch);

expectedException.expect(NotFoundException.class);
expectedException.expectMessage(String.format("Component '%s' on branch '%s' not found", project.getKey(), otherBranch.getKey()));

call(project.getKey(), otherBranch.getKey(), analysis.getUuid());
}

@Test
public void fail_when_analysis_does_not_belong_to_main_branch_of_project() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = new BranchDto()
.setBranchType(BranchType.LONG)
.setProjectUuid(project.uuid())
.setUuid(project.uuid())
.setKey("master");
db.components().insertComponent(project);
db.getDbClient().branchDao().insert(dbSession, branch);
logInAsProjectAdministrator(project);

ComponentDto otherProject = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
SnapshotDto otherAnalysis = db.components().insertSnapshot(otherProject);

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(String.format("Analysis '%s' does not belong to project '%s'",
otherAnalysis.getUuid(), project.getKey()));

call(ImmutableMap.of(PARAM_PROJECT, project.getKey(), PARAM_ANALYSIS, otherAnalysis.getUuid()));
}

@Test
public void fail_when_analysis_does_not_belong_to_non_main_branch_of_project() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.LONG);
db.components().insertProjectBranch(project, branch);
logInAsProjectAdministrator(project);

ComponentDto otherProject = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
SnapshotDto otherAnalysis = db.components().insertProjectAndSnapshot(otherProject);

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(String.format("Analysis '%s' does not belong to branch '%s' of project '%s'",
otherAnalysis.getUuid(), branch.getKey(), project.getKey()));

call(project.getKey(), branch.getKey(), otherAnalysis.getUuid());
}

@Test
public void fail_when_branch_is_not_long() {
ComponentDto project = ComponentTesting.newPrivateProjectDto(db.organizations().insert());
BranchDto branch = ComponentTesting.newBranchDto(project.projectUuid(), BranchType.SHORT);
db.components().insertProjectBranch(project, branch);
ComponentDto branchComponentDto = ComponentTesting.newProjectBranch(project, branch);
SnapshotDto analysis = db.components().insertSnapshot(branchComponentDto);
logInAsProjectAdministrator(project);

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(String.format("Not a long-living branch: '%s'", branch.getKey()));

call(project.getKey(), branch.getKey(), analysis.getUuid());
}

@Test
public void ws_parameters() {
WebService.Action definition = ws.getDef();

assertThat(definition.isPost()).isTrue();
assertThat(definition.key()).isEqualTo("set_baseline");
assertThat(definition.since()).isEqualTo("7.7");
assertThat(definition.isInternal()).isFalse();
}

private void logInAsProjectAdministrator(ComponentDto project) {
userSession.logIn().addProjectPermission(UserRole.ADMIN, project);
}

private void call(Map<String, String> params) {
TestRequest httpRequest = ws.newRequest().setMethod(POST.name());

for (Map.Entry<String, String> param : params.entrySet()) {
httpRequest.setParam(param.getKey(), param.getValue());
}

httpRequest.execute();
}

private void call(String projectKey, String branchKey, String analysisUuid) {
call(ImmutableMap.of(
PARAM_PROJECT, projectKey,
PARAM_BRANCH, branchKey,
PARAM_ANALYSIS, analysisUuid));
}

private static class MapBuilder {
private final Map<String, String> map;

private MapBuilder() {
this.map = Collections.emptyMap();
}

private MapBuilder(Map<String, String> map) {
this.map = map;
}

public MapBuilder put(String key, @Nullable String value) {
Map<String, String> copy = new HashMap<>(map);
if (value == null) {
copy.remove(key);
} else {
copy.put(key, value);
}
return new MapBuilder(copy);
}
}
}

+ 48
- 0
sonar-testing-harness/src/main/java/org/sonar/test/Matchers.java View File

@@ -0,0 +1,48 @@
/*
* 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.test;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

/**
* Utility class to provide various Matchers to use in ExpectedException.expectMessage.
*/
public class Matchers {

private Matchers() {
// utility class, forbidden constructor
}

public static Matcher<String> regexMatcher(String regex) {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
return item.matches(regex);
}

@Override
public void describeTo(Description description) {
description.appendText("matching regex ").appendValue(regex);
}
};
}
}

Loading…
Cancel
Save