diff --git a/api/src/main/java/com/cloud/projects/ProjectService.java b/api/src/main/java/com/cloud/projects/ProjectService.java index f93b3c576ef..5080cb5a781 100644 --- a/api/src/main/java/com/cloud/projects/ProjectService.java +++ b/api/src/main/java/com/cloud/projects/ProjectService.java @@ -78,9 +78,9 @@ public interface ProjectService { Project findByNameAndDomainId(String name, long domainId); - Project updateProject(long id, String displayText, String newOwnerName) throws ResourceAllocationException; + Project updateProject(long id, String name, String displayText, String newOwnerName) throws ResourceAllocationException; - Project updateProject(long id, String displayText, String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException; + Project updateProject(long id, String name, String displayText, String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException; boolean addAccountToProject(long projectId, String accountName, String email, Long projectRoleId, Role projectRoleType); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java index 8732f081681..6520aa63a64 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/project/UpdateProjectCmd.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.context.CallContext; import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import com.cloud.event.EventTypes; @@ -35,7 +36,6 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.ResourceAllocationException; import com.cloud.projects.Project; import com.cloud.projects.ProjectAccount; -import org.apache.commons.lang3.StringUtils; @APICommand(name = "updateProject", description = "Updates a project", responseObject = ProjectResponse.class, since = "3.0.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) @@ -67,6 +67,9 @@ public class UpdateProjectCmd extends BaseAsyncCmd { "to promote or demote the user/account based on the roleType (Regular or Admin) provided. Defaults to true") private Boolean swapOwner; + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "name of the project", since = "4.19.0") + private String name; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -87,6 +90,10 @@ public class UpdateProjectCmd extends BaseAsyncCmd { return userId; } + public String getName() { + return name; + } + public ProjectAccount.Role getRoleType(String role) { String type = role.substring(0, 1).toUpperCase() + role.substring(1).toLowerCase(); if (!EnumUtils.isValidEnum(ProjectAccount.Role.class, type)) { @@ -136,9 +143,9 @@ public class UpdateProjectCmd extends BaseAsyncCmd { Project project = null; if (isSwapOwner()) { - project = _projectService.updateProject(getId(), getDisplayText(), getAccountName()); + project = _projectService.updateProject(getId(), getName(), getDisplayText(), getAccountName()); } else { - project = _projectService.updateProject(getId(), getDisplayText(), getAccountName(), getUserId(), getAccountRole()); + project = _projectService.updateProject(getId(), getName(), getDisplayText(), getAccountName(), getUserId(), getAccountRole()); } if (project != null) { ProjectResponse response = _responseGenerator.createProjectResponse(project); diff --git a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java index f87ab4a6d65..19776d4993f 100644 --- a/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java +++ b/server/src/main/java/com/cloud/projects/ProjectManagerImpl.java @@ -18,9 +18,11 @@ package com.cloud.projects; import java.io.UnsupportedEncodingException; import java.security.SecureRandom; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.Executors; @@ -33,26 +35,20 @@ import javax.inject.Inject; import javax.mail.MessagingException; import javax.naming.ConfigurationException; -import com.cloud.network.dao.NetworkDao; -import com.cloud.network.dao.NetworkVO; -import com.cloud.network.vpc.Vpc; -import com.cloud.network.vpc.VpcManager; -import com.cloud.storage.VMTemplateVO; -import com.cloud.storage.VolumeVO; -import com.cloud.storage.dao.VMTemplateDao; -import com.cloud.storage.dao.VolumeDao; -import com.cloud.vm.UserVmVO; -import com.cloud.vm.dao.UserVmDao; -import com.cloud.vm.snapshot.VMSnapshotVO; -import com.cloud.vm.snapshot.dao.VMSnapshotDao; import org.apache.cloudstack.acl.ProjectRole; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.acl.dao.ProjectRoleDao; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.PublishScope; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.utils.mailing.MailAddress; +import org.apache.cloudstack.utils.mailing.SMTPMailProperties; +import org.apache.cloudstack.utils.mailing.SMTPMailSender; +import org.apache.commons.lang3.BooleanUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -72,11 +68,19 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.VpcManager; import com.cloud.projects.Project.State; import com.cloud.projects.ProjectAccount.Role; import com.cloud.projects.dao.ProjectAccountDao; import com.cloud.projects.dao.ProjectDao; import com.cloud.projects.dao.ProjectInvitationDao; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; import com.cloud.tags.dao.ResourceTagDao; import com.cloud.user.Account; import com.cloud.user.AccountManager; @@ -95,14 +99,10 @@ import com.cloud.utils.db.TransactionCallbackNoReturn; import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn; import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; -import java.util.HashSet; -import java.util.Set; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.utils.mailing.MailAddress; -import org.apache.cloudstack.utils.mailing.SMTPMailProperties; -import org.apache.cloudstack.utils.mailing.SMTPMailSender; -import org.apache.commons.lang3.BooleanUtils; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.VMSnapshotVO; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; @Component public class ProjectManagerImpl extends ManagerBase implements ProjectManager, Configurable { @@ -650,7 +650,7 @@ public class ProjectManagerImpl extends ManagerBase implements ProjectManager, C @Override @DB @ActionEvent(eventType = EventTypes.EVENT_PROJECT_UPDATE, eventDescription = "updating project", async = true) - public Project updateProject(final long projectId, final String displayText, final String newOwnerName) throws ResourceAllocationException { + public Project updateProject(final long projectId, String name, final String displayText, final String newOwnerName) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); //check that the project exists @@ -666,10 +666,7 @@ public class ProjectManagerImpl extends ManagerBase implements ProjectManager, C Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) throws ResourceAllocationException { - if (displayText != null) { - project.setDisplayText(displayText); - _projectDao.update(projectId, project); - } + updateProjectNameAndDisplayText(project, name, displayText); if (newOwnerName != null) { //check that the new owner exists @@ -717,7 +714,7 @@ public class ProjectManagerImpl extends ManagerBase implements ProjectManager, C @Override @DB @ActionEvent(eventType = EventTypes.EVENT_PROJECT_UPDATE, eventDescription = "updating project", async = true) - public Project updateProject(final long projectId, final String displayText, final String newOwnerName, Long userId, + public Project updateProject(final long projectId, String name, final String displayText, final String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException { Account caller = CallContext.current().getCallingAccount(); @@ -737,10 +734,8 @@ public class ProjectManagerImpl extends ManagerBase implements ProjectManager, C Transaction.execute(new TransactionCallbackWithExceptionNoReturn() { @Override public void doInTransactionWithoutResult(TransactionStatus status) throws ResourceAllocationException { - if (displayText != null) { - project.setDisplayText(displayText); - _projectDao.update(projectId, project); - } + updateProjectNameAndDisplayText(project, name, displayText); + if (newOwnerName != null) { //check that the new owner exists Account updatedAcc = _accountMgr.getActiveAccountByName(newOwnerName, project.getDomainId()); @@ -1443,4 +1438,17 @@ public class ProjectManagerImpl extends ManagerBase implements ProjectManager, C public ConfigKey[] getConfigKeys() { return new ConfigKey[] {ProjectSmtpEnabledSecurityProtocols, ProjectSmtpUseStartTLS}; } + + protected void updateProjectNameAndDisplayText(final ProjectVO project, String name, String displayText) { + if (name == null && displayText == null){ + return; + } + if (name != null) { + project.setName(name); + } + if (displayText != null) { + project.setDisplayText(displayText); + } + _projectDao.update(project.getId(), project); + } } diff --git a/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java b/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java index 988d675ec04..4aa50fe4bfb 100644 --- a/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java +++ b/server/src/test/java/com/cloud/projects/MockProjectManagerImpl.java @@ -85,12 +85,12 @@ public class MockProjectManagerImpl extends ManagerBase implements ProjectManage } @Override - public Project updateProject(long id, String displayText, String newOwnerName) throws ResourceAllocationException { + public Project updateProject(long id, String name, String displayText, String newOwnerName) throws ResourceAllocationException { return null; } @Override - public Project updateProject(long id, String displayText, String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException { + public Project updateProject(long id, String name, String displayText, String newOwnerName, Long userId, Role newRole) throws ResourceAllocationException { // TODO Auto-generated method stub return null; } diff --git a/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java b/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java new file mode 100644 index 00000000000..90bc2b4e22a --- /dev/null +++ b/server/src/test/java/com/cloud/projects/ProjectManagerImplTest.java @@ -0,0 +1,98 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.cloud.projects; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.cloud.projects.dao.ProjectDao; + + +@RunWith(MockitoJUnitRunner.class) +public class ProjectManagerImplTest { + + @Spy + @InjectMocks + ProjectManagerImpl projectManager; + + @Mock + ProjectDao projectDao; + + List updateProjects; + + @Before + public void setUp() throws Exception { + updateProjects = new ArrayList<>(); + Mockito.when(projectDao.update(Mockito.anyLong(), Mockito.any(ProjectVO.class))).thenAnswer((Answer) invocation -> { + ProjectVO project = (ProjectVO)invocation.getArguments()[1]; + updateProjects.add(project); + return true; + }); + } + + private void runUpdateProjectNameAndDisplayTextTest(boolean nonNullName, boolean nonNullDisplayText) { + ProjectVO projectVO = new ProjectVO(); + String newName = nonNullName ? "NewName" : null; + String newDisplayText = nonNullDisplayText ? "NewDisplayText" : null; + projectManager.updateProjectNameAndDisplayText(projectVO, newName, newDisplayText); + if (!nonNullName && !nonNullDisplayText) { + Assert.assertTrue(updateProjects.isEmpty()); + } else { + Assert.assertFalse(updateProjects.isEmpty()); + Assert.assertEquals(1, updateProjects.size()); + ProjectVO updatedProject = updateProjects.get(0); + Assert.assertNotNull(updatedProject); + if (nonNullName) { + Assert.assertEquals(newName, updatedProject.getName()); + } + if (nonNullDisplayText) { + Assert.assertEquals(newDisplayText, updatedProject.getDisplayText()); + } + } + } + + @Test + public void testUpdateProjectNameAndDisplayTextNoUpdate() { + runUpdateProjectNameAndDisplayTextTest(false, false); + } + + @Test + public void testUpdateProjectNameAndDisplayTextUpdateName() { + runUpdateProjectNameAndDisplayTextTest(true, false); + } + + @Test + public void testUpdateProjectNameAndDisplayTextUpdateDisplayText() { + runUpdateProjectNameAndDisplayTextTest(false, true); + } + + @Test + public void testUpdateProjectNameAndDisplayTextUpdateNameDisplayText() { + runUpdateProjectNameAndDisplayTextTest(true, true); + } +} diff --git a/test/integration/smoke/test_projects.py b/test/integration/smoke/test_projects.py index 91fc722527c..f0696b4ebef 100644 --- a/test/integration/smoke/test_projects.py +++ b/test/integration/smoke/test_projects.py @@ -1821,49 +1821,20 @@ class TestProjectSuspendActivate(cloudstackTestCase): ) return -class TestProjectWithEmptyDisplayText(cloudstackTestCase): - +class TestProjectWithNameDisplayTextAction(cloudstackTestCase): @classmethod def setUpClass(cls): cls.testClient = super( - TestProjectWithEmptyDisplayText, + TestProjectWithNameDisplayTextAction, cls).getClsTestClient() cls.api_client = cls.testClient.getApiClient() - cls.services = Services().services # Get Zone cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) - cls.hypervisor = cls.testClient.getHypervisorInfo() cls.domain = get_domain(cls.api_client) cls.services['mode'] = cls.zone.networktype - cls.template = get_test_template( - cls.api_client, - cls.zone.id, - cls.hypervisor - ) cls._cleanup = [] - cls.isGlobalSettingInvalid = False - configs = Configurations.list( - cls.api_client, - name='project.invite.required' - ) - if (configs[0].value).lower() != 'false': - cls.isGlobalSettingInvalid = True - return - - # Create account, service offering, disk offering etc. - cls.disk_offering = DiskOffering.create( - cls.api_client, - cls.services["disk_offering"] - ) - cls._cleanup.append(cls.disk_offering) - cls.service_offering = ServiceOffering.create( - cls.api_client, - cls.services["service_offering"], - domainid=cls.domain.id - ) - cls._cleanup.append(cls.service_offering) cls.account = Account.create( cls.api_client, cls.services["account"], @@ -1871,40 +1842,19 @@ class TestProjectWithEmptyDisplayText(cloudstackTestCase): domainid=cls.domain.id ) cls._cleanup.append(cls.account) - cls.user = Account.create( - cls.api_client, - cls.services["account"], - admin=True, - domainid=cls.domain.id - ) - cls._cleanup.append(cls.user) - - # Create project as a domain admin - cls.project = Project.create( - cls.api_client, - cls.services["project"], - account=cls.account.name, - domainid=cls.account.domainid - ) - cls._cleanup.append(cls.project) - cls.services["virtual_machine"]["zoneid"] = cls.zone.id return @classmethod def tearDownClass(cls): - super(TestProjectWithEmptyDisplayText, cls).tearDownClass() + super(TestProjectWithNameDisplayTextAction, cls).tearDownClass() def setUp(self): self.apiclient = self.testClient.getApiClient() self.dbclient = self.testClient.getDbConnection() self.cleanup = [] - if self.isGlobalSettingInvalid: - self.skipTest("'project.invite.required' should be set to false") - return - def tearDown(self): - super(TestProjectWithEmptyDisplayText, self).tearDown() + super(TestProjectWithNameDisplayTextAction, self).tearDown() @attr( tags=[ @@ -1919,20 +1869,17 @@ class TestProjectWithEmptyDisplayText(cloudstackTestCase): """ create Project with Empty DisplayText """ # Validate the following - # 1. Create a project. - # 2. Give empty displayText - # 3. Verify displayText takes content of Project name. + # 1. Create a project while giving empty displayText + # 2. Verify displayText takes content of Project name. self.services["project"]["displaytext"] = "" - # Create project as a domain admin project = Project.create( self.apiclient, self.services["project"], account=self.account.name, domainid=self.account.domainid ) - self.cleanup.append(project) self.assertEqual( @@ -1940,5 +1887,49 @@ class TestProjectWithEmptyDisplayText(cloudstackTestCase): project.name, "displayText does not matches project name" ) - + return + + @attr( + tags=[ + "advanced", + "basic", + "sg", + "eip", + "advancedns", + "simulator"], + required_hardware="false") + def test_12_update_project_name_display_text(self): + """ Create Project and update its name + """ + # Validate the following + # 1. Create a project + # 2. Update project name and display text + # 2. Verify name and display text for the project are updated + + project = Project.create( + self.apiclient, + self.services["project"], + account=self.account.name, + domainid=self.account.domainid + ) + self.cleanup.append(project) + + new_name = "NewName" + new_display_text = "NewDisplayText" + project.update(self.apiclient, name=new_name, displaytext=new_display_text) + updated_project = Project.list( + self.apiclient, + id=project.id, + listall=True + )[0] + self.assertEqual( + updated_project.name, + new_name, + "Project name not updated" + ) + self.assertEqual( + updated_project.displaytext, + new_display_text, + "Project displaytext not updated" + ) return diff --git a/ui/src/config/section/project.js b/ui/src/config/section/project.js index a9bfb954647..adbfac725bb 100644 --- a/ui/src/config/section/project.js +++ b/ui/src/config/section/project.js @@ -101,7 +101,7 @@ export default { icon: 'edit-outlined', label: 'label.edit.project.details', dataView: true, - args: ['displaytext'], + args: ['name', 'displaytext'], show: (record, store) => { return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin }