From 449d3c7cb1df75867a3e1a9c3648fc3675c6b39e Mon Sep 17 00:00:00 2001 From: Fabricio Duarte Date: Thu, 16 Jan 2025 11:19:32 -0300 Subject: [PATCH] Create API to list Quota credits (#9590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bernardo De Marco Gonçalves --- .../apache/cloudstack/api/ApiConstants.java | 16 +- .../java/com/cloud/domain/dao/DomainDao.java | 2 + .../com/cloud/domain/dao/DomainDaoImpl.java | 10 ++ .../META-INF/db/schema-42010to42100.sql | 7 + .../cloudstack/quota/dao/QuotaCreditsDao.java | 2 +- .../quota/dao/QuotaCreditsDaoImpl.java | 54 +++--- .../cloudstack/quota/vo/QuotaCreditsVO.java | 6 + .../api/command/QuotaCreditsListCmd.java | 122 +++++++++++++ .../api/response/QuotaBalanceResponse.java | 4 +- .../api/response/QuotaCreditsResponse.java | 63 ++++--- .../api/response/QuotaResponseBuilder.java | 3 + .../response/QuotaResponseBuilderImpl.java | 122 +++++++++++-- .../cloudstack/quota/QuotaServiceImpl.java | 2 + .../api/command/QuotaCreditsListCmdTest.java | 79 +++++++++ .../QuotaResponseBuilderImplTest.java | 165 +++++++++++++++++- 15 files changed, 572 insertions(+), 85 deletions(-) create mode 100644 plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java create mode 100644 plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.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 cf03f1d2699..a406e2d7a72 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1192,6 +1192,14 @@ public class ApiConstants { "numeric value will be applied; if the result is neither a boolean nor a numeric value, the tariff will not be applied. If the rule is not informed, the tariff " + "value will be applied."; + public static final String PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " + + "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " + + "added, it will be interpreted as \"00:00:00\"). If the recommended format is not used, the date will be considered in the server timezone."; + + public static final String PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " + + "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " + + "added, it will be interpreted as \"23:59:59\"). If the recommended format is not used, the date will be considered in the server timezone."; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). @@ -1214,14 +1222,6 @@ public class ApiConstants { } } - public static final String PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " + - "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " + - "added, it will be interpreted as \"00:00:00\"). If the recommended format is not used, the date will be considered in the server timezone."; - - public static final String PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS = "The recommended format is \"yyyy-MM-dd'T'HH:mm:ssZ\" (e.g.: \"2023-01-01T12:00:00+0100\"); " + - "however, the following formats are also accepted: \"yyyy-MM-dd HH:mm:ss\" (e.g.: \"2023-01-01 12:00:00\") and \"yyyy-MM-dd\" (e.g.: \"2023-01-01\" - if the time is not " + - "added, it will be interpreted as \"23:59:59\"). If the recommended format is not used, the date will be considered in the server timezone."; - public enum BootType { UEFI, BIOS; diff --git a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java index 937d99f5052..3aee371da25 100644 --- a/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java +++ b/engine/schema/src/main/java/com/cloud/domain/dao/DomainDao.java @@ -42,5 +42,7 @@ public interface DomainDao extends GenericDao { List getDomainChildrenIds(String path); + List getDomainAndChildrenIds(long domainId); + boolean domainIdListContainsAccessibleDomain(String domainIdList, Account caller, Long domainId); } 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 74f2932ca43..56d971bbe01 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 @@ -19,6 +19,7 @@ package com.cloud.domain.dao; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -238,6 +239,15 @@ public class DomainDaoImpl extends GenericDaoBase implements Dom return customSearch(sc, null); } + @Override + public List getDomainAndChildrenIds(long domainId) { + DomainVO domain = findById(domainId); + if (domain != null) { + return getDomainChildrenIds(domain.getPath()); + } + return new ArrayList<>(); + } + @Override public boolean isChildDomain(Long parentId, Long childId) { if ((parentId == null) || (childId == null)) { diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 91223bab798..47e7bebbee4 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -24,3 +24,10 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'console_endpoint_ -- Add client_address column to cloud.console_session table CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.console_session', 'client_address', 'VARCHAR(45)'); + +-- Allow default roles to use quotaCreditsList +INSERT INTO `cloud`.`role_permissions` (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'quotaCreditsList', permission, sort_order +FROM `cloud`.`role_permissions` rp +WHERE rp.rule = 'quotaStatement' +AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'quotaCreditsList'); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java index f08d8f96ca8..da36bc0b98c 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDao.java @@ -25,7 +25,7 @@ import com.cloud.utils.db.GenericDao; public interface QuotaCreditsDao extends GenericDao { - List findCredits(long accountId, long domainId, Date startDate, Date endDate); + List findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive); QuotaCreditsVO saveCredits(QuotaCreditsVO credits); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java index 40e18aa282b..ce51177d0ae 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/dao/QuotaCreditsDaoImpl.java @@ -16,19 +16,20 @@ //under the License. package org.apache.cloudstack.quota.dao; -import java.util.Collections; import java.util.Date; import java.util.List; import javax.inject.Inject; +import com.cloud.domain.dao.DomainDao; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchBuilder; import org.apache.cloudstack.quota.vo.QuotaBalanceVO; import org.apache.cloudstack.quota.vo.QuotaCreditsVO; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.stereotype.Component; -import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.QueryBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallback; @@ -39,25 +40,36 @@ import com.cloud.utils.db.TransactionStatus; public class QuotaCreditsDaoImpl extends GenericDaoBase implements QuotaCreditsDao { @Inject - QuotaBalanceDao _quotaBalanceDao; + DomainDao domainDao; + @Inject + QuotaBalanceDao quotaBalanceDao; + + private SearchBuilder quotaCreditsVoSearch; + + public QuotaCreditsDaoImpl() { + quotaCreditsVoSearch = createSearchBuilder(); + quotaCreditsVoSearch.and("updatedOn", quotaCreditsVoSearch.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN); + quotaCreditsVoSearch.and("accountId", quotaCreditsVoSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + quotaCreditsVoSearch.and("domainId", quotaCreditsVoSearch.entity().getDomainId(), SearchCriteria.Op.IN); + quotaCreditsVoSearch.done(); + } @Override - public List findCredits(final long accountId, final long domainId, final Date startDate, final Date endDate) { - return Transaction.execute(TransactionLegacy.USAGE_DB, new TransactionCallback>() { - @Override - public List doInTransaction(final TransactionStatus status) { - if ((startDate != null) && (endDate != null) && startDate.before(endDate)) { - Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE); - QueryBuilder qb = QueryBuilder.create(QuotaCreditsVO.class); - qb.and(qb.entity().getAccountId(), SearchCriteria.Op.EQ, accountId); - qb.and(qb.entity().getDomainId(), SearchCriteria.Op.EQ, domainId); - qb.and(qb.entity().getUpdatedOn(), SearchCriteria.Op.BETWEEN, startDate, endDate); - return search(qb.create(), filter); - } else { - return Collections. emptyList(); - } - } - }); + public List findCredits(Long accountId, Long domainId, Date startDate, Date endDate, boolean recursive) { + SearchCriteria sc = quotaCreditsVoSearch.create(); + Filter filter = new Filter(QuotaCreditsVO.class, "updatedOn", true, 0L, Long.MAX_VALUE); + + sc.setParametersIfNotNull("accountId", accountId); + if (domainId != null) { + List domainIds = recursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId); + sc.setParameters("domainId", domainIds.toArray()); + } + + if (ObjectUtils.allNotNull(startDate, endDate)) { + sc.setParameters("updatedOn", startDate, endDate); + } + + return Transaction.execute(TransactionLegacy.USAGE_DB, (TransactionCallback>) status -> search(sc, filter)); } @Override @@ -68,7 +80,7 @@ public class QuotaCreditsDaoImpl extends GenericDaoBase im persist(credits); // make an entry in the balance table QuotaBalanceVO bal = new QuotaBalanceVO(credits); - _quotaBalanceDao.persist(bal); + quotaBalanceDao.persist(bal); return credits; } }); diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java index f9c7b45b8a4..5cf6b1f0e57 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/vo/QuotaCreditsVO.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.quota.vo; import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import javax.persistence.Column; import javax.persistence.Entity; @@ -113,4 +114,9 @@ public class QuotaCreditsVO implements InternalIdentity { public long getId() { return this.id; } + + @Override + public String toString() { + return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "accountId", "domainId", "credit"); + } } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java new file mode 100644 index 00000000000..48bb7ef79e7 --- /dev/null +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/command/QuotaCreditsListCmd.java @@ -0,0 +1,122 @@ +//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 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.BaseCmd; +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.QuotaCreditsResponse; +import org.apache.cloudstack.api.response.QuotaResponseBuilder; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.time.DateUtils; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import javax.inject.Inject; + +@APICommand(name = "quotaCreditsList", responseObject = QuotaCreditsResponse.class, description = "Lists quota credits of an account.", since = "4.21.0", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class QuotaCreditsListCmd extends BaseCmd { + + @Inject + QuotaResponseBuilder quotaResponseBuilder; + + @ACL + @Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "ID of the account for which the credit statement will be generated.") + private Long accountId; + + @ACL + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "ID of the domain for which credit statement will be generated. " + + "Available only for administrators.") + private Long domainId; + + @Parameter(name = ApiConstants.END_DATE, type = CommandType.DATE, description = "End date of the credit statement. If not provided, the current date will be " + + "considered as the end date. " + ApiConstants.PARAMETER_DESCRIPTION_END_DATE_POSSIBLE_FORMATS) + private Date endDate; + + @Parameter(name = ApiConstants.START_DATE, type = CommandType.DATE, description = "Start date of the credit statement. If not provided, the first day of the current month " + + "will be considered as the start date. " + ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS) + private Date startDate; + + @Parameter(name = ApiConstants.IS_RECURSIVE, type = CommandType.BOOLEAN, description = "Whether to generate the credit statement for the provided domain and its children. " + + "Defaults to false.") + private Boolean recursive = false; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(Long domainId) { + this.domainId = domainId; + } + + public Date getEndDate() { + return ObjectUtils.defaultIfNull(endDate, new Date()); + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + + public Date getStartDate() { + return ObjectUtils.defaultIfNull(startDate, DateUtils.truncate(new Date(), Calendar.MONTH)); + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Boolean getRecursive() { + return recursive; + } + + public void setRecursive(Boolean recursive) { + this.recursive = recursive; + } + + @Override + public void execute() { + Pair, Integer> responses = quotaResponseBuilder.createQuotaCreditsListResponse(this); + ListResponse response = new ListResponse<>(); + response.setResponses(responses.first(), responses.second()); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + return -1; + } + +} diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java index fca6b6cbb1a..4c6cc750d99 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaBalanceResponse.java @@ -122,8 +122,8 @@ public class QuotaBalanceResponse extends BaseResponse { public void addCredits(QuotaBalanceVO credit) { QuotaCreditsResponse cr = new QuotaCreditsResponse(); - cr.setCredits(credit.getCreditBalance()); - cr.setUpdatedOn(credit.getUpdatedOn() == null ? null : new Date(credit.getUpdatedOn().getTime())); + cr.setCredit(credit.getCreditBalance()); + cr.setCreditedOn(credit.getUpdatedOn()); credits.add(0, cr); } diff --git a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java index 561937ddcca..c81f777268f 100644 --- a/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java +++ b/plugins/database/quota/src/main/java/org/apache/cloudstack/api/response/QuotaCreditsResponse.java @@ -20,65 +20,62 @@ import com.cloud.serializer.Param; import com.google.gson.annotations.SerializedName; import org.apache.cloudstack.api.BaseResponse; -import org.apache.cloudstack.quota.vo.QuotaCreditsVO; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Date; public class QuotaCreditsResponse extends BaseResponse { - @SerializedName("credits") - @Param(description = "the credit deposited") - private BigDecimal credits; + @SerializedName("credit") + @Param(description = "The credit deposited.") + private BigDecimal credit; - @SerializedName("updated_by") - @Param(description = "the user name of the admin who updated the credits") - private String updatedBy; + @SerializedName("creditoruserid") + @Param(description = "ID of the creditor user.") + private String creditorUserId; - @SerializedName("updated_on") - @Param(description = "the account name of the admin who updated the credits") - private Date updatedOn; + @SerializedName("creditorusername") + @Param(description = "Username of the creditor user.") + private String creditorUsername; + + @SerializedName("creditedon") + @Param(description = "When the credit was added.") + private Date creditedOn; @SerializedName("currency") - @Param(description = "currency") + @Param(description = "Credit's currency.") private String currency; - public QuotaCreditsResponse() { - super(); + public BigDecimal getCredit() { + return credit; } - public QuotaCreditsResponse(QuotaCreditsVO result, String updatedBy) { - super(); - if (result != null) { - setCredits(result.getCredit()); - setUpdatedBy(updatedBy); - setUpdatedOn(new Date()); - } + public void setCredit(BigDecimal credit) { + this.credit = credit; } - public BigDecimal getCredits() { - return credits; + public String getCreditorUserId() { + return creditorUserId; } - public void setCredits(BigDecimal credits) { - this.credits = credits.setScale(2, RoundingMode.HALF_EVEN); + public void setCreditorUserId(String creditorUserId) { + this.creditorUserId = creditorUserId; } - public String getUpdatedBy() { - return updatedBy; + public String getCreditorUsername() { + return creditorUsername; } - public void setUpdatedBy(String updatedBy) { - this.updatedBy = updatedBy; + public void setCreditorUsername(String creditorUsername) { + this.creditorUsername = creditorUsername; } - public Date getUpdatedOn() { - return updatedOn; + public Date getCreditedOn() { + return creditedOn; } - public void setUpdatedOn(Date updatedOn) { - this.updatedOn = updatedOn; + public void setCreditedOn(Date creditedOn) { + this.creditedOn = creditedOn; } public String getCurrency() { 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 56935a1360c..67f75ebf82f 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 @@ -19,6 +19,7 @@ package org.apache.cloudstack.api.response; import com.cloud.user.User; 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; @@ -90,5 +91,7 @@ public interface QuotaResponseBuilder { List listEmailConfiguration(long accountId); + Pair, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd); + QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd 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 733f7792356..7a987df0a35 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,12 +42,16 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.user.User; +import com.cloud.user.UserVO; import com.cloud.utils.DateUtil; 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; @@ -99,7 +103,6 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; import com.cloud.user.AccountManager; import com.cloud.user.AccountVO; -import com.cloud.user.User; import com.cloud.user.dao.AccountDao; import com.cloud.user.dao.UserDao; import com.cloud.utils.Pair; @@ -116,7 +119,7 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Inject private QuotaBalanceDao _quotaBalanceDao; @Inject - private QuotaCreditsDao _quotaCreditsDao; + private QuotaCreditsDao quotaCreditsDao; @Inject private QuotaUsageDao _quotaUsageDao; @Inject @@ -548,28 +551,32 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { @Override public QuotaCreditsResponse addQuotaCredits(Long accountId, Long domainId, Double amount, Long updatedBy, Boolean enforce) { - Date despositedOn = new Date(); - QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, despositedOn); + Date depositedOn = new Date(); + QuotaBalanceVO qb = _quotaBalanceDao.findLaterBalanceEntry(accountId, domainId, depositedOn); if (qb != null) { throw new InvalidParameterValueException(String.format("Incorrect deposit date [%s], as there are balance entries after this date.", - despositedOn)); + depositedOn)); } QuotaCreditsVO credits = new QuotaCreditsVO(accountId, domainId, new BigDecimal(amount), updatedBy); - credits.setUpdatedOn(despositedOn); - QuotaCreditsVO result = _quotaCreditsDao.saveCredits(credits); + credits.setUpdatedOn(depositedOn); + QuotaCreditsVO result = quotaCreditsDao.saveCredits(credits); + if (result == null) { + logger.error("Unable to add credits to account ID [{}].", accountId); + throw new CloudRuntimeException("Unable to add credits to account."); + } final AccountVO account = _accountDao.findById(accountId); if (account == null) { throw new InvalidParameterValueException("Account does not exist with account id " + accountId); } final boolean lockAccountEnforcement = "true".equalsIgnoreCase(QuotaConfig.QuotaEnableEnforcement.value()); - final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(despositedOn.getTime()))); + final BigDecimal currentAccountBalance = _quotaBalanceDao.lastQuotaBalance(accountId, domainId, startOfNextDay(new Date(depositedOn.getTime()))); logger.debug("Depositing [{}] credits on adjusted date [{}]; current balance is [{}].", amount, - DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), despositedOn), currentAccountBalance); + DateUtil.displayDateInTimezone(QuotaManagerImpl.getUsageAggregationTimeZone(), depositedOn), currentAccountBalance); // update quota account with the balance - _quotaService.saveQuotaAccount(account, currentAccountBalance, despositedOn); + _quotaService.saveQuotaAccount(account, currentAccountBalance, depositedOn); if (lockAccountEnforcement) { if (currentAccountBalance.compareTo(new BigDecimal(0)) >= 0) { if (account.getState() == Account.State.LOCKED) { @@ -584,14 +591,8 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { } } - String creditor = String.valueOf(Account.ACCOUNT_ID_SYSTEM); - User creditorUser = _userDao.getUser(updatedBy); - if (creditorUser != null) { - creditor = creditorUser.getUsername(); - } - QuotaCreditsResponse response = new QuotaCreditsResponse(result, creditor); - response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); - return response; + UserVO creditor = getCreditorForQuotaCredits(result); + return createQuotaCreditsResponse(result, creditor); } private QuotaEmailTemplateResponse createQuotaEmailResponse(QuotaEmailTemplatesVO template) { @@ -938,6 +939,91 @@ public class QuotaResponseBuilderImpl implements QuotaResponseBuilder { return quotaConfigureEmailResponse; } + @Override + public Pair, Integer> createQuotaCreditsListResponse(QuotaCreditsListCmd cmd) { + List credits = getCreditsForQuotaCreditsList(cmd); + + List creditResponses = new ArrayList<>(); + Map userMap = new HashMap<>(); + + for (QuotaCreditsVO credit : credits) { + UserVO creditor = getCreditorForQuotaCreditsList(credit, userMap); + QuotaCreditsResponse response = createQuotaCreditsResponse(credit, creditor); + creditResponses.add(response); + } + + return new Pair<>(creditResponses, creditResponses.size()); + } + + protected List getCreditsForQuotaCreditsList(QuotaCreditsListCmd cmd) { + Long accountId = cmd.getAccountId(); + Long domainId = cmd.getDomainId(); + Date startDate = cmd.getStartDate(); + Date endDate = cmd.getEndDate(); + boolean isRecursive = cmd.getRecursive(); + + if (ObjectUtils.allNull(accountId, domainId)) { + throw new InvalidParameterValueException("Please provide either account ID or domain ID."); + } + + if (startDate.after(endDate)) { + throw new InvalidParameterValueException("The start date must be before the end date."); + } + + Account caller = CallContext.current().getCallingAccount(); + if (domainId != null && _accountMgr.isNormalUser(caller.getAccountId())) { + throw new PermissionDeniedException("Regular users are not allowed to generate domain statements."); + } + + return quotaCreditsDao.findCredits(accountId, domainId, startDate, endDate, isRecursive); + } + + /** + * Returns the creditor user of a QuotaCreditsVO. If userMap contains the user, returns the + * user from the map; otherwise, obtains the user from the database and adds it to the map. + */ + protected UserVO getCreditorForQuotaCreditsList(QuotaCreditsVO credit, Map userMap) { + Long creditorUserId = credit.getUpdatedBy(); + + UserVO userVo = userMap.get(creditorUserId); + if (userVo != null) { + return userVo; + } + + userVo = getCreditorForQuotaCredits(credit); + userMap.put(creditorUserId, userVo); + return userVo; + } + + /** + * Returns the creditor user of a QuotaCreditsVO by obtaining it from the database. + */ + protected UserVO getCreditorForQuotaCredits(QuotaCreditsVO credit) { + Long creditorUserId = credit.getUpdatedBy(); + UserVO userVo = _userDao.findByIdIncludingRemoved(creditorUserId); + if (userVo == null) { + logger.error("Could not find creditor user with ID [{}] for credit [{}].", creditorUserId, credit.toString()); + throw new CloudRuntimeException("Could not find creditor user."); + } + return userVo; + } + + protected QuotaCreditsResponse createQuotaCreditsResponse(QuotaCreditsVO credit, UserVO creditor) { + QuotaCreditsResponse response = new QuotaCreditsResponse(); + + if (credit != null) { + response.setCredit(credit.getCredit()); + response.setCreditedOn(credit.getUpdatedOn()); + response.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + } + if (creditor != null) { + response.setCreditorUserId(creditor.getUuid()); + response.setCreditorUsername(creditor.getUsername()); + } + response.setObjectName("credit"); + return response; + } + @Override public QuotaValidateActivationRuleResponse validateActivationRule(QuotaValidateActivationRuleCmd cmd) { String message; 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 97d77b8aa22..e02f260c142 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 @@ -29,6 +29,7 @@ import javax.naming.ConfigurationException; import org.apache.cloudstack.api.command.QuotaBalanceCmd; import org.apache.cloudstack.api.command.QuotaConfigureEmailCmd; import org.apache.cloudstack.api.command.QuotaCreditsCmd; +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.QuotaEnabledCmd; @@ -115,6 +116,7 @@ public class QuotaServiceImpl extends ManagerBase implements QuotaService, Confi cmdList.add(QuotaTariffListCmd.class); cmdList.add(QuotaTariffUpdateCmd.class); cmdList.add(QuotaCreditsCmd.class); + cmdList.add(QuotaCreditsListCmd.class); cmdList.add(QuotaEmailTemplateListCmd.class); cmdList.add(QuotaEmailTemplateUpdateCmd.class); cmdList.add(QuotaTariffCreateCmd.class); diff --git a/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.java b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.java new file mode 100644 index 00000000000..26621a52dbc --- /dev/null +++ b/plugins/database/quota/src/test/java/org/apache/cloudstack/api/command/QuotaCreditsListCmdTest.java @@ -0,0 +1,79 @@ +// 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 java.util.Calendar; +import java.util.Date; + +import org.apache.commons.lang3.time.DateUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class QuotaCreditsListCmdTest { + + @Spy + QuotaCreditsListCmd quotaCreditsListCmdSpy; + + @Test + public void getEndDateTestReturnsNewDateWhenEndDateIsNull() { + quotaCreditsListCmdSpy.setEndDate(null); + + try (MockedConstruction dateMockedConstruction = Mockito.mockConstruction(Date.class)) { + Date result = quotaCreditsListCmdSpy.getEndDate(); + + Assert.assertEquals(1, dateMockedConstruction.constructed().size()); + Assert.assertEquals(dateMockedConstruction.constructed().get(0), result); + } + } + + @Test + public void getEndDateTestReturnsEndDateWhenItIsNotNull() { + Date expected = new Date(); + quotaCreditsListCmdSpy.setEndDate(expected); + + Date result = quotaCreditsListCmdSpy.getEndDate(); + + Assert.assertEquals(expected, result); + } + + @Test + public void getStartDateTestReturnsFirstDayOfTheCurrentMonthWhenStartDateIsNull() { + quotaCreditsListCmdSpy.setStartDate(null); + Date expected = DateUtils.truncate(new Date(), Calendar.MONTH); + + Date result = quotaCreditsListCmdSpy.getStartDate(); + + Assert.assertEquals(expected, result); + } + + @Test + public void getStartDateTestReturnsStartDateWhenItIsNotNull() { + Date expected = new Date(); + quotaCreditsListCmdSpy.setStartDate(expected); + + Date result = quotaCreditsListCmdSpy.getStartDate(); + + Assert.assertEquals(expected, result); + } +} 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 a26e6c0476d..1f5480404e4 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 @@ -24,6 +24,7 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -32,12 +33,18 @@ import java.util.function.Consumer; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.user.AccountManager; +import com.cloud.user.UserVO; import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.ServerApiException; 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.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; @@ -66,6 +73,7 @@ import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.commons.lang3.time.DateUtils; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -81,6 +89,7 @@ import com.cloud.user.dao.UserDao; import com.cloud.user.User; import junit.framework.TestCase; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) @@ -135,7 +144,8 @@ public class QuotaResponseBuilderImplTest extends TestCase { QuotaEmailConfigurationDao quotaEmailConfigurationDaoMock; @InjectMocks - QuotaResponseBuilderImpl quotaResponseBuilderSpy = Mockito.spy(QuotaResponseBuilderImpl.class); + @Spy + QuotaResponseBuilderImpl quotaResponseBuilderSpy; Date date = new Date(); @@ -154,6 +164,26 @@ public class QuotaResponseBuilderImplTest extends TestCase { @Mock QuotaEmailTemplatesVO quotaEmailTemplatesVoMock; + @Mock + QuotaCreditsVO quotaCreditsVoMock; + + @Mock + UserVO userVoMock; + + @Mock + AccountManager accountManagerMock; + + @Mock + Account callerAccountMock; + + @Mock + User callerUserMock; + + @Before + 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); @@ -221,13 +251,14 @@ public class QuotaResponseBuilderImplTest extends TestCase { Mockito.when(quotaCreditsDaoMock.saveCredits(Mockito.any(QuotaCreditsVO.class))).thenReturn(credit); Mockito.when(quotaBalanceDaoMock.lastQuotaBalance(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(Date.class))).thenReturn(new BigDecimal(111)); + Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCredits(credit); AccountVO account = new AccountVO(); account.setState(Account.State.LOCKED); Mockito.when(accountDaoMock.findById(Mockito.anyLong())).thenReturn(account); QuotaCreditsResponse resp = quotaResponseBuilderSpy.addQuotaCredits(accountId, domainId, amount, updatedBy, true); - assertTrue(resp.getCredits().compareTo(credit.getCredit()) == 0); + assertTrue(resp.getCredit().compareTo(credit.getCredit()) == 0); } @Test @@ -658,6 +689,136 @@ public class QuotaResponseBuilderImplTest extends TestCase { assertFalse(quotaResponseBuilderSpy.isUserAllowedToSeeActivationRules(userMock)); } + @Test + public void createQuotaCreditsListResponseTestReturnsObject() { + List credits = new ArrayList<>(); + credits.add(new QuotaCreditsVO()); + QuotaCreditsResponse expectedQuotaCreditsResponse = new QuotaCreditsResponse(); + + Mockito.doReturn(credits).when(quotaResponseBuilderSpy).getCreditsForQuotaCreditsList(Mockito.any()); + Mockito.doReturn(userVoMock).when(quotaResponseBuilderSpy).getCreditorForQuotaCreditsList(Mockito.any(), Mockito.any()); + Mockito.doReturn(expectedQuotaCreditsResponse).when(quotaResponseBuilderSpy).createQuotaCreditsResponse(credits.get(0), userVoMock); + + Pair, Integer> result = quotaResponseBuilderSpy.createQuotaCreditsListResponse(createQuotaCreditsListCmdForTests()); + + Assert.assertEquals(expectedQuotaCreditsResponse, result.first().get(0)); + Assert.assertEquals(1, (int) result.second()); + } + + private QuotaCreditsListCmd createQuotaCreditsListCmdForTests() { + Mockito.doReturn(false).when(accountManagerMock).isNormalUser(Mockito.anyLong()); + QuotaCreditsListCmd cmd = new QuotaCreditsListCmd(); + cmd.setAccountId(1L); + cmd.setDomainId(2L); + return cmd; + } + + @Test(expected = InvalidParameterValueException.class) + public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenBothAccountIdAndDomainIdAreNull() { + QuotaCreditsListCmd cmd = new QuotaCreditsListCmd(); + + quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void getCreditsForQuotaCreditsListTestThrowsInvalidParameterValueExceptionWhenStartDateIsAfterEndDate() { + QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); + cmd.setStartDate(new Date()); + cmd.setEndDate(DateUtils.addDays(new Date(), -1)); + + quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); + } + + @Test(expected = PermissionDeniedException.class) + public void getCreditsForQuotaCreditsListTestThrowsPermissionDeniedExceptionWhenDomainIdIsProvidedAndCallerIsNormalUser() { + QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); + Mockito.doReturn(true).when(accountManagerMock).isNormalUser(Mockito.anyLong()); + + quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); + } + + @Test + public void getCreditsForQuotaCreditsListTestReturnsData() { + QuotaCreditsListCmd cmd = createQuotaCreditsListCmdForTests(); + List expected = new ArrayList<>(); + expected.add(new QuotaCreditsVO()); + + Mockito.doReturn(expected).when(quotaCreditsDaoMock).findCredits(Mockito.anyLong(), Mockito.anyLong(), Mockito.any(), Mockito.any(), Mockito.anyBoolean()); + + List result = quotaResponseBuilderSpy.getCreditsForQuotaCreditsList(cmd); + + Assert.assertEquals(expected, result); + } + + @Test + public void getCreditorForQuotaCreditsListTestReturnsUserFromMapWhenMapHasCreditor() { + Long creditorId = 1L; + Map userMap = new HashMap<>(); + + userMap.put(creditorId, userVoMock); + Mockito.doReturn(creditorId).when(quotaCreditsVoMock).getUpdatedBy(); + + UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCreditsList(quotaCreditsVoMock, userMap); + + Assert.assertEquals(userVoMock, result); + } + + @Test + public void getCreditorForQuotaCreditsListTestGetsCreditorFromDatabaseAndAddsItToMapWhenMapDoesNotHaveCreditor() { + Long creditorId = 1L; + Map userMap = new HashMap<>(); + + Mockito.doReturn(creditorId).when(quotaCreditsVoMock).getUpdatedBy(); + Mockito.doReturn(userVoMock).when(userDaoMock).findByIdIncludingRemoved(creditorId); + + UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCreditsList(quotaCreditsVoMock, userMap); + + Assert.assertEquals(userVoMock, result); + Assert.assertEquals(userVoMock, userMap.get(creditorId)); + } + + @Test + public void getCreditorForQuotaCreditsTestReturnsCreditorWhenCreditorExists() { + Long creditorId = 1L; + + Mockito.when(quotaCreditsVoMock.getUpdatedBy()).thenReturn(creditorId); + Mockito.doReturn(userVoMock).when(userDaoMock).findByIdIncludingRemoved(creditorId); + + UserVO result = quotaResponseBuilderSpy.getCreditorForQuotaCredits(quotaCreditsVoMock); + + Assert.assertEquals(userVoMock, result); + } + + @Test(expected = CloudRuntimeException.class) + public void getCreditorForQuotaCreditsTestThrowsCloudRuntimeExceptionWhenCreditorDoesNotExist() { + quotaResponseBuilderSpy.getCreditorForQuotaCredits(quotaCreditsVoMock); + } + + @Test + public void createQuotaCreditsResponseTestReturnsObject() { + QuotaCreditsResponse expected = new QuotaCreditsResponse(); + expected.setCreditorUserId("test_uuid"); + expected.setCreditorUsername("test_name"); + expected.setCredit(new BigDecimal(41.5)); + expected.setCreditedOn(new Date()); + expected.setCurrency(QuotaConfig.QuotaCurrencySymbol.value()); + expected.setObjectName("credit"); + + Mockito.when(userVoMock.getUuid()).thenReturn(expected.getCreditorUserId()); + Mockito.when(userVoMock.getUsername()).thenReturn(expected.getCreditorUsername()); + Mockito.when(quotaCreditsVoMock.getCredit()).thenReturn(expected.getCredit()); + Mockito.when(quotaCreditsVoMock.getUpdatedOn()).thenReturn(expected.getCreditedOn()); + + QuotaCreditsResponse result = quotaResponseBuilderSpy.createQuotaCreditsResponse(quotaCreditsVoMock, userVoMock); + + Assert.assertEquals(expected.getCreditorUserId(), result.getCreditorUserId()); + Assert.assertEquals(expected.getCreditorUsername(), result.getCreditorUsername()); + Assert.assertEquals(expected.getCredit(), result.getCredit()); + Assert.assertEquals(expected.getCreditedOn(), result.getCreditedOn()); + Assert.assertEquals(expected.getCurrency(), result.getCurrency()); + Assert.assertEquals(expected.getObjectName(), result.getObjectName()); + } + @Test public void validateActivationRuleTestValidateActivationRuleReturnValidScriptResponse() { Mockito.doReturn("if (account.name == 'test') { true } else { false }").when(quotaValidateActivationRuleCmdMock).getActivationRule();