From a020d26f4cc59b29a32e6c4b5589e3fa7506d171 Mon Sep 17 00:00:00 2001 From: Edwin Kempin Date: Fri, 25 Oct 2019 16:36:45 +0200 Subject: [PATCH] Add REST endpoint to add new label definitions Bug: Issue 11522 Signed-off-by: Edwin Kempin Change-Id: I09b8642d266feeeef0c5ae2bdeb106615e4f5bfe --- Documentation/rest-api-projects.txt | 59 ++ .../extensions/api/projects/LabelApi.java | 7 + .../server/api/projects/LabelApiImpl.java | 53 +- .../server/api/projects/ProjectApiImpl.java | 10 +- .../server/restapi/project/CreateLabel.java | 172 ++++++ .../project/LabelDefinitionInputParser.java | 87 +++ .../gerrit/server/restapi/project/Module.java | 1 + .../server/restapi/project/SetLabel.java | 62 +- .../binding/ProjectsRestApiBindingsIT.java | 3 +- .../rest/project/CreateLabelIT.java | 567 ++++++++++++++++++ 10 files changed, 949 insertions(+), 72 deletions(-) create mode 100644 java/com/google/gerrit/server/restapi/project/CreateLabel.java create mode 100644 java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java create mode 100644 javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 65996a2818..a09075d488 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt @@ -3139,6 +3139,65 @@ returned that describes the label. } ---- +[[create-label]] +=== Create Label +-- +'PUT /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]' +-- + +Creates a new label definition in this project. + +The calling user must have write access to the `refs/meta/config` branch of the +project. + +If a label with this name is already defined in this project, this label +definition is updated (see link:#set-label[Set Label]). + +.Request +---- + PUT /projects/My-Project/labels/Foo HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "commit_message": "Create Foo Label", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + } + } +---- + +As response a link:#label-definition-info[LabelDefinitionInfo] entity is +returned that describes the created label. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "name": "Foo", + "project_name": "My-Project", + "function": "MaxWithBlock", + "values": { + " 0": "No score", + "-1": "I would prefer this is not merged as is", + "-2": "This shall not be merged", + "+1": "Looks good to me, but someone else must approve", + "+2": "Looks good to me, approved" + }, + "default_value": 0, + "can_override": true, + "copy_all_scores_if_no_change": true, + "allow_post_submit": true + } +---- + [[set-label]] === Set Label -- diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java index f11d3945b4..bee9e53b77 100644 --- a/java/com/google/gerrit/extensions/api/projects/LabelApi.java +++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java @@ -20,6 +20,8 @@ import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; public interface LabelApi { + LabelApi create(LabelDefinitionInput input) throws RestApiException; + LabelDefinitionInfo get() throws RestApiException; LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException; @@ -29,6 +31,11 @@ public interface LabelApi { * interface. */ class NotImplemented implements LabelApi { + @Override + public LabelApi create(LabelDefinitionInput input) throws RestApiException { + throw new NotImplementedException(); + } + @Override public LabelDefinitionInfo get() throws RestApiException { throw new NotImplementedException(); diff --git a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java index 4c8759e8b6..887f5ae2e7 100644 --- a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java +++ b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java @@ -19,33 +19,70 @@ import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import com.google.gerrit.extensions.api.projects.LabelApi; import com.google.gerrit.extensions.common.LabelDefinitionInfo; import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.restapi.project.CreateLabel; import com.google.gerrit.server.restapi.project.GetLabel; +import com.google.gerrit.server.restapi.project.LabelsCollection; import com.google.gerrit.server.restapi.project.SetLabel; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; public class LabelApiImpl implements LabelApi { interface Factory { - LabelApiImpl create(LabelResource rsrc); + LabelApiImpl create(ProjectResource project, String label); } + private final LabelsCollection labels; + private final CreateLabel createLabel; private final GetLabel getLabel; private final SetLabel setLabel; - private final LabelResource rsrc; + private final ProjectCache projectCache; + private final String label; + + private ProjectResource project; @Inject - LabelApiImpl(GetLabel getLabel, SetLabel setLabel, @Assisted LabelResource rsrc) { + LabelApiImpl( + LabelsCollection labels, + CreateLabel createLabel, + GetLabel getLabel, + SetLabel setLabel, + ProjectCache projectCache, + @Assisted ProjectResource project, + @Assisted String label) { + this.labels = labels; + this.createLabel = createLabel; this.getLabel = getLabel; this.setLabel = setLabel; - this.rsrc = rsrc; + this.projectCache = projectCache; + this.project = project; + this.label = label; + } + + @Override + public LabelApi create(LabelDefinitionInput input) throws RestApiException { + try { + createLabel.apply(project, IdString.fromDecoded(label), input); + + // recreate project resource because project state was updated by creating the new label and + // needs to be reloaded + project = + new ProjectResource(projectCache.checkedGet(project.getNameKey()), project.getUser()); + return this; + } catch (Exception e) { + throw asRestApiException("Cannot create branch", e); + } } @Override public LabelDefinitionInfo get() throws RestApiException { try { - return getLabel.apply(rsrc).value(); + return getLabel.apply(resource()).value(); } catch (Exception e) { throw asRestApiException("Cannot get label", e); } @@ -54,9 +91,13 @@ public class LabelApiImpl implements LabelApi { @Override public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException { try { - return setLabel.apply(rsrc, input).value(); + return setLabel.apply(resource(), input).value(); } catch (Exception e) { throw asRestApiException("Cannot update label", e); } } + + private LabelResource resource() throws RestApiException, PermissionBackendException { + return labels.parse(project, IdString.fromDecoded(label)); + } } diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index bce3b0a311..d7ab91b71c 100644 --- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java @@ -73,7 +73,6 @@ import com.google.gerrit.server.restapi.project.GetHead; import com.google.gerrit.server.restapi.project.GetParent; import com.google.gerrit.server.restapi.project.Index; import com.google.gerrit.server.restapi.project.IndexChanges; -import com.google.gerrit.server.restapi.project.LabelsCollection; import com.google.gerrit.server.restapi.project.ListBranches; import com.google.gerrit.server.restapi.project.ListDashboards; import com.google.gerrit.server.restapi.project.ListLabels; @@ -132,7 +131,6 @@ public class ProjectApiImpl implements ProjectApi { private final Index index; private final IndexChanges indexChanges; private final Provider listLabels; - private final LabelsCollection labels; private final LabelApiImpl.Factory labelApi; @AssistedInject @@ -171,7 +169,6 @@ public class ProjectApiImpl implements ProjectApi { IndexChanges indexChanges, Provider listLabels, LabelApiImpl.Factory labelApi, - LabelsCollection labels, @Assisted ProjectResource project) { this( permissionBackend, @@ -209,7 +206,6 @@ public class ProjectApiImpl implements ProjectApi { indexChanges, listLabels, labelApi, - labels, null); } @@ -249,7 +245,6 @@ public class ProjectApiImpl implements ProjectApi { IndexChanges indexChanges, Provider listLabels, LabelApiImpl.Factory labelApi, - LabelsCollection labels, @Assisted String name) { this( permissionBackend, @@ -287,7 +282,6 @@ public class ProjectApiImpl implements ProjectApi { indexChanges, listLabels, labelApi, - labels, name); } @@ -327,7 +321,6 @@ public class ProjectApiImpl implements ProjectApi { IndexChanges indexChanges, Provider listLabels, LabelApiImpl.Factory labelApi, - LabelsCollection labels, String name) { this.permissionBackend = permissionBackend; this.createProject = createProject; @@ -365,7 +358,6 @@ public class ProjectApiImpl implements ProjectApi { this.indexChanges = indexChanges; this.listLabels = listLabels; this.labelApi = labelApi; - this.labels = labels; } @Override @@ -715,7 +707,7 @@ public class ProjectApiImpl implements ProjectApi { @Override public LabelApi label(String labelName) throws RestApiException { try { - return labelApi.create(labels.parse(checkExists(), IdString.fromDecoded(labelName))); + return labelApi.create(checkExists(), labelName); } catch (Exception e) { throw asRestApiException("Cannot parse label", e); } diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java new file mode 100644 index 0000000000..3230017a76 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java @@ -0,0 +1,172 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestCollectionCreateView; +import com.google.gerrit.server.git.meta.MetaDataUpdate; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.project.LabelDefinitionJson; +import com.google.gerrit.server.project.LabelResource; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectConfig; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; + +@Singleton +public class CreateLabel + implements RestCollectionCreateView { + private final PermissionBackend permissionBackend; + private final MetaDataUpdate.User updateFactory; + private final ProjectConfig.Factory projectConfigFactory; + private final ProjectCache projectCache; + + @Inject + public CreateLabel( + PermissionBackend permissionBackend, + MetaDataUpdate.User updateFactory, + ProjectConfig.Factory projectConfigFactory, + ProjectCache projectCache) { + this.permissionBackend = permissionBackend; + this.updateFactory = updateFactory; + this.projectConfigFactory = projectConfigFactory; + this.projectCache = projectCache; + } + + @Override + public Response apply( + ProjectResource rsrc, IdString id, LabelDefinitionInput input) + throws AuthException, BadRequestException, ResourceConflictException, + PermissionBackendException, IOException, ConfigInvalidException { + permissionBackend + .currentUser() + .project(rsrc.getNameKey()) + .check(ProjectPermission.WRITE_CONFIG); + + if (input == null) { + input = new LabelDefinitionInput(); + } + + if (input.name != null && !input.name.equals(id.get())) { + throw new BadRequestException("name in input must match name in URL"); + } + + try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) { + ProjectConfig config = projectConfigFactory.read(md); + + if (config.getLabelSections().containsKey(id.get())) { + throw new ResourceConflictException("label " + id.get() + " already exists"); + } + + if (input.values == null || input.values.isEmpty()) { + throw new BadRequestException("values are required"); + } + + List values = LabelDefinitionInputParser.parseValues(input.values); + + LabelType labelType; + try { + labelType = new LabelType(id.get(), values); + } catch (IllegalArgumentException e) { + throw new BadRequestException("invalid name: " + id.get(), e); + } + + if (input.function != null && !input.function.trim().isEmpty()) { + labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function)); + } else { + labelType.setFunction(LabelFunction.MAX_WITH_BLOCK); + } + + if (input.defaultValue != null) { + labelType.setDefaultValue( + LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue)); + } + + if (input.branches != null) { + labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches)); + } + + if (input.canOverride != null) { + labelType.setCanOverride(input.canOverride); + } + + if (input.copyAnyScore != null) { + labelType.setCopyAnyScore(input.copyAnyScore); + } + + if (input.copyMinScore != null) { + labelType.setCopyMinScore(input.copyMinScore); + } + + if (input.copyMaxScore != null) { + labelType.setCopyMaxScore(input.copyMaxScore); + } + + if (input.copyAllScoresIfNoChange != null) { + labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange); + } + + if (input.copyAllScoresIfNoCodeChange != null) { + labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange); + } + + if (input.copyAllScoresOnTrivialRebase != null) { + labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase); + } + + if (input.copyAllScoresOnMergeFirstParentUpdate != null) { + labelType.setCopyAllScoresOnMergeFirstParentUpdate( + input.copyAllScoresOnMergeFirstParentUpdate); + } + + if (input.allowPostSubmit != null) { + labelType.setAllowPostSubmit(input.allowPostSubmit); + } + + if (input.ignoreSelfApproval != null) { + labelType.setIgnoreSelfApproval(input.ignoreSelfApproval); + } + + if (input.commitMessage != null) { + md.setMessage(Strings.emptyToNull(input.commitMessage.trim())); + } else { + md.setMessage("Update label"); + } + + config.getLabelSections().put(labelType.getName(), labelType); + config.commit(md); + + projectCache.evict(rsrc.getProjectState().getProject()); + + return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType)); + } + } +} diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java new file mode 100644 index 0000000000..a45c67fd3d --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java @@ -0,0 +1,87 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.restapi.project; + +import com.google.common.primitives.Shorts; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.exceptions.InvalidNameException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.server.project.RefPattern; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; + +public class LabelDefinitionInputParser { + public static LabelFunction parseFunction(String functionString) throws BadRequestException { + Optional function = LabelFunction.parse(functionString.trim()); + return function.orElseThrow( + () -> new BadRequestException("unknown function: " + functionString)); + } + + public static List parseValues(Map values) + throws BadRequestException { + List valueList = new ArrayList<>(); + for (Entry e : values.entrySet()) { + short value; + try { + value = Shorts.checkedCast(PermissionRule.parseInt(e.getKey().trim())); + } catch (NumberFormatException ex) { + throw new BadRequestException("invalid value: " + e.getKey(), ex); + } + String valueDescription = e.getValue().trim(); + if (valueDescription.isEmpty()) { + throw new BadRequestException("description for value '" + e.getKey() + "' cannot be empty"); + } + valueList.add(new LabelValue(value, valueDescription)); + } + return valueList; + } + + public static short parseDefaultValue(LabelType labelType, short defaultValue) + throws BadRequestException { + if (labelType.getValue(defaultValue) == null) { + throw new BadRequestException("invalid default value: " + defaultValue); + } + return defaultValue; + } + + public static List parseBranches(List branches) throws BadRequestException { + List validBranches = new ArrayList<>(); + for (String branch : branches) { + String newBranch = branch.trim(); + if (newBranch.isEmpty()) { + continue; + } + if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) { + newBranch = RefNames.REFS_HEADS + newBranch; + } + try { + RefPattern.validate(newBranch); + } catch (InvalidNameException e) { + throw new BadRequestException("invalid branch: " + branch, e); + } + validBranches.add(newBranch); + } + return validBranches; + } + + private LabelDefinitionInputParser() {} +} diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java index d8ea436122..7ec4e6db2f 100644 --- a/java/com/google/gerrit/server/restapi/project/Module.java +++ b/java/com/google/gerrit/server/restapi/project/Module.java @@ -68,6 +68,7 @@ public class Module extends RestApiModule { get(CHILD_PROJECT_KIND).to(GetChildProject.class); child(PROJECT_KIND, "labels").to(LabelsCollection.class); + create(LABEL_KIND).to(CreateLabel.class); get(LABEL_KIND).to(GetLabel.class); put(LABEL_KIND).to(SetLabel.class); diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java index b61a9530a7..e5459fb109 100644 --- a/java/com/google/gerrit/server/restapi/project/SetLabel.java +++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java @@ -15,13 +15,7 @@ package com.google.gerrit.server.restapi.project; import com.google.common.base.Strings; -import com.google.common.primitives.Shorts; -import com.google.gerrit.common.data.LabelFunction; import com.google.gerrit.common.data.LabelType; -import com.google.gerrit.common.data.LabelValue; -import com.google.gerrit.common.data.PermissionRule; -import com.google.gerrit.entities.RefNames; -import com.google.gerrit.exceptions.InvalidNameException; import com.google.gerrit.extensions.common.LabelDefinitionInfo; import com.google.gerrit.extensions.common.LabelDefinitionInput; import com.google.gerrit.extensions.restapi.AuthException; @@ -37,14 +31,9 @@ import com.google.gerrit.server.project.LabelDefinitionJson; import com.google.gerrit.server.project.LabelResource; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectConfig; -import com.google.gerrit.server.project.RefPattern; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map.Entry; -import java.util.Optional; import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton @@ -106,15 +95,10 @@ public class SetLabel implements RestModifyView newFunction = LabelFunction.parse(newFunctionName); - if (!newFunction.isPresent()) { - throw new BadRequestException("unknown function: " + input.function); - } - labelType.setFunction(newFunction.get()); + labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function)); dirty = true; } @@ -122,52 +106,18 @@ public class SetLabel implements RestModifyView newValues = new ArrayList<>(); - for (Entry e : input.values.entrySet()) { - short value; - try { - value = Shorts.checkedCast(PermissionRule.parseInt(e.getKey().trim())); - } catch (NumberFormatException ex) { - throw new BadRequestException("invalid value: " + e.getKey(), ex); - } - String valueDescription = e.getValue().trim(); - if (valueDescription.isEmpty()) { - throw new BadRequestException( - "description for value '" + e.getKey() + "' cannot be empty"); - } - newValues.add(new LabelValue(value, valueDescription)); - } - labelType.setValues(newValues); + labelType.setValues(LabelDefinitionInputParser.parseValues(input.values)); dirty = true; } if (input.defaultValue != null) { - if (labelType.getValue(input.defaultValue) == null) { - throw new BadRequestException("invalid default value: " + input.defaultValue); - } - labelType.setDefaultValue(input.defaultValue); + labelType.setDefaultValue( + LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue)); dirty = true; } if (input.branches != null) { - List newBranches = new ArrayList<>(); - for (String branch : input.branches) { - String newBranch = branch.trim(); - if (newBranch.isEmpty()) { - continue; - } - if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) { - newBranch = RefNames.REFS_HEADS + newBranch; - } - try { - RefPattern.validate(newBranch); - } catch (InvalidNameException e) { - throw new BadRequestException("invalid branch: " + branch, e); - } - newBranches.add(newBranch); - } - labelType.setRefPatterns(newBranches); + labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches)); dirty = true; } diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java index 79c44d8d2c..d39567b374 100644 --- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java +++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java @@ -83,7 +83,8 @@ public class ProjectsRestApiBindingsIT extends AbstractDaemonTest { // GET /projects//branches//commits is not implemented .expectedResponseCode(SC_NOT_FOUND) .build(), - RestCall.get("/projects/%s/dashboards")); + RestCall.get("/projects/%s/dashboards"), + RestCall.put("/projects/%s/labels/new-label")); /** * Child project REST endpoints to be tested, each URL contains placeholders for the parent diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java new file mode 100644 index 0000000000..2754ba89b2 --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java @@ -0,0 +1,567 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.common.data.LabelFunction; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.common.LabelDefinitionInfo; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.inject.Inject; +import org.junit.Test; + +@NoHttpd +public class CreateLabelIT extends AbstractDaemonTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + @Test + public void notAllowed() throws Exception { + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + AuthException thrown = + assertThrows( + AuthException.class, + () -> + gApi.projects() + .name(project.get()) + .label("Foo-Review") + .create(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted"); + } + + @Test + public void cannotCreateLabelIfNameDoesntMatch() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.name = "Foo"; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Bar").create(input)); + assertThat(thrown).hasMessageThat().contains("name in input must match name in URL"); + } + + @Test + public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception { + ResourceConflictException thrown = + assertThrows( + ResourceConflictException.class, + () -> + gApi.projects() + .name(allProjects.get()) + .label("Code-Review") + .create(new LabelDefinitionInput())); + assertThat(thrown).hasMessageThat().contains("label Code-Review already exists"); + } + + @Test + public void cannotCreateLabelWithInvalidName() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("INVALID_NAME").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid name: INVALID_NAME"); + } + + @Test + public void cannotCreateLabelWithoutValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("values are required"); + + input.values = ImmutableMap.of(); + thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("values are required"); + } + + @Test + public void cannotCreateLabelWithInvalidValues() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("invalidValue", "description"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid value: invalidValue"); + } + + @Test + public void cannotCreateLabelWithValuesThatHaveEmptyDescription() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", ""); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("description for value '+1' cannot be empty"); + } + + @Test + public void cannotCreateLabelWithInvalidDefaultValue() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + input.defaultValue = 5; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid default value: " + input.defaultValue); + } + + @Test + public void cannotCreateLabelWithUnknownFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + input.function = "UnknownFuction"; + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("unknown function: " + input.function); + } + + @Test + public void cannotCreateLabelWithInvalidBranch() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad"); + input.branches = ImmutableList.of("refs heads master"); + + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> gApi.projects().name(project.get()).label("Foo").create(input)); + assertThat(thrown).hasMessageThat().contains("invalid branch: refs heads master"); + } + + @Test + public void createWithNameAndValuesOnly() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.name).isEqualTo("Foo"); + assertThat(createdLabel.projectName).isEqualTo(project.get()); + assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName()); + assertThat(createdLabel.values).containsExactlyEntriesIn(input.values); + assertThat(createdLabel.defaultValue).isEqualTo(0); + assertThat(createdLabel.branches).isNull(); + assertThat(createdLabel.canOverride).isTrue(); + assertThat(createdLabel.copyAnyScore).isNull(); + assertThat(createdLabel.copyMinScore).isNull(); + assertThat(createdLabel.copyMaxScore).isNull(); + assertThat(createdLabel.copyAllScoresIfNoChange).isTrue(); + assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull(); + assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull(); + assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + assertThat(createdLabel.allowPostSubmit).isTrue(); + assertThat(createdLabel.ignoreSelfApproval).isNull(); + } + + @Test + public void createWithFunction() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.function = LabelFunction.NO_OP.getFunctionName(); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName()); + } + + @Test + public void functionEmptyAfterTrim() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.function = " "; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName()); + } + + @Test + public void valuesAndDescriptionsAreTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + // Positive values can be specified as '' or '+'. + input.values = + ImmutableMap.of( + " 2 ", + " Looks Very Good ", + " +1 ", + " Looks Good ", + " 0 ", + " Don't Know ", + " -1 ", + " Looks Bad ", + " -2 ", + " Looks Very Bad "); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.values) + .containsExactly( + "+2", "Looks Very Good", + "+1", "Looks Good", + " 0", "Don't Know", + "-1", "Looks Bad", + "-2", "Looks Very Bad"); + } + + @Test + public void createWithDefaultValue() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.defaultValue = 1; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + + assertThat(createdLabel.defaultValue).isEqualTo(input.defaultValue); + } + + @Test + public void createWithBranches() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + // Branches can be full ref, ref pattern or regular expression. + input.branches = + ImmutableList.of("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches).containsExactlyElementsIn(input.branches); + } + + @Test + public void branchesAreTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.branches = + ImmutableList.of(" refs/heads/master ", " refs/heads/foo/* ", " ^refs/heads/stable-.* "); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches) + .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*"); + } + + @Test + public void emptyBranchesAreIgnored() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.branches = ImmutableList.of("refs/heads/master", "", " "); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches).containsExactly("refs/heads/master"); + } + + @Test + public void branchesAreAutomaticallyPrefixedWithRefsHeads() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.branches = ImmutableList.of("master", "refs/meta/config"); + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("Foo").create(input).get(); + assertThat(createdLabel.branches).containsExactly("refs/heads/master", "refs/meta/config"); + } + + @Test + public void createWithCanOverride() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.canOverride = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.canOverride).isTrue(); + } + + @Test + public void createWithoutCanOverride() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.canOverride = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.canOverride).isNull(); + } + + @Test + public void createWithCopyAnyScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAnyScore = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAnyScore).isTrue(); + } + + @Test + public void createWithoutCopyAnyScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAnyScore = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAnyScore).isNull(); + } + + @Test + public void createWithCopyMinScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMinScore = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMinScore).isTrue(); + } + + @Test + public void createWithoutCopyMinScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMinScore = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMinScore).isNull(); + } + + @Test + public void createWithCopyMaxScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMaxScore = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMaxScore).isTrue(); + } + + @Test + public void createWithoutCopyMaxScore() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyMaxScore = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyMaxScore).isNull(); + } + + @Test + public void createWithCopyAllScoresIfNoChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoChange = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoChange).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresIfNoChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoChange = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoChange).isNull(); + } + + @Test + public void createWithCopyAllScoresIfNoCodeChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoCodeChange = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoCodeChange).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresIfNoCodeChange() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresIfNoCodeChange = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull(); + } + + @Test + public void createWithCopyAllScoresOnTrivialRebase() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnTrivialRebase = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnTrivialRebase).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresOnTrivialRebase() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnTrivialRebase = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull(); + } + + @Test + public void createWithCopyAllScoresOnMergeFirstParentUpdate() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnMergeFirstParentUpdate = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue(); + } + + @Test + public void createWithoutCopyAllScoresOnMergeFirstParentUpdate() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.copyAllScoresOnMergeFirstParentUpdate = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull(); + } + + @Test + public void createWithAllowPostSubmit() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.allowPostSubmit = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.allowPostSubmit).isTrue(); + } + + @Test + public void createWithoutAllowPostSubmit() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.allowPostSubmit = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.allowPostSubmit).isNull(); + } + + @Test + public void createWithIgnoreSelfApproval() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.ignoreSelfApproval = true; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.ignoreSelfApproval).isTrue(); + } + + @Test + public void createWithoutIgnoreSelfApproval() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.ignoreSelfApproval = false; + + LabelDefinitionInfo createdLabel = + gApi.projects().name(project.get()).label("foo").create(input).get(); + assertThat(createdLabel.ignoreSelfApproval).isNull(); + } + + @Test + public void defaultCommitMessage() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + gApi.projects().name(project.get()).label("Foo").create(input); + assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Update label"); + } + + @Test + public void withCommitMessage() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.commitMessage = "Add Foo Label"; + gApi.projects().name(project.get()).label("Foo").create(input); + assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo(input.commitMessage); + } + + @Test + public void commitMessageIsTrimmed() throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad"); + input.commitMessage = " Add Foo Label "; + gApi.projects().name(project.get()).label("Foo").create(input); + assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage()) + .isEqualTo("Add Foo Label"); + } +}