From 115d303190cbbe29356bff7f511b5bd61afb82ad Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Fri, 28 Feb 2025 13:48:14 -0300 Subject: [PATCH 01/20] Refactor Quota Summary API --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../com/cloud/domain/dao/DomainDaoImpl.java | 2 +- .../views/cloud_usage.quota_summary_view.sql | 48 ++++ .../quota/QuotaAccountStateFilter.java | 31 +++ .../cloudstack/quota/dao/QuotaSummaryDao.java | 32 +++ .../quota/dao/QuotaSummaryDaoImpl.java | 80 ++++++ .../cloudstack/quota/vo/QuotaSummaryVO.java | 154 +++++++++++ .../quota/spring-framework-quota-context.xml | 3 +- .../api/command/QuotaSummaryCmd.java | 43 ++- .../api/response/QuotaResponseBuilder.java | 3 + .../response/QuotaResponseBuilderImpl.java | 166 +++++++++--- .../api/response/QuotaSummaryResponse.java | 107 ++++---- .../cloudstack/quota/QuotaServiceImpl.java | 29 ++ .../api/command/QuotaSummaryCmdTest.java | 49 ++++ .../QuotaResponseBuilderImplTest.java | 248 ++++++++++++++---- .../quota/QuotaServiceImplTest.java | 49 ++-- 16 files changed, 888 insertions(+), 157 deletions(-) create mode 100644 engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java create mode 100644 framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java create mode 100644 plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 3e8b329cac78..e5ef62a570a8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -19,6 +19,7 @@ public class ApiConstants { public static final String ACCOUNT = "account"; public static final String ACCOUNTS = "accounts"; + public static final String ACCOUNT_STATE_TO_SHOW = "accountstatetoshow"; public static final String ACCOUNT_TYPE = "accounttype"; public static final String ACCOUNT_ID = "accountid"; public static final String ACCOUNT_IDS = "accountids"; diff --git a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java index 56d971bbe015..1afa0d22dcc1 100644 --- a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDaoImpl.java @@ -262,7 +262,7 @@ public boolean isChildDomain(Long parentId, Long childId) { SearchCriteria sc = DomainPairSearch.create(); sc.setParameters("id", parentId, childId); - List domainPair = listBy(sc); + List domainPair = listIncludingRemovedBy(sc); if ((domainPair != null) && (domainPair.size() == 2)) { DomainVO d1 = domainPair.get(0); diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql new file mode 100644 index 000000000000..0646c3b6f919 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud_usage.quota_summary_view.sql @@ -0,0 +1,48 @@ +-- 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. + +-- cloud_usage.quota_summary_view source + +-- Create view for quota summary +DROP VIEW IF EXISTS `cloud_usage`.`quota_summary_view`; +CREATE VIEW `cloud_usage`.`quota_summary_view` AS +SELECT + cloud_usage.quota_account.account_id AS account_id, + cloud_usage.quota_account.quota_balance AS quota_balance, + cloud_usage.quota_account.quota_balance_date AS quota_balance_date, + cloud_usage.quota_account.quota_enforce AS quota_enforce, + cloud_usage.quota_account.quota_min_balance AS quota_min_balance, + cloud_usage.quota_account.quota_alert_date AS quota_alert_date, + cloud_usage.quota_account.quota_alert_type AS quota_alert_type, + cloud_usage.quota_account.last_statement_date AS last_statement_date, + cloud.account.uuid AS account_uuid, + cloud.account.account_name AS account_name, + cloud.account.state AS account_state, + cloud.account.removed AS account_removed, + cloud.domain.id AS domain_id, + cloud.domain.uuid AS domain_uuid, + cloud.domain.name AS domain_name, + cloud.domain.path AS domain_path, + cloud.domain.removed AS domain_removed, + cloud.projects.uuid AS project_uuid, + cloud.projects.name AS project_name, + cloud.projects.removed AS project_removed +FROM + cloud_usage.quota_account + INNER JOIN cloud.account ON (cloud.account.id = cloud_usage.quota_account.account_id) + INNER JOIN cloud.domain ON (cloud.domain.id = cloud.account.domain_id) + LEFT JOIN cloud.projects ON (cloud.account.type = 5 AND cloud.account.id = cloud.projects.project_account_id); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java new file mode 100644 index 000000000000..10ad42f8b4ab --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java @@ -0,0 +1,31 @@ +// 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 org.apache.cloudstack.quota; + +public enum QuotaAccountStateFilter { + ALL, ACTIVE, REMOVED; + + public static QuotaAccountStateFilter getValue(String value) { + for (QuotaAccountStateFilter state : values()) { + if (state.name().equalsIgnoreCase(value)) { + return state; + } + } + + return null; + } +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java new file mode 100644 index 000000000000..d8ee26075014 --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDao.java @@ -0,0 +1,32 @@ +// 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 org.apache.cloudstack.quota.dao; + +import java.util.List; + +import org.apache.cloudstack.quota.QuotaAccountStateFilter; +import org.apache.cloudstack.quota.vo.QuotaSummaryVO; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.GenericDao; + +public interface QuotaSummaryDao extends GenericDao { + + Pair, Integer> listQuotaSummariesForAccountAndOrDomain(Long accountId, String accountName, Long domainId, String domainPath, + QuotaAccountStateFilter accountStateFilter, Long startIndex, Long pageSize); +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java new file mode 100644 index 000000000000..d90d5a75859e --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaSummaryDaoImpl.java @@ -0,0 +1,80 @@ +// 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 org.apache.cloudstack.quota.dao; + +import java.util.List; + +import org.apache.cloudstack.quota.QuotaAccountStateFilter; +import org.apache.cloudstack.quota.vo.QuotaSummaryVO; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.db.TransactionLegacy; + +public class QuotaSummaryDaoImpl extends GenericDaoBase implements QuotaSummaryDao { + + @Override + public Pair, Integer> listQuotaSummariesForAccountAndOrDomain(Long accountId, String accountName, Long domainId, String domainPath, + QuotaAccountStateFilter accountStateFilter, Long startIndex, Long pageSize) { + SearchCriteria searchCriteria = createListQuotaSummariesSearchCriteria(accountId, accountName, domainId, domainPath, accountStateFilter); + Filter filter = new Filter(QuotaSummaryVO.class, "accountName", true, startIndex, pageSize); + + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback, Integer>>) status -> searchAndCount(searchCriteria, filter)); + } + + protected SearchCriteria createListQuotaSummariesSearchCriteria(Long accountId, String accountName, Long domainId, String domainPath, + QuotaAccountStateFilter accountStateFilter) { + SearchCriteria searchCriteria = createListQuotaSummariesSearchBuilder(accountStateFilter).create(); + + searchCriteria.setParametersIfNotNull("accountId", accountId); + searchCriteria.setParametersIfNotNull("domainId", domainId); + + if (accountName != null) { + searchCriteria.setParameters("accountName", "%" + accountName + "%"); + } + + if (domainPath != null) { + searchCriteria.setParameters("domainPath", domainPath + "%"); + } + + return searchCriteria; + } + + protected SearchBuilder createListQuotaSummariesSearchBuilder(QuotaAccountStateFilter accountStateFilter) { + SearchBuilder searchBuilder = createSearchBuilder(); + + searchBuilder.and("accountId", searchBuilder.entity().getAccountId(), SearchCriteria.Op.EQ); + searchBuilder.and("accountName", searchBuilder.entity().getAccountName(), SearchCriteria.Op.LIKE); + searchBuilder.and("domainId", searchBuilder.entity().getDomainId(), SearchCriteria.Op.EQ); + searchBuilder.and("domainPath", searchBuilder.entity().getDomainPath(), SearchCriteria.Op.LIKE); + + if (QuotaAccountStateFilter.REMOVED.equals(accountStateFilter)) { + searchBuilder.and("accountRemoved", searchBuilder.entity().getAccountRemoved(), SearchCriteria.Op.NNULL); + } else if (QuotaAccountStateFilter.ACTIVE.equals(accountStateFilter)) { + searchBuilder.and("accountRemoved", searchBuilder.entity().getAccountRemoved(), SearchCriteria.Op.NULL); + } + + return searchBuilder; + } + +} diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java new file mode 100644 index 000000000000..f9796497d57d --- /dev/null +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaSummaryVO.java @@ -0,0 +1,154 @@ +// 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 org.apache.cloudstack.quota.vo; + +import java.math.BigDecimal; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.cloud.user.Account; + +@Entity +@Table(name = "quota_summary_view") +public class QuotaSummaryVO { + + @Id + @Column(name = "account_id") + private Long accountId = null; + + @Column(name = "quota_enforce") + private Integer quotaEnforce = 0; + + @Column(name = "quota_balance") + private BigDecimal quotaBalance; + + @Column(name = "quota_balance_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date quotaBalanceDate = null; + + @Column(name = "quota_min_balance") + private BigDecimal quotaMinBalance; + + @Column(name = "quota_alert_type") + private Integer quotaAlertType = null; + + @Column(name = "quota_alert_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date quotaAlertDate = null; + + @Column(name = "last_statement_date") + @Temporal(value = TemporalType.TIMESTAMP) + private Date lastStatementDate = null; + + @Column(name = "account_uuid") + private String accountUuid; + + @Column(name = "account_name") + private String accountName; + + @Column(name = "account_state") + @Enumerated(EnumType.STRING) + private Account.State accountState; + + @Column(name = "account_removed") + private Date accountRemoved; + + @Column(name = "domain_id") + private Long domainId; + + @Column(name = "domain_uuid") + private String domainUuid; + + @Column(name = "domain_name") + private String domainName; + + @Column(name = "domain_path") + private String domainPath; + + @Column(name = "domain_removed") + private Date domainRemoved; + + @Column(name = "project_uuid") + private String projectUuid; + + @Column(name = "project_name") + private String projectName; + + @Column(name = "project_removed") + private Date projectRemoved; + + public Long getAccountId() { + return accountId; + } + + public BigDecimal getQuotaBalance() { + return quotaBalance; + } + + public String getAccountUuid() { + return accountUuid; + } + + public String getAccountName() { + return accountName; + } + + public Date getAccountRemoved() { + return accountRemoved; + } + + public Account.State getAccountState() { + return accountState; + } + + public Long getDomainId() { + return domainId; + } + + public String getDomainUuid() { + return domainUuid; + } + + public String getDomainPath() { + return domainPath; + } + + public Date getDomainRemoved() { + return domainRemoved; + } + + public String getProjectUuid() { + return projectUuid; + } + + public String getProjectName() { + return projectName; + } + + public Date getProjectRemoved() { + return projectRemoved; + } +} diff --git a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml index e634321208f4..a46162ece8c8 100644 --- a/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml +++ b/framework/quota/src/main/resources/META-INF/cloudstack/quota/spring-framework-quota-context.xml @@ -19,7 +19,8 @@ - + + , Integer> responses; + Pair, Integer> responses = quotaResponseBuilder.createQuotaSummaryResponse(this); if (caller.getType() == Account.Type.ADMIN) { if (getAccountName() != null && getDomainId() != null) - responses = _responseBuilder.createQuotaSummaryResponse(getAccountName(), getDomainId()); + responses = quotaResponseBuilder.createQuotaSummaryResponse(getAccountName(), getDomainId()); else - responses = _responseBuilder.createQuotaSummaryResponse(isListAll(), getKeyword(), getStartIndex(), getPageSizeVal()); + responses = quotaResponseBuilder.createQuotaSummaryResponse(isListAll(), getKeyword(), getStartIndex(), getPageSizeVal()); } else { - responses = _responseBuilder.createQuotaSummaryResponse(caller.getAccountName(), caller.getDomainId()); + responses = quotaResponseBuilder.createQuotaSummaryResponse(caller.getAccountName(), caller.getDomainId()); } final ListResponse response = new ListResponse(); response.setResponses(responses.first(), responses.second()); @@ -87,13 +97,24 @@ public void setDomainId(Long domainId) { } public Boolean isListAll() { - return listAll == null ? false: listAll; + return BooleanUtils.toBoolean(listAll); } public void setListAll(Boolean listAll) { this.listAll = listAll; } + public QuotaAccountStateFilter getAccountStateToShow() { + if (StringUtils.isNotBlank(accountStateToShow)) { + QuotaAccountStateFilter state = QuotaAccountStateFilter.getValue(accountStateToShow); + if (state != null) { + return state; + } + } + + return QuotaAccountStateFilter.ACTIVE; + } + @Override public long getEntityOwnerId() { return Account.ACCOUNT_ID_SYSTEM; diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index 67f75ebf82fb..262526fc4223 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -24,6 +24,7 @@ import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; import org.apache.cloudstack.api.command.QuotaStatementCmd; +import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; import org.apache.cloudstack.api.command.QuotaTariffListCmd; import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; @@ -52,6 +53,8 @@ public interface QuotaResponseBuilder { QuotaBalanceResponse createQuotaBalanceResponse(List quotaUsage, Date startDate, Date endDate); + Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd); + Pair, Integer> createQuotaSummaryResponse(Boolean listAll); Pair, Integer> createQuotaSummaryResponse(Boolean listAll, String keyword, Long startIndex, Long pageSize); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 7a987df0a35b..433065923c0a 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -42,6 +42,7 @@ import javax.inject.Inject; +import com.cloud.domain.Domain; import com.cloud.exception.PermissionDeniedException; import com.cloud.user.User; import com.cloud.user.UserVO; @@ -49,17 +50,7 @@ import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.command.QuotaBalanceCmd; -import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; -import org.apache.cloudstack.api.command.QuotaCreditsListCmd; -import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; -import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; -import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; -import org.apache.cloudstack.api.command.QuotaStatementCmd; -import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; -import org.apache.cloudstack.api.command.QuotaTariffListCmd; -import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; -import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; +import org.apache.cloudstack.api.command.*; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper; @@ -74,22 +65,12 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaConfig; import org.apache.cloudstack.quota.constant.QuotaTypes; -import org.apache.cloudstack.quota.dao.QuotaAccountDao; -import org.apache.cloudstack.quota.dao.QuotaBalanceDao; -import org.apache.cloudstack.quota.dao.QuotaCreditsDao; -import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao; -import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao; -import org.apache.cloudstack.quota.dao.QuotaTariffDao; -import org.apache.cloudstack.quota.vo.QuotaAccountVO; -import org.apache.cloudstack.quota.dao.QuotaUsageDao; -import org.apache.cloudstack.quota.vo.QuotaBalanceVO; -import org.apache.cloudstack.quota.vo.QuotaCreditsVO; -import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; -import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; -import org.apache.cloudstack.quota.vo.QuotaTariffVO; -import org.apache.cloudstack.quota.vo.QuotaUsageVO; +import org.apache.cloudstack.quota.dao.*; +import org.apache.cloudstack.quota.vo.*; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.compress.utils.Sets; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.commons.lang3.ObjectUtils; @@ -121,7 +102,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private QuotaCreditsDao quotaCreditsDao; @Inject - private QuotaUsageDao _quotaUsageDao; + private QuotaUsageDao quotaUsageDao; @Inject private QuotaEmailTemplatesDao _quotaEmailTemplateDao; @@ -134,22 +115,25 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private QuotaAccountDao quotaAccountDao; @Inject - private DomainDao _domainDao; + private DomainDao domainDao; @Inject private AccountManager _accountMgr; @Inject - private QuotaStatement _statement; + private QuotaStatement quotaStatement; @Inject private QuotaManager _quotaManager; @Inject private QuotaEmailConfigurationDao quotaEmailConfigurationDao; @Inject + private QuotaSummaryDao quotaSummaryDao; + @Inject private JsInterpreterHelper jsInterpreterHelper; @Inject private ApiDiscoveryService apiDiscoveryService; private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; + private Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); @Override public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) { @@ -174,6 +158,128 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole return response; } + @Override + public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { + Account caller = CallContext.current().getCallingAccount(); + + if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { + return getQuotaSummaryResponse(caller.getAccountId(), null, null, null, cmd); + } + + return getQuotaSummaryResponseWithListAll(cmd, caller); + } + + protected Pair, Integer> getQuotaSummaryResponseWithListAll(QuotaSummaryCmd cmd, Account caller) { + Long accountId = cmd.getEntityOwnerId(); + if (accountId == -1) { + accountId = null; + } + + Long domainId = cmd.getDomainId(); + if (domainId != null) { + DomainVO domain = domainDao.findByIdIncludingRemoved(domainId); + if (domain == null) { + throw new InvalidParameterValueException(String.format("Domain [%s] does not exist.", domainId)); + } + } + + String domainPath = getDomainPathByDomainIdForDomainAdmin(caller); + + String keyword = null; + if (Account.Type.ADMIN.equals(caller.getType())) { + keyword = cmd.getKeyword(); + } + + return getQuotaSummaryResponse(accountId, keyword, domainId, domainPath, cmd); + } + + protected Long getAccountIdByAccountName(String accountName, Long domainId, Account caller) { + if (ObjectUtils.anyNull(accountName, domainId)) { + return null; + } + + Domain domain = domainDao.findByIdIncludingRemoved(domainId); + _accountMgr.checkAccess(caller, domain); + + Account account = _accountDao.findAccountIncludingRemoved(accountName, domainId); + + if (account == null) { + throw new InvalidParameterValueException(String.format("Account name [%s] or domain id [%s] is invalid.", accountName, domainId)); + } + + return account.getAccountId(); + } + + /** + * Retrieves the domain path of the caller's domain (if the caller is Domain Admin) for filtering in the quota summary query. + * @return null if the caller is an Admin or the domain path of the caller's domain if the caller is a Domain Admin. + * @throws InvalidParameterValueException if it cannot find the domain. + */ + protected String getDomainPathByDomainIdForDomainAdmin(Account caller) { + if (caller.getType() != Account.Type.DOMAIN_ADMIN) { + return null; + } + + Long domainId = caller.getDomainId(); + Domain domain = domainDao.findById(domainId); + _accountMgr.checkAccess(caller, domain); + + if (domain == null) { + throw new InvalidParameterValueException(String.format("Domain id [%s] is invalid.", domainId)); + } + + return domain.getPath(); + } + + protected Pair, Integer> getQuotaSummaryResponse(Long accountId, String accountName, Long domainId, String domainPath, QuotaSummaryCmd cmd) { + Pair, Integer> pairSummaries = quotaSummaryDao.listQuotaSummariesForAccountAndOrDomain(accountId, accountName, domainId, domainPath, + cmd.getAccountStateToShow(), cmd.getStartIndex(), cmd.getPageSizeVal()); + List summaries = pairSummaries.first(); + + if (CollectionUtils.isEmpty(summaries)) { + logger.info(String.format("There are no summaries to list for parameters [%s].", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "domainId", "listAll", "page", "pageSize"))); + return new Pair<>(new ArrayList<>(), 0); + } + + List responses = summaries.stream().map(this::getQuotaSummaryResponse).collect(Collectors.toList()); + + return new Pair<>(responses, pairSummaries.second()); + } + + protected QuotaSummaryResponse getQuotaSummaryResponse(QuotaSummaryVO summary) { + QuotaSummaryResponse response = new QuotaSummaryResponse(); + Account account = _accountDao.findByUuidIncludingRemoved(summary.getAccountUuid()); + + Calendar[] period = quotaStatement.getCurrentStatementTime(); + Date startDate = period[0].getTime(); + Date endDate = period[1].getTime(); + BigDecimal quotaUsage = quotaUsageDao.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, startDate, endDate); + + response.setQuotaUsage(quotaUsage); + response.setStartDate(startDate); + response.setEndDate(endDate); + response.setAccountId(summary.getAccountUuid()); + response.setAccountName(summary.getAccountName()); + response.setDomainId(summary.getDomainUuid()); + response.setDomainPath(summary.getDomainPath()); + response.setBalance(summary.getQuotaBalance()); + response.setState(summary.getAccountState()); + response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + response.setQuotaEnabled(QuotaConfig.QuotaAccountEnabled.valueIn(account.getId())); + response.setDomainRemoved(summary.getDomainRemoved() != null); + response.setAccountRemoved(summary.getAccountRemoved() != null); + response.setObjectName("summary"); + + if (summary.getProjectUuid() != null) { + response.setProjectId(summary.getProjectUuid()); + response.setProjectName(summary.getProjectName()); + response.setProjectRemoved(summary.getProjectRemoved() != null); + } + + return response; + } + @Override public Pair, Integer> createQuotaSummaryResponse(final String accountName, final Long domainId) { List result = new ArrayList(); @@ -220,13 +326,13 @@ public Pair, Integer> createQuotaSummaryResponse(Bool } protected QuotaSummaryResponse getQuotaSummaryResponse(final Account account) { - Calendar[] period = _statement.getCurrentStatementTime(); + Calendar[] period = quotaStatement.getCurrentStatementTime(); if (account != null) { QuotaSummaryResponse qr = new QuotaSummaryResponse(); DomainVO domain = _domainDao.findById(account.getDomainId()); BigDecimal curBalance = _quotaBalanceDao.lastQuotaBalance(account.getAccountId(), account.getDomainId(), period[1].getTime()); - BigDecimal quotaUsage = _quotaUsageDao.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, period[0].getTime(), period[1].getTime()); + BigDecimal quotaUsage = quotaUsageDao.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, period[0].getTime(), period[1].getTime()); qr.setAccountId(account.getUuid()); qr.setAccountName(account.getAccountName()); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java index 5b3526fba506..7425035195d8 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java @@ -17,7 +17,6 @@ package org.apache.cloudstack.api.response; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Date; import com.google.gson.annotations.SerializedName; @@ -30,52 +29,68 @@ public class QuotaSummaryResponse extends BaseResponse { @SerializedName("accountid") - @Param(description = "account id") + @Param(description = "Account's ID") private String accountId; @SerializedName("account") - @Param(description = "account name") + @Param(description = "Account's name") private String accountName; @SerializedName("domainid") - @Param(description = "domain id") + @Param(description = "Domain's ID") private String domainId; @SerializedName("domain") - @Param(description = "domain name") - private String domainName; + @Param(description = "Domain's path") + private String domainPath; @SerializedName("balance") - @Param(description = "account balance") + @Param(description = "Account's balance") private BigDecimal balance; @SerializedName("state") - @Param(description = "account state") + @Param(description = "Account's state") private State state; + @SerializedName("domainremoved") + @Param(description = "If the domain is removed or not") + private boolean domainRemoved; + + @SerializedName("accountremoved") + @Param(description = "If the account is removed or not") + private boolean accountRemoved; + @SerializedName("quota") - @Param(description = "quota usage of this period") + @Param(description = "Quota consumed between the startdate and enddate") private BigDecimal quotaUsage; @SerializedName("startdate") - @Param(description = "start date") - private Date startDate = null; + @Param(description = "Start date of the quota consumption") + private Date startDate; @SerializedName("enddate") - @Param(description = "end date") - private Date endDate = null; + @Param(description = "End date of the quota consumption") + private Date endDate; @SerializedName("currency") - @Param(description = "currency") + @Param(description = "Currency") private String currency; @SerializedName("quotaenabled") @Param(description = "if the account has the quota config enabled") private boolean quotaEnabled; - public QuotaSummaryResponse() { - super(); - } + @SerializedName("projectname") + @Param(description = "Name of the project") + private String projectName; + + @SerializedName("projectid") + @Param(description = "Project's id") + private String projectId; + + @SerializedName("projectremoved") + @Param(description = "Whether the project is removed or not") + private Boolean projectRemoved; public String getAccountId() { return accountId; @@ -101,28 +116,16 @@ public void setDomainId(String domainId) { this.domainId = domainId; } - public String getDomainName() { - return domainName; - } - - public void setDomainName(String domainName) { - this.domainName = domainName; - } - - public BigDecimal getQuotaUsage() { - return quotaUsage; - } - - public State getState() { - return state; + public void setDomainPath(String domainPath) { + this.domainPath = domainPath; } public void setState(State state) { this.state = state; } - public void setQuotaUsage(BigDecimal startQuota) { - this.quotaUsage = startQuota.setScale(2, RoundingMode.HALF_EVEN); + public void setQuotaUsage(BigDecimal quotaUsage) { + this.quotaUsage = quotaUsage; } public BigDecimal getBalance() { @@ -130,38 +133,42 @@ public BigDecimal getBalance() { } public void setBalance(BigDecimal balance) { - this.balance = balance.setScale(2, RoundingMode.HALF_EVEN); + this.balance = balance; } - public Date getStartDate() { - return startDate == null ? null : new Date(startDate.getTime()); + public void setStartDate(Date startDate) { + this.startDate = startDate; } - public void setStartDate(Date startDate) { - this.startDate = startDate == null ? null : new Date(startDate.getTime()); + public void setEndDate(Date endDate) { + this.endDate = endDate; } - public Date getEndDate() { - return endDate == null ? null : new Date(endDate.getTime()); + public void setCurrency(String currency) { + this.currency = currency; } - public void setEndDate(Date endDate) { - this.endDate = endDate == null ? null : new Date(endDate.getTime()); + public void setQuotaEnabled(boolean quotaEnabled) { + this.quotaEnabled = quotaEnabled; } - public String getCurrency() { - return currency; + public void setProjectName(String projectName) { + this.projectName = projectName; } - public void setCurrency(String currency) { - this.currency = currency; + public void setProjectId(String projectId) { + this.projectId = projectId; } - public boolean getQuotaEnabled() { - return quotaEnabled; + public void setProjectRemoved(Boolean projectRemoved) { + this.projectRemoved = projectRemoved; } - public void setQuotaEnabled(boolean quotaEnabled) { - this.quotaEnabled = quotaEnabled; + public void setDomainRemoved(boolean domainRemoved) { + this.domainRemoved = domainRemoved; + } + + public void setAccountRemoved(boolean accountRemoved) { + this.accountRemoved = accountRemoved; } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index e02f260c142c..158b075eab0c 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -288,4 +288,33 @@ public void setMinBalance(Long accountId, Double balance) { } } + protected Long getAccountToWhomQuotaBalancesWillBeListed(Long accountId, String accountName, Long domainId) { + if (accountId != null) { + Account account = _accountDao.findByIdIncludingRemoved(accountId); + if (account == null) { + throw new InvalidParameterValueException(String.format("Unable to find account [%s].", accountId)); + } + return accountId; + } + + validateIsChildDomain(accountName, domainId); + + Account account = _accountDao.findActiveAccount(accountName, domainId); + if (account == null) { + throw new InvalidParameterValueException(String.format("Unable to find active account [%s] in domain [%s].", accountName, domainId)); + } + return account.getAccountId(); + } + + protected void validateIsChildDomain(String accountName, Long domainId) { + Account caller = CallContext.current().getCallingAccount(); + + long callerDomainId = caller.getDomainId(); + if (_domainDao.isChildDomain(callerDomainId, domainId)) { + return; + } + + logger.debug(String.format("Domain with ID [%s] is not a child of the caller's domain [%s].", domainId, callerDomainId)); + throw new PermissionDeniedException(String.format("Account [%s] or domain [%s] is invalid.", accountName, domainId)); + } } diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java new file mode 100644 index 000000000000..17473ac4fdc8 --- /dev/null +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java @@ -0,0 +1,49 @@ +// 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 org.apache.cloudstack.api.command; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class QuotaSummaryCmdTest { + + QuotaSummaryCmd quotaSummaryCmdSpy = Mockito.spy(QuotaSummaryCmd.class); + + @Test + public void isListAllTestNullReturnsFalse() { + quotaSummaryCmdSpy.setListAll(null); + Assert.assertFalse(quotaSummaryCmdSpy.isListAll()); + } + + @Test + public void isListAllTestFalseReturnsFalse() { + quotaSummaryCmdSpy.setListAll(false); + Assert.assertFalse(quotaSummaryCmdSpy.isListAll()); + } + + @Test + public void isListAllTestTrueReturnsTrue() { + quotaSummaryCmdSpy.setListAll(true); + Assert.assertTrue(quotaSummaryCmdSpy.isListAll()); + } + +} diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 1f5480404e4b..6c25c73f1c9b 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -16,13 +16,11 @@ // under the License. package org.apache.cloudstack.api.response; -import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; -import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -31,6 +29,7 @@ import java.util.HashSet; import java.util.function.Consumer; +import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.PermissionDeniedException; @@ -43,10 +42,10 @@ import org.apache.cloudstack.api.command.QuotaCreditsListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; +import org.apache.cloudstack.api.command.QuotaSummaryCmd; import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.discovery.ApiDiscoveryService; -import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper; import org.apache.cloudstack.quota.QuotaService; import org.apache.cloudstack.quota.QuotaStatement; @@ -70,6 +69,7 @@ import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; +import org.apache.commons.compress.utils.Sets; import org.apache.commons.lang3.time.DateUtils; import org.junit.Assert; @@ -79,7 +79,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; @@ -89,8 +92,7 @@ import com.cloud.user.User; import junit.framework.TestCase; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; + @RunWith(MockitoJUnitRunner.class) public class QuotaResponseBuilderImplTest extends TestCase { @@ -153,7 +155,7 @@ public class QuotaResponseBuilderImplTest extends TestCase { Account accountMock; @Mock - DomainVO domainVOMock; + DomainVO domainVoMock; @Mock QuotaConfigureEmailCmd quotaConfigureEmailCmdMock; @@ -161,6 +163,9 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock QuotaAccountVO quotaAccountVOMock; + @Mock + CallContext callContextMock; + @Mock QuotaEmailTemplatesVO quotaEmailTemplatesVoMock; @@ -184,17 +189,8 @@ public void setup() { CallContext.register(callerUserMock, callerAccountMock); } - private void overrideDefaultQuotaEnabledConfigValue(final Object value) throws IllegalAccessException, NoSuchFieldException { - Field f = ConfigKey.class.getDeclaredField("_defaultValue"); - f.setAccessible(true); - f.set(QuotaConfig.QuotaAccountEnabled, value); - } - - private Calendar[] createPeriodForQuotaSummary() { - final Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - return new Calendar[] {calendar, calendar}; - } + @Mock + Pair, Integer> quotaSummaryResponseMock1, quotaSummaryResponseMock2; @Mock QuotaValidateActivationRuleCmd quotaValidateActivationRuleCmdMock = Mockito.mock(QuotaValidateActivationRuleCmd.class); @@ -466,36 +462,6 @@ public void deleteQuotaTariffTestUpdateRemoved() { Mockito.verify(quotaTariffVoMock).setRemoved(Mockito.any(Date.class)); } - @Test - public void getQuotaSummaryResponseTestAccountIsNotNullQuotaIsDisabledShouldReturnFalse() throws NoSuchFieldException, IllegalAccessException { - Calendar[] period = createPeriodForQuotaSummary(); - overrideDefaultQuotaEnabledConfigValue("false"); - - Mockito.doReturn(period).when(quotaStatementMock).getCurrentStatementTime(); - Mockito.doReturn(domainVOMock).when(domainDaoMock).findById(Mockito.anyLong()); - Mockito.doReturn(BigDecimal.ZERO).when(quotaBalanceDaoMock).lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class)); - Mockito.doReturn(BigDecimal.ZERO).when(quotaUsageDaoMock).findTotalQuotaUsage(Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Date.class), Mockito.any(Date.class)); - - QuotaSummaryResponse quotaSummaryResponse = quotaResponseBuilderSpy.getQuotaSummaryResponse(accountMock); - - assertFalse(quotaSummaryResponse.getQuotaEnabled()); - } - - @Test - public void getQuotaSummaryResponseTestAccountIsNotNullQuotaIsEnabledShouldReturnTrue() throws NoSuchFieldException, IllegalAccessException { - Calendar[] period = createPeriodForQuotaSummary(); - overrideDefaultQuotaEnabledConfigValue("true"); - - Mockito.doReturn(period).when(quotaStatementMock).getCurrentStatementTime(); - Mockito.doReturn(domainVOMock).when(domainDaoMock).findById(Mockito.anyLong()); - Mockito.doReturn(BigDecimal.ZERO).when(quotaBalanceDaoMock).lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class)); - Mockito.doReturn(BigDecimal.ZERO).when(quotaUsageDaoMock).findTotalQuotaUsage(Mockito.anyLong(), Mockito.anyLong(), Mockito.isNull(), Mockito.any(Date.class), Mockito.any(Date.class)); - - QuotaSummaryResponse quotaSummaryResponse = quotaResponseBuilderSpy.getQuotaSummaryResponse(accountMock); - - assertTrue(quotaSummaryResponse.getQuotaEnabled()); - } - @Test public void filterSupportedTypesTestReturnWhenQuotaTypeDoesNotMatch() throws NoSuchFieldException { List> variables = new ArrayList<>(); @@ -576,6 +542,98 @@ public void validateQuotaConfigureEmailCmdParametersTestWithTemplateNameAndEnabl quotaResponseBuilderSpy.validateQuotaConfigureEmailCmdParameters(quotaConfigureEmailCmdMock); } + @Test + public void createQuotaSummaryResponseTestNotListAllAndAllAccountTypesReturnsSingleRecord() { + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + cmd.setListAll(false); + + try(MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); + + Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + for (Account.Type type : Account.Type.values()) { + Mockito.doReturn(type).when(accountMock).getType(); + + Pair, Integer> result = quotaResponseBuilderSpy.createQuotaSummaryResponse(cmd); + Assert.assertEquals(quotaSummaryResponseMock1, result); + } + + Mockito.verify(quotaResponseBuilderSpy, Mockito.times(Account.Type.values().length)).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.any()); + }; + } + + @Test + public void createQuotaSummaryResponseTestListAllAndAccountTypesAdminReturnsAllAndTheRestReturnsSingleRecord() { + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + cmd.setListAll(true); + + try(MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + + Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); + + Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(quotaSummaryResponseMock2).when(quotaResponseBuilderSpy).getQuotaSummaryResponseWithListAll(Mockito.any(), Mockito.any()); + + Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); + + for (Account.Type type : Account.Type.values()) { + Mockito.doReturn(type).when(accountMock).getType(); + + Pair, Integer> result = quotaResponseBuilderSpy.createQuotaSummaryResponse(cmd); + + if (accountTypesThatCanListAllQuotaSummaries.contains(type)) { + Assert.assertEquals(quotaSummaryResponseMock2, result); + } else { + Assert.assertEquals(quotaSummaryResponseMock1, result); + } + } + + Mockito.verify(quotaResponseBuilderSpy, Mockito.times(Account.Type.values().length - accountTypesThatCanListAllQuotaSummaries.size())) + .getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + Mockito.verify(quotaResponseBuilderSpy, Mockito.times(accountTypesThatCanListAllQuotaSummaries.size())).getQuotaSummaryResponseWithListAll(Mockito.any(), Mockito.any()); + } + } + + @Test + public void getDomainPathByDomainIdForDomainAdminTestAccountNotDomainAdminReturnsNull() { + for (Account.Type type : Account.Type.values()) { + if (Account.Type.DOMAIN_ADMIN.equals(type)) { + continue; + } + + Mockito.doReturn(type).when(accountMock).getType(); + Assert.assertNull(quotaResponseBuilderSpy.getDomainPathByDomainIdForDomainAdmin(accountMock)); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void getDomainPathByDomainIdForDomainAdminTestDomainFromCallerIsNullThrowsInvalidParameterValueException() { + Mockito.doReturn(Account.Type.DOMAIN_ADMIN).when(accountMock).getType(); + Mockito.doReturn(null).when(domainDaoMock).findById(Mockito.anyLong()); + Mockito.lenient().doNothing().when(accountManagerMock).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + + quotaResponseBuilderSpy.getDomainPathByDomainIdForDomainAdmin(accountMock); + } + + @Test + public void getDomainPathByDomainIdForDomainAdminTestDomainFromCallerIsNotNullReturnsPath() { + String expected = "/test/"; + + Mockito.doReturn(Account.Type.DOMAIN_ADMIN).when(accountMock).getType(); + Mockito.doReturn(domainVoMock).when(domainDaoMock).findById(Mockito.anyLong()); + Mockito.doNothing().when(accountManagerMock).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + Mockito.doReturn(expected).when(domainVoMock).getPath(); + + String result = quotaResponseBuilderSpy.getDomainPathByDomainIdForDomainAdmin(accountMock); + Assert.assertEquals(expected, result); + } + @Test public void getQuotaEmailConfigurationVoTestTemplateNameIsNull() { Mockito.doReturn(null).when(quotaConfigureEmailCmdMock).getTemplateName(); @@ -627,6 +685,102 @@ public void getQuotaEmailConfigurationVoTestExistingConfiguration() { assertFalse(result.isEnabled()); } + @Test + public void getAccountIdByAccountNameTestAccountNameIsNullReturnsNull() { + Assert.assertNull(quotaResponseBuilderSpy.getAccountIdByAccountName(null, 1l, accountMock)); + } + + @Test + public void getAccountIdByAccountNameTestDomainIdIsNullReturnsNull() { + Assert.assertNull(quotaResponseBuilderSpy.getAccountIdByAccountName("test", null, accountMock)); + } + + @Test(expected = InvalidParameterValueException.class) + public void getAccountIdByAccountNameTestAccountIsNullThrowsInvalidParameterValueException() { + Mockito.lenient().doNothing().when(accountManagerMock).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + Mockito.doReturn(null).when(accountDaoMock).findAccountIncludingRemoved(Mockito.anyString(), Mockito.anyLong()); + + quotaResponseBuilderSpy.getAccountIdByAccountName("test", 1l, accountMock); + } + + @Test + public void getAccountIdByAccountNameTestAccountIsNotNullReturnsAccountId() { + Long expected = 61l; + + Mockito.lenient().doNothing().when(accountManagerMock).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + Mockito.doReturn(accountMock).when(accountDaoMock).findAccountIncludingRemoved(Mockito.anyString(), Mockito.anyLong()); + Mockito.doReturn(expected).when(accountMock).getAccountId(); + + Long result = quotaResponseBuilderSpy.getAccountIdByAccountName("test", 1l, accountMock); + + Assert.assertEquals(expected, result); + } + + @Test + public void getQuotaSummaryResponseWithListAllTestAccountNameAndDomainIdAreNullPassDomainIdAsNull() { + Long expectedDomainId = null; + + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + cmd.setAccountName(null); + cmd.setDomainId(null); + + Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); + Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); + + Assert.assertEquals(quotaSummaryResponseMock1, result); + Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); + } + + @Test + public void getQuotaSummaryResponseWithListAllTestAccountNameIsNullAndDomainIdIsNotNullPassDomainId() { + Long expectedDomainId = 26l; + + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + cmd.setAccountName(null); + cmd.setDomainId(expectedDomainId); + + Mockito.doReturn(domainVoMock).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); + Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); + + Assert.assertEquals(quotaSummaryResponseMock1, result); + Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); + } + + @Test(expected = InvalidParameterValueException.class) + public void getQuotaSummaryResponseWithListAllTestAccountNameIsNullAndDomainIdIsNotNullButDomainDoesNotExistThrowInvalidParameterValueException() { + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + cmd.setAccountName(null); + cmd.setDomainId(1L); + + Mockito.doReturn(null).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); + } + + @Test + public void getQuotaSummaryResponseWithListAllTestAccountNameAndDomainIdAreNotNullPassDomainId() { + Long expectedDomainId = 9837l; + + QuotaSummaryCmd cmd = new QuotaSummaryCmd(); + cmd.setAccountName("test"); + cmd.setDomainId(expectedDomainId); + + Mockito.doReturn(domainVoMock).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + + Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); + Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); + + Assert.assertEquals(quotaSummaryResponseMock1, result); + Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); + } + @Test public void validatePositionOnCreatingNewQuotaTariffTestNullValueDoNothing() { quotaResponseBuilderSpy.validatePositionOnCreatingNewQuotaTariff(quotaTariffVoMock, null); diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index 19e756d1d973..56449958b2db 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -18,6 +18,7 @@ import com.cloud.configuration.Config; import com.cloud.domain.dao.DomainDao; +import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; import com.cloud.utils.db.TransactionLegacy; import junit.framework.TestCase; @@ -30,6 +31,7 @@ import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.joda.time.DateTime; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,7 +50,7 @@ public class QuotaServiceImplTest extends TestCase { @Mock - AccountDao accountDao; + AccountDao accountDaoMock; @Mock QuotaAccountDao quotaAcc; @Mock @@ -61,8 +63,10 @@ public class QuotaServiceImplTest extends TestCase { QuotaBalanceDao quotaBalanceDao; @Mock QuotaResponseBuilder respBldr; + @Mock + private AccountVO accountVoMock; - QuotaServiceImpl quotaService = new QuotaServiceImpl(); + QuotaServiceImpl quotaServiceImplSpy = new QuotaServiceImpl(); @Before public void setup() throws IllegalAccessException, NoSuchFieldException, ConfigurationException { @@ -71,34 +75,34 @@ public void setup() throws IllegalAccessException, NoSuchFieldException, Configu Field accountDaoField = QuotaServiceImpl.class.getDeclaredField("_accountDao"); accountDaoField.setAccessible(true); - accountDaoField.set(quotaService, accountDao); + accountDaoField.set(quotaServiceImplSpy, accountDaoMock); Field quotaAccountDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaAcc"); quotaAccountDaoField.setAccessible(true); - quotaAccountDaoField.set(quotaService, quotaAcc); + quotaAccountDaoField.set(quotaServiceImplSpy, quotaAcc); Field quotaUsageDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaUsageDao"); quotaUsageDaoField.setAccessible(true); - quotaUsageDaoField.set(quotaService, quotaUsageDao); + quotaUsageDaoField.set(quotaServiceImplSpy, quotaUsageDao); Field domainDaoField = QuotaServiceImpl.class.getDeclaredField("_domainDao"); domainDaoField.setAccessible(true); - domainDaoField.set(quotaService, domainDao); + domainDaoField.set(quotaServiceImplSpy, domainDao); Field configDaoField = QuotaServiceImpl.class.getDeclaredField("_configDao"); configDaoField.setAccessible(true); - configDaoField.set(quotaService, configDao); + configDaoField.set(quotaServiceImplSpy, configDao); Field balanceDaoField = QuotaServiceImpl.class.getDeclaredField("_quotaBalanceDao"); balanceDaoField.setAccessible(true); - balanceDaoField.set(quotaService, quotaBalanceDao); + balanceDaoField.set(quotaServiceImplSpy, quotaBalanceDao); Field QuotaResponseBuilderField = QuotaServiceImpl.class.getDeclaredField("_respBldr"); QuotaResponseBuilderField.setAccessible(true); - QuotaResponseBuilderField.set(quotaService, respBldr); + QuotaResponseBuilderField.set(quotaServiceImplSpy, respBldr); Mockito.when(configDao.getValue(Mockito.eq(Config.UsageAggregationTimezone.toString()))).thenReturn("IST"); - quotaService.configure("randomName", null); + quotaServiceImplSpy.configure("randomName", null); } @Test @@ -120,9 +124,9 @@ public void testFindQuotaBalanceVO() { Mockito.when(quotaBalanceDao.lastQuotaBalanceVO(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.any(Date.class))).thenReturn(records); // with enddate - assertTrue(quotaService.findQuotaBalanceVO(accountId, accountName, domainId, startDate, endDate).get(0).equals(qb)); + assertTrue(quotaServiceImplSpy.findQuotaBalanceVO(accountId, accountName, domainId, startDate, endDate).get(0).equals(qb)); // without enddate - assertTrue(quotaService.findQuotaBalanceVO(accountId, accountName, domainId, startDate, null).get(0).equals(qb)); + assertTrue(quotaServiceImplSpy.findQuotaBalanceVO(accountId, accountName, domainId, startDate, null).get(0).equals(qb)); } @Test @@ -133,7 +137,9 @@ public void testGetQuotaUsage() { final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - quotaService.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); + Mockito.doReturn(accountId).when(quotaServiceImplSpy).getAccountToWhomQuotaBalancesWillBeListed(Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong()); + + quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); Mockito.verify(quotaUsageDao, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(Date.class), Mockito.any(Date.class)); } @@ -142,13 +148,13 @@ public void testSetLockAccount() { // existing account QuotaAccountVO quotaAccountVO = new QuotaAccountVO(); Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(quotaAccountVO); - quotaService.setLockAccount(2L, true); + quotaServiceImplSpy.setLockAccount(2L, true); Mockito.verify(quotaAcc, Mockito.times(0)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); Mockito.verify(quotaAcc, Mockito.times(1)).updateQuotaAccount(Mockito.anyLong(), Mockito.any(QuotaAccountVO.class)); // new account Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(null); - quotaService.setLockAccount(2L, true); + quotaServiceImplSpy.setLockAccount(2L, true); Mockito.verify(quotaAcc, Mockito.times(1)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); } @@ -160,13 +166,22 @@ public void testSetMinBalance() { // existing account setting QuotaAccountVO quotaAccountVO = new QuotaAccountVO(); Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(quotaAccountVO); - quotaService.setMinBalance(accountId, balance); + quotaServiceImplSpy.setMinBalance(accountId, balance); Mockito.verify(quotaAcc, Mockito.times(0)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); Mockito.verify(quotaAcc, Mockito.times(1)).updateQuotaAccount(Mockito.anyLong(), Mockito.any(QuotaAccountVO.class)); // no account with limit set Mockito.when(quotaAcc.findByIdQuotaAccount(Mockito.anyLong())).thenReturn(null); - quotaService.setMinBalance(accountId, balance); + quotaServiceImplSpy.setMinBalance(accountId, balance); Mockito.verify(quotaAcc, Mockito.times(1)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); } + + @Test + public void getAccountToWhomQuotaBalancesWillBeListedTestAccountIdIsNotNullReturnsExistingAccount() { + long expected = 1L; + Mockito.doReturn(accountVoMock).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); + long result = quotaServiceImplSpy.getAccountToWhomQuotaBalancesWillBeListed(expected, "test", 2L); + Assert.assertEquals(expected, result); + } + } From 36fe76e7ded73cb521f95b9d742c673d1d89d264 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Thu, 6 Mar 2025 14:28:35 -0300 Subject: [PATCH 02/20] Fixes imports --- .../response/QuotaResponseBuilderImpl.java | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 433065923c0a..29a3cdb49df8 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -50,7 +50,19 @@ import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.command.*; + +import org.apache.cloudstack.api.command.QuotaBalanceCmd; +import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; +import org.apache.cloudstack.api.command.QuotaCreditsListCmd; +import org.apache.cloudstack.api.command.QuotaEmailTemplateListCmd; +import org.apache.cloudstack.api.command.QuotaEmailTemplateUpdateCmd; +import org.apache.cloudstack.api.command.QuotaPresetVariablesListCmd; +import org.apache.cloudstack.api.command.QuotaStatementCmd; +import org.apache.cloudstack.api.command.QuotaSummaryCmd; +import org.apache.cloudstack.api.command.QuotaTariffCreateCmd; +import org.apache.cloudstack.api.command.QuotaTariffListCmd; +import org.apache.cloudstack.api.command.QuotaTariffUpdateCmd; +import org.apache.cloudstack.api.command.QuotaValidateActivationRuleCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.discovery.ApiDiscoveryService; import org.apache.cloudstack.jsinterpreter.JsInterpreterHelper; @@ -65,8 +77,23 @@ import org.apache.cloudstack.quota.activationrule.presetvariables.Value; import org.apache.cloudstack.quota.constant.QuotaConfig; import org.apache.cloudstack.quota.constant.QuotaTypes; -import org.apache.cloudstack.quota.dao.*; -import org.apache.cloudstack.quota.vo.*; + +import org.apache.cloudstack.quota.dao.QuotaAccountDao; +import org.apache.cloudstack.quota.dao.QuotaBalanceDao; +import org.apache.cloudstack.quota.dao.QuotaCreditsDao; +import org.apache.cloudstack.quota.dao.QuotaEmailConfigurationDao; +import org.apache.cloudstack.quota.dao.QuotaEmailTemplatesDao; +import org.apache.cloudstack.quota.dao.QuotaSummaryDao; +import org.apache.cloudstack.quota.dao.QuotaTariffDao; +import org.apache.cloudstack.quota.dao.QuotaUsageDao; +import org.apache.cloudstack.quota.vo.QuotaAccountVO; +import org.apache.cloudstack.quota.vo.QuotaBalanceVO; +import org.apache.cloudstack.quota.vo.QuotaCreditsVO; +import org.apache.cloudstack.quota.vo.QuotaEmailConfigurationVO; +import org.apache.cloudstack.quota.vo.QuotaEmailTemplatesVO; +import org.apache.cloudstack.quota.vo.QuotaSummaryVO; +import org.apache.cloudstack.quota.vo.QuotaTariffVO; +import org.apache.cloudstack.quota.vo.QuotaUsageVO; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections4.CollectionUtils; @@ -330,14 +357,14 @@ protected QuotaSummaryResponse getQuotaSummaryResponse(final Account account) { if (account != null) { QuotaSummaryResponse qr = new QuotaSummaryResponse(); - DomainVO domain = _domainDao.findById(account.getDomainId()); + DomainVO domain = domainDao.findById(account.getDomainId()); BigDecimal curBalance = _quotaBalanceDao.lastQuotaBalance(account.getAccountId(), account.getDomainId(), period[1].getTime()); BigDecimal quotaUsage = quotaUsageDao.findTotalQuotaUsage(account.getAccountId(), account.getDomainId(), null, period[0].getTime(), period[1].getTime()); qr.setAccountId(account.getUuid()); qr.setAccountName(account.getAccountName()); qr.setDomainId(domain.getUuid()); - qr.setDomainName(domain.getName()); + qr.setDomainPath(domain.getName()); qr.setBalance(curBalance); qr.setQuotaUsage(quotaUsage); qr.setState(account.getState()); From 6897eeb32b5bddb9b6fd4a0bc6c7bc66ca4af8f3 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Tue, 11 Mar 2025 14:04:21 -0300 Subject: [PATCH 03/20] Fix QuotaServiceImplTest --- .../apache/cloudstack/quota/QuotaServiceImplTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index 56449958b2db..c8ca8d3b4754 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -35,8 +35,10 @@ 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 javax.naming.ConfigurationException; @@ -66,7 +68,10 @@ public class QuotaServiceImplTest extends TestCase { @Mock private AccountVO accountVoMock; - QuotaServiceImpl quotaServiceImplSpy = new QuotaServiceImpl(); + @Spy + @InjectMocks + QuotaServiceImpl quotaServiceImplSpy; + @Before public void setup() throws IllegalAccessException, NoSuchFieldException, ConfigurationException { @@ -137,7 +142,7 @@ public void testGetQuotaUsage() { final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - Mockito.doReturn(accountId).when(quotaServiceImplSpy).getAccountToWhomQuotaBalancesWillBeListed(Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong()); + Mockito.lenient().doReturn(accountId).when(quotaServiceImplSpy).getAccountToWhomQuotaBalancesWillBeListed(Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong()); quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); Mockito.verify(quotaUsageDao, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(Date.class), Mockito.any(Date.class)); From 87feb02fdae299c1297e0b8178c6051324b8cb85 Mon Sep 17 00:00:00 2001 From: julien-vaz <54545601+julien-vaz@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:08:22 -0300 Subject: [PATCH 04/20] Update plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java Co-authored-by: Fabricio Duarte --- .../java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index e1931cc5bda7..ab1ad865fe05 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -97,7 +97,7 @@ public void setDomainId(Long domainId) { } public Boolean isListAll() { - return BooleanUtils.toBoolean(listAll); + return ObjectUtils.defaultIfNull(listAll, Boolean.FALSE); } public void setListAll(Boolean listAll) { From 1dcfbbf310fcdaf5272d2ba479fa54997e1f9139 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Wed, 4 Jun 2025 19:15:02 -0300 Subject: [PATCH 05/20] Fix QuotaSummaryCmd --- .../cloudstack/api/command/QuotaSummaryCmd.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index ab1ad865fe05..330b152a3a09 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -31,6 +31,7 @@ import org.apache.cloudstack.quota.QuotaAccountStateFilter; import org.apache.cloudstack.quota.QuotaService; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -64,17 +65,9 @@ public class QuotaSummaryCmd extends BaseListCmd { @Override public void execute() { - Account caller = CallContext.current().getCallingAccount(); Pair, Integer> responses = quotaResponseBuilder.createQuotaSummaryResponse(this); - if (caller.getType() == Account.Type.ADMIN) { - if (getAccountName() != null && getDomainId() != null) - responses = quotaResponseBuilder.createQuotaSummaryResponse(getAccountName(), getDomainId()); - else - responses = quotaResponseBuilder.createQuotaSummaryResponse(isListAll(), getKeyword(), getStartIndex(), getPageSizeVal()); - } else { - responses = quotaResponseBuilder.createQuotaSummaryResponse(caller.getAccountName(), caller.getDomainId()); - } - final ListResponse response = new ListResponse(); + ListResponse response = new ListResponse<>(); + response.setResponses(responses.first(), responses.second()); response.setResponseName(getCommandName()); setResponseObject(response); From 6e7eeb5e5df48483c1f545e4546ee31e4c7245bf Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Mon, 9 Jun 2025 13:59:06 -0300 Subject: [PATCH 06/20] Remove unnecessary imports --- .../java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index 330b152a3a09..8b00443d532c 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -27,10 +27,8 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaSummaryResponse; -import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.quota.QuotaAccountStateFilter; import org.apache.cloudstack.quota.QuotaService; -import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; From 1cceb8148880fc54bcda3c6f13ada748c0c02b3a Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Tue, 1 Jul 2025 14:22:11 -0300 Subject: [PATCH 07/20] Remove unused createQuotaSummaryResponse declarations --- .../api/response/QuotaResponseBuilder.java | 6 --- .../response/QuotaResponseBuilderImpl.java | 45 ------------------- 2 files changed, 51 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java index 262526fc4223..177fb00d4b55 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilder.java @@ -55,12 +55,6 @@ public interface QuotaResponseBuilder { Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd); - Pair, Integer> createQuotaSummaryResponse(Boolean listAll); - - Pair, Integer> createQuotaSummaryResponse(Boolean listAll, String keyword, Long startIndex, Long pageSize); - - Pair, Integer> createQuotaSummaryResponse(String accountName, Long domainId); - QuotaBalanceResponse createQuotaLastBalanceResponse(List quotaBalance, Date startDate); List getQuotaUsage(QuotaStatementCmd cmd); diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 29a3cdb49df8..be850f7c10e5 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -307,51 +307,6 @@ protected QuotaSummaryResponse getQuotaSummaryResponse(QuotaSummaryVO summary) { return response; } - @Override - public Pair, Integer> createQuotaSummaryResponse(final String accountName, final Long domainId) { - List result = new ArrayList(); - - if (accountName != null && domainId != null) { - Account account = _accountDao.findActiveAccount(accountName, domainId); - QuotaSummaryResponse qr = getQuotaSummaryResponse(account); - result.add(qr); - } - - return new Pair<>(result, result.size()); - } - - @Override - public Pair, Integer> createQuotaSummaryResponse(Boolean listAll) { - return createQuotaSummaryResponse(listAll, null, null, null); - } - - @Override - public Pair, Integer> createQuotaSummaryResponse(Boolean listAll, final String keyword, final Long startIndex, final Long pageSize) { - List result = new ArrayList(); - Integer count = 0; - if (listAll) { - Filter filter = new Filter(AccountVO.class, "accountName", true, startIndex, pageSize); - Pair, Integer> data = _accountDao.findAccountsLike(keyword, filter); - count = data.second(); - for (final AccountVO account : data.first()) { - QuotaSummaryResponse qr = getQuotaSummaryResponse(account); - result.add(qr); - } - } else { - Pair, Integer> data = quotaAccountDao.listAllQuotaAccount(startIndex, pageSize); - count = data.second(); - for (final QuotaAccountVO quotaAccount : data.first()) { - AccountVO account = _accountDao.findById(quotaAccount.getId()); - if (account == null) { - continue; - } - QuotaSummaryResponse qr = getQuotaSummaryResponse(account); - result.add(qr); - } - } - return new Pair<>(result, count); - } - protected QuotaSummaryResponse getQuotaSummaryResponse(final Account account) { Calendar[] period = quotaStatement.getCurrentStatementTime(); From e723223311e538dc06fda4fd5bcbc8e0f510e251 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Wed, 2 Jul 2025 15:05:18 -0300 Subject: [PATCH 08/20] Remove unnecessary imports --- .../apache/cloudstack/api/response/QuotaResponseBuilderImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index be850f7c10e5..22d9dbe30eb5 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -114,7 +114,6 @@ import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.UserDao; import com.cloud.utils.Pair; -import com.cloud.utils.db.Filter; import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; From 28002392893467437366d1641f495a659feb90b9 Mon Sep 17 00:00:00 2001 From: julien-vaz <54545601+julien-vaz@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:00:27 -0300 Subject: [PATCH 09/20] Update plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java Co-authored-by: dahn --- .../org/apache/cloudstack/api/command/QuotaSummaryCmd.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index 8b00443d532c..8f2f4aefca27 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -52,7 +52,8 @@ public class QuotaSummaryCmd extends BaseListCmd { private Boolean listAll; @Parameter(name = ApiConstants.ACCOUNT_STATE_TO_SHOW, type = CommandType.STRING, description = "Possible values are [ALL, ACTIVE, REMOVED]. ALL will list summaries for " + - "active and removed accounts; ACTIVE will list summaries only for active accounts; REMOVED will list summaries only for removed accounts. The default value is ACTIVE.") + "active and removed accounts; ACTIVE will list summaries only for active accounts; REMOVED will list summaries only for removed accounts. The default value is ACTIVE.”, + since = “4.21.0) private String accountStateToShow; @Inject From 4d3ad22edeea6773aa8d831a53c96590e6e2308e Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Thu, 10 Jul 2025 13:33:00 -0300 Subject: [PATCH 10/20] Fix QuotaSummaryCmd --- .../api/command/QuotaSummaryCmd.java | 47 +++++++++++++---- .../api/response/QuotaSummaryResponse.java | 10 ++-- .../apache/cloudstack/quota/QuotaService.java | 2 + .../cloudstack/quota/QuotaServiceImpl.java | 52 +++++++++++++++++++ 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index 8f2f4aefca27..4418c1bb6b69 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -16,17 +16,20 @@ //under the License. package org.apache.cloudstack.api.command; -import com.cloud.user.Account; import com.cloud.utils.Pair; + +import org.apache.cloudstack.api.ACL; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.AccountResponse; import org.apache.cloudstack.api.response.DomainResponse; -import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.QuotaResponseBuilder; import org.apache.cloudstack.api.response.QuotaSummaryResponse; +import org.apache.cloudstack.api.response.ProjectResponse; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.quota.QuotaAccountStateFilter; import org.apache.cloudstack.quota.QuotaService; import org.apache.commons.lang3.ObjectUtils; @@ -41,6 +44,16 @@ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class QuotaSummaryCmd extends BaseListCmd { + @Inject + QuotaResponseBuilder quotaResponseBuilder; + + @Inject + QuotaService quotaService; + + @ACL + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which balance will be listed. Can not be specified with projectId.") + private Long accountId; + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = false, description = "Optional, Account Id for which statement needs to be generated") private String accountName; @@ -52,15 +65,13 @@ public class QuotaSummaryCmd extends BaseListCmd { private Boolean listAll; @Parameter(name = ApiConstants.ACCOUNT_STATE_TO_SHOW, type = CommandType.STRING, description = "Possible values are [ALL, ACTIVE, REMOVED]. ALL will list summaries for " + - "active and removed accounts; ACTIVE will list summaries only for active accounts; REMOVED will list summaries only for removed accounts. The default value is ACTIVE.”, - since = “4.21.0) + "active and removed accounts; ACTIVE will list summaries only for active accounts; REMOVED will list summaries only for removed accounts. The default value is ACTIVE.", + since = "4.21.0") private String accountStateToShow; - @Inject - QuotaResponseBuilder quotaResponseBuilder; - - @Inject - QuotaService quotaService; + @ACL + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Project Id for which balance will be listed. Can not be specified with accountId.") + private Long projectId; @Override public void execute() { @@ -72,6 +83,14 @@ public void execute() { setResponseObject(response); } + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + public String getAccountName() { return accountName; } @@ -96,6 +115,10 @@ public void setListAll(Boolean listAll) { this.listAll = listAll; } + public Long getProjectId() { + return projectId; + } + public QuotaAccountStateFilter getAccountStateToShow() { if (StringUtils.isNotBlank(accountStateToShow)) { QuotaAccountStateFilter state = QuotaAccountStateFilter.getValue(accountStateToShow); @@ -109,7 +132,9 @@ public QuotaAccountStateFilter getAccountStateToShow() { @Override public long getEntityOwnerId() { - return Account.ACCOUNT_ID_SYSTEM; + if (ObjectUtils.allNull(accountId, accountName, projectId)) { + return -1; + } + return quotaService.finalizeAccountId(accountId, accountName, domainId, projectId); } - } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java index 7425035195d8..329500eec384 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaSummaryResponse.java @@ -53,11 +53,11 @@ public class QuotaSummaryResponse extends BaseResponse { private State state; @SerializedName("domainremoved") - @Param(description = "If the domain is removed or not") + @Param(description = "If the domain is removed or not", since = "4.21.0") private boolean domainRemoved; @SerializedName("accountremoved") - @Param(description = "If the account is removed or not") + @Param(description = "If the account is removed or not", since = "4.21.0") private boolean accountRemoved; @SerializedName("quota") @@ -81,15 +81,15 @@ public class QuotaSummaryResponse extends BaseResponse { private boolean quotaEnabled; @SerializedName("projectname") - @Param(description = "Name of the project") + @Param(description = "Name of the project", since = "4.21.0") private String projectName; @SerializedName("projectid") - @Param(description = "Project's id") + @Param(description = "Project's id", since = "4.21.0") private String projectId; @SerializedName("projectremoved") - @Param(description = "Whether the project is removed or not") + @Param(description = "Whether the project is removed or not", since = "4.21.0") private Boolean projectRemoved; public String getAccountId() { diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index 8f3c34982c0b..e4525d83a3a1 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -40,4 +40,6 @@ public interface QuotaService extends PluggableService { boolean saveQuotaAccount(AccountVO account, BigDecimal aggrUsage, Date endDate); + Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId); + } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index 158b075eab0c..12bfc31567bb 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -26,6 +26,12 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.projects.Project; +import com.cloud.projects.ProjectManager; +import com.cloud.user.AccountService; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsCmd; @@ -74,6 +80,8 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi @Inject private AccountDao _accountDao; @Inject + private AccountService accountService; + @Inject private QuotaAccountDao _quotaAcc; @Inject private QuotaUsageDao _quotaUsageDao; @@ -85,6 +93,8 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi private QuotaBalanceDao _quotaBalanceDao; @Inject private QuotaResponseBuilder _respBldr; + @Inject + private ProjectManager projectMgr; private TimeZone _usageTimezone; @@ -275,6 +285,48 @@ public boolean saveQuotaAccount(final AccountVO account, final BigDecimal aggrUs } } + /** + * Returns the Id of the account that will be used when provided with either accountId, projectId or accountName and domainId. + */ + @Override + public Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId) { + if (projectId != null) { + if (accountId != null || accountName != null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Project and account can not be specified together."); + } + final Project project = projectMgr.getProject(projectId); + if (project == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find project with id: [%s].", projectId)); + } + if (project.getState() != Project.State.Active) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Project with projectId [%s] is not active.", projectId)); + } + return project.getProjectAccountId(); + } + + if (accountId != null) { + if (accountService.getActiveAccountById(accountId) != null) { + return accountId; + } + throw new InvalidParameterValueException(String.format("Unable to find account with accountId: [%s].", accountId)); + } + + if (accountName == null && domainId == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Either %s or %s is required.", ApiConstants.ACCOUNT_ID, ApiConstants.PROJECT_ID)); + } + try { + Account activeAccount = accountService.getActiveAccountByName(accountName, domainId); + if (activeAccount != null) { + return activeAccount.getId(); + } + } catch (InvalidParameterValueException exception) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Both %s and %s are needed if using either. Consider using %s instead.", + ApiConstants.ACCOUNT, ApiConstants.DOMAIN_ID, ApiConstants.ACCOUNT_ID)); + } + throw new InvalidParameterValueException(String.format("Unable to find account by name: [%s] on domain: [%s]", accountName, domainId)); + } + + @Override public void setMinBalance(Long accountId, Double balance) { QuotaAccountVO acc = _quotaAcc.findByIdQuotaAccount(accountId); From 34fb78b0a732d9a06529a193108e231bf0a6720c Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Fri, 11 Jul 2025 14:33:57 -0300 Subject: [PATCH 11/20] Fix QuotaResponseBuilderImplTest --- .../api/response/QuotaResponseBuilderImplTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 6c25c73f1c9b..8e27c5feb58b 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -198,6 +198,9 @@ public void setup() { @Mock JsInterpreterHelper jsInterpreterHelperMock = Mockito.mock(JsInterpreterHelper.class); + @Mock + QuotaSummaryCmd quotaSummaryCmdMock = Mockito.mock(QuotaSummaryCmd.class); + private QuotaTariffVO makeTariffTestData() { QuotaTariffVO tariffVO = new QuotaTariffVO(); tariffVO.setUsageType(QuotaTypes.IP_ADDRESS); @@ -766,16 +769,15 @@ public void getQuotaSummaryResponseWithListAllTestAccountNameIsNullAndDomainIdIs public void getQuotaSummaryResponseWithListAllTestAccountNameAndDomainIdAreNotNullPassDomainId() { Long expectedDomainId = 9837l; - QuotaSummaryCmd cmd = new QuotaSummaryCmd(); - cmd.setAccountName("test"); - cmd.setDomainId(expectedDomainId); + Mockito.lenient().doReturn("test").when(quotaSummaryCmdMock).getAccountName(); + Mockito.doReturn(expectedDomainId).when(quotaSummaryCmdMock).getDomainId(); Mockito.doReturn(domainVoMock).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); + Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(quotaSummaryCmdMock, accountMock); Assert.assertEquals(quotaSummaryResponseMock1, result); Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); From b4db39031916c5b64bc50348d433e9adf568095e Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Fri, 11 Jul 2025 14:43:31 -0300 Subject: [PATCH 12/20] Refactor test --- .../api/response/QuotaResponseBuilderImplTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 8e27c5feb58b..44bf6c635b61 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -198,9 +198,6 @@ public void setup() { @Mock JsInterpreterHelper jsInterpreterHelperMock = Mockito.mock(JsInterpreterHelper.class); - @Mock - QuotaSummaryCmd quotaSummaryCmdMock = Mockito.mock(QuotaSummaryCmd.class); - private QuotaTariffVO makeTariffTestData() { QuotaTariffVO tariffVO = new QuotaTariffVO(); tariffVO.setUsageType(QuotaTypes.IP_ADDRESS); @@ -769,15 +766,16 @@ public void getQuotaSummaryResponseWithListAllTestAccountNameIsNullAndDomainIdIs public void getQuotaSummaryResponseWithListAllTestAccountNameAndDomainIdAreNotNullPassDomainId() { Long expectedDomainId = 9837l; - Mockito.lenient().doReturn("test").when(quotaSummaryCmdMock).getAccountName(); - Mockito.doReturn(expectedDomainId).when(quotaSummaryCmdMock).getDomainId(); + QuotaSummaryCmd cmd = Mockito.mock(QuotaSummaryCmd.class); + Mockito.lenient().doReturn("test").when(cmd).getAccountName(); + Mockito.doReturn(expectedDomainId).when(cmd).getDomainId(); Mockito.doReturn(domainVoMock).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(quotaSummaryCmdMock, accountMock); + Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); Assert.assertEquals(quotaSummaryResponseMock1, result); Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); From d9984c01d37caeb2afd8db6220ea0f4407c5aa29 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Thu, 7 Aug 2025 17:18:57 -0300 Subject: [PATCH 13/20] Fix QuotaSummaryCmd --- .../apache/cloudstack/api/command/QuotaSummaryCmd.java | 4 ++-- .../api/response/QuotaResponseBuilderImpl.java | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index 4418c1bb6b69..8663dd41f1f4 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -51,7 +51,7 @@ public class QuotaSummaryCmd extends BaseListCmd { QuotaService quotaService; @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which balance will be listed. Can not be specified with projectId.") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which balance will be listed. Can not be specified with projectId.", since = "4.21.0") private Long accountId; @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = false, description = "Optional, Account Id for which statement needs to be generated") @@ -70,7 +70,7 @@ public class QuotaSummaryCmd extends BaseListCmd { private String accountStateToShow; @ACL - @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Project Id for which balance will be listed. Can not be specified with accountId.") + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Project Id for which balance will be listed. Can not be specified with accountId.", since = "4.21.0") private Long projectId; @Override diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 22d9dbe30eb5..216cc8a3b35b 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -188,7 +188,15 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { + if (cmd.getAccountId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { + return getQuotaSummaryResponse(cmd.getAccountId(), null, null, null, cmd); + } + + else if (cmd.getDomainId() != null && caller.getType() == Account.Type.DOMAIN_ADMIN && !cmd.isListAll()) { + return getQuotaSummaryResponse(null, null, cmd.getDomainId(), null, cmd); + } + + else if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { return getQuotaSummaryResponse(caller.getAccountId(), null, null, null, cmd); } From 8cdd7fd42d51d7e8d99a33cc1aa8cc3e0f0687df Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Fri, 8 Aug 2025 14:57:20 -0300 Subject: [PATCH 14/20] Fix projectid behavior --- .../api/response/QuotaResponseBuilderImpl.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 216cc8a3b35b..c9b24fcb7dea 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -44,6 +44,8 @@ import com.cloud.domain.Domain; import com.cloud.exception.PermissionDeniedException; +import com.cloud.projects.ProjectVO; +import com.cloud.projects.dao.ProjectDao; import com.cloud.user.User; import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; @@ -139,6 +141,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private AccountDao _accountDao; @Inject + private ProjectDao projectDao; + @Inject private QuotaAccountDao quotaAccountDao; @Inject private DomainDao domainDao; @@ -188,7 +192,13 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - if (cmd.getAccountId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { + if (cmd.getProjectId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { + ProjectVO projectVO = projectDao.findById(cmd.getProjectId()); + Long projectAccountId = projectVO.getProjectAccountId(); + return getQuotaSummaryResponse(projectAccountId, null, null, null, cmd); + } + + else if (cmd.getAccountId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { return getQuotaSummaryResponse(cmd.getAccountId(), null, null, null, cmd); } From 37ebd686515ac0378696a2bb96f1bbbb8fffe23f Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Mon, 18 Aug 2025 15:05:18 -0300 Subject: [PATCH 15/20] Simplify QuotaSummary and deprecate listall --- .../api/command/QuotaSummaryCmd.java | 1 + .../response/QuotaResponseBuilderImpl.java | 34 ++---- .../api/command/QuotaSummaryCmdTest.java | 49 --------- .../QuotaResponseBuilderImplTest.java | 101 +----------------- 4 files changed, 8 insertions(+), 177 deletions(-) delete mode 100644 plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index 8663dd41f1f4..a91c789f6a23 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -60,6 +60,7 @@ public class QuotaSummaryCmd extends BaseListCmd { @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "Optional, If domain Id is given and the caller is domain admin then the statement is generated for domain.") private Long domainId; + @Deprecated @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "False (default) lists balance summary for account. True lists balance summary for " + "accounts which the caller has access.") private Boolean listAll; diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index c9b24fcb7dea..9fcb6f162248 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -44,7 +44,6 @@ import com.cloud.domain.Domain; import com.cloud.exception.PermissionDeniedException; -import com.cloud.projects.ProjectVO; import com.cloud.projects.dao.ProjectDao; import com.cloud.user.User; import com.cloud.user.UserVO; @@ -163,8 +162,6 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; - private Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); - @Override public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) { final QuotaTariffResponse response = new QuotaTariffResponse(); @@ -192,31 +189,12 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - if (cmd.getProjectId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { - ProjectVO projectVO = projectDao.findById(cmd.getProjectId()); - Long projectAccountId = projectVO.getProjectAccountId(); - return getQuotaSummaryResponse(projectAccountId, null, null, null, cmd); - } - - else if (cmd.getAccountId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { - return getQuotaSummaryResponse(cmd.getAccountId(), null, null, null, cmd); - } - - else if (cmd.getDomainId() != null && caller.getType() == Account.Type.DOMAIN_ADMIN && !cmd.isListAll()) { - return getQuotaSummaryResponse(null, null, cmd.getDomainId(), null, cmd); - } - - else if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { - return getQuotaSummaryResponse(caller.getAccountId(), null, null, null, cmd); - } - - return getQuotaSummaryResponseWithListAll(cmd, caller); - } - - protected Pair, Integer> getQuotaSummaryResponseWithListAll(QuotaSummaryCmd cmd, Account caller) { - Long accountId = cmd.getEntityOwnerId(); - if (accountId == -1) { - accountId = null; + Long accountId = caller.getId(); + if (caller.getType() != Account.Type.NORMAL) { + accountId = cmd.getEntityOwnerId(); + if (accountId == -1) { + accountId = null; + } } Long domainId = cmd.getDomainId(); diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java deleted file mode 100644 index 17473ac4fdc8..000000000000 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaSummaryCmdTest.java +++ /dev/null @@ -1,49 +0,0 @@ -// 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 org.apache.cloudstack.api.command; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class QuotaSummaryCmdTest { - - QuotaSummaryCmd quotaSummaryCmdSpy = Mockito.spy(QuotaSummaryCmd.class); - - @Test - public void isListAllTestNullReturnsFalse() { - quotaSummaryCmdSpy.setListAll(null); - Assert.assertFalse(quotaSummaryCmdSpy.isListAll()); - } - - @Test - public void isListAllTestFalseReturnsFalse() { - quotaSummaryCmdSpy.setListAll(false); - Assert.assertFalse(quotaSummaryCmdSpy.isListAll()); - } - - @Test - public void isListAllTestTrueReturnsTrue() { - quotaSummaryCmdSpy.setListAll(true); - Assert.assertTrue(quotaSummaryCmdSpy.isListAll()); - } - -} diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 44bf6c635b61..9478d6a6046c 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -545,7 +545,6 @@ public void validateQuotaConfigureEmailCmdParametersTestWithTemplateNameAndEnabl @Test public void createQuotaSummaryResponseTestNotListAllAndAllAccountTypesReturnsSingleRecord() { QuotaSummaryCmd cmd = new QuotaSummaryCmd(); - cmd.setListAll(false); try(MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { callContextMocked.when(CallContext::current).thenReturn(callContextMock); @@ -556,6 +555,7 @@ public void createQuotaSummaryResponseTestNotListAllAndAllAccountTypesReturnsSin for (Account.Type type : Account.Type.values()) { Mockito.doReturn(type).when(accountMock).getType(); + Mockito.doReturn("ROOT").when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(accountMock); Pair, Integer> result = quotaResponseBuilderSpy.createQuotaSummaryResponse(cmd); Assert.assertEquals(quotaSummaryResponseMock1, result); @@ -566,40 +566,6 @@ public void createQuotaSummaryResponseTestNotListAllAndAllAccountTypesReturnsSin }; } - @Test - public void createQuotaSummaryResponseTestListAllAndAccountTypesAdminReturnsAllAndTheRestReturnsSingleRecord() { - QuotaSummaryCmd cmd = new QuotaSummaryCmd(); - cmd.setListAll(true); - - try(MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - callContextMocked.when(CallContext::current).thenReturn(callContextMock); - - Mockito.doReturn(accountMock).when(callContextMock).getCallingAccount(); - - Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - Mockito.doReturn(quotaSummaryResponseMock2).when(quotaResponseBuilderSpy).getQuotaSummaryResponseWithListAll(Mockito.any(), Mockito.any()); - - Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); - - for (Account.Type type : Account.Type.values()) { - Mockito.doReturn(type).when(accountMock).getType(); - - Pair, Integer> result = quotaResponseBuilderSpy.createQuotaSummaryResponse(cmd); - - if (accountTypesThatCanListAllQuotaSummaries.contains(type)) { - Assert.assertEquals(quotaSummaryResponseMock2, result); - } else { - Assert.assertEquals(quotaSummaryResponseMock1, result); - } - } - - Mockito.verify(quotaResponseBuilderSpy, Mockito.times(Account.Type.values().length - accountTypesThatCanListAllQuotaSummaries.size())) - .getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - - Mockito.verify(quotaResponseBuilderSpy, Mockito.times(accountTypesThatCanListAllQuotaSummaries.size())).getQuotaSummaryResponseWithListAll(Mockito.any(), Mockito.any()); - } - } - @Test public void getDomainPathByDomainIdForDomainAdminTestAccountNotDomainAdminReturnsNull() { for (Account.Type type : Account.Type.values()) { @@ -716,71 +682,6 @@ public void getAccountIdByAccountNameTestAccountIsNotNullReturnsAccountId() { Assert.assertEquals(expected, result); } - @Test - public void getQuotaSummaryResponseWithListAllTestAccountNameAndDomainIdAreNullPassDomainIdAsNull() { - Long expectedDomainId = null; - - QuotaSummaryCmd cmd = new QuotaSummaryCmd(); - cmd.setAccountName(null); - cmd.setDomainId(null); - - Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); - Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - - Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); - - Assert.assertEquals(quotaSummaryResponseMock1, result); - Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); - } - - @Test - public void getQuotaSummaryResponseWithListAllTestAccountNameIsNullAndDomainIdIsNotNullPassDomainId() { - Long expectedDomainId = 26l; - - QuotaSummaryCmd cmd = new QuotaSummaryCmd(); - cmd.setAccountName(null); - cmd.setDomainId(expectedDomainId); - - Mockito.doReturn(domainVoMock).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); - - Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); - Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - - Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); - - Assert.assertEquals(quotaSummaryResponseMock1, result); - Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); - } - - @Test(expected = InvalidParameterValueException.class) - public void getQuotaSummaryResponseWithListAllTestAccountNameIsNullAndDomainIdIsNotNullButDomainDoesNotExistThrowInvalidParameterValueException() { - QuotaSummaryCmd cmd = new QuotaSummaryCmd(); - cmd.setAccountName(null); - cmd.setDomainId(1L); - - Mockito.doReturn(null).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); - quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); - } - - @Test - public void getQuotaSummaryResponseWithListAllTestAccountNameAndDomainIdAreNotNullPassDomainId() { - Long expectedDomainId = 9837l; - - QuotaSummaryCmd cmd = Mockito.mock(QuotaSummaryCmd.class); - Mockito.lenient().doReturn("test").when(cmd).getAccountName(); - Mockito.doReturn(expectedDomainId).when(cmd).getDomainId(); - - Mockito.doReturn(domainVoMock).when(domainDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); - - Mockito.doReturn(null).when(quotaResponseBuilderSpy).getDomainPathByDomainIdForDomainAdmin(Mockito.any()); - Mockito.doReturn(quotaSummaryResponseMock1).when(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); - - Pair, Integer> result = quotaResponseBuilderSpy.getQuotaSummaryResponseWithListAll(cmd, accountMock); - - Assert.assertEquals(quotaSummaryResponseMock1, result); - Mockito.verify(quotaResponseBuilderSpy).getQuotaSummaryResponse(Mockito.any(), Mockito.any(), Mockito.eq(expectedDomainId), Mockito.any(), Mockito.any()); - } - @Test public void validatePositionOnCreatingNewQuotaTariffTestNullValueDoNothing() { quotaResponseBuilderSpy.validatePositionOnCreatingNewQuotaTariff(quotaTariffVoMock, null); From 75e851ed9055026f0234e67e05e36a90738b83d7 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Thu, 28 Aug 2025 12:23:27 -0300 Subject: [PATCH 16/20] Fix createQuotaSummaryResponse --- .../response/QuotaResponseBuilderImpl.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 9fcb6f162248..de0e87c2bba5 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -44,6 +44,7 @@ import com.cloud.domain.Domain; import com.cloud.exception.PermissionDeniedException; +import com.cloud.projects.ProjectVO; import com.cloud.projects.dao.ProjectDao; import com.cloud.user.User; import com.cloud.user.UserVO; @@ -162,6 +163,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { private final Class[] assignableClasses = {GenericPresetVariable.class, ComputingResources.class}; + private Set accountTypesThatCanListAllQuotaSummaries = Sets.newHashSet(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN); + @Override public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boolean returnActivationRule) { final QuotaTariffResponse response = new QuotaTariffResponse(); @@ -189,12 +192,31 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - Long accountId = caller.getId(); - if (caller.getType() != Account.Type.NORMAL) { - accountId = cmd.getEntityOwnerId(); - if (accountId == -1) { - accountId = null; - } + if (cmd.getProjectId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { + ProjectVO projectVO = projectDao.findById(cmd.getProjectId()); + Long projectAccountId = projectVO.getProjectAccountId(); + return getQuotaSummaryResponse(projectAccountId, null, null, null, cmd); + } + + else if (cmd.getAccountId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { + return getQuotaSummaryResponse(cmd.getAccountId(), null, null, null, cmd); + } + + else if (cmd.getDomainId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { + return getQuotaSummaryResponse(null, null, cmd.getDomainId(), null, cmd); + } + + else if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { + return getQuotaSummaryResponse(caller.getAccountId(), null, null, null, cmd); + } + + return getQuotaSummaryResponseWithListAll(cmd, caller); + } + + protected Pair, Integer> getQuotaSummaryResponseWithListAll(QuotaSummaryCmd cmd, Account caller) { + Long accountId = cmd.getEntityOwnerId(); + if (accountId == -1) { + accountId = null; } Long domainId = cmd.getDomainId(); @@ -215,6 +237,7 @@ public Pair, Integer> createQuotaSummaryResponse(Quot return getQuotaSummaryResponse(accountId, keyword, domainId, domainPath, cmd); } + protected Long getAccountIdByAccountName(String accountName, Long domainId, Account caller) { if (ObjectUtils.anyNull(accountName, domainId)) { return null; From ba99ec875da29771945c91ac8afb991d33f46b63 Mon Sep 17 00:00:00 2001 From: Julien Hervot de Mattos Vaz Date: Tue, 2 Sep 2025 14:33:29 -0300 Subject: [PATCH 17/20] Remove unused import --- .../cloudstack/api/response/QuotaResponseBuilderImplTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java index 9478d6a6046c..0e9d817183a6 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImplTest.java @@ -69,7 +69,6 @@ import org.apache.cloudstack.quota.vo.QuotaTariffVO; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; -import org.apache.commons.compress.utils.Sets; import org.apache.commons.lang3.time.DateUtils; import org.junit.Assert; From fc92e396c7e98218751e723738cef542ae452e0a Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Tue, 3 Feb 2026 10:33:10 -0300 Subject: [PATCH 18/20] Apply suggestions + some adjustments --- .../java/com/cloud/user/AccountService.java | 2 + .../quota/QuotaAccountStateFilter.java | 6 +- .../api/command/QuotaSummaryCmd.java | 11 ++- .../response/QuotaResponseBuilderImpl.java | 23 +----- .../apache/cloudstack/quota/QuotaService.java | 2 - .../cloudstack/quota/QuotaServiceImpl.java | 76 ------------------- .../quota/QuotaServiceImplTest.java | 11 --- .../com/cloud/user/AccountManagerImpl.java | 45 +++++++++++ 8 files changed, 61 insertions(+), 115 deletions(-) diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index b92654bfe174..67230d19d260 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -126,6 +126,8 @@ User createUser(String userName, String password, String firstName, String lastN Long finalizeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly); + Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId); + /** * returns the user account object for a given user id * @param userId user id diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java index 10ad42f8b4ab..cc6ca203ef79 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaAccountStateFilter.java @@ -16,16 +16,20 @@ // under the License. package org.apache.cloudstack.quota; +import org.apache.commons.lang3.StringUtils; + public enum QuotaAccountStateFilter { ALL, ACTIVE, REMOVED; public static QuotaAccountStateFilter getValue(String value) { + if (StringUtils.isBlank(value)) { + return null; + } for (QuotaAccountStateFilter state : values()) { if (state.name().equalsIgnoreCase(value)) { return state; } } - return null; } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index b0c51a0edd5f..e83e716c7ae0 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -51,7 +51,7 @@ public class QuotaSummaryCmd extends BaseListCmd { QuotaService quotaService; @ACL - @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which balance will be listed. Can not be specified with projectId.", since = "4.21.0") + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which balance will be listed. Can not be specified with projectId.", since = "4.23.0") private Long accountId; @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = false, description = "Optional, Account Id for which statement needs to be generated") @@ -60,18 +60,17 @@ public class QuotaSummaryCmd extends BaseListCmd { @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "Optional, If domain Id is given and the caller is domain admin then the statement is generated for domain.") private Long domainId; - @Deprecated - @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "False (default) lists balance summary for account. True lists balance summary for " + + @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "False (default) lists the Quota balance summary for calling Account. True lists balance summary for " + "Accounts which the caller has access.") private Boolean listAll; @Parameter(name = ApiConstants.ACCOUNT_STATE_TO_SHOW, type = CommandType.STRING, description = "Possible values are [ALL, ACTIVE, REMOVED]. ALL will list summaries for " + "active and removed accounts; ACTIVE will list summaries only for active accounts; REMOVED will list summaries only for removed accounts. The default value is ACTIVE.", - since = "4.21.0") + since = "4.23.0") private String accountStateToShow; @ACL - @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the project for which balance will be listed. Can not be specified with accountId.", since = "4.21.0") + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "ID of the project for which balance will be listed. Can not be specified with accountId.", since = "4.23.0") private Long projectId; @Override @@ -136,6 +135,6 @@ public long getEntityOwnerId() { if (ObjectUtils.allNull(accountId, accountName, projectId)) { return -1; } - return quotaService.finalizeAccountId(accountId, accountName, domainId, projectId); + return _accountService.finalizeAccountId(accountId, accountName, domainId, projectId); } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 6ee20c97f1e4..42e374a2a5f4 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -198,21 +198,7 @@ public QuotaTariffResponse createQuotaTariffResponse(QuotaTariffVO tariff, boole public Pair, Integer> createQuotaSummaryResponse(QuotaSummaryCmd cmd) { Account caller = CallContext.current().getCallingAccount(); - if (cmd.getProjectId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { - ProjectVO projectVO = projectDao.findById(cmd.getProjectId()); - Long projectAccountId = projectVO.getProjectAccountId(); - return getQuotaSummaryResponse(projectAccountId, null, null, null, cmd); - } - - else if (cmd.getAccountId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { - return getQuotaSummaryResponse(cmd.getAccountId(), null, null, null, cmd); - } - - else if (cmd.getDomainId() != null && !cmd.isListAll() && accountTypesThatCanListAllQuotaSummaries.contains(caller.getType())) { - return getQuotaSummaryResponse(null, null, cmd.getDomainId(), null, cmd); - } - - else if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { + if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || !cmd.isListAll()) { return getQuotaSummaryResponse(caller.getAccountId(), null, null, null, cmd); } @@ -222,7 +208,7 @@ else if (!accountTypesThatCanListAllQuotaSummaries.contains(caller.getType()) || protected Pair, Integer> getQuotaSummaryResponseWithListAll(QuotaSummaryCmd cmd, Account caller) { Long accountId = cmd.getEntityOwnerId(); if (accountId == -1) { - accountId = null; + accountId = cmd.isListAll() ? null : caller.getAccountId(); } Long domainId = cmd.getDomainId(); @@ -276,7 +262,7 @@ protected String getDomainPathByDomainIdForDomainAdmin(Account caller) { _accountMgr.checkAccess(caller, domain); if (domain == null) { - throw new InvalidParameterValueException(String.format("Domain id [%s] is invalid.", domainId)); + throw new InvalidParameterValueException(String.format("Domain ID [%s] is invalid.", domainId)); } return domain.getPath(); @@ -288,8 +274,7 @@ protected Pair, Integer> getQuotaSummaryResponse(Long List summaries = pairSummaries.first(); if (CollectionUtils.isEmpty(summaries)) { - logger.info(String.format("There are no summaries to list for parameters [%s].", - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "domainId", "listAll", "page", "pageSize"))); + logger.info("There are no summaries to list for parameters [{}].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(cmd, "accountName", "domainId", "listAll", "page", "pageSize")); return new Pair<>(new ArrayList<>(), 0); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java index 937012de40b9..f6a34e01be8d 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaService.java @@ -40,8 +40,6 @@ public interface QuotaService extends PluggableService { boolean saveQuotaAccount(AccountVO account, BigDecimal aggrUsage, Date endDate); - Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId); - boolean isJsInterpretationEnabled(); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java index d64f23d7235b..f39153ae0ac6 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/quota/QuotaServiceImpl.java @@ -26,12 +26,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.projects.Project; import com.cloud.projects.ProjectManager; import com.cloud.user.AccountService; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsCmd; @@ -290,48 +286,6 @@ public boolean saveQuotaAccount(final AccountVO account, final BigDecimal aggrUs } } - /** - * Returns the Id of the account that will be used when provided with either accountId, projectId or accountName and domainId. - */ - @Override - public Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId) { - if (projectId != null) { - if (accountId != null || accountName != null) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Project and account can not be specified together."); - } - final Project project = projectMgr.getProject(projectId); - if (project == null) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find project with id: [%s].", projectId)); - } - if (project.getState() != Project.State.Active) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Project with projectId [%s] is not active.", projectId)); - } - return project.getProjectAccountId(); - } - - if (accountId != null) { - if (accountService.getActiveAccountById(accountId) != null) { - return accountId; - } - throw new InvalidParameterValueException(String.format("Unable to find account with accountId: [%s].", accountId)); - } - - if (accountName == null && domainId == null) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Either %s or %s is required.", ApiConstants.ACCOUNT_ID, ApiConstants.PROJECT_ID)); - } - try { - Account activeAccount = accountService.getActiveAccountByName(accountName, domainId); - if (activeAccount != null) { - return activeAccount.getId(); - } - } catch (InvalidParameterValueException exception) { - throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Both %s and %s are needed if using either. Consider using %s instead.", - ApiConstants.ACCOUNT, ApiConstants.DOMAIN_ID, ApiConstants.ACCOUNT_ID)); - } - throw new InvalidParameterValueException(String.format("Unable to find account by name: [%s] on domain: [%s]", accountName, domainId)); - } - - @Override public void setMinBalance(Long accountId, Double balance) { QuotaAccountVO acc = _quotaAcc.findByIdQuotaAccount(accountId); @@ -345,36 +299,6 @@ public void setMinBalance(Long accountId, Double balance) { } } - protected Long getAccountToWhomQuotaBalancesWillBeListed(Long accountId, String accountName, Long domainId) { - if (accountId != null) { - Account account = _accountDao.findByIdIncludingRemoved(accountId); - if (account == null) { - throw new InvalidParameterValueException(String.format("Unable to find account [%s].", accountId)); - } - return accountId; - } - - validateIsChildDomain(accountName, domainId); - - Account account = _accountDao.findActiveAccount(accountName, domainId); - if (account == null) { - throw new InvalidParameterValueException(String.format("Unable to find active account [%s] in domain [%s].", accountName, domainId)); - } - return account.getAccountId(); - } - - protected void validateIsChildDomain(String accountName, Long domainId) { - Account caller = CallContext.current().getCallingAccount(); - - long callerDomainId = caller.getDomainId(); - if (_domainDao.isChildDomain(callerDomainId, domainId)) { - return; - } - - logger.debug(String.format("Domain with ID [%s] is not a child of the caller's domain [%s].", domainId, callerDomainId)); - throw new PermissionDeniedException(String.format("Account [%s] or domain [%s] is invalid.", accountName, domainId)); - } - @Override public boolean isJsInterpretationEnabled() { return jsInterpretationEnabled; diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java index c8ca8d3b4754..259264f3b0e0 100644 --- a/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/quota/QuotaServiceImplTest.java @@ -31,7 +31,6 @@ import org.apache.cloudstack.quota.vo.QuotaAccountVO; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.joda.time.DateTime; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -142,8 +141,6 @@ public void testGetQuotaUsage() { final Date startDate = new DateTime().minusDays(2).toDate(); final Date endDate = new Date(); - Mockito.lenient().doReturn(accountId).when(quotaServiceImplSpy).getAccountToWhomQuotaBalancesWillBeListed(Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong()); - quotaServiceImplSpy.getQuotaUsage(accountId, accountName, domainId, QuotaTypes.IP_ADDRESS, startDate, endDate); Mockito.verify(quotaUsageDao, Mockito.times(1)).findQuotaUsage(Mockito.eq(accountId), Mockito.eq(domainId), Mockito.eq(QuotaTypes.IP_ADDRESS), Mockito.any(Date.class), Mockito.any(Date.class)); } @@ -181,12 +178,4 @@ public void testSetMinBalance() { Mockito.verify(quotaAcc, Mockito.times(1)).persistQuotaAccount(Mockito.any(QuotaAccountVO.class)); } - @Test - public void getAccountToWhomQuotaBalancesWillBeListedTestAccountIdIsNotNullReturnsExistingAccount() { - long expected = 1L; - Mockito.doReturn(accountVoMock).when(accountDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); - long result = quotaServiceImplSpy.getAccountToWhomQuotaBalancesWillBeListed(expected, "test", 2L); - Assert.assertEquals(expected, result); - } - } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 53b88690654e..010e192c8600 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -56,6 +56,8 @@ import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; @@ -86,6 +88,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -3506,6 +3509,48 @@ public Long finalizeAccountId(final String accountName, final Long domainId, fin return null; } + @Override + public Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId) { + if (projectId != null) { + if (ObjectUtils.anyNotNull(accountId, accountName)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Project and account can not be specified together."); + } + return getActiveProjectAccountByProjectId(projectId); + } + if (accountId != null) { + if (getActiveAccountById(accountId) != null) { + return accountId; + } + throw new InvalidParameterValueException(String.format("Unable to find account with ID [%s].", accountId)); + } + + if (accountName == null && domainId == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Either %s or %s is required.", ApiConstants.ACCOUNT_ID, ApiConstants.PROJECT_ID)); + } + + try { + Account activeAccount = getActiveAccountByName(accountName, domainId); + if (activeAccount != null) { + return activeAccount.getId(); + } + } catch (InvalidParameterValueException exception) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Both %s and %s are needed if using either. Consider using %s instead.", + ApiConstants.ACCOUNT, ApiConstants.DOMAIN_ID, ApiConstants.ACCOUNT_ID)); + } + throw new InvalidParameterValueException(String.format("Unable to find account by name [%s] on domain [%s].", accountName, domainId)); + } + + protected long getActiveProjectAccountByProjectId(long projectId) { + Project project = _projectMgr.getProject(projectId); + if (project == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find project with ID [%s].", projectId)); + } + if (project.getState() != Project.State.Active) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Project with ID [%s] is not active.", projectId)); + } + return project.getProjectAccountId(); + } + @Override public UserAccount getUserAccountById(Long userId) { UserAccount userAccount = _userAccountDao.findById(userId); From b532758865bf8af121c1bd5912481623c097e368 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Tue, 3 Feb 2026 10:36:29 -0300 Subject: [PATCH 19/20] Remove duplicated check --- .../apache/cloudstack/api/command/QuotaSummaryCmd.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index e83e716c7ae0..a62524997804 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -120,13 +120,10 @@ public Long getProjectId() { } public QuotaAccountStateFilter getAccountStateToShow() { - if (StringUtils.isNotBlank(accountStateToShow)) { - QuotaAccountStateFilter state = QuotaAccountStateFilter.getValue(accountStateToShow); - if (state != null) { - return state; - } + QuotaAccountStateFilter state = QuotaAccountStateFilter.getValue(accountStateToShow); + if (state != null) { + return state; } - return QuotaAccountStateFilter.ACTIVE; } From c90b6094f028f34c5ffe181a635cf1ec86dcf186 Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Tue, 3 Feb 2026 10:43:48 -0300 Subject: [PATCH 20/20] Fix checkstyle --- .../org/apache/cloudstack/api/command/QuotaSummaryCmd.java | 2 -- .../cloudstack/api/response/QuotaResponseBuilderImpl.java | 1 - .../network/contrail/management/MockAccountManager.java | 6 ++++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java index a62524997804..aa61ac15668d 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaSummaryCmd.java @@ -33,8 +33,6 @@ import org.apache.cloudstack.quota.QuotaAccountStateFilter; import org.apache.cloudstack.quota.QuotaService; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; - import java.util.List; diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java index 42e374a2a5f4..a9d5f82cb7cd 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaResponseBuilderImpl.java @@ -44,7 +44,6 @@ import com.cloud.domain.Domain; import com.cloud.exception.PermissionDeniedException; -import com.cloud.projects.ProjectVO; import com.cloud.projects.dao.ProjectDao; import com.cloud.user.User; import com.cloud.user.UserVO; diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 4dffb405d1a3..1a52bb7d67f9 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -472,6 +472,12 @@ public Long finalizeAccountId(String accountName, Long domainId, Long projectId, return null; } + @Override + public Long finalizeAccountId(Long accountId, String accountName, Long domainId, Long projectId) { + // TODO Auto-generated method stub + return null; + } + @Override public void checkAccess(Account account, ServiceOffering so, DataCenter zone) throws PermissionDeniedException { // TODO Auto-generated method stub