From fc7f6ab3d4a5ad872d6716602a0571d059ac9f9a Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:15:10 +0530 Subject: [PATCH 1/7] Implement soft and hard deletion for Varadhi Resources --- .../java/com/flipkart/varadhi/Constants.java | 5 + .../entities/ResourceDeletionType.java | 28 ++ .../varadhi/entities/VaradhiSubscription.java | 209 +++++++++-- .../varadhi/entities/VaradhiTopic.java | 84 ++++- .../flipkart/varadhi/db/VaradhiMetaStore.java | 6 + .../varadhi/services/SubscriptionService.java | 332 ++++++++++++++---- .../varadhi/services/VaradhiTopicService.java | 174 +++++++-- .../web/v1/admin/SubscriptionHandlers.java | 304 ++++++++++++---- .../varadhi/web/v1/admin/TopicHandlers.java | 187 +++++++--- .../varadhi/web/admin/TopicHandlersTest.java | 11 +- .../flipkart/varadhi/spi/db/MetaStore.java | 2 + 11 files changed, 1095 insertions(+), 247 deletions(-) create mode 100644 entities/src/main/java/com/flipkart/varadhi/entities/ResourceDeletionType.java diff --git a/common/src/main/java/com/flipkart/varadhi/Constants.java b/common/src/main/java/com/flipkart/varadhi/Constants.java index a2a87adc..cb29f501 100644 --- a/common/src/main/java/com/flipkart/varadhi/Constants.java +++ b/common/src/main/java/com/flipkart/varadhi/Constants.java @@ -22,6 +22,11 @@ public static class PathParams { public static final String PATH_PARAM_SUBSCRIPTION = "subscription"; } + public static class QueryParams { + public static final String QUERY_PARAM_DELETION_TYPE = "deletionType"; + public static final String QUERY_PARAM_IGNORE_CONSTRAINTS = "ignoreConstraints"; + } + public static class Tags { public static final String TAG_REGION = "region"; public static final String TAG_ORG = "org"; diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/ResourceDeletionType.java b/entities/src/main/java/com/flipkart/varadhi/entities/ResourceDeletionType.java new file mode 100644 index 00000000..7d4f3d92 --- /dev/null +++ b/entities/src/main/java/com/flipkart/varadhi/entities/ResourceDeletionType.java @@ -0,0 +1,28 @@ +package com.flipkart.varadhi.entities; + +/** + * Enum representing the types of resource deletion. + */ +public enum ResourceDeletionType { + SOFT_DELETE, + HARD_DELETE, + DEFAULT; + + /** + * Converts a string value to a ResourceDeletionType. + * + * @param value the string value to convert + * + * @return the corresponding ResourceDeletionType, or DEFAULT if the value is null, blank, or unrecognized + */ + public static ResourceDeletionType fromValue(String value) { + if (value == null || value.isBlank()) { + return DEFAULT; + } + return switch (value.trim().toUpperCase()) { + case "SOFT_DELETE" -> SOFT_DELETE; + case "HARD_DELETE" -> HARD_DELETE; + default -> DEFAULT; + }; + } +} diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java index 0b549668..ea99b8c0 100644 --- a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java +++ b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java @@ -2,15 +2,18 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.google.common.collect.Maps; import lombok.*; import java.util.Map; +/** + * Represents a subscription in the Varadhi. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) public class VaradhiSubscription extends MetaStoreEntity { + private final String project; private final String topic; private String description; @@ -22,7 +25,25 @@ public class VaradhiSubscription extends MetaStoreEntity { private Status status; private Map properties; + private static final String SHARDS_ERROR = "Shards cannot be null or empty"; + private static final String PROPERTIES_ERROR = "Properties cannot be null or empty"; + /** + * Constructs a new VaradhiSubscription instance. + * + * @param name the name of the subscription + * @param version the version of the subscription + * @param project the project associated with the subscription + * @param topic the topic associated with the subscription + * @param description the description of the subscription + * @param grouped whether the subscription is grouped + * @param endpoint the endpoint of the subscription + * @param retryPolicy the retry policy of the subscription + * @param consumptionPolicy the consumption policy of the subscription + * @param shards the shards of the subscription + * @param status the status of the subscription + * @param properties the properties of the subscription + */ private VaradhiSubscription( String name, int version, @@ -38,24 +59,34 @@ private VaradhiSubscription( Map properties ) { super(name, version); - this.project = project; - this.topic = topic; + this.project = validateNotNullOrEmpty(project, "Project"); + this.topic = validateNotNullOrEmpty(topic, "Topic"); this.description = description; this.grouped = grouped; this.endpoint = endpoint; this.retryPolicy = retryPolicy; this.consumptionPolicy = consumptionPolicy; - if (shards == null || shards.getShardCount() <= 0) { - throw new IllegalArgumentException("shards cannot be null or empty"); - } - this.shards = shards; + this.shards = validateShards(shards); this.status = status; - if (null == properties || properties.isEmpty()) { - throw new IllegalArgumentException("properties cannot be null or empty"); - } - this.properties = properties; + this.properties = validateProperties(properties); } + /** + * Creates a new VaradhiSubscription instance. + * + * @param name the name of the subscription + * @param project the project associated with the subscription + * @param topic the topic associated with the subscription + * @param description the description of the subscription + * @param grouped whether the subscription is grouped + * @param endpoint the endpoint of the subscription + * @param retryPolicy the retry policy of the subscription + * @param consumptionPolicy the consumption policy of the subscription + * @param shards the shards of the subscription + * @param properties the properties of the subscription + * + * @return a new VaradhiSubscription instance + */ public static VaradhiSubscription of( String name, String project, @@ -70,52 +101,180 @@ public static VaradhiSubscription of( ) { return new VaradhiSubscription( name, INITIAL_VERSION, project, topic, description, grouped, endpoint, retryPolicy, consumptionPolicy, - shards, new Status(State.Creating), properties + shards, new Status(State.CREATING), properties ); } + /** + * Checks if the subscription is well provisioned. + * + * @return true if the subscription is well provisioned, false otherwise + */ @JsonIgnore public boolean isWellProvisioned() { - return status.state == State.Created; + return State.CREATED.equals(status.getState()); + } + + /** + * Checks if the subscription is active. + * + * @return true if the subscription not inactive, false otherwise + */ + @JsonIgnore + public boolean isActive() { + return !State.INACTIVE.equals(status.getState()); } + /** + * Marks the subscription as failed to create. + * + * @param message the failure message + */ public void markCreateFailed(String message) { - status.message = message; - status.state = State.CreateFailed; + updateStatus(State.CREATE_FAILED, message); } + /** + * Marks the subscription as created. + */ public void markCreated() { - status.state = State.Created; + updateStatus(State.CREATED, null); } + /** + * Marks the subscription as failed to delete. + * + * @param message the failure message + */ public void markDeleteFailed(String message) { - status.message = message; - status.state = State.DeleteFailed; + updateStatus(State.DELETE_FAILED, message); } + /** + * Marks the subscription as deleting. + */ public void markDeleting() { - status.state = State.Deleting; + updateStatus(State.DELETING, null); + } + + /** + * Marks the subscription as inactive. + */ + public void markInactive() { + updateStatus(State.INACTIVE, null); } + /** + * Restores the subscription to the created state. + */ + public void restore() { + updateStatus(State.CREATED, "Entity restored to created state."); + } + + /** + * Retrieves the integer value of a property. + * + * @param property the property name + * + * @return the integer value of the property + * + * @throws IllegalArgumentException if the property is not found or cannot be parsed as an integer + */ @JsonIgnore public int getIntProperty(String property) { - return Integer.parseInt(properties.get(property)); + String value = properties.get(property); + if (value == null) { + throw new IllegalArgumentException("Property not found: " + property); + } + return Integer.parseInt(value); + } + + /** + * Validates that a string is not null or empty. + * + * @param value the string value to validate + * @param fieldName the name of the field being validated + * + * @return the validated string value + * + * @throws IllegalArgumentException if the value is null or empty + */ + private static String validateNotNullOrEmpty(String value, String fieldName) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(fieldName + " cannot be null or empty"); + } + return value; + } + + /** + * Validates that the shards are not null or empty. + * + * @param shards the shards to validate + * + * @return the validated shards + * + * @throws IllegalArgumentException if the shards are null or empty + */ + private static SubscriptionShards validateShards(SubscriptionShards shards) { + if (shards == null || shards.getShardCount() <= 0) { + throw new IllegalArgumentException(SHARDS_ERROR); + } + return shards; + } + + /** + * Validates that the properties are not null or empty. + * + * @param properties the properties to validate + * + * @return the validated properties + * + * @throws IllegalArgumentException if the properties are null or empty + */ + private static Map validateProperties(Map properties) { + if (properties == null || properties.isEmpty()) { + throw new IllegalArgumentException(PROPERTIES_ERROR); + } + return properties; + } + + /** + * Updates the status of the subscription. + * + * @param state the new state of the subscription + * @param message the status message + */ + private void updateStatus(State state, String message) { + status.setState(state); + status.setMessage(message); } + /** + * Enum representing the state of the subscription. + */ public enum State { - Creating, - CreateFailed, - Created, - Deleting, - DeleteFailed + CREATING, + CREATE_FAILED, + CREATED, + DELETING, + DELETE_FAILED, + INACTIVE } + /** + * Represents the status of the subscription. + */ @Data @AllArgsConstructor(onConstructor = @__(@JsonCreator)) public static class Status { String message; State state; + /** + * Constructs a new Status instance with the given state. + * + * @param state the state of the subscription + */ public Status(State state) { this.state = state; } diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java index e20b433f..bcc32fd8 100644 --- a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java +++ b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java @@ -7,6 +7,9 @@ import java.util.HashMap; import java.util.Map; +/** + * Represents a topic in the Varadhi. + */ @Getter @EqualsAndHashCode(callSuper = true) public class VaradhiTopic extends AbstractTopic { @@ -14,35 +17,112 @@ public class VaradhiTopic extends AbstractTopic { private final Map internalTopics; private final boolean grouped; private final TopicCapacityPolicy capacity; + private Status status; + /** + * Constructs a new VaradhiTopic instance. + * + * @param name the name of the topic + * @param version the version of the topic + * @param grouped whether the topic is grouped + * @param capacity the capacity policy of the topic + * @param internalTopics the internal topics associated with this topic + * @param status the status of the topic + */ private VaradhiTopic( String name, int version, boolean grouped, TopicCapacityPolicy capacity, - Map internalTopics + Map internalTopics, Status status ) { super(name, version); this.grouped = grouped; this.capacity = capacity; this.internalTopics = internalTopics; + this.status = status; } + /** + * Creates a new VaradhiTopic instance. + * + * @param project the project associated with the topic + * @param name the name of the topic + * @param grouped whether the topic is grouped + * @param capacity the capacity policy of the topic + * @return a new VaradhiTopic instance + */ public static VaradhiTopic of(String project, String name, boolean grouped, TopicCapacityPolicy capacity) { - return new VaradhiTopic(buildTopicName(project, name), INITIAL_VERSION, grouped, capacity, new HashMap<>()); + return new VaradhiTopic( + buildTopicName(project, name), INITIAL_VERSION, grouped, capacity, new HashMap<>(), Status.ACTIVE); } + /** + * Builds the topic name from the project name and topic name. + * + * @param projectName the name of the project + * @param topicName the name of the topic + * @return the constructed topic name + */ public static String buildTopicName(String projectName, String topicName) { return String.join(NAME_SEPARATOR, projectName, topicName); } + /** + * Adds an internal topic for a specific region. + * + * @param region the region for the internal topic + * @param internalTopic the internal topic to add + */ public void addInternalTopic(String region, InternalCompositeTopic internalTopic) { this.internalTopics.put(region, internalTopic); } + /** + * Retrieves the project name from the topic name. + * + * @return the project name + */ @JsonIgnore public String getProjectName() { return getName().split(NAME_SEPARATOR_REGEX)[0]; } + /** + * Retrieves the produce topic for a specific region. + * + * @param region the region for which to retrieve the produce topic + * @return the produce topic for the specified region + */ public InternalCompositeTopic getProduceTopicForRegion(String region) { return internalTopics.get(region); } + + /** + * Updates the status of the topic. + * + * @param newStatus the new status to set + * + * @throws IllegalArgumentException if the new status is null + */ + public void updateStatus(Status newStatus) { + if (newStatus == null) { + throw new IllegalArgumentException("Status cannot be null"); + } + this.status = newStatus; + } + + /** + * Checks if the topic is active. + * + * @return true if the topic is active, false otherwise + */ + public boolean isActive() { + return this.status == Status.ACTIVE; + } + + /** + * Enum representing the status of the topic. + */ + public enum Status { + ACTIVE, + INACTIVE + } } diff --git a/server/src/main/java/com/flipkart/varadhi/db/VaradhiMetaStore.java b/server/src/main/java/com/flipkart/varadhi/db/VaradhiMetaStore.java index ad6e2a86..daa2a6fe 100644 --- a/server/src/main/java/com/flipkart/varadhi/db/VaradhiMetaStore.java +++ b/server/src/main/java/com/flipkart/varadhi/db/VaradhiMetaStore.java @@ -190,6 +190,12 @@ public void deleteTopic(String topicName) { zkMetaStore.deleteZNode(znode); } + @Override + public void updateTopic(VaradhiTopic topic) { + ZNode znode = ZNode.OfTopic(topic.getName()); + zkMetaStore.updateZNodeWithData(znode, topic); + } + @Override public List getAllSubscriptionNames() { ZNode znode = ZNode.OfEntityType(SUBSCRIPTION); diff --git a/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java b/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java index 17a5ac6c..d01d297e 100644 --- a/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java +++ b/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java @@ -4,43 +4,84 @@ import com.flipkart.varadhi.entities.*; import com.flipkart.varadhi.entities.cluster.SubscriptionOperation; import com.flipkart.varadhi.exceptions.InvalidOperationForResourceException; +import com.flipkart.varadhi.exceptions.ResourceNotFoundException; import com.flipkart.varadhi.spi.db.MetaStore; import com.flipkart.varadhi.utils.ShardProvisioner; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +/** + * Service class for managing subscriptions. + */ @Slf4j public class SubscriptionService { private final MetaStore metaStore; private final ControllerRestApi controllerClient; private final ShardProvisioner shardProvisioner; - public SubscriptionService(ShardProvisioner shardProvisioner, ControllerRestApi controllerClient, MetaStore metaStore) { + /** + * Constructs a new SubscriptionService instance. + * + * @param shardProvisioner the shard provisioner + * @param controllerClient the controller REST API client + * @param metaStore the meta store + */ + public SubscriptionService( + ShardProvisioner shardProvisioner, ControllerRestApi controllerClient, + MetaStore metaStore + ) { this.shardProvisioner = shardProvisioner; this.metaStore = metaStore; this.controllerClient = controllerClient; } + /** + * Retrieves the list of subscription names for a given project. + * + * @param projectName the name of the project + * + * @return the list of subscription names + */ public List getSubscriptionList(String projectName) { - return metaStore.getSubscriptionNames(projectName); + return metaStore.getSubscriptionNames(projectName).stream() + .filter(this::isActiveOrWellProvisionedByName) + .toList(); } + /** + * Retrieves a subscription by its name. + * + * @param subscriptionName the name of the subscription + * @return the subscription + */ public VaradhiSubscription getSubscription(String subscriptionName) { - return metaStore.getSubscription(subscriptionName); + return getValidatedSubscription(subscriptionName); } + /** + * Creates a new subscription. + * + * @param subscribedTopic the subscribed topic + * @param subscription the subscription to create + * @param subProject the project associated with the subscription + * + * @return the created subscription + */ public VaradhiSubscription createSubscription( - VaradhiTopic subscribedTopic, VaradhiSubscription subscription, Project subProject + VaradhiTopic subscribedTopic, VaradhiSubscription subscription, + Project subProject ) { - validateCreation(subscribedTopic, subscription); + validateGroupedSubscription(subscribedTopic, subscription); metaStore.createSubscription(subscription); + try { shardProvisioner.provision(subscription, subProject); subscription.markCreated(); } catch (Exception e) { - log.error("create subscription failed", e); + log.error("Failed to create subscription", e); subscription.markCreateFailed(e.getMessage()); throw e; } finally { @@ -49,89 +90,248 @@ public VaradhiSubscription createSubscription( return subscription; } + /** + * Starts a subscription. + * + * @param subscriptionName the name of the subscription + * @param requestedBy the user requesting the operation + * @return a CompletableFuture representing the subscription operation + */ public CompletableFuture start(String subscriptionName, String requestedBy) { - VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); - if (subscription.isWellProvisioned()) { - return controllerClient.startSubscription(subscriptionName, requestedBy); - } - throw new InvalidOperationForResourceException( - "Subscription is in state %s. It can't be started/stopped.".formatted( - subscription.getStatus().getState())); + return performSubscriptionOperation(subscriptionName, requestedBy, controllerClient::startSubscription); } + /** + * Stops a subscription. + * + * @param subscriptionName the name of the subscription + * @param requestedBy the user requesting the operation + * + * @return a CompletableFuture representing the subscription operation + */ public CompletableFuture stop(String subscriptionName, String requestedBy) { - VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); - if (subscription.isWellProvisioned()) { - return controllerClient.stopSubscription(subscriptionName, requestedBy); - } - throw new InvalidOperationForResourceException( - "Subscription is in state %s. It can't be started/stopped.".formatted( - subscription.getStatus().getState())); - } - - private void validateCreation(VaradhiTopic topic, VaradhiSubscription subscription) { - if (subscription.isGrouped() && !topic.isGrouped()) { - throw new IllegalArgumentException( - "Cannot create grouped Subscription as it's Topic(%s) is not grouped".formatted( - subscription.getTopic())); - } + return performSubscriptionOperation(subscriptionName, requestedBy, controllerClient::stopSubscription); } + /** + * Updates an existing subscription. + * + * @param subscriptionName the name of the subscription + * @param fromVersion the current version of the subscription + * @param description the new description of the subscription + * @param grouped whether the subscription is grouped + * @param endpoint the new endpoint of the subscription + * @param retryPolicy the new retry policy of the subscription + * @param consumptionPolicy the new consumption policy of the subscription + * @param requestedBy the user requesting the update + * + * @return a CompletableFuture representing the updated subscription + */ public CompletableFuture updateSubscription( String subscriptionName, int fromVersion, String description, boolean grouped, Endpoint endpoint, RetryPolicy retryPolicy, ConsumptionPolicy consumptionPolicy, String requestedBy ) { - VaradhiSubscription existingSubscription = metaStore.getSubscription(subscriptionName); - validateForConflictingUpdate(fromVersion, existingSubscription.getVersion()); - VaradhiTopic subscribedTopic = metaStore.getTopic(existingSubscription.getTopic()); - validateForSubscribedTopic(subscribedTopic, grouped); - return controllerClient.getSubscriptionState(subscriptionName, requestedBy).thenApply(ss -> { - existingSubscription.setGrouped(grouped); - existingSubscription.setDescription(description); - existingSubscription.setEndpoint(endpoint); - existingSubscription.setRetryPolicy(retryPolicy); - existingSubscription.setConsumptionPolicy(consumptionPolicy); + VaradhiSubscription subscription = getValidatedSubscription(subscriptionName); + validateVersionForUpdate(fromVersion, subscription.getVersion()); + + boolean originalGrouped = subscription.isGrouped(); + + subscription.setGrouped(grouped); + validateGroupedSubscription(metaStore.getTopic(subscription.getTopic()), subscription); - metaStore.updateSubscription(existingSubscription); - return existingSubscription; - }); + subscription.setGrouped(originalGrouped); + return controllerClient.getSubscriptionState(subscriptionName, requestedBy) + .thenApply(state -> { + subscription.setGrouped(grouped); + subscription.setDescription(description); + subscription.setEndpoint(endpoint); + subscription.setRetryPolicy(retryPolicy); + subscription.setConsumptionPolicy(consumptionPolicy); + metaStore.updateSubscription(subscription); + return subscription; + }); } - private void validateForConflictingUpdate(int fromVersion, int latestVersion) { + /** + * Deletes a subscription. + * + * @param subscriptionName the name of the subscription + * @param subProject the project associated with the subscription + * @param requestedBy the user requesting the deletion + * @param deletionType the type of deletion (soft or hard) + * + * @return a CompletableFuture representing the deletion operation + */ + public CompletableFuture deleteSubscription( + String subscriptionName, Project subProject, String requestedBy, + ResourceDeletionType deletionType + ) { + VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); + + return controllerClient.getSubscriptionState(subscriptionName, requestedBy) + .thenAccept(state -> { + if (!state.isStoppedSuccessfully()) { + throw new IllegalArgumentException( + String.format("Cannot delete subscription in state: %s", state)); + } + + if (deletionType.equals(ResourceDeletionType.HARD_DELETE)) { + handleHardDelete(subscription, subProject); + } else { + handleSoftDelete(subscription); + } + }); + } + + /** + * Restores a subscription. + * + * @param subscriptionName the name of the subscription + * @param requestedBy the user requesting the restoration + * + * @return a CompletableFuture representing the restored subscription + */ + public CompletableFuture restoreSubscription(String subscriptionName, String requestedBy) { + VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); + + if (subscription.isActive()) { + throw new InvalidOperationForResourceException( + "Subscription '%s' is already active.".formatted(subscriptionName)); + } + + return controllerClient.getSubscriptionState(subscriptionName, requestedBy) + .thenApply(state -> { + subscription.restore(); + metaStore.updateSubscription(subscription); + log.info("Subscription '{}' restored successfully.", subscriptionName); + return subscription; + }); + } + + /** + * Checks if a subscription is active or well-provisioned. + * + * @param subscription the subscription to check + * + * @return true if the subscription is active or well-provisioned, false otherwise + */ + private boolean isActiveOrWellProvisioned(VaradhiSubscription subscription) { + return subscription.isActive() || subscription.isWellProvisioned(); + } + + /** + * Checks if a subscription is active or well-provisioned by its name. + * + * @param subscriptionName the name of the subscription + * + * @return true if the subscription is active or well-provisioned, false otherwise + */ + private boolean isActiveOrWellProvisionedByName(String subscriptionName) { + return isActiveOrWellProvisioned(metaStore.getSubscription(subscriptionName)); + } + + /** + * Retrieves and validates a subscription by its name. + * + * @param subscriptionName the name of the subscription + * + * @return the validated subscription + * + * @throws ResourceNotFoundException if the subscription is not found or in an invalid state + */ + private VaradhiSubscription getValidatedSubscription(String subscriptionName) { + VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); + if (!isActiveOrWellProvisioned(subscription)) { + throw new ResourceNotFoundException(String.format( + "Subscription '%s' not found or in invalid state.", subscriptionName)); + } + return subscription; + } + + /** + * Validates if a grouped subscription can be created or updated for a given topic. + * + * @param topic the topic to validate against + * @param subscription the subscription to validate + * + * @throws IllegalArgumentException if the subscription is grouped and the topic is not grouped + */ + private void validateGroupedSubscription(VaradhiTopic topic, VaradhiSubscription subscription) { + if (subscription.isGrouped() && !topic.isGrouped()) { + throw new IllegalArgumentException(String.format( + "Grouped subscription cannot be created or updated for a non-grouped topic '%s'", topic.getName())); + } + } + + /** + * Validates the version for updating a subscription. + * + * @param fromVersion the current version of the subscription + * @param latestVersion the latest version of the subscription + * + * @throws InvalidOperationForResourceException if the versions do not match + */ + private void validateVersionForUpdate(int fromVersion, int latestVersion) { if (fromVersion != latestVersion) { throw new InvalidOperationForResourceException( - "Conflicting update, Subscription has been modified. Fetch latest and try again."); + "Conflicting update detected. Fetch the latest version and try again."); } } - private void validateForSubscribedTopic(VaradhiTopic subscribedTopic, boolean groupedUpdated) { - if (groupedUpdated && !subscribedTopic.isGrouped()) { - throw new IllegalArgumentException( - "Cannot update Subscription to grouped as it's Topic(%s) is not grouped".formatted( - subscribedTopic.getName())); + /** + * Performs a subscription operation. + * + * @param subscriptionName the name of the subscription + * @param requestedBy the user requesting the operation + * @param operation the operation to perform + * + * @return a CompletableFuture representing the subscription operation + */ + private CompletableFuture performSubscriptionOperation( + String subscriptionName, + String requestedBy, + BiFunction> operation + ) { + VaradhiSubscription subscription = getValidatedSubscription(subscriptionName); + if (!subscription.isWellProvisioned()) { + throw new InvalidOperationForResourceException(String.format( + "Subscription '%s' is not well-provisioned for this operation.", subscription.getName())); } + return operation.apply(subscriptionName, requestedBy); } - public CompletableFuture deleteSubscription(String subscriptionName, Project subProject, String requestedBy) { - VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); - return controllerClient.getSubscriptionState(subscriptionName, requestedBy).thenAccept(ss -> { - if (!ss.isStoppedSuccessfully()) { - throw new IllegalArgumentException( - String.format("Subscription deletion not allowed in state: %s.", ss)); - } - subscription.markDeleting(); + /** + * Handles the hard deletion of a subscription. + * + * @param subscription the subscription to delete + * @param subProject the project associated with the subscription + */ + private void handleHardDelete(VaradhiSubscription subscription, Project subProject) { + subscription.markDeleting(); + metaStore.updateSubscription(subscription); + + try { + shardProvisioner.deProvision(subscription, subProject); + metaStore.deleteSubscription(subscription.getName()); + log.info("Subscription '{}' deleted successfully.", subscription.getName()); + } catch (Exception e) { + log.error("Failed to hard delete subscription '{}'.", subscription.getName(), e); + subscription.markDeleteFailed(e.getMessage()); metaStore.updateSubscription(subscription); - try { - shardProvisioner.deProvision(subscription, subProject); - metaStore.deleteSubscription(subscriptionName); - } catch (Exception e) { - log.error("Delete failed.", e); - subscription.markDeleteFailed(e.getMessage()); - metaStore.updateSubscription(subscription); - throw e; - } - }); + throw e; + } + } + + /** + * Handles the soft deletion of a subscription. + * + * @param subscription the subscription to delete + */ + private void handleSoftDelete(VaradhiSubscription subscription) { + subscription.markInactive(); + metaStore.updateSubscription(subscription); + log.info("Subscription '{}' marked inactive successfully.", subscription.getName()); } } diff --git a/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java b/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java index 172ba0d8..d8ed5155 100644 --- a/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java +++ b/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java @@ -1,74 +1,178 @@ package com.flipkart.varadhi.services; import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.StorageTopic; -import com.flipkart.varadhi.entities.VaradhiSubscription; import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.exceptions.InvalidOperationForResourceException; +import com.flipkart.varadhi.exceptions.ResourceNotFoundException; import com.flipkart.varadhi.spi.db.MetaStore; import com.flipkart.varadhi.spi.services.StorageTopicService; import lombok.extern.slf4j.Slf4j; import java.util.List; -// TODO::This need to move to server.services +/** + * Service class for managing Varadhi topics. + */ @Slf4j public class VaradhiTopicService { - private final StorageTopicService topicService; + private final StorageTopicService storageTopicService; private final MetaStore metaStore; - public VaradhiTopicService( - StorageTopicService storageTopicService, - MetaStore metaStore - ) { - this.topicService = storageTopicService; + /** + * Constructs a VaradhiTopicService with the specified storage topic service and meta store. + * + * @param storageTopicService the storage topic service + * @param metaStore the meta store + */ + public VaradhiTopicService(StorageTopicService storageTopicService, MetaStore metaStore) { + this.storageTopicService = storageTopicService; this.metaStore = metaStore; } + /** + * Creates a new Varadhi topic. + * + * @param varadhiTopic the Varadhi topic to create + * @param project the project associated with the topic + */ public void create(VaradhiTopic varadhiTopic, Project project) { - log.info("Creating Varadhi topic {}", varadhiTopic.getName()); - // StorageTopicService.create() to ensure if pre-existing topic can be re-used. - // i.e. topic creation at storage level need to be idempotent. - varadhiTopic.getInternalTopics().forEach((region, internalTopic) -> internalTopic.getActiveTopics() - .forEach(storageTopic -> topicService.create(storageTopic, project))); + log.info("Creating Varadhi topic: {}", varadhiTopic.getName()); + // Ensure StorageTopicService.create() is idempotent, allowing reuse of pre-existing topics. + varadhiTopic.getInternalTopics().forEach((region, internalTopic) -> + internalTopic.getActiveTopics().forEach(storageTopic -> + storageTopicService.create(storageTopic, project) + ) + ); metaStore.createTopic(varadhiTopic); } + /** + * Retrieves a Varadhi topic by its name. + * + * @param topicName the name of the topic + * @return the Varadhi topic + * @throws ResourceNotFoundException if the topic is not found or inactive + */ public VaradhiTopic get(String topicName) { - return metaStore.getTopic(topicName); + VaradhiTopic varadhiTopic = metaStore.getTopic(topicName); + + if (!varadhiTopic.isActive()) { + throw new ResourceNotFoundException("Topic %s not found.".formatted(topicName)); + } + + return varadhiTopic; + } + + /** + * Deletes a Varadhi topic by its name. + * + * @param topicName the name of the topic + * @param deletionType the type of deletion (hard or soft) + */ + public void delete(String topicName, ResourceDeletionType deletionType) { + log.info("Deleting Varadhi topic: {}", topicName); + // TODO: If the only topic in a namespace, also delete the namespace and tenant. Perform cleanup independently of the delete operation. + VaradhiTopic varadhiTopic = metaStore.getTopic(topicName); + validateTopicForDeletion(topicName); + + if (deletionType.equals(ResourceDeletionType.HARD_DELETE)) { + handleHardDelete(varadhiTopic); + } else { + handleSoftDelete(varadhiTopic); + } + } + + /** + * Handles the soft deletion of a Varadhi topic. + * + * @param varadhiTopic the Varadhi topic to soft delete + */ + public void handleSoftDelete(VaradhiTopic varadhiTopic) { + log.info("Soft deleting Varadhi topic: {}", varadhiTopic.getName()); + varadhiTopic.updateStatus(VaradhiTopic.Status.INACTIVE); + metaStore.updateTopic(varadhiTopic); } - public void delete(String varadhiTopicName) { - log.info("Deleting Varadhi topic {}", varadhiTopicName); - /* TODO : delete namespace, tenant also if the only Topic in the namespace+tenant is deleted / cleanup independent of delete - */ - VaradhiTopic varadhiTopic = metaStore.getTopic(varadhiTopicName); - String projectName = varadhiTopic.getProjectName(); - Project project = metaStore.getProject(projectName); - validateDelete(varadhiTopicName); - varadhiTopic.getInternalTopics().forEach((region, internalTopic) -> internalTopic.getActiveTopics() - .forEach(storageTopic -> topicService.delete(storageTopic.getName(), project))); + /** + * Handles the hard deletion of a Varadhi topic. + * + * @param varadhiTopic the Varadhi topic to hard delete + */ + public void handleHardDelete(VaradhiTopic varadhiTopic) { + log.info("Hard deleting Varadhi topic: {}", varadhiTopic.getName()); + + Project project = metaStore.getProject(varadhiTopic.getProjectName()); + + varadhiTopic.getInternalTopics().forEach((region, internalTopic) -> + internalTopic.getActiveTopics().forEach(storageTopic -> + storageTopicService.delete(storageTopic.getName(), project) + ) + ); metaStore.deleteTopic(varadhiTopic.getName()); } - private void validateDelete(String varadhiTopicName) { - // TODO: optimize this flow, currently it scans all subscriptions across all projects - List subscriptionNames = metaStore.getAllSubscriptionNames(); - subscriptionNames.forEach(subscriptionName -> { - VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); - if (subscription.getTopic().equals(varadhiTopicName)) { - throw new InvalidOperationForResourceException( - "Cannot delete topic as it is being used by subscription " + subscriptionName); - } - }); + /** + * Restores a deleted Varadhi topic. + * + * @param topicName the name of the topic to restore + * + * @throws InvalidOperationForResourceException if the topic is not deleted + */ + public void restore(String topicName) { + log.info("Restoring Varadhi topic: {}", topicName); + + VaradhiTopic varadhiTopic = metaStore.getTopic(topicName); + + if (varadhiTopic.isActive()) { + throw new InvalidOperationForResourceException("Topic %s is not deleted.".formatted(topicName)); + } + varadhiTopic.updateStatus(VaradhiTopic.Status.ACTIVE); + metaStore.updateTopic(varadhiTopic); + } + + /** + * Validates if a topic can be deleted. + * + * @param topicName the name of the topic to validate + * + * @throws InvalidOperationForResourceException if the topic is being used by a subscription + */ + private void validateTopicForDeletion(String topicName) { + // TODO: Improve efficiency by avoiding a full scan of all subscriptions across projects. + List subscriptions = metaStore.getAllSubscriptionNames(); + boolean isTopicInUse = subscriptions.stream() + .map(metaStore::getSubscription) + .anyMatch(subscription -> subscription.getTopic().equals(topicName)); + + if (isTopicInUse) { + throw new InvalidOperationForResourceException( + "Cannot delete topic as it is being used by a subscription." + ); + } } + /** + * Checks if a topic exists. + * + * @param topicName the name of the topic + * @return true if the topic exists, false otherwise + */ public boolean exists(String topicName) { return metaStore.checkTopicExists(topicName); } + /** + * Retrieves a list of active Varadhi topics for a project. + * + * @param projectName the name of the project + * @return a list of active Varadhi topic names + */ public List getVaradhiTopics(String projectName) { - return metaStore.getTopicNames(projectName); + return metaStore.getTopicNames(projectName).stream() + .filter(topicName -> metaStore.getTopic(topicName).isActive()) + .toList(); } } diff --git a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java index ba6dcd0a..cd38eda6 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java +++ b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java @@ -1,16 +1,14 @@ package com.flipkart.varadhi.web.v1.admin; import com.flipkart.varadhi.config.RestOptions; -import com.flipkart.varadhi.entities.auth.ResourceType; -import com.flipkart.varadhi.services.VaradhiTopicService; import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.auth.ResourceType; import com.flipkart.varadhi.services.ProjectService; import com.flipkart.varadhi.services.SubscriptionService; +import com.flipkart.varadhi.services.VaradhiTopicService; import com.flipkart.varadhi.utils.SubscriptionPropertyValidator; import com.flipkart.varadhi.utils.VaradhiSubscriptionFactory; import com.flipkart.varadhi.web.Extensions; -import com.flipkart.varadhi.entities.Hierarchies; -import com.flipkart.varadhi.entities.ResourceHierarchy; import com.flipkart.varadhi.web.entities.SubscriptionResource; import com.flipkart.varadhi.web.routes.RouteDefinition; import com.flipkart.varadhi.web.routes.RouteProvider; @@ -20,49 +18,68 @@ import lombok.experimental.ExtensionMethod; import lombok.extern.slf4j.Slf4j; -import java.util.HashMap; import java.util.List; import java.util.Map; import static com.flipkart.varadhi.Constants.CONTEXT_KEY_BODY; -import static com.flipkart.varadhi.Constants.PathParams.*; -import static com.flipkart.varadhi.entities.Hierarchies.*; +import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_PROJECT; +import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_SUBSCRIPTION; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_DELETION_TYPE; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_IGNORE_CONSTRAINTS; +import static com.flipkart.varadhi.entities.Hierarchies.SubscriptionHierarchy; +import static com.flipkart.varadhi.entities.Hierarchies.TopicHierarchy; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; import static com.flipkart.varadhi.entities.auth.ResourceAction.*; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +/** + * Handles various subscription-related operations such as listing, creating, updating, deleting, restoring, + * starting, and stopping subscriptions. + */ @Slf4j @ExtensionMethod({Extensions.RequestBodyExtension.class, Extensions.RoutingContextExtension.class}) public class SubscriptionHandlers implements RouteProvider { - private final Map propertyValidators = new HashMap<>(); - private final Map propertyDefaultValueProviders = new HashMap<>(); - private final int NUMBER_OF_RETRIES_ALLOWED = 3; + + private static final int NUMBER_OF_RETRIES_ALLOWED = 3; + private final SubscriptionService subscriptionService; private final ProjectService projectService; private final VaradhiTopicService topicService; private final VaradhiSubscriptionFactory varadhiSubscriptionFactory; + private final Map propertyValidators; + private final Map propertyDefaultValueProviders; + /** + * Constructs a new SubscriptionHandlers instance. + * + * @param subscriptionService the service to manage subscriptions + * @param projectService the service to manage projects + * @param topicService the service to manage topics + * @param subscriptionFactory the factory to create subscriptions + * @param restOptions the REST options configuration + */ public SubscriptionHandlers( - SubscriptionService subscriptionService, ProjectService projectService, - VaradhiTopicService topicService, VaradhiSubscriptionFactory subscriptionFactory, + SubscriptionService subscriptionService, + ProjectService projectService, + VaradhiTopicService topicService, + VaradhiSubscriptionFactory subscriptionFactory, RestOptions restOptions ) { this.subscriptionService = subscriptionService; this.projectService = projectService; this.topicService = topicService; this.varadhiSubscriptionFactory = subscriptionFactory; - this.propertyValidators.putAll(SubscriptionPropertyValidator.createPropertyValidators(restOptions)); - this.propertyDefaultValueProviders.putAll( - SubscriptionPropertyValidator.createPropertyDefaultValueProviders(restOptions)); - } - - public static String getSubscriptionFqn(RoutingContext ctx) { - String projectName = ctx.pathParam(PATH_PARAM_PROJECT); - String subscriptionName = ctx.pathParam(PATH_PARAM_SUBSCRIPTION); - return SubscriptionResource.buildInternalName(projectName, subscriptionName); + this.propertyValidators = SubscriptionPropertyValidator.createPropertyValidators(restOptions); + this.propertyDefaultValueProviders = + SubscriptionPropertyValidator.createPropertyDefaultValueProviders(restOptions); } + /** + * Returns the list of route definitions for subscription-related operations. + * + * @return the list of route definitions + */ @Override public List get() { return new SubRoutes( @@ -96,6 +113,11 @@ public List get() { .nonBlocking() .authorize(SUBSCRIPTION_DELETE) .build(this::getHierarchies, this::delete), + RouteDefinition + .post("RestoreSubscription", "/:subscription/restore") + .nonBlocking() + .authorize(SUBSCRIPTION_UPDATE) + .build(this::getHierarchies, this::restore), RouteDefinition .post("StartSubscription", "/:subscription/start") .nonBlocking() @@ -109,23 +131,39 @@ public List get() { ).get(); } + /** + * Parses and sets the subscription resource from the request body. + * + * @param ctx the routing context + */ public void setSubscription(RoutingContext ctx) { SubscriptionResource subscriptionResource = ctx.body().asValidatedPojo(SubscriptionResource.class); ctx.put(CONTEXT_KEY_BODY, subscriptionResource); } + /** + * Retrieves the resource hierarchies for the given context. + * + * @param ctx the routing context + * @param hasBody whether the request has a body + * + * @return the map of resource types to their hierarchies + */ public Map getHierarchies(RoutingContext ctx, boolean hasBody) { Project subscriptionProject = projectService.getCachedProject(ctx.request().getParam(PATH_PARAM_PROJECT)); if (hasBody) { SubscriptionResource subscriptionResource = ctx.get(CONTEXT_KEY_BODY); Project topicProject = projectService.getProject(subscriptionResource.getTopicProject()); - return Map.ofEntries(Map.entry( - ResourceType.SUBSCRIPTION, - new SubscriptionHierarchy(subscriptionProject, subscriptionResource.getName()) - ), Map.entry(ResourceType.TOPIC, new TopicHierarchy(topicProject, subscriptionResource.getTopic()))); + return Map.ofEntries( + Map.entry( + ResourceType.SUBSCRIPTION, + new SubscriptionHierarchy(subscriptionProject, subscriptionResource.getName()) + ), + Map.entry(ResourceType.TOPIC, new TopicHierarchy(topicProject, subscriptionResource.getTopic())) + ); } String subscriptionName = ctx.request().getParam(PATH_PARAM_SUBSCRIPTION); - if (null == subscriptionName) { + if (subscriptionName == null) { return Map.of(ResourceType.PROJECT, new Hierarchies.ProjectHierarchy(subscriptionProject)); } @@ -133,18 +171,29 @@ public Map getHierarchies(RoutingContext ctx, b String[] topicNameSegments = subscription.getTopic().split(NAME_SEPARATOR_REGEX); Project topicProject = projectService.getProject(topicNameSegments[0]); String topicName = topicNameSegments[1]; + return Map.ofEntries( Map.entry(ResourceType.SUBSCRIPTION, new SubscriptionHierarchy(subscriptionProject, subscriptionName)), Map.entry(ResourceType.TOPIC, new TopicHierarchy(topicProject, topicName)) ); } + /** + * Lists all subscriptions for a given project. + * + * @param ctx the routing context + */ public void list(RoutingContext ctx) { String projectName = ctx.pathParam(PATH_PARAM_PROJECT); List subscriptionNames = subscriptionService.getSubscriptionList(projectName); ctx.endApiWithResponse(subscriptionNames); } + /** + * Retrieves a specific subscription. + * + * @param ctx the routing context + */ public void get(RoutingContext ctx) { String internalSubscriptionName = getSubscriptionFqn(ctx); SubscriptionResource subscription = @@ -152,65 +201,145 @@ public void get(RoutingContext ctx) { ctx.endApiWithResponse(subscription); } + /** + * Creates a new subscription. + * + * @param ctx the routing context + */ public void create(RoutingContext ctx) { SubscriptionResource subscription = getValidSubscriptionResource(ctx); VaradhiTopic subscribedTopic = getSubscribedTopic(subscription); Project subProject = projectService.getCachedProject(subscription.getProject()); + VaradhiSubscription varadhiSubscription = varadhiSubscriptionFactory.get(subscription, subProject, subscribedTopic); VaradhiSubscription createdSubscription = subscriptionService.createSubscription(subscribedTopic, varadhiSubscription, subProject); + ctx.endApiWithResponse(SubscriptionResource.from(createdSubscription)); } + /** + * Updates an existing subscription. + * + * @param ctx the routing context + */ public void update(RoutingContext ctx) { SubscriptionResource subscription = getValidSubscriptionResource(ctx); - //TODO::Evaluate separating these into individual update APIs. - //Fix:: Update is allowed, though no change in the subscription, this can be avoided. - ctx.handleResponse(subscriptionService.updateSubscription(subscription.getSubscriptionInternalName(), - subscription.getVersion(), - subscription.getDescription(), subscription.isGrouped(), subscription.getEndpoint(), - subscription.getRetryPolicy(), subscription.getConsumptionPolicy(), ctx.getIdentityOrDefault() - ).thenApply(SubscriptionResource::from)); + // TODO: Consider splitting these into separate update APIs. + // Note: Updates are currently allowed even if there are no changes to the subscription, which should be avoided. + ctx.handleResponse( + subscriptionService.updateSubscription( + subscription.getSubscriptionInternalName(), + subscription.getVersion(), + subscription.getDescription(), + subscription.isGrouped(), + subscription.getEndpoint(), + subscription.getRetryPolicy(), + subscription.getConsumptionPolicy(), + ctx.getIdentityOrDefault() + ).thenApply(SubscriptionResource::from)); } + /** + * Deletes a subscription. + * + * @param ctx the routing context + */ public void delete(RoutingContext ctx) { - String projectName = ctx.pathParam(PATH_PARAM_PROJECT); - Project subProject = projectService.getCachedProject(projectName); - String deletedBy = ctx.getIdentityOrDefault(); - ctx.handleResponse(subscriptionService.deleteSubscription(getSubscriptionFqn(ctx), subProject, deletedBy)); + ResourceDeletionType deletionType = ctx.queryParam(QUERY_PARAM_DELETION_TYPE).stream() + .map(ResourceDeletionType::fromValue) + .findFirst() + .orElse(ResourceDeletionType.SOFT_DELETE); + + ctx.handleResponse( + subscriptionService.deleteSubscription( + getSubscriptionFqn(ctx), + projectService.getCachedProject(ctx.pathParam(PATH_PARAM_PROJECT)), + ctx.getIdentityOrDefault(), + deletionType + ) + ); } + /** + * Restores a deleted subscription. + * + * @param ctx the routing context + */ + public void restore(RoutingContext ctx) { + ctx.handleResponse(subscriptionService.restoreSubscription(getSubscriptionFqn(ctx), ctx.getIdentityOrDefault()) + .thenApply(SubscriptionResource::from)); + } + + /** + * Starts a subscription. + * + * @param ctx the routing context + */ public void start(RoutingContext ctx) { - String startedBy = ctx.getIdentityOrDefault(); - ctx.handleResponse(subscriptionService.start(getSubscriptionFqn(ctx), startedBy)); + ctx.handleResponse(subscriptionService.start(getSubscriptionFqn(ctx), ctx.getIdentityOrDefault())); } + /** + * Stops a subscription. + * + * @param ctx the routing context + */ public void stop(RoutingContext ctx) { - String stoppedBy = ctx.getIdentityOrDefault(); - ctx.handleResponse(subscriptionService.stop(getSubscriptionFqn(ctx), stoppedBy)); + ctx.handleResponse(subscriptionService.stop(getSubscriptionFqn(ctx), ctx.getIdentityOrDefault())); + } + + /** + * Constructs the fully qualified name of a subscription. + * + * @param ctx the routing context + * + * @return the fully qualified name of the subscription + */ + public static String getSubscriptionFqn(RoutingContext ctx) { + return SubscriptionResource.buildInternalName( + ctx.pathParam(PATH_PARAM_PROJECT), + ctx.pathParam(PATH_PARAM_SUBSCRIPTION) + ); } + /** + * Validates and retrieves the subscription resource from the context. + * + * @param ctx the routing context + * + * @return the validated subscription resource + */ private SubscriptionResource getValidSubscriptionResource(RoutingContext ctx) { - String projectName = ctx.pathParam(PATH_PARAM_PROJECT); SubscriptionResource subscription = ctx.get(CONTEXT_KEY_BODY); - boolean ignoreConstraints = - ctx.queryParam("ignoreConstraints").stream().map(Boolean::parseBoolean).findFirst().orElse(false); - boolean isSuperAdmin = ctx.isSuperUser(); - if (!isSuperAdmin && ignoreConstraints) { - throw new HttpException(HTTP_UNAUTHORIZED, "ignoreConstraints is only allowed for super admins."); - } - - // ensure project name consistent - if (!projectName.equals(subscription.getProject())) { - throw new IllegalArgumentException("Specified Project name is different from Project name in url"); - } - ensureValidatedProperties(subscription.getProperties(), ignoreConstraints); + boolean ignoreConstraints = ignoreConstraints(ctx); + validateSuperUserConstraints(ctx, ignoreConstraints); + validateProjectConsistency(ctx.pathParam(PATH_PARAM_PROJECT), subscription.getProject()); + validateProperties(subscription.getProperties(), ignoreConstraints); validateRetryPolicy(subscription.getRetryPolicy()); + return subscription; } + /** + * Validates super user constraints. + * + * @param ctx the routing context + * @param ignoreConstraints whether to ignore constraints + */ + private void validateSuperUserConstraints(RoutingContext ctx, boolean ignoreConstraints) { + if (ignoreConstraints && !ctx.isSuperUser()) { + throw new HttpException(HTTP_UNAUTHORIZED, "ignoreConstraints is restricted to super admins only."); + } + } + + /** + * Validates the retry policy. + * + * @param retryPolicy the retry policy to validate + */ private void validateRetryPolicy(RetryPolicy retryPolicy) { if (retryPolicy.getRetryAttempts() != NUMBER_OF_RETRIES_ALLOWED) { throw new IllegalArgumentException( @@ -218,33 +347,72 @@ private void validateRetryPolicy(RetryPolicy retryPolicy) { } } + /** + * Validates the consistency of the project name between the URL and the request body. + * + * @param projectPath the project name from the URL + * @param projectInRequest the project name from the request body + */ + private void validateProjectConsistency(String projectPath, String projectInRequest) { + if (!projectPath.equals(projectInRequest)) { + throw new IllegalArgumentException("Project name mismatch between URL and request body."); + } + } + /** + * Retrieves the subscribed topic for a given subscription. + * + * @param subscription the subscription resource + * + * @return the subscribed topic + */ private VaradhiTopic getSubscribedTopic(SubscriptionResource subscription) { - String projectName = subscription.getTopicProject(); - String topicResourceName = subscription.getTopic(); - String topicName = String.join(NAME_SEPARATOR, projectName, topicResourceName); + String topicName = String.join(NAME_SEPARATOR, subscription.getTopicProject(), subscription.getTopic()); return topicService.get(topicName); } + /** + * Determines whether to ignore constraints based on the query parameters. + * + * @param ctx the routing context + * + * @return true if constraints should be ignored, false otherwise + */ + private boolean ignoreConstraints(RoutingContext ctx) { + return ctx.queryParam(QUERY_PARAM_IGNORE_CONSTRAINTS).stream() + .map(Boolean::parseBoolean) + .findFirst() + .orElse(false); + } + + /** + * Validates the properties of a subscription. + * + * @param properties the properties to validate + * @param usePermissible if true, skips full validations (e.g., max/min values) on property values, + * but the validator may still perform minimal validation, such as checking if + * the value is syntactically correct (valid integer, enum, string) + * + * @throws IllegalArgumentException if there are unsupported properties or invalid values + */ + private void validateProperties(Map properties, boolean usePermissible) { + List unsupportedKeys = properties.keySet().stream() + .filter(key -> !propertyValidators.containsKey(key)).toList(); - // usePermissible -- avoids full validations (e.g max/min values) on property value, but validator - // may still perform minimal validation e.g. if value is syntactically correct (valid integer, enum, string) - private void ensureValidatedProperties(Map properties, boolean usePermissible) { - List unsupported = - properties.keySet().stream().filter(key -> !propertyValidators.containsKey(key)).toList(); - if (!unsupported.isEmpty()) { - throw new IllegalArgumentException("Unsupported properties found: " + String.join(", ", unsupported)); + if (!unsupportedKeys.isEmpty()) { + throw new IllegalArgumentException("Unsupported properties: " + String.join(", ", unsupportedKeys)); } + propertyDefaultValueProviders.forEach((propName, defaultValue) -> { if (!properties.containsKey(propName)) { properties.put(propName, defaultValue); } }); - propertyValidators.forEach((propName, propValidator) -> { - String propertyValue = properties.get(propName); - if (!propValidator.isValid(propertyValue, usePermissible)) { - throw new IllegalArgumentException("Invalid value for property " + propName + ": " + propertyValue); + propertyValidators.forEach((key, validator) -> { + String value = properties.get(key); + if (!validator.isValid(value, usePermissible)) { + throw new IllegalArgumentException("Invalid value for property: " + key); } }); } diff --git a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java index f51b0cb3..705da692 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java +++ b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java @@ -1,14 +1,16 @@ package com.flipkart.varadhi.web.v1.admin; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceDeletionType; +import com.flipkart.varadhi.entities.ResourceHierarchy; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.entities.auth.ResourceType; -import com.flipkart.varadhi.utils.VaradhiTopicFactory; -import com.flipkart.varadhi.services.VaradhiTopicService; -import com.flipkart.varadhi.entities.*; import com.flipkart.varadhi.exceptions.DuplicateResourceException; import com.flipkart.varadhi.services.ProjectService; +import com.flipkart.varadhi.services.VaradhiTopicService; +import com.flipkart.varadhi.utils.VaradhiTopicFactory; import com.flipkart.varadhi.web.Extensions.RequestBodyExtension; import com.flipkart.varadhi.web.Extensions.RoutingContextExtension; -import com.flipkart.varadhi.entities.ResourceHierarchy; import com.flipkart.varadhi.web.entities.TopicResource; import com.flipkart.varadhi.web.routes.RouteDefinition; import com.flipkart.varadhi.web.routes.RouteProvider; @@ -17,25 +19,37 @@ import lombok.experimental.ExtensionMethod; import lombok.extern.slf4j.Slf4j; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import static com.flipkart.varadhi.entities.Hierarchies.*; import static com.flipkart.varadhi.Constants.CONTEXT_KEY_BODY; import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_PROJECT; import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_TOPIC; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_DELETION_TYPE; +import static com.flipkart.varadhi.entities.Hierarchies.ProjectHierarchy; +import static com.flipkart.varadhi.entities.Hierarchies.TopicHierarchy; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; import static com.flipkart.varadhi.entities.auth.ResourceAction.*; +/** + * Handler class for managing topics in the Varadhi. + */ @Slf4j @ExtensionMethod({RequestBodyExtension.class, RoutingContextExtension.class}) public class TopicHandlers implements RouteProvider { + private final VaradhiTopicFactory varadhiTopicFactory; private final VaradhiTopicService varadhiTopicService; private final ProjectService projectService; + /** + * Constructs a new TopicHandlers instance. + * + * @param varadhiTopicFactory the factory for creating VaradhiTopic instances + * @param varadhiTopicService the service for managing VaradhiTopic instances + * @param projectService the service for managing projects + */ public TopicHandlers( VaradhiTopicFactory varadhiTopicFactory, VaradhiTopicService varadhiTopicService, @@ -46,6 +60,11 @@ public TopicHandlers( this.projectService = projectService; } + /** + * Returns the list of route definitions for topic management. + * + * @return the list of route definitions + */ @Override public List get() { return new SubRoutes( @@ -56,7 +75,7 @@ public List get() { .build(this::getHierarchies, this::get), RouteDefinition.post("CreateTopic", "") .hasBody() - .bodyParser(this::setTopic) + .bodyParser(this::setRequestBody) .authorize(TOPIC_CREATE) .build(this::getHierarchies, this::create), RouteDefinition.delete("DeleteTopic", "/:topic") @@ -64,84 +83,160 @@ public List get() { .build(this::getHierarchies, this::delete), RouteDefinition.get("ListTopics", "") .authorize(TOPIC_LIST) - .build(this::getHierarchies, this::listTopics) + .build(this::getHierarchies, this::listTopics), + RouteDefinition.post("RestoreTopic", "/:topic/restore") + .authorize(TOPIC_UPDATE) + .build(this::getHierarchies, this::restore) ) ).get(); } - public void setTopic(RoutingContext ctx) { - TopicResource topic = ctx.body().asValidatedPojo(TopicResource.class); - ctx.put(CONTEXT_KEY_BODY, topic); + /** + * Sets the request body in the routing context. + * + * @param ctx the routing context + */ + public void setRequestBody(RoutingContext ctx) { + TopicResource topicResource = ctx.body().asValidatedPojo(TopicResource.class); + ctx.put(CONTEXT_KEY_BODY, topicResource); } + /** + * Retrieves the resource hierarchies for authorization. + * + * @param ctx the routing context + * @param hasBody whether the request has a body + * + * @return the map of resource types to resource hierarchies + */ public Map getHierarchies(RoutingContext ctx, boolean hasBody) { String projectName = ctx.request().getParam(PATH_PARAM_PROJECT); Project project = projectService.getCachedProject(projectName); + if (hasBody) { TopicResource topicResource = ctx.get(CONTEXT_KEY_BODY); return Map.of(ResourceType.TOPIC, new TopicHierarchy(project, topicResource.getName())); } + String topicName = ctx.request().getParam(PATH_PARAM_TOPIC); - if (null == topicName) { + if (topicName == null) { return Map.of(ResourceType.PROJECT, new ProjectHierarchy(project)); } + return Map.of(ResourceType.TOPIC, new TopicHierarchy(project, topicName)); } + /** + * Handles the GET request to retrieve a topic. + * + * @param ctx the routing context + */ public void get(RoutingContext ctx) { - String varadhiTopicName = getVaradhiTopicName(ctx); - VaradhiTopic varadhiTopic = varadhiTopicService.get(varadhiTopicName); - TopicResource topicResource = TopicResource.from(varadhiTopic); - ctx.endApiWithResponse(topicResource); + VaradhiTopic varadhiTopic = varadhiTopicService.get(getVaradhiTopicName(ctx)); + ctx.endApiWithResponse(TopicResource.from(varadhiTopic)); } + /** + * Handles the POST request to create a new topic. + * + * @param ctx the routing context + */ public void create(RoutingContext ctx) { - //TODO:: Consider using Vertx ValidationHandlers to validate the request body. - //TODO:: Consider reverting on failure and ≠≠ kind of semantics for all operations. - + // TODO: Consider using Vertx ValidationHandlers to validate the request body. + // TODO: Consider implementing rollback mechanisms for failure scenarios and ≠≠ kind of semantics for all operations. String projectName = ctx.pathParam(PATH_PARAM_PROJECT); TopicResource topicResource = ctx.get(CONTEXT_KEY_BODY); - if (!projectName.equals(topicResource.getProject())) { - throw new IllegalArgumentException("Specified Project name is different from Project name in url"); - } + + validateProjectName(projectName, topicResource); Project project = projectService.getCachedProject(topicResource.getProject()); - String varadhiTopicName = String.join(NAME_SEPARATOR, projectName, topicResource.getName()); - boolean found = varadhiTopicService.exists(varadhiTopicName); - if (found) { - throw new DuplicateResourceException( - String.format("Specified Topic(%s) already exists.", varadhiTopicName)); + String topicName = buildTopicName(projectName, topicResource.getName()); + + if (varadhiTopicService.exists(topicName)) { + throw new DuplicateResourceException(String.format("Topic '%s' already exists.", topicName)); } - VaradhiTopic vt = varadhiTopicFactory.get(project, topicResource); - varadhiTopicService.create(vt, project); - ctx.endApiWithResponse(TopicResource.from(vt)); + + VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); + varadhiTopicService.create(varadhiTopic, project); + ctx.endApiWithResponse(TopicResource.from(varadhiTopic)); } + /** + * Handles the DELETE request to delete a topic. + * + * @param ctx the routing context + */ public void delete(RoutingContext ctx) { - String varadhiTopicName = getVaradhiTopicName(ctx); - varadhiTopicService.delete(varadhiTopicName); + ResourceDeletionType deletionType = ctx.queryParam(QUERY_PARAM_DELETION_TYPE).stream() + .map(ResourceDeletionType::fromValue) + .findFirst() + .orElse(ResourceDeletionType.SOFT_DELETE); + + varadhiTopicService.delete(getVaradhiTopicName(ctx), deletionType); ctx.endApi(); } + /** + * Handles the POST request to restore a topic. + * + * @param ctx the routing context + */ + public void restore(RoutingContext ctx) { + varadhiTopicService.restore(getVaradhiTopicName(ctx)); + ctx.endApi(); + } + + /** + * Handles the GET request to list topics for a project. + * + * @param ctx the routing context + */ public void listTopics(RoutingContext ctx) { String projectName = ctx.pathParam(PATH_PARAM_PROJECT); - List varadhiTopics = varadhiTopicService.getVaradhiTopics(projectName); - - String projectPrefixOfVaradhiTopic = projectName + NAME_SEPARATOR; - List topicResourceNames = new ArrayList<>(); - varadhiTopics.forEach(varadhiTopic -> { - if (varadhiTopic.startsWith(projectPrefixOfVaradhiTopic)) { - String[] splits = varadhiTopic.split(NAME_SEPARATOR_REGEX); - topicResourceNames.add(splits[1]); - } - } - ); - ctx.endApiWithResponse(topicResourceNames); + List topics = varadhiTopicService.getVaradhiTopics(projectName).stream() + .filter(topic -> topic.startsWith(projectName + NAME_SEPARATOR)) + .map(topic -> topic.split(NAME_SEPARATOR_REGEX)[1]) + .toList(); + + ctx.endApiWithResponse(topics); } + /** + * Retrieves the full topic name from the routing context. + * + * @param ctx the routing context + * + * @return the full topic name + */ private String getVaradhiTopicName(RoutingContext ctx) { String projectName = ctx.pathParam(PATH_PARAM_PROJECT); - String topicResourceName = ctx.pathParam(PATH_PARAM_TOPIC); - return String.join(NAME_SEPARATOR, projectName, topicResourceName); + String topicName = ctx.pathParam(PATH_PARAM_TOPIC); + return buildTopicName(projectName, topicName); + } + + /** + * Validates that the project name in the URL matches the project name in the request body. + * + * @param projectName the project name from the URL + * @param topicResource the topic resource from the request body + * + * @throws IllegalArgumentException if the project names do not match + */ + private void validateProjectName(String projectName, TopicResource topicResource) { + if (!projectName.equals(topicResource.getProject())) { + throw new IllegalArgumentException("Project name in URL and request body do not match."); + } + } + + /** + * Builds the full topic name from the project name and topic name. + * + * @param projectName the project name + * @param topicName the topic name + * + * @return the full topic name + */ + private String buildTopicName(String projectName, String topicName) { + return String.join(NAME_SEPARATOR, projectName, topicName); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java index 24b40767..3568a63b 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java @@ -1,10 +1,11 @@ package com.flipkart.varadhi.web.admin; import com.flipkart.varadhi.Constants; -import com.flipkart.varadhi.utils.VaradhiTopicFactory; -import com.flipkart.varadhi.services.VaradhiTopicService; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.services.ProjectService; +import com.flipkart.varadhi.services.VaradhiTopicService; +import com.flipkart.varadhi.utils.VaradhiTopicFactory; import com.flipkart.varadhi.web.RequestTelemetryConfigurator; import com.flipkart.varadhi.web.SpanProvider; import com.flipkart.varadhi.web.WebTestBase; @@ -13,9 +14,9 @@ import com.flipkart.varadhi.web.v1.admin.TopicHandlers; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.opentelemetry.api.trace.Span; +import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Route; -import io.vertx.core.buffer.Buffer; import io.vertx.ext.web.client.HttpRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -57,7 +58,7 @@ public void PreTest() throws InterruptedException { Route routeCreate = router.post("/projects/:project/topics").handler(bodyHandler) .handler(bodyHandler).handler(ctx -> { - topicHandlers.setTopic(ctx); + topicHandlers.setRequestBody(ctx); ctx.next(); }) .handler(ctx -> { diff --git a/spi/src/main/java/com/flipkart/varadhi/spi/db/MetaStore.java b/spi/src/main/java/com/flipkart/varadhi/spi/db/MetaStore.java index 6eb9bcea..b2217953 100644 --- a/spi/src/main/java/com/flipkart/varadhi/spi/db/MetaStore.java +++ b/spi/src/main/java/com/flipkart/varadhi/spi/db/MetaStore.java @@ -49,6 +49,8 @@ public interface MetaStore { void deleteTopic(String topicName); + void updateTopic(VaradhiTopic topic); + List getAllSubscriptionNames(); List getSubscriptionNames(String projectName); From 5477f1967329860e46e4b4f89f35b5c85e6aedab Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Sat, 25 Jan 2025 02:39:24 +0530 Subject: [PATCH 2/7] Refactor Constants - Add Javadoc comments to the Constants class and its inner classes. - Add private constructors to inner classes to prevent instantiation. --- .../java/com/flipkart/varadhi/Constants.java | 79 ++++++++++++++++++- .../services/ProducerServiceTests.java | 2 +- .../flipkart/varadhi/config/RestOptions.java | 2 +- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/com/flipkart/varadhi/Constants.java b/common/src/main/java/com/flipkart/varadhi/Constants.java index cb29f501..8a3cb300 100644 --- a/common/src/main/java/com/flipkart/varadhi/Constants.java +++ b/common/src/main/java/com/flipkart/varadhi/Constants.java @@ -2,31 +2,66 @@ import com.flipkart.varadhi.entities.TopicCapacityPolicy; +/** + * Constants class contains various constant values used throughout the application. + * This class is not meant to be instantiated. + */ public class Constants { + // Length of the random partition key public static final int RANDOM_PARTITION_KEY_LENGTH = 5; + // Context key for the body public static final String CONTEXT_KEY_BODY = "varadhi.body"; + // Context key for resource hierarchy public static final String CONTEXT_KEY_RESOURCE_HIERARCHY = "varadhi.resourceHierarchy"; + // Context key to check if the user is a super-user public static final String CONTEXT_KEY_IS_SUPER_USER = "varadhi.isSuperUser"; - // TODO: this header is only for testing and x_ convention may cause it to be sent to the destination during consume + // TODO: This header is intended for testing purposes only. The "x_" prefix may result in it being sent to the destination during consumption. + // Header for user ID, intended for testing purposes only public static final String USER_ID_HEADER = "x_user_id"; + // Default topic capacity policy public static final TopicCapacityPolicy DefaultTopicCapacity = new TopicCapacityPolicy(100, 400, 2); - public static String SYSTEM_IDENTITY = "System"; + // System identity constant + public static final String SYSTEM_IDENTITY = "System"; + private Constants() { + // Private constructor to prevent instantiation + } + + /** + * PathParams class contains constants for various path parameters. + * This class is not meant to be instantiated. + */ public static class PathParams { public static final String PATH_PARAM_ORG = "org"; public static final String PATH_PARAM_TEAM = "team"; public static final String PATH_PARAM_PROJECT = "project"; public static final String PATH_PARAM_TOPIC = "topic"; public static final String PATH_PARAM_SUBSCRIPTION = "subscription"; + + private PathParams() { + // Private constructor to prevent instantiation + } } + /** + * QueryParams class contains constants for various query parameters. + * This class is not meant to be instantiated. + */ public static class QueryParams { public static final String QUERY_PARAM_DELETION_TYPE = "deletionType"; public static final String QUERY_PARAM_IGNORE_CONSTRAINTS = "ignoreConstraints"; + + private QueryParams() { + // Private constructor to prevent instantiation + } } + /** + * Tags class contains constants for various tags used in the application. + * This class is not meant to be instantiated. + */ public static class Tags { public static final String TAG_REGION = "region"; public static final String TAG_ORG = "org"; @@ -35,25 +70,58 @@ public static class Tags { public static final String TAG_TOPIC = "topic"; public static final String TAG_SUBSCRIPTION = "subscription"; public static final String TAG_IDENTITY = "identity"; - public static final String TAG_REMOTEHOST = "host"; + public static final String TAG_REMOTE_HOST = "host"; public static final String TAG_PRODUCE_RESULT = "result"; public static final String TAG_VALUE_RESULT_SUCCESS = "success"; public static final String TAG_VALUE_RESULT_FAILED = "failed"; + + private Tags() { + // Private constructor to prevent instantiation + } } + /** + * HttpCodes class contains constants for various HTTP status codes. + * This class is not meant to be instantiated. + */ public static class HttpCodes { public static final int HTTP_RATE_LIMITED = 429; public static final int HTTP_UNPROCESSABLE_ENTITY = 422; + + private HttpCodes() { + // Private constructor to prevent instantiation + } } + /** + * Meters class contains constants for various meters used in the application. + * This class is not meant to be instantiated. + */ public static class Meters { + + /** + * Produce class contains constants for produce meters. + * This class is not meant to be instantiated. + */ public static class Produce { public static final String BYTES_METER = "produce.bytes"; public static final String LATENCY_METER = "produce.latency"; + + private Produce() { + // Private constructor to prevent instantiation + } + } + + private Meters() { + // Private constructor to prevent instantiation } } - public static class REST_DEFAULTS { + /** + * RestDefaults class contains default values for REST configurations. + * This class is not meant to be instantiated. + */ + public static class RestDefaults { public static final int PAYLOAD_SIZE_MAX = 5 * 1024 * 1024; public static final int HEADERS_ALLOWED_MAX = 10; public static final int HEADER_NAME_SIZE_MAX = 64; @@ -62,5 +130,8 @@ public static class REST_DEFAULTS { public static final String DEFAULT_TEAM = "public"; public static final String DEFAULT_PROJECT = "public"; + private RestDefaults() { + // Private constructor to prevent instantiation + } } } diff --git a/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java b/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java index 3b5a9eb7..13fd2ab4 100644 --- a/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java +++ b/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java @@ -284,7 +284,7 @@ public ProducerMetricsEmitter getMetricEmitter(String topic, Project project, St produceAttributes.put(TAG_PROJECT, project.getName()); produceAttributes.put(TAG_TOPIC, topic); produceAttributes.put(TAG_IDENTITY, ANONYMOUS_IDENTITY); - produceAttributes.put(TAG_REMOTEHOST, "remoteHost"); + produceAttributes.put(TAG_REMOTE_HOST, "remoteHost"); return new ProducerMetricsEmitterImpl(meterRegistry, 0, produceAttributes); } diff --git a/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java b/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java index 7bb7401b..ff02a8c7 100644 --- a/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java +++ b/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java @@ -6,7 +6,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; -import static com.flipkart.varadhi.Constants.REST_DEFAULTS.*; +import static com.flipkart.varadhi.Constants.RestDefaults.*; @Data public class RestOptions { From 2b28458a1fdba258035fc0ba858afcfcf40cfdd1 Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Mon, 27 Jan 2025 01:08:00 +0530 Subject: [PATCH 3/7] Add unit tests for VaradhiTopic, TopicHandlers, and VaradhiTopicService - Added unit tests for VaradhiTopicService, covering topic creation, deletion (hard and soft), restoration, and existence checks. - Implemented tests for VaradhiTopic entity to verify its creation and status updates. - Added tests for VaradhiTopicHandler to ensure correct handling of topic related requests. - Refactored existing tests to reduce code duplication by extracting common setup logic into helper methods. - Ensured comprehensive coverage of various edge cases, such as filtering out inactive topics and handling meta store failures. --- .../varadhi/entities/VaradhiTopicTest.java | 130 ++++++- .../varadhi/services/VaradhiTopicService.java | 2 +- .../services/SubscriptionServiceTest.java | 3 +- .../services/VaradhiTopicServiceTest.java | 351 +++++++++++++----- .../web/admin/SubscriptionHandlersTest.java | 4 +- .../varadhi/web/admin/TopicHandlersTest.java | 266 +++++++++---- 6 files changed, 570 insertions(+), 186 deletions(-) diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java index ee903278..979eeca6 100644 --- a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java +++ b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java @@ -3,36 +3,130 @@ import lombok.EqualsAndHashCode; import org.junit.jupiter.api.Test; -import static com.flipkart.varadhi.entities.VersionedEntity.INITIAL_VERSION; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class VaradhiTopicTest { - private static final String projectName = "project1"; - private static final String topicName = "topic1"; - private static final TopicCapacityPolicy topicCapacity = new TopicCapacityPolicy(100, 400, 2); + private static final String PROJECT_NAME = "project1"; + private static final String TOPIC_NAME = "topic1"; + private static final TopicCapacityPolicy TOPIC_CAPACITY = new TopicCapacityPolicy(100, 400, 2); + + @EqualsAndHashCode(callSuper = true) + public static class DummyStorageTopic extends StorageTopic { + public DummyStorageTopic(String name, int version) { + super(name, version, TOPIC_CAPACITY); + } + } + + private VaradhiTopic createDefaultVaradhiTopic(boolean grouped) { + return VaradhiTopic.of(PROJECT_NAME, TOPIC_NAME, grouped, TOPIC_CAPACITY); + } @Test - void buildTopicName() { - String expected = String.join(".", projectName, topicName); - String actual = VaradhiTopic.buildTopicName(projectName, topicName); - assertEquals(expected, actual); + void of_WithValidInputs_CreatesVaradhiTopic() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + + assertAll( + () -> assertEquals("project1.topic1", varadhiTopic.getName(), "Topic name mismatch"), + () -> assertEquals(VaradhiTopic.INITIAL_VERSION, varadhiTopic.getVersion(), "Version mismatch"), + () -> assertFalse(varadhiTopic.isGrouped(), "Grouped flag mismatch"), + () -> assertEquals(TOPIC_CAPACITY, varadhiTopic.getCapacity(), "Capacity mismatch"), + () -> assertTrue(varadhiTopic.isActive(), "Active status mismatch") + ); } @Test - void addAndGetInternalTopic() { - VaradhiTopic varadhiTopic = VaradhiTopic.of(projectName, topicName, false, topicCapacity); - StorageTopic st = new DummyStorageTopic(varadhiTopic.getName(), 0); - varadhiTopic.addInternalTopic("region1", InternalCompositeTopic.of(st)); - assertEquals(st.getName(), varadhiTopic.getProduceTopicForRegion("region1").getTopicToProduce().getName()); + void of_WithGroupedFlag_CreatesGroupedVaradhiTopic() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(true); + + assertAll( + () -> assertEquals("project1.topic1", varadhiTopic.getName(), "Topic name mismatch"), + () -> assertTrue(varadhiTopic.isGrouped(), "Grouped flag mismatch"), + () -> assertEquals(TOPIC_CAPACITY, varadhiTopic.getCapacity(), "Capacity mismatch"), + () -> assertTrue(varadhiTopic.isActive(), "Active status mismatch") + ); } - @EqualsAndHashCode(callSuper = true) - public static class DummyStorageTopic extends StorageTopic { - public DummyStorageTopic(String name, int version) { - super(name, version, topicCapacity); + @Test + void buildTopicName_ReturnsExpectedFormat() { + String expected = "project1.topic1"; + String actual = VaradhiTopic.buildTopicName(PROJECT_NAME, TOPIC_NAME); - } + assertEquals(expected, actual, "Topic name format mismatch"); + } + + @Test + void addInternalTopic_AddsTopicSuccessfully() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + StorageTopic storageTopic = new DummyStorageTopic(varadhiTopic.getName(), 0); + + varadhiTopic.addInternalTopic("region1", InternalCompositeTopic.of(storageTopic)); + + assertEquals( + storageTopic.getName(), + varadhiTopic.getProduceTopicForRegion("region1").getTopicToProduce().getName(), + "Internal topic addition failed" + ); + } + + @Test + void getProjectName_ReturnsCorrectProjectName() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + + assertEquals(PROJECT_NAME, varadhiTopic.getProjectName(), "Project name mismatch"); + } + + @Test + void getProduceTopicForRegion_WithValidRegion_ReturnsCorrectTopic() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + StorageTopic storageTopic = new DummyStorageTopic(varadhiTopic.getName(), 0); + + varadhiTopic.addInternalTopic("region1", InternalCompositeTopic.of(storageTopic)); + + assertAll( + () -> assertNotNull(varadhiTopic.getProduceTopicForRegion("region1"), "Region topic not found"), + () -> assertEquals( + storageTopic.getName(), + varadhiTopic.getProduceTopicForRegion("region1").getTopicToProduce().getName(), + "Region topic name mismatch" + ) + ); + } + + @Test + void getProduceTopicForRegion_WithUnknownRegion_ReturnsNull() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + + assertNull(varadhiTopic.getProduceTopicForRegion("unknownRegion"), "Unknown region should return null"); + } + + @Test + void updateStatus_ChangesActiveState() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + + varadhiTopic.updateStatus(VaradhiTopic.Status.INACTIVE); + assertFalse(varadhiTopic.isActive(), "Inactive status update failed"); + + varadhiTopic.updateStatus(VaradhiTopic.Status.ACTIVE); + assertTrue(varadhiTopic.isActive(), "Active status update failed"); + } + + @Test + void updateStatus_WithNull_ThrowsIllegalArgumentException() { + VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> varadhiTopic.updateStatus(null), + "Null status should throw IllegalArgumentException" + ); + + assertEquals("Status cannot be null", exception.getMessage(), "Exception message mismatch"); } } diff --git a/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java b/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java index d8ed5155..b8e72ea0 100644 --- a/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java +++ b/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java @@ -88,7 +88,7 @@ public void delete(String topicName, ResourceDeletionType deletionType) { /** * Handles the soft deletion of a Varadhi topic. * - * @param varadhiTopic the Varadhi topic to soft delete + * @param varadhiTopic the Varadhi topic to soft-delete */ public void handleSoftDelete(VaradhiTopic varadhiTopic) { log.info("Soft deleting Varadhi topic: {}", varadhiTopic.getName()); diff --git a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java index 34d2d7dd..b685e3ef 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java @@ -347,7 +347,8 @@ void deleteSubscriptionRemovesSubscription(VertxTestContext ctx) { CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); doReturn(status).when(controllerRestApi).getSubscriptionState(name, requestedBy); - Future.fromCompletionStage(subscriptionService.deleteSubscription(name, o1t1p1, requestedBy)) + Future.fromCompletionStage( + subscriptionService.deleteSubscription(name, o1t1p1, requestedBy, ResourceDeletionType.HARD_DELETE)) .onComplete(ctx.succeeding( v -> { Exception exception = assertThrows( diff --git a/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java index a49c3aff..8c7f7865 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java @@ -1,7 +1,15 @@ package com.flipkart.varadhi.services; import com.flipkart.varadhi.Constants; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.InternalQueueCategory; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceDeletionType; +import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; +import com.flipkart.varadhi.exceptions.InvalidOperationForResourceException; +import com.flipkart.varadhi.exceptions.ResourceNotFoundException; import com.flipkart.varadhi.exceptions.VaradhiException; import com.flipkart.varadhi.pulsar.entities.PulsarStorageTopic; import com.flipkart.varadhi.spi.db.MetaStore; @@ -12,159 +20,312 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import java.util.List; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -public class VaradhiTopicServiceTest { +class VaradhiTopicServiceTest { - private final String region = "local"; - private final String topicName = "testTopic"; - StorageTopicFactory storageTopicFactory; - private VaradhiTopicFactory varadhiTopicFactory; + private static final String REGION = "local"; + private static final String TOPIC_NAME = "testTopic"; + private static final TopicCapacityPolicy DEFAULT_CAPACITY_POLICY = Constants.DefaultTopicCapacity; + + @Mock private StorageTopicService storageTopicService; + + @Mock private MetaStore metaStore; + + @Mock + private StorageTopicFactory storageTopicFactory; + + @InjectMocks private VaradhiTopicService varadhiTopicService; + + private VaradhiTopicFactory varadhiTopicFactory; private Project project; private String vTopicName; - private TopicCapacityPolicy capacityPolicy; - + private PulsarStorageTopic pulsarStorageTopic; @BeforeEach public void setUp() { - storageTopicService = mock(StorageTopicService.class); - metaStore = mock(MetaStore.class); - storageTopicFactory = mock(StorageTopicFactory.class); - varadhiTopicFactory = spy(new VaradhiTopicFactory(storageTopicFactory, region, Constants.DefaultTopicCapacity)); - varadhiTopicService = new VaradhiTopicService(storageTopicService, metaStore); + MockitoAnnotations.openMocks(this); + varadhiTopicFactory = spy(new VaradhiTopicFactory(storageTopicFactory, REGION, DEFAULT_CAPACITY_POLICY)); project = Project.of("default", "", "public", "public"); - vTopicName = String.format("%s.%s", project.getName(), topicName); - String pTopicName = - String.format("persistent://%s/%s/%s", project.getOrg(), project.getName(), vTopicName); - capacityPolicy = Constants.DefaultTopicCapacity; - PulsarStorageTopic pTopic = PulsarStorageTopic.of(pTopicName, 1, capacityPolicy); - Mockito.doReturn(pTopic).when(storageTopicFactory) - .getTopic(vTopicName, project, capacityPolicy, InternalQueueCategory.MAIN); + vTopicName = String.format("%s.%s", project.getName(), TOPIC_NAME); + String pTopicName = String.format("persistent://%s/%s/%s", project.getOrg(), project.getName(), vTopicName); + + pulsarStorageTopic = PulsarStorageTopic.of(pTopicName, 1, DEFAULT_CAPACITY_POLICY); + doReturn(pulsarStorageTopic).when(storageTopicFactory) + .getTopic(vTopicName, project, DEFAULT_CAPACITY_POLICY, InternalQueueCategory.MAIN); } @Test - public void createVaradhiTopic() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); + void createVaradhiTopic_SuccessfulCreation() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); varadhiTopicService.create(varadhiTopic, project); + verify(metaStore, times(1)).createTopic(varadhiTopic); - StorageTopic st = varadhiTopic.getProduceTopicForRegion(region).getTopicToProduce(); - verify(storageTopicService, times(1)).create(st, project); - verify(storageTopicFactory, times(1)).getTopic(vTopicName, project, capacityPolicy, InternalQueueCategory.MAIN); - } - - @Test - public void createVaradhiTopicWhenMetaStoreFails() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - StorageTopic st = varadhiTopic.getProduceTopicForRegion(region).getTopicToProduce(); - doThrow(new VaradhiException("Some error")).when(metaStore).createTopic(varadhiTopic); - Exception exception = - Assertions.assertThrows(VaradhiException.class, () -> varadhiTopicService.create( - varadhiTopic, - project - )); + verify(storageTopicService, times(1)).create(pulsarStorageTopic, project); + verify(storageTopicFactory, times(1)) + .getTopic(vTopicName, project, DEFAULT_CAPACITY_POLICY, InternalQueueCategory.MAIN); + } + + @Test + void createVaradhiTopic_MetaStoreFailure_ThrowsException() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + doThrow(new VaradhiException("MetaStore error")).when(metaStore).createTopic(varadhiTopic); + + Exception exception = assertThrows( + VaradhiException.class, () -> + varadhiTopicService.create(varadhiTopic, project) + ); + verify(metaStore, times(1)).createTopic(varadhiTopic); - verify(storageTopicService, times(1)).create(st, project); - Assertions.assertEquals(exception.getClass(), VaradhiException.class); + verify(storageTopicService, times(1)).create(pulsarStorageTopic, project); + assertEquals(VaradhiException.class, exception.getClass()); + assertEquals("MetaStore error", exception.getMessage()); } @Test - public void createVaradhiTopicWhenStorageTopicServiceFails() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - StorageTopic st = varadhiTopic.getProduceTopicForRegion(region).getTopicToProduce(); - doThrow(new VaradhiException("Some error")).when(storageTopicService).create(st, project); - Exception exception = - Assertions.assertThrows(VaradhiException.class, () -> varadhiTopicService.create( - varadhiTopic, - project - )); - verify(metaStore, times(0)).createTopic(varadhiTopic); - verify(storageTopicService, times(1)).create(st, project); - Assertions.assertEquals(exception.getClass(), VaradhiException.class); + void createVaradhiTopic_StorageTopicServiceFailure_ThrowsException() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + doThrow(new VaradhiException("StorageTopicService error")).when(storageTopicService) + .create(pulsarStorageTopic, project); + + Exception exception = assertThrows( + VaradhiException.class, () -> + varadhiTopicService.create(varadhiTopic, project) + ); + + verify(metaStore, never()).createTopic(varadhiTopic); + verify(storageTopicService, times(1)).create(pulsarStorageTopic, project); + assertEquals(VaradhiException.class, exception.getClass()); + assertEquals("StorageTopicService error", exception.getMessage()); } @Test - public void deleteVaradhiTopicSuccessfully() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - StorageTopic st = varadhiTopic.getProduceTopicForRegion(region).getTopicToProduce(); - when(storageTopicService.exists(st.getName())).thenReturn(true); - when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); - when(metaStore.getProject(project.getName())).thenReturn(project); + void deleteVaradhiTopic_SuccessfulHardDelete() { + VaradhiTopic varadhiTopic = mockDeleteSetup(); - varadhiTopicService.delete(varadhiTopic.getName()); + varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE); - verify(storageTopicService, times(1)).delete(st.getName(), project); + verify(storageTopicService, times(1)).delete(pulsarStorageTopic.getName(), project); verify(metaStore, times(1)).deleteTopic(varadhiTopic.getName()); } @Test - public void deleteVaradhiTopicWhenStorageTopicDoesNotExist() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - StorageTopic st = varadhiTopic.getProduceTopicForRegion(region).getTopicToProduce(); - when(storageTopicService.exists(st.getName())).thenReturn(false); + void deleteVaradhiTopic_StorageTopicDoesNotExist_SuccessfulHardDelete() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + when(storageTopicService.exists(pulsarStorageTopic.getName())).thenReturn(false); when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); when(metaStore.getProject(project.getName())).thenReturn(project); - varadhiTopicService.delete(varadhiTopic.getName()); - verify(storageTopicService, times(1)).delete(st.getName(), project); + + varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE); + + verify(storageTopicService, times(1)).delete(pulsarStorageTopic.getName(), project); verify(metaStore, times(1)).deleteTopic(varadhiTopic.getName()); } @Test - public void deleteVaradhiTopicWhenMetaStoreFails() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - StorageTopic st = varadhiTopic.getProduceTopicForRegion(region).getTopicToProduce(); - when(storageTopicService.exists(st.getName())).thenReturn(true); - when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); - when(metaStore.getProject(project.getName())).thenReturn(project); - doThrow(new VaradhiException("Some error")).when(metaStore).deleteTopic(varadhiTopic.getName()); + void deleteVaradhiTopic_MetaStoreFailure_ThrowsException() { + VaradhiTopic varadhiTopic = mockDeleteSetup(); + doThrow(new VaradhiException("MetaStore deletion failed")).when(metaStore).deleteTopic(varadhiTopic.getName()); - Exception exception = Assertions.assertThrows( + Exception exception = assertThrows( VaradhiException.class, - () -> varadhiTopicService.delete(varadhiTopic.getName()) + () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE) ); - verify(storageTopicService, times(1)).delete(st.getName(), project); + verify(storageTopicService, times(1)).delete(pulsarStorageTopic.getName(), project); verify(metaStore, times(1)).deleteTopic(varadhiTopic.getName()); - Assertions.assertEquals(exception.getClass(), VaradhiException.class); + assertEquals(VaradhiException.class, exception.getClass()); + assertEquals("MetaStore deletion failed", exception.getMessage()); + } + + @Test + void deleteVaradhiTopic_TopicInUse_ThrowsException() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + VaradhiSubscription subscription = mock(VaradhiSubscription.class); + + when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + when(metaStore.getAllSubscriptionNames()).thenReturn(List.of("subscription1")); + when(metaStore.getSubscription("subscription1")).thenReturn(subscription); + when(subscription.getTopic()).thenReturn(varadhiTopic.getName()); + + Exception exception = assertThrows( + InvalidOperationForResourceException.class, + () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE) + ); + + verify(metaStore, never()).deleteTopic(varadhiTopic.getName()); + assertEquals(InvalidOperationForResourceException.class, exception.getClass()); + } + + @Test + void softDeleteVaradhiTopic_MetaStoreSuccess_UpdatesTopicStatus() { + VaradhiTopic varadhiTopic = mockDeleteSetup(); + + varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.SOFT_DELETE); + + verify(metaStore, times(1)).updateTopic(varadhiTopic); + assertFalse(varadhiTopic.isActive()); + } + + @Test + void softDeleteVaradhiTopic_MetaStoreFailure_ThrowsException() { + VaradhiTopic varadhiTopic = mockDeleteSetup(); + doThrow(new VaradhiException("MetaStore update failed")).when(metaStore).updateTopic(varadhiTopic); + + Exception exception = assertThrows( + VaradhiException.class, + () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.SOFT_DELETE) + ); + + verify(metaStore, times(1)).updateTopic(varadhiTopic); + assertEquals(VaradhiException.class, exception.getClass()); + assertEquals("MetaStore update failed", exception.getMessage()); } @Test - public void checkVaradhiTopicExistsWhenTopicExists() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); + void restoreVaradhiTopic_SuccessfulRestore() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + varadhiTopic.updateStatus(VaradhiTopic.Status.INACTIVE); + when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + + varadhiTopicService.restore(varadhiTopic.getName()); + + verify(metaStore, times(1)).updateTopic(varadhiTopic); + Assertions.assertTrue(varadhiTopic.isActive()); + } + + @Test + void restoreVaradhiTopic_AlreadyActive_ThrowsException() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + + Exception exception = assertThrows( + InvalidOperationForResourceException.class, + () -> varadhiTopicService.restore(varadhiTopic.getName()) + ); + + verify(metaStore, never()).updateTopic(varadhiTopic); + assertEquals(InvalidOperationForResourceException.class, exception.getClass()); + assertEquals("Topic default.testTopic is not deleted.", exception.getMessage()); + } + + @Test + void checkVaradhiTopicExists_TopicExists_ReturnsTrue() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); when(metaStore.checkTopicExists(varadhiTopic.getName())).thenReturn(true); boolean exists = varadhiTopicService.exists(varadhiTopic.getName()); - Assertions.assertTrue(exists); + assertTrue(exists); verify(metaStore, times(1)).checkTopicExists(varadhiTopic.getName()); } @Test - public void checkVaradhiTopicExistsWhenTopicDoesNotExist() { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); + void checkVaradhiTopicExists_TopicDoesNotExist_ReturnsFalse() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); when(metaStore.checkTopicExists(varadhiTopic.getName())).thenReturn(false); boolean exists = varadhiTopicService.exists(varadhiTopic.getName()); - Assertions.assertFalse(exists); + assertFalse(exists); verify(metaStore, times(1)).checkTopicExists(varadhiTopic.getName()); } - private TopicResource getTopicResource(String topicName, Project project) { - return TopicResource.grouped(topicName, project.getName(), capacityPolicy); + @Test + void getVaradhiTopic_TopicExists_ReturnsTopic() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + + VaradhiTopic retrievedTopic = varadhiTopicService.get(varadhiTopic.getName()); + + assertNotNull(retrievedTopic); + assertEquals(varadhiTopic, retrievedTopic); } -} + @Test + void getVaradhiTopic_TopicDoesNotExist_ThrowsException() { + String nonExistentTopicName = "nonExistentTopic"; + when(metaStore.getTopic(nonExistentTopicName)).thenThrow(new ResourceNotFoundException("Topic not found")); + + Exception exception = assertThrows( + ResourceNotFoundException.class, + () -> varadhiTopicService.get(nonExistentTopicName) + ); + + assertEquals(ResourceNotFoundException.class, exception.getClass()); + assertEquals("Topic not found", exception.getMessage()); + } + + @Test + void getVaradhiTopicsForProject_ValidProject_ReturnsActiveTopics() { + String projectName = project.getName(); + List topicNames = List.of("topic1", "topic2"); + List topicStatuses = List.of(true, true); + + setupMockTopics(projectName, topicNames, topicStatuses); + + List activeTopics = varadhiTopicService.getVaradhiTopics(projectName); + + assertNotNull(activeTopics); + assertEquals(2, activeTopics.size()); + assertEquals(topicNames, activeTopics); + } + + @Test + void getVaradhiTopicsForProject_MultipleStatuses_FiltersInactiveTopics() { + String projectName = project.getName(); + List topicNames = List.of("topic1", "topic2", "topic3"); + List topicStatuses = List.of(true, false, true); + + setupMockTopics(projectName, topicNames, topicStatuses); + + List activeTopics = varadhiTopicService.getVaradhiTopics(projectName); + + assertNotNull(activeTopics); + assertEquals(2, activeTopics.size()); + assertEquals(List.of("topic1", "topic3"), activeTopics); + } + + private VaradhiTopic createVaradhiTopicMock() { + TopicResource topicResource = TopicResource.grouped(TOPIC_NAME, project.getName(), DEFAULT_CAPACITY_POLICY); + return varadhiTopicFactory.get(project, topicResource); + } + + private VaradhiTopic mockDeleteSetup() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + when(storageTopicService.exists(pulsarStorageTopic.getName())).thenReturn(true); + when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + when(metaStore.getProject(project.getName())).thenReturn(project); + return varadhiTopic; + } + + private void setupMockTopics(String projectName, List topicNames, List topicStatuses) { + when(metaStore.getTopicNames(projectName)).thenReturn(topicNames); + for (int i = 0; i < topicNames.size(); i++) { + VaradhiTopic topic = mock(VaradhiTopic.class); + when(metaStore.getTopic(topicNames.get(i))).thenReturn(topic); + when(topic.isActive()).thenReturn(topicStatuses.get(i)); + } + } +} diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java index cd588111..ffda52a1 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java @@ -163,12 +163,12 @@ void testSubscriptionDelete() throws InterruptedException { ArgumentCaptor captorSubName = ArgumentCaptor.forClass(String.class); ArgumentCaptor captorProject = ArgumentCaptor.forClass(Project.class); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) - .deleteSubscription(captorSubName.capture(), captorProject.capture(), any()); + .deleteSubscription(captorSubName.capture(), captorProject.capture(), any(), any()); sendRequestWithoutBody(request, null); assertEquals(captorSubName.getValue(), resource.getSubscriptionInternalName()); assertEquals(captorProject.getValue().getName(), project.getName()); - verify(subscriptionService, times(1)).deleteSubscription(any(), any(), any()); + verify(subscriptionService, times(1)).deleteSubscription(any(), any(), any(), any()); } @Test diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java index 3568a63b..7ca64181 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java @@ -2,10 +2,13 @@ import com.flipkart.varadhi.Constants; import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.services.ProjectService; import com.flipkart.varadhi.services.VaradhiTopicService; +import com.flipkart.varadhi.utils.JsonMapper; import com.flipkart.varadhi.utils.VaradhiTopicFactory; +import com.flipkart.varadhi.web.ErrorResponse; import com.flipkart.varadhi.web.RequestTelemetryConfigurator; import com.flipkart.varadhi.web.SpanProvider; import com.flipkart.varadhi.web.WebTestBase; @@ -16,48 +19,72 @@ import io.opentelemetry.api.trace.Span; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.Route; import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.HttpResponse; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static com.flipkart.varadhi.web.RequestTelemetryConfigurator.REQUEST_SPAN_NAME; -import static org.mockito.Mockito.*; - -public class TopicHandlersTest extends WebTestBase { - private final String topicName = "topic1"; - private final String team1 = "team1"; - private final String org1 = "org1"; - private final Project project = Project.of("project1", "", team1, org1); - TopicHandlers topicHandlers; - VaradhiTopicService varadhiTopicService; - VaradhiTopicFactory varadhiTopicFactory; - ProjectService projectService; - RequestTelemetryConfigurator requestTelemetryConfigurator; - SpanProvider spanProvider; - Span span; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; + +class TopicHandlersTest extends WebTestBase { + + private static final String TOPIC_NAME = "topic1"; + private static final String TEAM_NAME = "team1"; + private static final String ORG_NAME = "org1"; + private static final String DEFAULT_PROJECT_NAME = "project1"; + + private final Project project = Project.of(DEFAULT_PROJECT_NAME, "", TEAM_NAME, ORG_NAME); + + @InjectMocks + private TopicHandlers topicHandlers; + + @Mock + private VaradhiTopicService varadhiTopicService; + + @Mock + private VaradhiTopicFactory varadhiTopicFactory; + + @Mock + private ProjectService projectService; + + @Mock + private SpanProvider spanProvider; + + @Mock + private Span span; + + private RequestTelemetryConfigurator requestTelemetryConfigurator; @BeforeEach public void PreTest() throws InterruptedException { + MockitoAnnotations.openMocks(this); super.setUp(); - spanProvider = mock(SpanProvider.class); - span = mock(Span.class); doReturn(span).when(spanProvider).addSpan(REQUEST_SPAN_NAME); requestTelemetryConfigurator = new RequestTelemetryConfigurator(spanProvider, new SimpleMeterRegistry()); - - varadhiTopicService = mock(VaradhiTopicService.class); - varadhiTopicFactory = mock(VaradhiTopicFactory.class); - projectService = mock(ProjectService.class); - topicHandlers = new TopicHandlers(varadhiTopicFactory, varadhiTopicService, projectService); doReturn(project).when(projectService).getCachedProject(project.getName()); - Route routeCreate = router.post("/projects/:project/topics").handler(bodyHandler) - .handler(bodyHandler).handler(ctx -> { + setupRoutes(); + } + + private void setupRoutes() { + router.post("/projects/:project/topics") + .handler(bodyHandler) + .handler(ctx -> { topicHandlers.setRequestBody(ctx); ctx.next(); }) @@ -66,79 +93,180 @@ public void PreTest() throws InterruptedException { ctx.next(); }) .handler(wrapBlocking(topicHandlers::create)); - setupFailureHandler(routeCreate); - Route routeGet = router.get("/projects/:project/topics/:topic").handler(wrapBlocking(topicHandlers::get)); - setupFailureHandler(routeGet); - Route routeListAll = router.get("/projects/:project/topics").handler(bodyHandler) + + router.get("/projects/:project/topics/:topic") + .handler(wrapBlocking(topicHandlers::get)); + + router.get("/projects/:project/topics") + .handler(bodyHandler) .handler(wrapBlocking(topicHandlers::listTopics)); - setupFailureHandler(routeListAll); - Route routeDelete = - router.delete("/projects/:project/topics/:topic").handler(wrapBlocking(topicHandlers::delete)); - setupFailureHandler(routeDelete); + + router.delete("/projects/:project/topics/:topic") + .handler(wrapBlocking(topicHandlers::delete)); + + router.post("/projects/:project/topics/:topic/restore") + .handler(wrapBlocking(topicHandlers::restore)); + + setupFailureHandlers(); + } + + private void setupFailureHandlers() { + router.getRoutes().forEach(this::setupFailureHandler); } @AfterEach - public void PostTest() throws InterruptedException { + void PostTest() throws InterruptedException { super.tearDown(); } @Test - public void testTopicCreate() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getTopicsUrl(project)); - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic vt = topicResource.toVaradhiTopic(); - doReturn(vt).when(varadhiTopicFactory).get(project, topicResource); - TopicResource t1Created = sendRequestWithBody(request, topicResource, TopicResource.class); - Assertions.assertEquals(topicResource.getProject(), t1Created.getProject()); - verify(spanProvider, times(1)).addSpan(eq(REQUEST_SPAN_NAME)); + void createTopic_WithValidRequest_ShouldCreateTopicSuccessfully() throws InterruptedException { + TopicResource topicResource = getTopicResource(project); + VaradhiTopic varadhiTopic = topicResource.toVaradhiTopic(); + + doReturn(varadhiTopic).when(varadhiTopicFactory).get(project, topicResource); + + TopicResource createdTopic = sendRequestWithBody( + createRequest(HttpMethod.POST, getTopicsUrl(project)), + topicResource, + TopicResource.class + ); + + assertEquals(topicResource.getProject(), createdTopic.getProject()); + verify(spanProvider).addSpan(REQUEST_SPAN_NAME); + verify(varadhiTopicService).create(any(), eq(project)); + } + + @Test + void createTopic_WithDuplicateResource_ShouldReturnConflict() throws InterruptedException { + TopicResource topicResource = getTopicResource(project); + + doReturn(true).when(varadhiTopicService).exists(anyString()); + + HttpResponse response = sendRequest( + createRequest(HttpMethod.POST, getTopicsUrl(project)), + JsonMapper.jsonSerialize(topicResource).getBytes() + ); + + assertEquals(409, response.statusCode()); + assertErrorResponse(response, "Topic 'project1.topic1' already exists."); + } + + @Test + void createTopic_WithMismatchedProjectName_ShouldReturnBadRequest() throws InterruptedException { + TopicResource topicResource = + getTopicResource(Project.of("differentProject", "", TEAM_NAME, ORG_NAME)); + + HttpResponse response = sendRequest( + createRequest(HttpMethod.POST, getTopicsUrl(project)), + JsonMapper.jsonSerialize(topicResource).getBytes() + ); + + assertEquals(400, response.statusCode()); + assertErrorResponse(response, "Project name in URL and request body do not match."); + } + + @Test + void getTopic_WithValidRequest_ShouldReturnTopicSuccessfully() throws InterruptedException { + TopicResource topicResource = getTopicResource(project); + VaradhiTopic varadhiTopic = topicResource.toVaradhiTopic(); + String varadhiTopicName = String.join(".", project.getName(), TOPIC_NAME); + + doReturn(varadhiTopic).when(varadhiTopicService).get(varadhiTopicName); + + TopicResource retrievedTopic = sendRequestWithoutBody( + createRequest(HttpMethod.GET, getTopicUrl(project)), + TopicResource.class + ); + + assertEquals(topicResource.getProject(), retrievedTopic.getProject()); } + @Test + void listTopics_WithTopicsAvailable_ShouldReturnAllTopics() throws InterruptedException { + List topics = List.of(String.join(".", project.getName(), TOPIC_NAME)); + + doReturn(topics).when(varadhiTopicService).getVaradhiTopics(project.getName()); + + List retrievedTopics = sendRequestWithoutBody( + createRequest(HttpMethod.GET, getTopicsUrl(project)), + List.class + ); + + assertEquals(topics.size(), retrievedTopics.size()); + } @Test - public void testTopicGet() throws InterruptedException { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic t1 = topicResource.toVaradhiTopic(); - String varadhiTopicName = String.join(".", project.getName(), topicName); - doReturn(t1).when(varadhiTopicService).get(varadhiTopicName); + void listTopics_WithNoTopicsAvailable_ShouldReturnEmptyList() throws InterruptedException { + doReturn(Collections.emptyList()).when(varadhiTopicService).getVaradhiTopics(project.getName()); + + List retrievedTopics = sendRequestWithoutBody( + createRequest(HttpMethod.GET, getTopicsUrl(project)), + List.class + ); + + assertTrue(retrievedTopics.isEmpty()); + } - HttpRequest request = createRequest(HttpMethod.GET, getTopicUrl(topicName, project)); - TopicResource t1Created = sendRequestWithoutBody(request, TopicResource.class); - Assertions.assertEquals(topicResource.getProject(), t1Created.getProject()); + @Test + void deleteTopic_WithHardDelete_ShouldDeleteTopicSuccessfully() throws InterruptedException { + verifyDeleteRequest("HARD_DELETE", ResourceDeletionType.HARD_DELETE); } @Test - public void testListTopics() throws InterruptedException { - TopicResource topicResource = getTopicResource(topicName, project); - VaradhiTopic t1 = topicResource.toVaradhiTopic(); - List listOfTopics = new ArrayList<>(); - listOfTopics.add(t1.getName()); + void deleteTopic_WithSoftDelete_ShouldDeleteTopicSuccessfully() throws InterruptedException { + verifyDeleteRequest("SOFT_DELETE", ResourceDeletionType.SOFT_DELETE); + } - doReturn(listOfTopics).when(varadhiTopicService).getVaradhiTopics(project.getName()); + @Test + void deleteTopic_WithNoDeletionType_ShouldDefaultToSoftDelete() throws InterruptedException { + verifyDeleteRequest(null, ResourceDeletionType.SOFT_DELETE); + } - HttpRequest request = createRequest(HttpMethod.GET, getTopicsUrl(project)); - List t1Created = sendRequestWithoutBody(request, List.class); - Assertions.assertEquals(t1Created.size(), listOfTopics.size()); + @Test + void deleteTopic_WithInvalidDeletionType_ShouldDefaultToDefault() throws InterruptedException { + verifyDeleteRequest("INVALID_TYPE", ResourceDeletionType.DEFAULT); } @Test - public void testTopicDelete() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.DELETE, getTopicUrl(topicName, project)); - doNothing().when(varadhiTopicService).delete(any()); + void restoreTopic_WithValidRequest_ShouldRestoreTopicSuccessfully() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, getTopicUrl(project) + "/restore"); + doNothing().when(varadhiTopicService).restore(any()); + + sendRequestWithoutBody(request, null); + + verify(varadhiTopicService).restore(any()); + } + + private void verifyDeleteRequest(String deletionType, ResourceDeletionType expectedDeletionType) + throws InterruptedException { + String url = getTopicUrl(project); + if (deletionType != null) { + url += "?deletionType=" + deletionType; + } + + HttpRequest request = createRequest(HttpMethod.DELETE, url); + doNothing().when(varadhiTopicService).delete(any(), eq(expectedDeletionType)); sendRequestWithoutBody(request, null); - verify(varadhiTopicService, times(1)).delete(any()); + + verify(varadhiTopicService).delete(any(), eq(expectedDeletionType)); + } + + private void assertErrorResponse(HttpResponse response, String expectedReason) { + ErrorResponse errorResponse = JsonMapper.jsonDeserialize(response.bodyAsString(), ErrorResponse.class); + assertEquals(expectedReason, errorResponse.reason()); } - private TopicResource getTopicResource(String topicName, Project project) { - return TopicResource.grouped(topicName, project.getName(), Constants.DefaultTopicCapacity); + private TopicResource getTopicResource(Project project) { + return TopicResource.grouped(TOPIC_NAME, project.getName(), Constants.DefaultTopicCapacity); } private String getTopicsUrl(Project project) { return String.join("/", "/projects", project.getName(), "topics"); } - - private String getTopicUrl(String topicName, Project project) { - return String.join("/", getTopicsUrl(project), topicName); + private String getTopicUrl(Project project) { + return String.join("/", getTopicsUrl(project), TOPIC_NAME); } } From bbb85658ddee9d0660b0a0be8009838cdd8f8317 Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Mon, 27 Jan 2025 19:20:36 +0530 Subject: [PATCH 4/7] Fix E2E Test Failures - Overloaded the `makeDeleteRequest` and `makeHttpDeleteRequest` method in `E2EBase` to accept a deletion type parameter. - Updated cleanup methods to use hard delete to ensure resources are fully removed. - Ensured that resources are hard deleted during cleanup to prevent conflicts when creating new ones with the same name. - Corrected exception messages in tests to match the expected responses. - Added `@JsonIgnore` annotation on `isActive` field in `VaradhiTopic` entity to prevent it from being serialized. --- .../varadhi/entities/VaradhiTopic.java | 1 + .../services/SubscriptionServiceTest.java | 8 +++++--- .../web/admin/SubscriptionHandlersTest.java | 2 +- .../java/com/flipkart/varadhi/E2EBase.java | 20 ++++++++++++++++--- .../flipkart/varadhi/SubscriptionTests.java | 6 +++--- .../java/com/flipkart/varadhi/TopicTests.java | 8 ++++---- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java index bcc32fd8..2721ada8 100644 --- a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java +++ b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java @@ -114,6 +114,7 @@ public void updateStatus(Status newStatus) { * * @return true if the topic is active, false otherwise */ + @JsonIgnore public boolean isActive() { return this.status == Status.ACTIVE; } diff --git a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java index b685e3ef..33fd14c3 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java @@ -236,7 +236,8 @@ void testCreateSubscriptionWithNonGroupedTopic() { () -> subscriptionService.createSubscription(unGroupedTopic, subscription, o1t1p1) ); - String expectedMessage = "Cannot create grouped Subscription as it's Topic(%s) is not grouped".formatted( + String expectedMessage = + "Grouped subscription cannot be created or updated for a non-grouped topic '%s'".formatted( unGroupedTopic.getName()); String actualMessage = exception.getMessage(); @@ -294,7 +295,7 @@ void updateSubscriptionWithVersionConflictThrows(VertxTestContext ctx) { CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), requestedBy); - String expectedMessage = "Conflicting update, Subscription has been modified. Fetch latest and try again."; + String expectedMessage = "Conflicting update detected. Fetch the latest version and try again."; InvalidOperationForResourceException e = assertThrows( InvalidOperationForResourceException.class, () -> { @@ -321,7 +322,8 @@ void updateSubscriptionWithUnGroupedTopicThrows(VertxTestContext ctx) { doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), requestedBy); String expectedMessage = - "Cannot update Subscription to grouped as it's Topic(%s) is not grouped".formatted(update.getTopic()); + "Grouped subscription cannot be created or updated for a non-grouped topic '%s'".formatted( + update.getTopic()); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, () -> { CompletableFuture result = updateSubscription(update); diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java index ffda52a1..449fafcd 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java @@ -118,7 +118,7 @@ void testSubscriptionCreateInconsistentProjectNameFailure() throws InterruptedEx SubscriptionResource resource = getSubscriptionResource("sub1", Project.of("project2", "", "team1", "org1"), topicResource); - String errMsg = "Specified Project name is different from Project name in url"; + String errMsg = "Project name mismatch between URL and request body."; ErrorResponse resp = sendRequestWithBody(request, resource, 400, errMsg, ErrorResponse.class); assertEquals(errMsg, resp.reason()); } diff --git a/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java b/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java index 46cb0065..46353614 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java @@ -134,7 +134,7 @@ static void cleanupProject(Project project) { } static void cleanupTopic(String topicName, Project project) { - makeDeleteRequest(getTopicsUri(project, topicName), 200); + makeDeleteRequest(getTopicsUri(project, topicName), "HARD_DELETE", 200); } // this method traverses the resource hierarchy and clean-ups all subscriptions on the matching topics @@ -153,7 +153,7 @@ static void cleanupSubscriptionsOnTopics(List topicNames, String project SubscriptionResource res = makeGetRequest(getSubscriptionsUri(project, sub), SubscriptionResource.class, 200); if (topicNames.contains(res.getTopic()) && projectName.equals(res.getTopicProject())) { - makeDeleteRequest(getSubscriptionsUri(project, sub), 200); + makeDeleteRequest(getSubscriptionsUri(project, sub), "HARD_DELETE", 200); } }); }); @@ -163,7 +163,8 @@ static void cleanupSubscriptionsOnTopics(List topicNames, String project static void cleanupSubscriptionsOnProject(Project project) { getSubscriptions(makeListRequest(getSubscriptionsUri(project), 200)).forEach( - s -> makeDeleteRequest(getSubscriptionsUri(project, s.split(NAME_SEPARATOR_REGEX)[1]), 200)); + s -> makeDeleteRequest( + getSubscriptionsUri(project, s.split(NAME_SEPARATOR_REGEX)[1]), "HARD_DELETE", 200)); } static Client getClient() { @@ -251,6 +252,11 @@ static void makeDeleteRequest(String targetUrl, int expectedStatus) { Assertions.assertEquals(expectedStatus, response.getStatus()); } + static void makeDeleteRequest(String targetUrl, String deletionType, int expectedStatus) { + Response response = makeHttpDeleteRequest(targetUrl, deletionType); + Assertions.assertEquals(expectedStatus, response.getStatus()); + } + static void makeDeleteRequest(String targetUrl, int expectedStatus, String expectedResponse, boolean isErrored) { Response response = makeHttpDeleteRequest(targetUrl); Assertions.assertEquals(expectedStatus, response.getStatus()); @@ -293,6 +299,14 @@ static Response makeHttpDeleteRequest(String targetUrl) { .delete(); } + static Response makeHttpDeleteRequest(String targetUrl, String deletionType) { + return getClient() + .target(targetUrl) + .queryParam("deletionType", deletionType) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(USER_ID_HEADER, SUPER_USER) + .delete(); + } @Provider public static class ObjectMapperContextResolver implements ContextResolver { diff --git a/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java b/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java index 96913d17..8db5d4b5 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java @@ -81,7 +81,7 @@ void createSubscription() { makeCreateRequest( getSubscriptionsUri(o1t1p1), sub, 409, "Subscription(default.sub1) already exists.", true); - makeDeleteRequest(getSubscriptionsUri(o1t1p1, subName), 200); + makeDeleteRequest(getSubscriptionsUri(o1t1p1, subName), "HARD_DELETE", 200); } @Test @@ -122,7 +122,7 @@ void updateSubscription() { assertEquals(update.getName(), updated.getName()); assertEquals(update.getDescription(), updated.getDescription()); assertEquals(2, updated.getVersion()); - makeDeleteRequest(getSubscriptionsUri(o1t1p1, subName), 200); + makeDeleteRequest(getSubscriptionsUri(o1t1p1, subName), "HARD_DELETE", 200); } @Test @@ -192,7 +192,7 @@ void createSubscriptionWithValidationFailure() { ); makeCreateRequest( getSubscriptionsUri(o1t1p1), groupedOnUnGroupTopic, 400, - "Cannot create grouped Subscription as it's Topic(default.topic1) is not grouped", true + "Grouped subscription cannot be created or updated for a non-grouped topic 'default.topic1'", true ); } } diff --git a/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java b/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java index 5f65c83c..e76ca7c0 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java @@ -59,12 +59,12 @@ public void createTopic() { Assertions.assertNotNull(r.getCapacity()); String errorDuplicateTopic = String.format( - "Specified Topic(%s) already exists.", + "Topic '%s' already exists.", String.join(NAME_SEPARATOR, topic.getProject(), topic.getName()) ); makeCreateRequest(getTopicsUri(o1t1Project1), topic, 409, errorDuplicateTopic, true); makeGetRequest(getTopicsUri(o1t1Project1, topicName), TopicResource.class, 200); - makeDeleteRequest(getTopicsUri(o1t1Project1, topicName), 200); + makeDeleteRequest(getTopicsUri(o1t1Project1, topicName), "HARD_DELETE", 200); } @Test @@ -95,7 +95,7 @@ public void createTopicsWithMultiTenancy() { Assertions.assertEquals(topic2.getName(), r2.getName()); Assertions.assertEquals(topic2.getProject(), r2.getProject()); - makeDeleteRequest(getTopicsUri(o1t1Project1, topic1.getName()), 200); - makeDeleteRequest(getTopicsUri(o2t1Project1, topic2.getName()), 200); + makeDeleteRequest(getTopicsUri(o1t1Project1, topic1.getName()), "HARD_DELETE", 200); + makeDeleteRequest(getTopicsUri(o2t1Project1, topic2.getName()), "HARD_DELETE", 200); } } From 58c48c1f3b246632705b21a694feccf2602a4722 Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Tue, 28 Jan 2025 22:55:24 +0530 Subject: [PATCH 5/7] Add comprehensive unit tests for VaradhiSubscription and related services - Added comprehensive unit tests for VaradhiSubscription, SubscriptionHandlers & SubscriptionService. - Refactored SubscriptionTestBase and WebTestBase for better readability and maintainability along with certain enhancements. - Enhanced tests for entity, handler, and service layers, covering all edge cases. - Ensured consistency with existing test styles and used appropriate mocking and stubbing libraries. --- .../entities/VaradhiSubscriptionTest.java | 241 ++++++ .../web/v1/admin/SubscriptionHandlers.java | 3 +- .../varadhi/services/DlqServiceTest.java | 18 +- .../services/SubscriptionServiceTest.java | 759 ++++++++++++++---- .../com/flipkart/varadhi/web/WebTestBase.java | 179 +++-- .../varadhi/web/admin/DlqHandlersTest.java | 102 ++- .../varadhi/web/admin/OrgHandlersTest.java | 18 +- .../web/admin/ProjectHandlersTest.java | 24 +- .../web/admin/SubscriptionHandlersTest.java | 486 ++++++++--- .../web/admin/SubscriptionTestBase.java | 168 ++-- .../varadhi/web/admin/TeamHandlersTest.java | 18 +- .../varadhi/web/admin/TopicHandlersTest.java | 12 +- .../web/authz/IamPolicyHandlersTest.java | 8 +- .../varadhi/web/produce/BodyHandlerTest.java | 10 +- .../web/produce/HeaderValidationTest.java | 12 +- .../web/produce/ProduceHandlersTest.java | 21 +- 16 files changed, 1556 insertions(+), 523 deletions(-) create mode 100644 entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java new file mode 100644 index 00000000..e357f25b --- /dev/null +++ b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java @@ -0,0 +1,241 @@ +package com.flipkart.varadhi.entities; + +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VaradhiSubscriptionTest { + + private static final Endpoint DEFAULT_ENDPOINT = new Endpoint.HttpEndpoint( + URI.create("http://localhost:8080"), + "GET", "", 500, 500, false + ); + + private static final RetryPolicy DEFAULT_RETRY_POLICY = new RetryPolicy( + new CodeRange[]{new CodeRange(500, 502)}, + RetryPolicy.BackoffType.LINEAR, + 1, 1, 1, 3 + ); + + private static final ConsumptionPolicy DEFAULT_CONSUMPTION_POLICY = new ConsumptionPolicy( + 10, 1, 1, + false, 1, null + ); + + private static final TopicCapacityPolicy DEFAULT_CAPACITY_POLICY = new TopicCapacityPolicy( + 1, 10, 1); + + private static final SubscriptionShards DEFAULT_SHARDS = new SubscriptionUnitShard( + 0, DEFAULT_CAPACITY_POLICY, null, null, null); + + @Test + void createSubscription_Success() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + assertEquals("sub1", subscription.getName()); + assertEquals("project1", subscription.getProject()); + assertEquals("topic1", subscription.getTopic()); + assertEquals("description", subscription.getDescription()); + assertTrue(subscription.isGrouped()); + assertNotNull(subscription.getEndpoint()); + assertNotNull(subscription.getRetryPolicy()); + assertNotNull(subscription.getConsumptionPolicy()); + assertNotNull(subscription.getShards()); + assertNotNull(subscription.getProperties()); + } + + @Test + void createSubscription_InvalidProject_ThrowsException() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> { + VaradhiSubscription.of( + "sub1", "", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + } + ); + + assertEquals("Project cannot be null or empty", exception.getMessage()); + } + + @Test + void createSubscription_InvalidTopic_ThrowsException() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> { + VaradhiSubscription.of( + "sub1", "project1", "", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + } + ); + + assertEquals("Topic cannot be null or empty", exception.getMessage()); + } + + @Test + void createSubscription_NullShards_ThrowsException() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> { + VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + null, Map.of("key", "value") + ); + } + ); + + assertEquals("Shards cannot be null or empty", exception.getMessage()); + } + + @Test + void createSubscription_NullProperties_ThrowsException() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> { + VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, null + ); + } + ); + + assertEquals("Properties cannot be null or empty", exception.getMessage()); + } + + @Test + void markCreateFailed_ChangesStateToCreateFailed() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + subscription.markCreateFailed("Creation failed"); + assertEquals(VaradhiSubscription.State.CREATE_FAILED, subscription.getStatus().getState()); + assertEquals("Creation failed", subscription.getStatus().getMessage()); + } + + @Test + void markCreated_ChangesStateToCreated() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + subscription.markCreated(); + assertEquals(VaradhiSubscription.State.CREATED, subscription.getStatus().getState()); + assertNull(subscription.getStatus().getMessage()); + } + + @Test + void markDeleteFailed_ChangesStateToDeleteFailed() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + subscription.markDeleteFailed("Deletion failed"); + assertEquals(VaradhiSubscription.State.DELETE_FAILED, subscription.getStatus().getState()); + assertEquals("Deletion failed", subscription.getStatus().getMessage()); + } + + @Test + void markDeleting_ChangesStateToDeleting() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + subscription.markDeleting(); + assertEquals(VaradhiSubscription.State.DELETING, subscription.getStatus().getState()); + assertNull(subscription.getStatus().getMessage()); + } + + @Test + void markInactive_ChangesStateToInactive() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + subscription.markInactive(); + assertEquals(VaradhiSubscription.State.INACTIVE, subscription.getStatus().getState()); + assertNull(subscription.getStatus().getMessage()); + } + + @Test + void restore_ChangesStateToCreated() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + subscription.markInactive(); + subscription.restore(); + assertEquals(VaradhiSubscription.State.CREATED, subscription.getStatus().getState()); + assertEquals("Entity restored to created state.", subscription.getStatus().getMessage()); + } + + @Test + void getIntProperty_ReturnsCorrectValue() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "10") + ); + + assertEquals(10, subscription.getIntProperty("key")); + } + + @Test + void getIntProperty_PropertyNotFound_ThrowsException() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "value") + ); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> { + subscription.getIntProperty("nonExistentKey"); + } + ); + + assertEquals("Property not found: nonExistentKey", exception.getMessage()); + } + + @Test + void getIntProperty_InvalidValue_ThrowsException() { + VaradhiSubscription subscription = VaradhiSubscription.of( + "sub1", "project1", "topic1", "description", true, + DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, Map.of("key", "invalid") + ); + + NumberFormatException exception = assertThrows( + NumberFormatException.class, () -> { + subscription.getIntProperty("key"); + } + ); + + assertEquals("For input string: \"invalid\"", exception.getMessage()); + } +} diff --git a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java index cd38eda6..54997614 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java +++ b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java @@ -123,7 +123,8 @@ public List get() { .nonBlocking() .authorize(SUBSCRIPTION_UPDATE) .build(this::getHierarchies, this::start), - RouteDefinition.post("StopSubscription", "/:subscription/stop") + RouteDefinition + .post("StopSubscription", "/:subscription/stop") .nonBlocking() .authorize(SUBSCRIPTION_UPDATE) .build(this::getHierarchies, this::stop) diff --git a/server/src/test/java/com/flipkart/varadhi/services/DlqServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/DlqServiceTest.java index 7a08cc28..c2d0bf0c 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/DlqServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/DlqServiceTest.java @@ -44,8 +44,8 @@ public void setUp() { @Test void testUnsideline() { - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - VaradhiSubscription subscription = spy(getUngroupedSubscription("sub12", project, vTopic)); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = spy(createUngroupedSubscription("sub12", PROJECT, vTopic)); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofFailedAt(System.currentTimeMillis()); String requestedBy = "testUser"; SubscriptionOperation operation = @@ -64,8 +64,8 @@ void testUnsideline() { @Test void testUnsidelineInvalidState() { - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - VaradhiSubscription subscription = spy(getUngroupedSubscription("sub12", project, vTopic)); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = spy(createUngroupedSubscription("sub12", PROJECT, vTopic)); when(subscription.isWellProvisioned()).thenReturn(false); InvalidOperationForResourceException exception = assertThrows( InvalidOperationForResourceException.class, @@ -80,7 +80,7 @@ void testGetMessages() { int limit = 10; VaradhiSubscription subscription = setupSubscriptionForGetMessages(); - List shard1Messages = List.of(getDlqMessage(1), getDlqMessage(2), getDlqMessage(1)); + List shard1Messages = List.of(createDlqMessage(1), createDlqMessage(2), createDlqMessage(1)); String shard1NextPage = shard1Messages.get(1).getOffset().toString() + "," + shard1Messages.get(2).getOffset().toString(); doReturn(CompletableFuture.completedFuture(new ShardDlqMessageResponse(shard1Messages, shard1NextPage))).when( @@ -134,8 +134,8 @@ void testGetMessagesShardGetMessageFails() { @Test void testGetMessagesInvalidState() { - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - VaradhiSubscription subscription = spy(getUngroupedSubscription("sub12", project, vTopic)); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = spy(createUngroupedSubscription("sub12", PROJECT, vTopic)); when(subscription.isWellProvisioned()).thenReturn(false); InvalidOperationForResourceException exception = assertThrows( InvalidOperationForResourceException.class, @@ -179,8 +179,8 @@ void testGetMessageSubWithNoAssignments() { private VaradhiSubscription setupSubscriptionForGetMessages() { String consumerId = "consumerId"; - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - VaradhiSubscription subscription = spy(getUngroupedSubscription("sub12", project, vTopic)); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = spy(createUngroupedSubscription("sub12", PROJECT, vTopic)); SubscriptionShards shards = subscription.getShards(); List assignments = new ArrayList<>(); for (int i = 0; i < shards.getShardCount(); i++) { diff --git a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java index 33fd14c3..9ed92eb8 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java @@ -5,7 +5,24 @@ import com.flipkart.varadhi.config.RestOptions; import com.flipkart.varadhi.core.cluster.ControllerRestApi; import com.flipkart.varadhi.db.VaradhiMetaStore; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.CodeRange; +import com.flipkart.varadhi.entities.ConsumptionPolicy; +import com.flipkart.varadhi.entities.Endpoint; +import com.flipkart.varadhi.entities.InternalCompositeSubscription; +import com.flipkart.varadhi.entities.InternalCompositeTopic; +import com.flipkart.varadhi.entities.InternalQueueCategory; +import com.flipkart.varadhi.entities.InternalQueueType; +import com.flipkart.varadhi.entities.Org; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceDeletionType; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.RetrySubscription; +import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.Team; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; +import com.flipkart.varadhi.entities.cluster.SubscriptionOperation; import com.flipkart.varadhi.entities.cluster.SubscriptionState; import com.flipkart.varadhi.exceptions.InvalidOperationForResourceException; import com.flipkart.varadhi.exceptions.ResourceNotFoundException; @@ -23,7 +40,6 @@ import com.flipkart.varadhi.utils.ShardProvisioner; import com.flipkart.varadhi.utils.SubscriptionPropertyValidator; import com.flipkart.varadhi.utils.VaradhiSubscriptionFactory; -import com.flipkart.varadhi.web.admin.SubscriptionHandlersTest; import com.flipkart.varadhi.web.entities.SubscriptionResource; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; @@ -37,50 +53,70 @@ import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.apache.curator.test.TestingServer; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import java.net.URI; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import static com.flipkart.varadhi.MessageConstants.ANONYMOUS_IDENTITY; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; -import static com.flipkart.varadhi.web.admin.SubscriptionHandlersTest.getGroupedSubscription; -import static com.flipkart.varadhi.web.admin.SubscriptionHandlersTest.getUngroupedSubscription; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static com.flipkart.varadhi.web.admin.SubscriptionTestBase.createGroupedSubscription; +import static com.flipkart.varadhi.web.admin.SubscriptionTestBase.createUngroupedSubscription; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(VertxExtension.class) class SubscriptionServiceTest { - TestingServer zkCuratorTestingServer; - OrgService orgService; - TeamService teamService; - ProjectService projectService; - CuratorFramework zkCurator; - VaradhiMetaStore varadhiMetaStore; - ControllerRestApi controllerRestApi; - SubscriptionService subscriptionService; - ShardProvisioner shardProvisioner; - MeterRegistry meterRegistry; - Org o1; - Team o1t1; - Project o1t1p1, o1t1p2; - VaradhiTopic unGroupedTopic, groupedTopic; - VaradhiSubscription sub1, sub2; - String requestedBy = ANONYMOUS_IDENTITY; + private TestingServer zkCuratorTestingServer; + private CuratorFramework zkCurator; + private VaradhiMetaStore varadhiMetaStore; + private OrgService orgService; + private TeamService teamService; + private ProjectService projectService; + private SubscriptionService subscriptionService; + private ShardProvisioner shardProvisioner; + private ControllerRestApi controllerRestApi; + private MeterRegistry meterRegistry; + + private Org org; + private Team team; + private Project project1, project2; + private VaradhiTopic unGroupedTopic, groupedTopic; + private VaradhiSubscription subscription1, subscription2; + + private static final String REQUESTED_BY = ANONYMOUS_IDENTITY; @BeforeEach void setUp() throws Exception { - JsonMapper.getMapper().registerSubtypes(new NamedType(PulsarStorageTopic.class, "PulsarTopic")); - JsonMapper.getMapper().registerSubtypes(new NamedType(PulsarSubscription.class, "PulsarSubscription")); + JsonMapper.getMapper().registerSubtypes( + new NamedType(PulsarStorageTopic.class, "PulsarTopic"), + new NamedType(PulsarSubscription.class, "PulsarSubscription") + ); zkCuratorTestingServer = new TestingServer(); zkCurator = spy(CuratorFrameworkFactory.newClient( - zkCuratorTestingServer.getConnectString(), new ExponentialBackoffRetry(1000, 1))); + zkCuratorTestingServer.getConnectString(), new ExponentialBackoffRetry(1000, 1) + )); zkCurator.start(); + varadhiMetaStore = spy(new VaradhiMetaStore(zkCurator)); orgService = new OrgService(varadhiMetaStore); @@ -88,56 +124,72 @@ void setUp() throws Exception { meterRegistry = new JmxMeterRegistry(JmxConfig.DEFAULT, Clock.SYSTEM); projectService = new ProjectService(varadhiMetaStore, "", meterRegistry); - o1 = Org.of("TestOrg1"); - o1t1 = Team.of("TestTeam1", o1.getName()); - o1t1p1 = Project.of("o1t1p1", "", o1t1.getName(), o1t1.getOrg()); - o1t1p2 = Project.of("o1t1p2", "", o1t1.getName(), o1t1.getOrg()); - unGroupedTopic = VaradhiTopic.of("topic1", o1t1p1.getName(), false,null); - groupedTopic = VaradhiTopic.of("topic2", o1t1p2.getName(), true,null); + org = Org.of("Org"); + team = Team.of("Team", org.getName()); + project1 = Project.of("Project1", "", team.getName(), team.getOrg()); + project2 = Project.of("Project2", "", team.getName(), team.getOrg()); + unGroupedTopic = VaradhiTopic.of("UngroupedTopic", project1.getName(), false, null); + groupedTopic = VaradhiTopic.of("GroupedTopic", project2.getName(), true, null); + + subscription1 = createUngroupedSubscription("Sub1", project1, unGroupedTopic); + subscription2 = createUngroupedSubscription("Sub2", project1, unGroupedTopic); - sub1 = SubscriptionHandlersTest.getUngroupedSubscription("sub1", o1t1p1, unGroupedTopic); - sub2 = SubscriptionHandlersTest.getUngroupedSubscription("sub2", o1t1p1, unGroupedTopic); + orgService.createOrg(org); + teamService.createTeam(team); + projectService.createProject(project1); + projectService.createProject(project2); - orgService.createOrg(o1); - teamService.createTeam(o1t1); - projectService.createProject(o1t1p1); - projectService.createProject(o1t1p2); shardProvisioner = mock(ShardProvisioner.class); doNothing().when(shardProvisioner).provision(any(), any()); + controllerRestApi = mock(ControllerRestApi.class); subscriptionService = new SubscriptionService(shardProvisioner, controllerRestApi, varadhiMetaStore); } + @AfterEach + void tearDown() throws Exception { + zkCurator.close(); + zkCuratorTestingServer.close(); + } + @Test - void testSubscriptionEntitiesSerDe() { - Endpoint endpoint; + void serializeDeserializeSubscription_Success() { + Endpoint endpoint = new Endpoint.HttpEndpoint( + URI.create("http://localhost:8080"), "GET", "", + 500, 500, false + ); + RetryPolicy retryPolicy = new RetryPolicy( new CodeRange[]{new CodeRange(500, 502)}, RetryPolicy.BackoffType.LINEAR, 1, 1, 1, 1 ); - ConsumptionPolicy consumptionPolicy = new ConsumptionPolicy(10, 1, 1, false, 1, null); - endpoint = new Endpoint.HttpEndpoint(URI.create("http://localhost:8080"), "GET", "", 500, 500, false); - TopicCapacityPolicy capacity = Constants.DefaultTopicCapacity; - String region = "default"; - TopicPlanner planner = new TopicPlanner(new PulsarConfig()); - StorageSubscriptionFactory psf = new PulsarSubscriptionFactory(); - StorageTopicFactory ptf = new PulsarTopicFactory(planner); - StorageTopicService pts = new PulsarTopicService(null, planner); - - VaradhiTopic vt = VaradhiTopic.of("topic2", o1t1p2.getName(), true, capacity); + ConsumptionPolicy consumptionPolicy = new ConsumptionPolicy( + 10, 1, + 1, false, 1, null + ); - StorageTopic storageTopic = ptf.getTopic(vt.getName(), o1t1p2, capacity, InternalQueueCategory.MAIN); - vt.addInternalTopic(region, InternalCompositeTopic.of(storageTopic)); + TopicCapacityPolicy capacity = Constants.DefaultTopicCapacity; + String region = "default"; - SubscriptionResource subRes = SubscriptionResource.of( - "sub12", - o1t1p2.getName(), - "topic2", - o1t1p2.getName(), - "desc", + TopicPlanner planner = new TopicPlanner(new PulsarConfig()); + StorageSubscriptionFactory subscriptionFactory = new PulsarSubscriptionFactory(); + StorageTopicFactory topicFactory = new PulsarTopicFactory(planner); + StorageTopicService topicService = new PulsarTopicService(null, planner); + + VaradhiTopic topic = VaradhiTopic.of("GroupedTopic", project2.getName(), true, capacity); + StorageTopic storageTopic = + topicFactory.getTopic(topic.getName(), project2, capacity, InternalQueueCategory.MAIN); + topic.addInternalTopic(region, InternalCompositeTopic.of(storageTopic)); + + SubscriptionResource subscriptionResource = SubscriptionResource.of( + "SubscriptionResource", + project2.getName(), + "GroupedTopic", + project2.getName(), + "Description", false, endpoint, retryPolicy, @@ -145,231 +197,602 @@ void testSubscriptionEntitiesSerDe() { SubscriptionPropertyValidator.createPropertyDefaultValueProviders(new RestOptions()) ); - VaradhiSubscriptionFactory factory = new VaradhiSubscriptionFactory(pts, psf, ptf, region); - VaradhiSubscription sub = factory.get(subRes, o1t1p2, vt); + VaradhiSubscriptionFactory varadhiFactory = + new VaradhiSubscriptionFactory(topicService, subscriptionFactory, topicFactory, region); + VaradhiSubscription subscription = varadhiFactory.get(subscriptionResource, project2, topic); + + String subscriptionJson = JsonMapper.jsonSerialize(subscription); + VaradhiSubscription deserializedSubscription = + JsonMapper.jsonDeserialize(subscriptionJson, VaradhiSubscription.class); + + assertEquals(subscription.getName(), deserializedSubscription.getName()); + assertEquals(subscription.getProject(), deserializedSubscription.getProject()); + assertEquals(subscription.getTopic(), deserializedSubscription.getTopic()); + assertEquals(subscription.getDescription(), deserializedSubscription.getDescription()); + assertEquals(subscription.isGrouped(), deserializedSubscription.isGrouped()); + assertEquals(subscription.getEndpoint().getProtocol(), deserializedSubscription.getEndpoint().getProtocol()); + assertEquals(subscription.getRetryPolicy(), deserializedSubscription.getRetryPolicy()); + assertEquals(subscription.getConsumptionPolicy(), deserializedSubscription.getConsumptionPolicy()); + assertEquals(subscription.getShards().getShardCount(), deserializedSubscription.getShards().getShardCount()); + assertEquals(subscription.getStatus(), deserializedSubscription.getStatus()); + assertEquals(subscription.getProperties(), deserializedSubscription.getProperties()); + } - InternalQueueType.Retry r = new InternalQueueType.Retry(1); - String rjson = JsonMapper.jsonSerialize(r); - JsonMapper.jsonDeserialize(rjson, InternalQueueType.Retry.class); + @Test + void serializeDeserializeInternalQueueTypeRetry_Success() { + InternalQueueType.Retry retryQueue = new InternalQueueType.Retry(1); + String retryQueueJson = JsonMapper.jsonSerialize(retryQueue); + InternalQueueType.Retry deserializedRetryQueue = + JsonMapper.jsonDeserialize(retryQueueJson, InternalQueueType.Retry.class); + assertEquals(retryQueue, deserializedRetryQueue); + } - InternalCompositeSubscription ics1 = sub.getShards().getShard(0).getDeadLetterSubscription(); - String ics1Json = JsonMapper.jsonSerialize(ics1); - JsonMapper.jsonDeserialize(ics1Json, InternalCompositeSubscription.class); + @Test + void serializeDeserializeInternalCompositeSubscription_Success() { + InternalCompositeSubscription compositeSubscription = + subscription1.getShards().getShard(0).getDeadLetterSubscription(); + String compositeJson = JsonMapper.jsonSerialize(compositeSubscription); + InternalCompositeSubscription deserializedComposite = + JsonMapper.jsonDeserialize(compositeJson, InternalCompositeSubscription.class); + + assertEquals(compositeSubscription, deserializedComposite); + } - RetrySubscription rs1 = sub.getShards().getShard(0).getRetrySubscription(); - String rs1Json = JsonMapper.jsonSerialize(rs1); - JsonMapper.jsonDeserialize(rs1Json, RetrySubscription.class); + @Test + void serializeDeserializeRetrySubscription_Success() { + RetrySubscription retrySubscription = subscription1.getShards().getShard(0).getRetrySubscription(); + String retryJson = JsonMapper.jsonSerialize(retrySubscription); + RetrySubscription deserializedRetry = JsonMapper.jsonDeserialize(retryJson, RetrySubscription.class); - String json = JsonMapper.jsonSerialize(sub); - JsonMapper.jsonDeserialize(json, VaradhiSubscription.class); + assertEquals(retrySubscription, deserializedRetry); } @Test - void getSubscriptionListReturnsCorrectSubscriptions() { + void getSubscriptionList_ValidProject_ReturnsCorrectSubscriptions() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - // create multiple subs - subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); - subscriptionService.createSubscription(unGroupedTopic, sub2, o1t1p1); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + subscriptionService.createSubscription(unGroupedTopic, subscription2, project1); subscriptionService.createSubscription( - unGroupedTopic, SubscriptionHandlersTest.getUngroupedSubscription("sub3", o1t1p2, unGroupedTopic), - o1t1p2 + unGroupedTopic, createUngroupedSubscription("Sub3", project2, unGroupedTopic), + project2 ); - List actualSubscriptions = subscriptionService.getSubscriptionList(o1t1p1.getName()); + List actualProject1Subscriptions = subscriptionService.getSubscriptionList(project1.getName()); + assertEquals(List.of("Project1.Sub1", "Project1.Sub2"), actualProject1Subscriptions); - assertEquals(List.of("o1t1p1.sub2", "o1t1p1.sub1"), actualSubscriptions); + List actualProject2Subscriptions = subscriptionService.getSubscriptionList(project2.getName()); + assertEquals(List.of("Project2.Sub3"), actualProject2Subscriptions); + } - actualSubscriptions = subscriptionService.getSubscriptionList(o1t1p2.getName()); + @Test + void getSubscriptionList_MetaStoreFailure_ThrowsException() { + String projectName = project1.getName(); + when(varadhiMetaStore.getSubscriptionNames(projectName)) + .thenThrow(new RuntimeException("MetaStore listing failed")); - assertEquals(List.of("o1t1p2.sub3"), actualSubscriptions); + Exception exception = assertThrows( + RuntimeException.class, + () -> subscriptionService.getSubscriptionList(projectName) + ); + + assertEquals("MetaStore listing failed", exception.getMessage()); + verify(varadhiMetaStore, times(1)).getSubscriptionNames(projectName); } @Test - void getSubscriptionReturnsCorrectSubscription() { + void getSubscriptionList_InactiveSubscriptionsAreFilteredOut() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + subscriptionService.createSubscription(unGroupedTopic, subscription2, project1); + + subscription2.markInactive(); + varadhiMetaStore.updateSubscription(subscription2); - VaradhiSubscription actualSubscription = subscriptionService.getSubscription(sub1.getName()); + when(varadhiMetaStore.getSubscriptionNames(project1.getName())) + .thenReturn(List.of(subscription1.getName(), subscription2.getName())); + when(varadhiMetaStore.getSubscription(subscription1.getName())).thenReturn(subscription1); + when(varadhiMetaStore.getSubscription(subscription2.getName())).thenReturn(subscription2); - assertSubscriptionsSame(sub1, actualSubscription); + List actualSubscriptions = subscriptionService.getSubscriptionList(project1.getName()); + + assertEquals(List.of(subscription1.getName()), actualSubscriptions); + verify(varadhiMetaStore, times(1)).getSubscriptionNames(project1.getName()); + verify(varadhiMetaStore, times(1)).getSubscription(subscription1.getName()); + verify(varadhiMetaStore, times(1)).getSubscription(subscription2.getName()); } - private void assertSubscriptionsSame(VaradhiSubscription expected, VaradhiSubscription actual) { - assertEquals(expected.getName(), actual.getName()); - assertEquals(expected.getTopic(), actual.getTopic()); - assertEquals(expected.isGrouped(), actual.isGrouped()); - assertEquals(expected.getDescription(), actual.getDescription()); + @Test + void getSubscription_ExistingSubscription_ReturnsCorrectSubscription() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + VaradhiSubscription actualSubscription = subscriptionService.getSubscription(subscription1.getName()); + + assertSubscriptionsEqual(subscription1, actualSubscription); } @Test - void getSubscriptionForNonExistentThrows() { - String subscriptionName = sub1.getName(); + void getSubscription_NonExistentSubscription_ThrowsException() { + String subscriptionName = subscription1.getName(); Exception exception = assertThrows( ResourceNotFoundException.class, () -> subscriptionService.getSubscription(subscriptionName) ); - String expectedMessage = "Subscription(%s) not found.".formatted(subscriptionName); - String actualMessage = exception.getMessage(); + assertEquals("Subscription(%s) not found.".formatted(subscriptionName), exception.getMessage()); + } + + @Test + void getSubscription_InactiveSubscription_ThrowsException() { + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + subscription1.markInactive(); + varadhiMetaStore.updateSubscription(subscription1); + + when(varadhiMetaStore.getSubscription(subscription1.getName())).thenReturn(subscription1); - assertEquals(expectedMessage, actualMessage); + Exception exception = assertThrows( + ResourceNotFoundException.class, + () -> subscriptionService.getSubscription(subscription1.getName()) + ); + + assertEquals( + "Subscription '%s' not found or in invalid state.".formatted(subscription1.getName()), + exception.getMessage() + ); + verify(varadhiMetaStore, times(1)).getSubscription(subscription1.getName()); } @Test - void testCreateSubscription() { + void getSubscription_MetaStoreFailure_ThrowsException() { + String subscriptionName = subscription1.getName(); + doThrow(new RuntimeException("MetaStore retrieval failed")).when(varadhiMetaStore) + .getSubscription(subscriptionName); + + Exception exception = assertThrows( + RuntimeException.class, + () -> subscriptionService.getSubscription(subscriptionName) + ); + + assertEquals("MetaStore retrieval failed", exception.getMessage()); + verify(varadhiMetaStore, times(1)).getSubscription(subscriptionName); + } + + @Test + void createSubscription_ValidUngroupedTopic_CreatesSuccessfully() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - VaradhiSubscription result = subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); - assertSubscriptionsSame(sub1, result); - assertSubscriptionsSame(sub1, subscriptionService.getSubscription(sub1.getName())); - verify(shardProvisioner, times(1)).provision(sub1, o1t1p1); + + VaradhiSubscription result = subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + assertSubscriptionsEqual(subscription1, result); + assertSubscriptionsEqual(subscription1, subscriptionService.getSubscription(subscription1.getName())); + verify(shardProvisioner, times(1)).provision(subscription1, project1); + verify(varadhiMetaStore, times(1)).createSubscription(subscription1); } @Test - void testCreateSubscriptionWithNonGroupedTopic() { + void createSubscription_NonGroupedTopicWithGroupedSubscription_ThrowsException() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - VaradhiSubscription subscription = getGroupedSubscription("sub1", o1t1p1, unGroupedTopic); + VaradhiSubscription subscription = createGroupedSubscription("Sub1", project1, unGroupedTopic); Exception exception = assertThrows( IllegalArgumentException.class, - () -> subscriptionService.createSubscription(unGroupedTopic, subscription, o1t1p1) + () -> subscriptionService.createSubscription(unGroupedTopic, subscription, project1) ); String expectedMessage = "Grouped subscription cannot be created or updated for a non-grouped topic '%s'".formatted( unGroupedTopic.getName()); - String actualMessage = exception.getMessage(); - - assertEquals(expectedMessage, actualMessage); + assertEquals(expectedMessage, exception.getMessage()); } @Test - void testCreateSubscriptionWithGroupedTopic() { + void createSubscription_GroupedTopic_AllowsBothGroupedAndUngroupedSubscriptions() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); doReturn(groupedTopic).when(varadhiMetaStore).getTopic(groupedTopic.getName()); - VaradhiSubscription unGroupedSub = getUngroupedSubscription("sub1", o1t1p1, groupedTopic); - VaradhiSubscription groupedSub = getGroupedSubscription("sub2", o1t1p1, groupedTopic); + VaradhiSubscription unGroupedSub = createUngroupedSubscription("Sub1", project1, groupedTopic); + VaradhiSubscription groupedSub = createGroupedSubscription("Sub2", project1, groupedTopic); assertDoesNotThrow(() -> { - subscriptionService.createSubscription(groupedTopic, unGroupedSub, o1t1p1); + subscriptionService.createSubscription(groupedTopic, unGroupedSub, project1); }); assertDoesNotThrow(() -> { - subscriptionService.createSubscription(groupedTopic, groupedSub, o1t1p1); + subscriptionService.createSubscription(groupedTopic, groupedSub, project1); }); } @Test - void updateSubscriptionUpdatesCorrectly(VertxTestContext ctx) { + void createSubscription_ProvisionFailure_SetsStateToCreateFailed() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + doThrow(new RuntimeException("Provision failed")).when(shardProvisioner).provision(any(), any()); + + Exception exception = assertThrows( + RuntimeException.class, + () -> subscriptionService.createSubscription(unGroupedTopic, subscription1, project1) + ); + + assertEquals("Provision failed", exception.getMessage()); + assertEquals(VaradhiSubscription.State.CREATE_FAILED, subscription1.getStatus().getState()); + verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); + } + + @Test + void createSubscription_MetaStoreFailure_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + doThrow(new RuntimeException("MetaStore creation failed")).when(varadhiMetaStore).createSubscription(any()); + + Exception exception = assertThrows( + RuntimeException.class, + () -> subscriptionService.createSubscription(unGroupedTopic, subscription1, project1) + ); + + assertEquals("MetaStore creation failed", exception.getMessage()); + verify(varadhiMetaStore, times(1)).createSubscription(subscription1); + } + + @Test + void updateSubscription_ValidInput_UpdatesCorrectly(VertxTestContext ctx) { Checkpoint checkpoint = ctx.checkpoint(1); + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + VaradhiSubscription update = - SubscriptionHandlersTest.getUngroupedSubscription( - sub1.getName().split(NAME_SEPARATOR_REGEX)[1], o1t1p1, unGroupedTopic); + createUngroupedSubscription( + subscription1.getName().split(NAME_SEPARATOR_REGEX)[1], project1, unGroupedTopic); update.setVersion(1); + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); - doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), requestedBy); + doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), REQUESTED_BY); - Future.fromCompletionStage(updateSubscription(update)).onComplete(ctx.succeeding( - sub -> { - assertEquals(update.getDescription(), sub.getDescription()); - assertEquals(2, sub.getVersion()); - checkpoint.flag(); - } - )); + Future.fromCompletionStage(updateSubscription(update)).onComplete(ctx.succeeding(sub -> { + assertEquals(update.getDescription(), sub.getDescription()); + assertEquals(2, sub.getVersion()); + checkpoint.flag(); + })); } @Test - void updateSubscriptionWithVersionConflictThrows(VertxTestContext ctx) { + void updateSubscription_VersionConflict_ThrowsException(VertxTestContext ctx) { Checkpoint checkpoint = ctx.checkpoint(1); + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + VaradhiSubscription update = - SubscriptionHandlersTest.getUngroupedSubscription( - sub1.getName().split(NAME_SEPARATOR_REGEX)[1], o1t1p1, unGroupedTopic); + createUngroupedSubscription( + subscription1.getName().split(NAME_SEPARATOR_REGEX)[1], project1, unGroupedTopic); update.setVersion(2); + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); - doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), requestedBy); + doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), REQUESTED_BY); + String expectedMessage = "Conflicting update detected. Fetch the latest version and try again."; - InvalidOperationForResourceException e = assertThrows( - InvalidOperationForResourceException.class, () -> { - CompletableFuture result = updateSubscription(update); - Future.fromCompletionStage(result) - .onComplete((v) -> ctx.failNow("Update Subscription didn't fail")); - } + InvalidOperationForResourceException exception = assertThrows( + InvalidOperationForResourceException.class, () -> updateSubscription(update).join() ); - assertEquals(expectedMessage, e.getMessage()); + + assertEquals(expectedMessage, exception.getMessage()); checkpoint.flag(); } - @Test - void updateSubscriptionWithUnGroupedTopicThrows(VertxTestContext ctx) { + void updateSubscription_UnGroupedTopic_ThrowsException(VertxTestContext ctx) { Checkpoint checkpoint = ctx.checkpoint(1); + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + VaradhiSubscription update = - getGroupedSubscription(sub1.getName().split(NAME_SEPARATOR_REGEX)[1], o1t1p1, unGroupedTopic); + createGroupedSubscription( + subscription1.getName().split(NAME_SEPARATOR_REGEX)[1], project1, unGroupedTopic); update.setVersion(1); + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); - doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), requestedBy); + doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), REQUESTED_BY); String expectedMessage = "Grouped subscription cannot be created or updated for a non-grouped topic '%s'".formatted( update.getTopic()); - IllegalArgumentException e = assertThrows( - IllegalArgumentException.class, () -> { - CompletableFuture result = updateSubscription(update); - Future.fromCompletionStage(result) - .onComplete((v) -> ctx.failNow("Update Subscription didn't fail")); - } + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> updateSubscription(update).join() ); - assertEquals(expectedMessage, e.getMessage()); + + assertEquals(expectedMessage, exception.getMessage()); checkpoint.flag(); } @Test - void deleteSubscriptionRemovesSubscription(VertxTestContext ctx) { - Checkpoint checkpoint = ctx.checkpoint(1); + void updateSubscription_MetaStoreFailure_ThrowsRuntimeException() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); - subscriptionService.createSubscription(unGroupedTopic, sub1, o1t1p1); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + VaradhiSubscription update = createUngroupedSubscription( + subscription1.getName().split(NAME_SEPARATOR_REGEX)[1], project1, unGroupedTopic); + update.setVersion(1); - String name = sub1.getName(); + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(status).when(controllerRestApi).getSubscriptionState(update.getName(), REQUESTED_BY); + + doThrow(new RuntimeException("MetaStore update failed")).when(varadhiMetaStore).updateSubscription(any()); + + Exception exception = assertThrows( + ExecutionException.class, + () -> subscriptionService.updateSubscription( + update.getName(), update.getVersion(), update.getDescription(), update.isGrouped(), + update.getEndpoint(), update.getRetryPolicy(), update.getConsumptionPolicy(), REQUESTED_BY + ).get() + ); + + Throwable cause = exception.getCause(); + assertEquals(RuntimeException.class, cause.getClass()); + assertEquals("MetaStore update failed", cause.getMessage()); + + ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass(VaradhiSubscription.class); + verify(varadhiMetaStore, times(2)).updateSubscription(subscriptionCaptor.capture()); + VaradhiSubscription capturedSubscription = subscriptionCaptor.getValue(); + assertEquals(update.getName(), capturedSubscription.getName()); + assertEquals(update.getVersion(), capturedSubscription.getVersion()); + } + + @Test + void deleteSubscription_HardDelete_Success(VertxTestContext ctx) { + Checkpoint checkpoint = ctx.checkpoint(1); - VaradhiSubscription subscription = subscriptionService.getSubscription(name); - assertNotNull(subscription); + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + String subscriptionName = subscription1.getName(); CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); - doReturn(status).when(controllerRestApi).getSubscriptionState(name, requestedBy); + doReturn(status).when(controllerRestApi).getSubscriptionState(subscriptionName, REQUESTED_BY); + Future.fromCompletionStage( - subscriptionService.deleteSubscription(name, o1t1p1, requestedBy, ResourceDeletionType.HARD_DELETE)) - .onComplete(ctx.succeeding( - v -> { - Exception exception = assertThrows( - ResourceNotFoundException.class, - () -> subscriptionService.getSubscription(name) - ); - String expectedMessage = "Subscription(%s) not found.".formatted(name); - String actualMessage = exception.getMessage(); - assertEquals(expectedMessage, actualMessage); - checkpoint.flag(); - } - )); + subscriptionService.deleteSubscription( + subscriptionName, project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE)) + .onComplete(ctx.succeeding(result -> { + ResourceNotFoundException exception = assertThrows( + ResourceNotFoundException.class, + () -> subscriptionService.getSubscription(subscriptionName) + ); + assertEquals("Subscription(%s) not found.".formatted(subscriptionName), exception.getMessage()); + checkpoint.flag(); + })); + } + + @Test + void deleteSubscription_SoftDelete_UpdatesSubscriptionState() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + CompletableFuture stoppedState = + CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(stoppedState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + + assertDoesNotThrow(() -> subscriptionService.deleteSubscription( + subscription1.getName(), project1, REQUESTED_BY, + ResourceDeletionType.SOFT_DELETE + ).get()); + + verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); + VaradhiSubscription updatedSubscription = varadhiMetaStore.getSubscription(subscription1.getName()); + assertEquals(VaradhiSubscription.State.INACTIVE, updatedSubscription.getStatus().getState()); + } + + @Test + void deleteSubscription_ResourceNotStopped_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + CompletableFuture activeState = + CompletableFuture.completedFuture(SubscriptionState.forRunning()); + doReturn(activeState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> subscriptionService.deleteSubscription( + subscription1.getName(), project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE).get() + ); + + Throwable cause = exception.getCause(); + assertEquals(IllegalArgumentException.class, cause.getClass()); + assertEquals("Cannot delete subscription in state: SubscriptionState(ASSIGNED, CONSUMING)", cause.getMessage()); + verify(varadhiMetaStore, never()).deleteSubscription(subscription1.getName()); + } + + @Test + void deleteSubscription_MetaStoreFailure_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + CompletableFuture stoppedState = + CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(stoppedState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + doThrow(new RuntimeException("MetaStore deletion failed")).when(varadhiMetaStore) + .deleteSubscription(subscription1.getName()); + + ExecutionException exception = assertThrows( + ExecutionException.class, + () -> subscriptionService.deleteSubscription( + subscription1.getName(), project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE).get() + ); + + Throwable cause = exception.getCause(); + assertEquals(RuntimeException.class, cause.getClass()); + assertEquals("MetaStore deletion failed", cause.getMessage()); + verify(varadhiMetaStore, times(1)).deleteSubscription(subscription1.getName()); + } + + @Test + void deleteSubscription_DeProvisionFailure_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + CompletableFuture stoppedState = + CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(stoppedState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + doThrow(new RuntimeException("DeProvision failed")).when(shardProvisioner).deProvision(any(), any()); + + Exception exception = assertThrows( + ExecutionException.class, + () -> subscriptionService.deleteSubscription( + subscription1.getName(), project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE).get() + ); + + Throwable cause = exception.getCause(); + assertEquals(RuntimeException.class, cause.getClass()); + assertEquals("DeProvision failed", cause.getMessage()); + + VaradhiSubscription updatedSubscription = varadhiMetaStore.getSubscription(subscription1.getName()); + assertEquals(VaradhiSubscription.State.DELETE_FAILED, updatedSubscription.getStatus().getState()); + verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); + } + + @Test + void startSubscription_SuccessfulStart() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + CompletableFuture startFuture = CompletableFuture.completedFuture( + SubscriptionOperation.startOp(subscription1.getName(), REQUESTED_BY) + ); + doReturn(startFuture).when(controllerRestApi).startSubscription(subscription1.getName(), REQUESTED_BY); + + assertDoesNotThrow(() -> subscriptionService.start(subscription1.getName(), REQUESTED_BY).get()); + verify(controllerRestApi, times(1)).startSubscription(subscription1.getName(), REQUESTED_BY); + } + + @Test + void stopSubscription_SuccessfulStop() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + CompletableFuture stopFuture = CompletableFuture.completedFuture( + SubscriptionOperation.stopOp(subscription1.getName(), REQUESTED_BY) + ); + doReturn(stopFuture).when(controllerRestApi).stopSubscription(subscription1.getName(), REQUESTED_BY); + + assertDoesNotThrow(() -> subscriptionService.stop(subscription1.getName(), REQUESTED_BY).get()); + verify(controllerRestApi, times(1)).stopSubscription(subscription1.getName(), REQUESTED_BY); + } + + @Test + void startSubscription_NotProvisioned_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + doThrow(new RuntimeException("Provision failed")).when(shardProvisioner).provision(any(), any()); + + assertThrows( + RuntimeException.class, + () -> subscriptionService.createSubscription(unGroupedTopic, subscription1, project1) + ); + + InvalidOperationForResourceException exception = assertThrows( + InvalidOperationForResourceException.class, + () -> subscriptionService.start(subscription1.getName(), REQUESTED_BY).get() + ); + + String expectedMessage = "Subscription 'Project1.Sub1' is not well-provisioned for this operation."; + assertEquals(expectedMessage, exception.getMessage()); + verify(controllerRestApi, times(0)).startSubscription(subscription1.getName(), REQUESTED_BY); + } + + @Test + void stopSubscription_NotProvisioned_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + doThrow(new RuntimeException("Provision failed")).when(shardProvisioner).provision(any(), any()); + + assertThrows( + RuntimeException.class, + () -> subscriptionService.createSubscription(unGroupedTopic, subscription1, project1) + ); + + InvalidOperationForResourceException exception = assertThrows( + InvalidOperationForResourceException.class, + () -> subscriptionService.stop(subscription1.getName(), REQUESTED_BY).get() + ); + + String expectedMessage = "Subscription 'Project1.Sub1' is not well-provisioned for this operation."; + assertEquals(expectedMessage, exception.getMessage()); + verify(controllerRestApi, times(0)).stopSubscription(subscription1.getName(), REQUESTED_BY); + } + + @Test + void restoreSubscription_Success() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + subscription1.markInactive(); + varadhiMetaStore.updateSubscription(subscription1); + + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(status).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + + CompletableFuture result = + subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY); + + assertDoesNotThrow(() -> { + VaradhiSubscription restoredSubscription = result.get(); + assertEquals(VaradhiSubscription.State.CREATED, restoredSubscription.getStatus().getState()); + }); + verify(varadhiMetaStore, times(2)).updateSubscription(subscription1); + } + + @Test + void restoreSubscription_MetaStoreFailure_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + subscription1.markInactive(); + varadhiMetaStore.updateSubscription(subscription1); + + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(status).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + doThrow(new RuntimeException("MetaStore update failed")).when(varadhiMetaStore).updateSubscription(any()); + + CompletableFuture result = + subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY); + + Exception exception = assertThrows(ExecutionException.class, result::get); + Throwable cause = exception.getCause(); + assertEquals(RuntimeException.class, cause.getClass()); + assertEquals("MetaStore update failed", cause.getMessage()); + verify(varadhiMetaStore, times(2)).updateSubscription(subscription1); + } + + @Test + void restoreSubscription_AlreadyWellProvisioned_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + InvalidOperationForResourceException exception = assertThrows( + InvalidOperationForResourceException.class, () -> { + subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY).get(); + } + ); + + String expectedMessage = "Subscription '%s' is already active.".formatted(subscription1.getName()); + assertEquals(expectedMessage, exception.getMessage()); + verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); + } + + private void assertSubscriptionsEqual(VaradhiSubscription expected, VaradhiSubscription actual) { + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getTopic(), actual.getTopic()); + assertEquals(expected.isGrouped(), actual.isGrouped()); + assertEquals(expected.getDescription(), actual.getDescription()); } private CompletableFuture updateSubscription(VaradhiSubscription to) { return subscriptionService.updateSubscription( to.getName(), to.getVersion(), to.getDescription(), to.isGrouped(), to.getEndpoint(), - to.getRetryPolicy(), to.getConsumptionPolicy(), requestedBy + to.getRetryPolicy(), to.getConsumptionPolicy(), REQUESTED_BY ); } - } diff --git a/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java b/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java index 38debe44..1793c4d0 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java +++ b/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java @@ -1,8 +1,8 @@ package com.flipkart.varadhi.web; import com.fasterxml.jackson.databind.JavaType; -import com.flipkart.varadhi.verticles.webserver.WebServerVerticle; import com.flipkart.varadhi.utils.JsonMapper; +import com.flipkart.varadhi.verticles.webserver.WebServerVerticle; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; @@ -18,16 +18,22 @@ import io.vertx.ext.web.client.HttpResponse; import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.handler.BodyHandler; -import org.junit.jupiter.api.Assertions; import java.util.Collection; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import static io.vertx.core.http.HttpMethod.*; +import static io.vertx.core.http.HttpMethod.DELETE; +import static io.vertx.core.http.HttpMethod.GET; +import static io.vertx.core.http.HttpMethod.POST; +import static io.vertx.core.http.HttpMethod.PUT; import static java.net.HttpURLConnection.HTTP_OK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class WebTestBase { @@ -37,14 +43,19 @@ public class WebTestBase { protected Router router; protected BodyHandler bodyHandler; protected FailureHandler failureHandler; - protected int defaultPort = 9090; //use port different from default 8080, conflicts with server port in e2e. - protected String defaultHost = "localhost"; - - public static R jsonDeserialize(String data, Class collectionClass, Class clazz) - throws Exception { - JavaType type = JsonMapper.getMapper().getTypeFactory().constructCollectionType(collectionClass, clazz); - return JsonMapper.getMapper().readValue(data, type); + protected static final int DEFAULT_PORT = 9090; + protected static final String DEFAULT_HOST = "localhost"; + private static final long LATCH_TIMEOUT = 60L; + + public static R jsonDeserialize(String data, Class collectionClass, Class clazz) { + try { + JavaType valueType = + JsonMapper.getMapper().getTypeFactory().constructCollectionType(collectionClass, clazz); + return JsonMapper.getMapper().readValue(data, valueType); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize JSON", e); + } } public void setUp() throws InterruptedException { @@ -52,15 +63,16 @@ public void setUp() throws InterruptedException { router = Router.router(vertx); server = vertx.createHttpServer(getHttpServerOptions()); webClient = WebClient.create(vertx); + bodyHandler = BodyHandler.create(false); + failureHandler = new FailureHandler(); + CountDownLatch latch = new CountDownLatch(1); server.requestHandler(router).listen().onComplete(onSuccess(res -> latch.countDown())); awaitLatch(latch); - bodyHandler = BodyHandler.create(false); - failureHandler = new FailureHandler(); } protected HttpServerOptions getHttpServerOptions() { - return new HttpServerOptions().setPort(defaultPort).setHost(defaultHost); + return new HttpServerOptions().setPort(DEFAULT_PORT).setHost(DEFAULT_HOST); } public void tearDown() throws InterruptedException { @@ -69,8 +81,8 @@ public void tearDown() throws InterruptedException { } if (server != null) { CountDownLatch latch = new CountDownLatch(1); - server.close().onComplete((asyncResult) -> { - assertTrue(asyncResult.succeeded()); + server.close().onComplete(asyncResult -> { + assertTrue(asyncResult.succeeded(), "Server close failed"); latch.countDown(); }); awaitLatch(latch); @@ -87,131 +99,126 @@ public Route setupFailureHandler(Route route) { public HttpRequest createRequest(HttpMethod method, String path) { if (POST == method) { - return webClient.post(defaultPort, defaultHost, path); + return webClient.post(DEFAULT_PORT, DEFAULT_HOST, path); } else if (GET == method) { - return webClient.get(defaultPort, defaultHost, path); + return webClient.get(DEFAULT_PORT, DEFAULT_HOST, path); } else if (DELETE == method) { - return webClient.delete(defaultPort, defaultHost, path); + return webClient.delete(DEFAULT_PORT, DEFAULT_HOST, path); } else if (PUT == method) { - return webClient.put(defaultPort, defaultHost, path); + return webClient.put(DEFAULT_PORT, DEFAULT_HOST, path); + } else { + throw new UnsupportedOperationException("Unsupported HTTP method"); } - Assertions.fail("Unsupported method"); - return null; } - public R sendRequestWithByteBufferBody(HttpRequest request, byte[] payload, Class responseClazz) + public R sendRequestWithPayload(HttpRequest request, byte[] payload, Class responseClass) throws InterruptedException { HttpResponse response = sendRequest(request, payload); - Assertions.assertEquals(HTTP_OK, response.statusCode()); - return JsonMapper.jsonDeserialize(response.bodyAsString(), responseClazz); + assertEquals(HTTP_OK, response.statusCode(), "Unexpected status code"); + return JsonMapper.jsonDeserialize(response.bodyAsString(), responseClass); } - public R sendRequestWithByteBufferBody( - HttpRequest request, byte[] payload, int statusCode, String statusMessage, Class responseClazz - ) - throws InterruptedException { + public R sendRequestWithPayload( + HttpRequest request, byte[] payload, int expectedStatusCode, String expectedStatusMessage, + Class responseClass + ) throws InterruptedException { HttpResponse response = sendRequest(request, payload); - Assertions.assertEquals(statusCode, response.statusCode()); - if (null != statusMessage) { - Assertions.assertEquals(statusMessage, response.statusMessage()); - } - if (null != responseClazz) { - return JsonMapper.jsonDeserialize(response.bodyAsString(), responseClazz); - } - return null; - } + assertEquals(expectedStatusCode, response.statusCode(), "Unexpected status code"); + Optional.ofNullable(expectedStatusMessage) + .ifPresent(statusMessage -> + assertEquals(statusMessage, response.statusMessage(), "Unexpected status message")); - public R sendRequestWithBody(HttpRequest request, T entity, Class responseClazz) - throws InterruptedException { - return sendRequestWithByteBufferBody(request, JsonMapper.jsonSerialize(entity).getBytes(), responseClazz); + return Optional.ofNullable(responseClass) + .map(clazz -> JsonMapper.jsonDeserialize(response.bodyAsString(), clazz)) + .orElse(null); } - public R sendRequestWithBody( - HttpRequest request, T entity, int statusCode, String statusMessage, Class responseClazz - ) + public R sendRequestWithEntity(HttpRequest request, T entity, Class responseClass) throws InterruptedException { - return sendRequestWithByteBufferBody( - request, JsonMapper.jsonSerialize(entity).getBytes(), statusCode, statusMessage, responseClazz); + return sendRequestWithPayload(request, JsonMapper.jsonSerialize(entity).getBytes(), responseClass); + } + + public R sendRequestWithEntity( + HttpRequest request, T entity, int expectedStatusCode, String expectedStatusMessage, + Class responseClass + ) throws InterruptedException { + return sendRequestWithPayload( + request, JsonMapper.jsonSerialize(entity).getBytes(), + expectedStatusCode, expectedStatusMessage, responseClass + ); } - public R sendRequestWithoutBody(HttpRequest request, Class responseClazz) + public R sendRequestWithoutPayload(HttpRequest request, Class responseClass) throws InterruptedException { HttpResponse response = sendRequest(request, null); - Assertions.assertEquals(HTTP_OK, response.statusCode()); + assertEquals(HTTP_OK, response.statusCode(), "Unexpected status code"); - System.out.println("BODY == " + response.bodyAsString()); - if (null != responseClazz) { - return JsonMapper.jsonDeserialize(response.bodyAsString(), responseClazz); - } - return null; + return Optional.ofNullable(responseClass) + .map(clazz -> JsonMapper.jsonDeserialize(response.bodyAsString(), clazz)) + .orElse(null); } - public byte[] sendRequestWithoutBody(HttpRequest request) - throws InterruptedException { + + public byte[] sendRequestWithoutPayload(HttpRequest request) throws InterruptedException { HttpResponse response = sendRequest(request, null); - Assertions.assertEquals(HTTP_OK, response.statusCode()); + assertEquals(HTTP_OK, response.statusCode(), "Unexpected status code"); return response.body().getBytes(); } - public void sendRequestWithoutBody(HttpRequest request, int statusCode, String statusMessage) - throws InterruptedException { + public void sendRequestWithoutPayload( + HttpRequest request, int expectedStatusCode, String expectedStatusMessage + ) throws InterruptedException { HttpResponse response = sendRequest(request, null); - Assertions.assertEquals(statusCode, response.statusCode()); - if (null != statusMessage) { - Assertions.assertEquals(statusMessage, response.statusMessage()); - } + assertEquals(expectedStatusCode, response.statusCode(), "Unexpected status code"); + + Optional.ofNullable(expectedStatusMessage) + .ifPresent(statusMessage -> + assertEquals(statusMessage, response.statusMessage(), "Unexpected status message")); + ErrorResponse error = JsonMapper.jsonDeserialize(response.body().getBytes(), ErrorResponse.class); - Assertions.assertEquals(statusMessage, error.reason()); + assertEquals(expectedStatusMessage, error.reason()); } public HttpResponse sendRequest(HttpRequest request, byte[] payload) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); - Future> responseFuture; - if (null != payload) { - Buffer reqBuffer = Buffer.buffer(payload); - responseFuture = request.sendBuffer(reqBuffer); - } else { - responseFuture = request.send(); - } + Future> responseFuture = payload != null + ? request.sendBuffer(Buffer.buffer(payload)) + : request.send(); - class PostResponseCapture { - Throwable throwable; - T response; - } PostResponseCapture> responseCapture = new PostResponseCapture<>(); - responseFuture.onComplete(r -> { - responseCapture.response = r.result(); - responseCapture.throwable = r.cause(); + responseFuture.onComplete(result -> { + if (result.succeeded()) { + responseCapture.response = result.result(); + } else { + responseCapture.throwable = result.cause(); + } latch.countDown(); }); awaitLatch(latch); - // post shouldn't fail. - Assertions.assertNull(responseCapture.throwable); + + assertNull(responseCapture.throwable, "Request failed"); return responseCapture.response; } public void awaitLatch(CountDownLatch latch) throws InterruptedException { - awaitLatch(latch, 60, TimeUnit.SECONDS); - } - - public void awaitLatch(CountDownLatch latch, long timeout, TimeUnit unit) throws InterruptedException { - assertTrue(latch.await(timeout, unit)); + assertTrue(latch.await(LATCH_TIMEOUT, TimeUnit.SECONDS), "Latch await timeout"); } public Handler> onSuccess(Consumer consumer) { return result -> { if (result.failed()) { result.cause().printStackTrace(); - fail(result.cause().getMessage()); + fail("Async operation failed: " + result.cause().getMessage()); } else { consumer.accept(result.result()); } }; } - public void fail(String message) { - Assertions.fail(message); + private static class PostResponseCapture { + private T response; + private Throwable throwable; } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/DlqHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/DlqHandlersTest.java index e5b226d1..f744069f 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/DlqHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/DlqHandlersTest.java @@ -67,10 +67,10 @@ public void PostTest() throws InterruptedException { } private VaradhiSubscription prepUnsidelineRequest(UnsidelineRequest unsidelineRequest, ArgumentCaptor captor) { - SubscriptionResource subResource = getSubscriptionResource("sub12", project, topicResource); - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - VaradhiSubscription subscription = getUngroupedSubscription("sub12", project, vTopic); - doReturn(project).when(projectService).getCachedProject(project.getName()); + SubscriptionResource subResource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = createUngroupedSubscription("sub12", PROJECT, vTopic); + doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(subscription).when(subscriptionService).getSubscription(subResource.getSubscriptionInternalName()); SubscriptionOperation op = SubscriptionOperation.unsidelineOp(subscription.getName(), unsidelineRequest, "foobar"); CompletableFuture future = CompletableFuture.completedFuture(op); @@ -80,11 +80,12 @@ private VaradhiSubscription prepUnsidelineRequest(UnsidelineRequest unsidelineRe @Test void dlqUnsidelineOfTimeStamp() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", project)); + HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", PROJECT)); ArgumentCaptor captor = ArgumentCaptor.forClass(UnsidelineRequest.class); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofFailedAt(System.currentTimeMillis()); prepUnsidelineRequest(unsidelineRequest, captor); - SubscriptionOperation operation = sendRequestWithBody(request, unsidelineRequest, SubscriptionOperation.class); + SubscriptionOperation operation = + sendRequestWithEntity(request, unsidelineRequest, SubscriptionOperation.class); UnsidelineRequest actual = captor.getValue(); assertEquals(unsidelineRequest.getLatestFailedAt(), actual.getLatestFailedAt()); assertEquals(unsidelineRequest.getMessageIds().size(), 0); @@ -94,67 +95,81 @@ void dlqUnsidelineOfTimeStamp() throws InterruptedException { @Test void dlqUnsidelineOfGroupIds() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", project)); + HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", PROJECT)); ArgumentCaptor captor = ArgumentCaptor.forClass(UnsidelineRequest.class); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofGroupIds(List.of("grp1", "grp2")); VaradhiSubscription subscription = prepUnsidelineRequest(unsidelineRequest, captor); subscription.getProperties().put(UNSIDELINE_API_GROUP_COUNT, "5"); - sendRequestWithBody(request, unsidelineRequest, 400, "Selective unsideline is not yet supported.", ErrorResponse.class); + sendRequestWithEntity( + request, unsidelineRequest, 400, "Selective unsideline is not yet supported.", ErrorResponse.class); verify(dlqService, never()).unsideline(any(), any(), any()); } @Test void dlqUnsidelineOfHigherGroupCount() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", project)); + HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", PROJECT)); ArgumentCaptor captor = ArgumentCaptor.forClass(UnsidelineRequest.class); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofGroupIds(List.of("grp1", "grp2")); VaradhiSubscription subscription = prepUnsidelineRequest(unsidelineRequest, captor); subscription.getProperties().put(UNSIDELINE_API_GROUP_COUNT, "1"); - sendRequestWithBody(request, unsidelineRequest, 400, "Number of groupIds in one API call cannot be more than 1.", ErrorResponse.class); + sendRequestWithEntity( + request, unsidelineRequest, 400, "Number of groupIds in one API call cannot be more than 1.", + ErrorResponse.class + ); verify(dlqService, never()).unsideline(any(), any(), any()); } @Test void dlqUnsidelineOfMessageIds() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", project)); + HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", PROJECT)); ArgumentCaptor captor = ArgumentCaptor.forClass(UnsidelineRequest.class); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofMessageIds(List.of("mid1", "mid2")); VaradhiSubscription subscription = prepUnsidelineRequest(unsidelineRequest, captor); subscription.getProperties().put(UNSIDELINE_API_MESSAGE_COUNT, "5"); - sendRequestWithBody(request, unsidelineRequest, 400, "Selective unsideline is not yet supported.", ErrorResponse.class); + sendRequestWithEntity( + request, unsidelineRequest, 400, "Selective unsideline is not yet supported.", ErrorResponse.class); verify(dlqService, never()).unsideline(any(), any(), any()); } @Test void dlqUnsidelineOfHigherMessageCount() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", project)); + HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", PROJECT)); ArgumentCaptor captor = ArgumentCaptor.forClass(UnsidelineRequest.class); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofMessageIds(List.of("mid1", "mid2")); VaradhiSubscription subscription = prepUnsidelineRequest(unsidelineRequest, captor); subscription.getProperties().put(UNSIDELINE_API_MESSAGE_COUNT, "1"); - sendRequestWithBody(request, unsidelineRequest, 400, "Number of messageIds in one API call cannot be more than 1.", ErrorResponse.class); + sendRequestWithEntity( + request, unsidelineRequest, 400, "Number of messageIds in one API call cannot be more than 1.", + ErrorResponse.class + ); verify(dlqService, never()).unsideline(any(), any(), any()); } @Test void dlqUnsidelineNoCriteria() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", project)); + HttpRequest request = createRequest(HttpMethod.POST, getUnsidelineUrl("sub12", PROJECT)); ArgumentCaptor captor = ArgumentCaptor.forClass(UnsidelineRequest.class); UnsidelineRequest unsidelineRequest = UnsidelineRequest.ofFailedAt(UnsidelineRequest.UNSPECIFIED_TS); VaradhiSubscription subscription = prepUnsidelineRequest(unsidelineRequest, captor); subscription.getProperties().put(UNSIDELINE_API_MESSAGE_COUNT, "1"); - sendRequestWithBody(request, unsidelineRequest, 400, "At least one unsideline criteria needs to be specified.", ErrorResponse.class); + sendRequestWithEntity( + request, unsidelineRequest, 400, "At least one unsideline criteria needs to be specified.", + ErrorResponse.class + ); verify(dlqService, never()).unsideline(any(), any(), any()); } @Test void testDlqGetMessages() throws Exception { - HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", project, System.currentTimeMillis(), "", -1)); + HttpRequest request = createRequest( + HttpMethod.GET, + getMessagesUrl("sub12", PROJECT, System.currentTimeMillis(), "", -1) + ); ArgumentCaptor captor = ArgumentCaptor.forClass(DlqPageMarker.class); VaradhiSubscription subscription = setupSubscriptionForGetMessages(); - List shard1Messages = List.of(getDlqMessage(1), getDlqMessage(2), getDlqMessage(1)); - List shard2Messages = List.of(getDlqMessage(3), getDlqMessage(3), getDlqMessage(4)); + List shard1Messages = List.of(createDlqMessage(1), createDlqMessage(2), createDlqMessage(1)); + List shard2Messages = List.of(createDlqMessage(3), createDlqMessage(3), createDlqMessage(4)); DlqPageMarker pageMarker = new DlqPageMarker(new HashMap<>()); pageMarker.addShardMarker(1, shard1Messages.get(1).getOffset().toString() + "," + shard1Messages.get(2).getOffset().toString()); pageMarker.addShardMarker(2, shard1Messages.get(1).getOffset().toString() + "," + shard1Messages.get(2).getOffset().toString()); @@ -167,7 +182,7 @@ void testDlqGetMessages() throws Exception { responseWriter.accept(DlqMessagesResponse.of(pageMarker, new ArrayList<>())); return future; }).when(dlqService).getMessages(eq(subscription), anyLong(), captor.capture(), anyInt(), any()); - byte[] response = sendRequestWithoutBody(request); + byte[] response = sendRequestWithoutPayload(request); List dlqResponses = readMessageResponse(response); assertEquals(3, dlqResponses.size()); assertEquals(pageMarker.toString(), dlqResponses.get(2).getNextPage()); @@ -176,13 +191,14 @@ void testDlqGetMessages() throws Exception { @Test void testDlqGetMessagesFromMarker() throws Exception { - List shard1Messages = List.of(getDlqMessage(1), getDlqMessage(2), getDlqMessage(1)); - List shard2Messages = List.of(getDlqMessage(3), getDlqMessage(3), getDlqMessage(4)); + List shard1Messages = List.of(createDlqMessage(1), createDlqMessage(2), createDlqMessage(1)); + List shard2Messages = List.of(createDlqMessage(3), createDlqMessage(3), createDlqMessage(4)); DlqPageMarker pageMarker = new DlqPageMarker(new HashMap<>()); pageMarker.addShardMarker(1, shard1Messages.get(1).getOffset().toString() + "," + shard1Messages.get(2).getOffset().toString()); pageMarker.addShardMarker(2, shard1Messages.get(1).getOffset().toString() + "," + shard1Messages.get(2).getOffset().toString()); - HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", project, 0, pageMarker.toString(), -1)); + HttpRequest request = + createRequest(HttpMethod.GET, getMessagesUrl("sub12", PROJECT, 0, pageMarker.toString(), -1)); ArgumentCaptor captor = ArgumentCaptor.forClass(DlqPageMarker.class); VaradhiSubscription subscription = setupSubscriptionForGetMessages(); @@ -194,7 +210,7 @@ void testDlqGetMessagesFromMarker() throws Exception { responseWriter.accept(DlqMessagesResponse.of(DlqPageMarker.fromString(""), new ArrayList<>())); return future; }).when(dlqService).getMessages(eq(subscription), anyLong(), captor.capture(), anyInt(), any()); - byte[] response = sendRequestWithoutBody(request); + byte[] response = sendRequestWithoutPayload(request); List dlqResponses = readMessageResponse(response); assertEquals(3, dlqResponses.size()); assertEquals(pageMarker.toString(), captor.getValue().toString()); @@ -204,37 +220,46 @@ void testDlqGetMessagesFromMarker() throws Exception { @Test void testDlqGetMessagesInvalidLimit() throws Exception { - HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", project, System.currentTimeMillis(), "", 5000)); + HttpRequest request = createRequest( + HttpMethod.GET, + getMessagesUrl("sub12", PROJECT, System.currentTimeMillis(), "", 5000) + ); setupSubscriptionForGetMessages(); - sendRequestWithoutBody(request, 400, "Limit cannot be more than 100."); + sendRequestWithoutPayload(request, 400, "Limit cannot be more than 100."); } @Test void testDlqGetMessagesNoCriteria() throws Exception { - HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", project, 0, "", -1)); + HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", PROJECT, 0, "", -1)); setupSubscriptionForGetMessages(); - sendRequestWithoutBody(request, 400, "At least one get messages criteria needs to be specified."); + sendRequestWithoutPayload(request, 400, "At least one get messages criteria needs to be specified."); } @Test void testDlqGetMessagesMultipleCriteria() throws Exception { - HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", project, System.currentTimeMillis(), "1=mId:1:2:3", -1)); + HttpRequest request = createRequest( + HttpMethod.GET, + getMessagesUrl("sub12", PROJECT, System.currentTimeMillis(), "1=mId:1:2:3", -1) + ); setupSubscriptionForGetMessages(); - sendRequestWithoutBody(request, 400, "Only one of the get messages criteria should be specified."); + sendRequestWithoutPayload(request, 400, "Only one of the get messages criteria should be specified."); } @Test void testDlqGetMessagesInvalidNextPageMarker() throws Exception { - HttpRequest request = createRequest(HttpMethod.GET, getMessagesUrl("sub12", project, System.currentTimeMillis(), "mId:1:2:3", -1)); + HttpRequest request = createRequest( + HttpMethod.GET, + getMessagesUrl("sub12", PROJECT, System.currentTimeMillis(), "mId:1:2:3", -1) + ); setupSubscriptionForGetMessages(); - sendRequestWithoutBody(request, 400, "Invalid page marker: mId:1:2:3"); + sendRequestWithoutPayload(request, 400, "Invalid page marker: mId:1:2:3"); } private VaradhiSubscription setupSubscriptionForGetMessages(){ - SubscriptionResource subResource = getSubscriptionResource("sub12", project, topicResource); - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - VaradhiSubscription subscription = getUngroupedSubscription("sub12", project, vTopic); - doReturn(project).when(projectService).getCachedProject(project.getName()); + SubscriptionResource subResource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = createUngroupedSubscription("sub12", PROJECT, vTopic); + doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(subscription).when(subscriptionService).getSubscription(subResource.getSubscriptionInternalName()); return subscription; } @@ -266,11 +291,12 @@ private List readMessageResponse(byte[] response) throws Ex private String getUnsidelineUrl(String subscriptionName, Project project) { - return String.join("/", getSubscriptionUrl(subscriptionName, project), "dlq", "messages", "unsideline"); + return String.join("/", buildSubscriptionUrl(subscriptionName, project), "dlq", "messages", "unsideline"); } private String getMessagesUrl(String subscriptionName, Project project, long failedAt, String nextPage, int limit) { - StringBuilder sb = new StringBuilder(String.join("/", getSubscriptionUrl(subscriptionName, project), "dlq", "messages")); + StringBuilder sb = + new StringBuilder(String.join("/", buildSubscriptionUrl(subscriptionName, project), "dlq", "messages")); ArrayList queryParams = new ArrayList<>(); if ( 0 != failedAt) { queryParams.add(String.format("earliestFailedAt=%d", failedAt)); diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/OrgHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/OrgHandlersTest.java index aea756e6..432402bb 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/OrgHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/OrgHandlersTest.java @@ -64,18 +64,18 @@ public void testOrgCreation() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.POST, orgsPath); Org org1 = Org.of("name1"); doReturn(org1).when(orgService).createOrg(eq(org1)); - Org org1Created = sendRequestWithBody(request, org1, Org.class); + Org org1Created = sendRequestWithEntity(request, org1, Org.class); Assertions.assertEquals(org1, org1Created); verify(orgService, times(1)).createOrg(eq(org1)); String duplicateOrgError = String.format("Org(%s) already exists. Org is globally unique.", org1.getName()); doThrow(new DuplicateResourceException(duplicateOrgError)).when(orgService).createOrg(org1); - ErrorResponse response = sendRequestWithBody(request, org1, 409, duplicateOrgError, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity(request, org1, 409, duplicateOrgError, ErrorResponse.class); Assertions.assertEquals(duplicateOrgError, response.reason()); String someInternalError = "Some random error"; doThrow(new MetaStoreException(someInternalError)).when(orgService).createOrg(org1); - response = sendRequestWithBody(request, org1, 500, someInternalError, ErrorResponse.class); + response = sendRequestWithEntity(request, org1, 500, someInternalError, ErrorResponse.class); Assertions.assertEquals(someInternalError, response.reason()); } @@ -95,7 +95,7 @@ private void sendInvalidName(String name) throws InterruptedException { HttpRequest request = createRequest(HttpMethod.POST, orgsPath); String orgNameErr = "Invalid Org name. Check naming constraints."; Org org = Org.of(name); - ErrorResponse response = sendRequestWithBody(request, org, 400, orgNameErr, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity(request, org, 400, orgNameErr, ErrorResponse.class); Assertions.assertEquals(orgNameErr, response.reason()); } @@ -106,13 +106,13 @@ public void testOrgGet() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.GET, getOrgUrl(org1.getName())); doReturn(org1).when(orgService).getOrg(org1.getName()); - Org org1Get = sendRequestWithoutBody(request, Org.class); + Org org1Get = sendRequestWithoutPayload(request, Org.class); Assertions.assertEquals(org1, org1Get); verify(orgService, times(1)).getOrg(org1.getName()); String notFoundError = String.format("Org(%s) not found.", org1.getName()); doThrow(new ResourceNotFoundException(notFoundError)).when(orgService).getOrg(org1.getName()); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); } @Test @@ -138,16 +138,16 @@ public void testOrgDelete() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.DELETE, getOrgUrl(org1.getName())); doNothing().when(orgService).deleteOrg(org1.getName()); - sendRequestWithoutBody(request, null); + sendRequestWithoutPayload(request, null); verify(orgService, times(1)).deleteOrg(org1.getName()); String notFoundError = String.format("Org(%s) not found.", org1.getName()); doThrow(new ResourceNotFoundException(notFoundError)).when(orgService).deleteOrg(org1.getName()); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); String invalidOpError = String.format("Can not delete Org(%s) as it has associated Team(s).", org1.getName()); doThrow(new InvalidOperationForResourceException(invalidOpError)).when(orgService).deleteOrg(org1.getName()); - sendRequestWithoutBody(request, 409, invalidOpError); + sendRequestWithoutPayload(request, 409, invalidOpError); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/ProjectHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/ProjectHandlersTest.java index 9c1e55cf..28f6180a 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/ProjectHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/ProjectHandlersTest.java @@ -80,24 +80,24 @@ public void testProjectCreate() throws InterruptedException { Project p1 = getProject("project1"); doReturn(p1).when(projectService).createProject(p1); - Project p1Created = sendRequestWithBody(request, p1, Project.class); + Project p1Created = sendRequestWithEntity(request, p1, Project.class); Assertions.assertEquals(p1, p1Created); verify(projectService, times(1)).createProject(eq(p1)); String orgNotFoundError = String.format("Org(%s) not found.", t1.getOrg()); doThrow(new ResourceNotFoundException(orgNotFoundError)).when(projectService).createProject(p1); - ErrorResponse response = sendRequestWithBody(request, p1, 404, orgNotFoundError, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity(request, p1, 404, orgNotFoundError, ErrorResponse.class); Assertions.assertEquals(orgNotFoundError, response.reason()); String duplicateOrgError = String.format("Project(%s) already exists. Projects are globally unique.", p1.getName()); doThrow(new DuplicateResourceException(duplicateOrgError)).when(projectService).createProject(p1); - response = sendRequestWithBody(request, p1, 409, duplicateOrgError, ErrorResponse.class); + response = sendRequestWithEntity(request, p1, 409, duplicateOrgError, ErrorResponse.class); Assertions.assertEquals(duplicateOrgError, response.reason()); String someInternalError = "Some random error"; doThrow(new MetaStoreException(someInternalError)).when(projectService).createProject(p1); - response = sendRequestWithBody(request, p1, 500, someInternalError, ErrorResponse.class); + response = sendRequestWithEntity(request, p1, 500, someInternalError, ErrorResponse.class); Assertions.assertEquals(someInternalError, response.reason()); } @@ -109,13 +109,13 @@ public void testProjectGet() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.GET, getProjectUrl(p1.getName())); doReturn(p1).when(projectService).getProject(p1.getName()); - Project p1Get = sendRequestWithoutBody(request, Project.class); + Project p1Get = sendRequestWithoutPayload(request, Project.class); Assertions.assertEquals(p1, p1Get); verify(projectService, times(1)).getProject(p1.getName()); String notFoundError = String.format("Project(%s) not found.", p1.getName()); doThrow(new ResourceNotFoundException(notFoundError)).when(projectService).getProject(p1.getName()); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); } @@ -124,12 +124,12 @@ public void testProjectUpdate() throws Exception { Project p1 = getProject("project1"); HttpRequest request = createRequest(HttpMethod.PUT, getProjectsUrl()); doReturn(p1).when(projectService).updateProject(p1); - Project p1Updated = sendRequestWithBody(request, p1, Project.class); + Project p1Updated = sendRequestWithEntity(request, p1, Project.class); Assertions.assertEquals(p1, p1Updated); String argumentError = String.format("Project(%s) can not be moved across organisation.", p1.getName()); doThrow(new IllegalArgumentException(argumentError)).when(projectService).updateProject(p1); - ErrorResponse response = sendRequestWithBody(request, p1, 400, argumentError, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity(request, p1, 400, argumentError, ErrorResponse.class); Assertions.assertEquals(argumentError, response.reason()); String invalidOpError = @@ -138,7 +138,7 @@ public void testProjectUpdate() throws Exception { p1.getName() ); doThrow(new InvalidOperationForResourceException(invalidOpError)).when(projectService).updateProject(p1); - response = sendRequestWithBody(request, p1, 409, invalidOpError, ErrorResponse.class); + response = sendRequestWithEntity(request, p1, 409, invalidOpError, ErrorResponse.class); Assertions.assertEquals(invalidOpError, response.reason()); } @@ -148,18 +148,18 @@ public void testProjectDelete() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.DELETE, getProjectUrl(p1.getName())); doNothing().when(projectService).deleteProject(p1.getName()); - sendRequestWithoutBody(request, null); + sendRequestWithoutPayload(request, null); verify(projectService, times(1)).deleteProject(p1.getName()); String notFoundError = String.format("Project(%s) not found.", p1.getName()); doThrow(new ResourceNotFoundException(notFoundError)).when(projectService) .deleteProject(p1.getName()); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); String invalidOpError = String.format("Can not delete Project(%s), it has associated entities.", p1.getName()); doThrow(new InvalidOperationForResourceException(invalidOpError)).when(projectService) .deleteProject(p1.getName()); - sendRequestWithoutBody(request, 409, invalidOpError); + sendRequestWithoutPayload(request, 409, invalidOpError); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java index 449fafcd..62b93fd5 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java @@ -1,64 +1,110 @@ package com.flipkart.varadhi.web.admin; import com.flipkart.varadhi.config.RestOptions; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceDeletionType; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.exceptions.ResourceNotFoundException; import com.flipkart.varadhi.web.ErrorResponse; import com.flipkart.varadhi.web.entities.SubscriptionResource; import com.flipkart.varadhi.web.v1.admin.SubscriptionHandlers; +import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Route; +import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.handler.HttpException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; import java.util.List; import java.util.concurrent.CompletableFuture; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SubscriptionHandlersTest extends SubscriptionTestBase { -public class SubscriptionHandlersTest extends SubscriptionTestBase { private SubscriptionHandlers subscriptionHandlers; + @Captor + private ArgumentCaptor stringCaptor; + + @Captor + private ArgumentCaptor integerCaptor; @BeforeEach public void PreTest() throws InterruptedException { super.setUp(); + MockitoAnnotations.openMocks(this); subscriptionHandlers = new SubscriptionHandlers( subscriptionService, projectService, topicService, subscriptionFactory, new RestOptions()); + configureRoutes(); + } - Route routeCreate = router.post("/projects/:project/subscriptions").handler(bodyHandler).handler(ctx -> { - subscriptionHandlers.setSubscription(ctx); - ctx.next(); - }) - .handler(wrapBlocking(subscriptionHandlers::create)); - setupFailureHandler(routeCreate); - - Route routeGet = router.get("/projects/:project/subscriptions/:subscription") - .handler(wrapBlocking(subscriptionHandlers::get)); - setupFailureHandler(routeGet); - - Route routeListAll = - router.get("/projects/:project/subscriptions").handler(wrapBlocking(subscriptionHandlers::list)); - setupFailureHandler(routeListAll); - - Route routeDelete = router.delete("/projects/:project/subscriptions/:subscription") - .handler(wrapBlocking(subscriptionHandlers::delete)); - setupFailureHandler(routeDelete); + private void configureRoutes() { + createRoute( + HttpMethod.POST, "/projects/:project/subscriptions", + subscriptionHandlers::create, true + ); + createRoute( + HttpMethod.GET, "/projects/:project/subscriptions/:subscription", + subscriptionHandlers::get, false + ); + createRoute( + HttpMethod.GET, "/projects/:project/subscriptions", + subscriptionHandlers::list, false + ); + createRoute( + HttpMethod.DELETE, "/projects/:project/subscriptions/:subscription", + subscriptionHandlers::delete, false + ); + createRoute( + HttpMethod.PUT, "/projects/:project/subscriptions/:subscription", + subscriptionHandlers::update, true + ); + createRoute( + HttpMethod.POST, "/projects/:project/subscriptions/:subscription/restore", + subscriptionHandlers::restore, false + ); + createRoute( + HttpMethod.POST, "/projects/:project/subscriptions/:subscription/start", + subscriptionHandlers::start, false + ); + createRoute( + HttpMethod.POST, "/projects/:project/subscriptions/:subscription/stop", + subscriptionHandlers::stop, false + ); + } - Route routeUpdate = router.put("/projects/:project/subscriptions/:subscription").handler(bodyHandler) - .handler(ctx -> { - subscriptionHandlers.setSubscription(ctx); - ctx.next(); - }) - .handler(wrapBlocking(subscriptionHandlers::update)); - setupFailureHandler(routeUpdate); + private void createRoute(HttpMethod method, String path, Handler handler, boolean requiresBody) { + Route route = router.route(method, path); + if (requiresBody) { + route.handler(bodyHandler).handler(ctx -> { + subscriptionHandlers.setSubscription(ctx); + ctx.next(); + }); + } + route.handler(wrapBlocking(handler)); + setupFailureHandler(route); } @AfterEach @@ -67,133 +113,361 @@ public void PostTest() throws InterruptedException { } @Test - void testSubscriptionCreate() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getSubscriptionsUrl(project)); - SubscriptionResource resource = getSubscriptionResource("sub12", project, topicResource); - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - doReturn(vTopic).when(topicService).get(topicResource.getProject() + "." + topicResource.getName()); + void createSubscription_ValidInput_CreatesSubscriptionSuccessfully() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + VaradhiSubscription subscription = createUngroupedSubscription("sub12", PROJECT, vTopic); - VaradhiSubscription subscription = getUngroupedSubscription("sub12", project, vTopic); + doReturn(vTopic).when(topicService).get(TOPIC_RESOURCE.getProject() + "." + TOPIC_RESOURCE.getName()); when(subscriptionService.createSubscription(any(), any(), any())).thenReturn(subscription); - SubscriptionResource created = sendRequestWithBody(request, resource, SubscriptionResource.class); - assertEquals(subscription.getName(), created.getSubscriptionInternalName()); + + SubscriptionResource createdResource = sendRequestWithEntity(request, resource, SubscriptionResource.class); + + assertEquals(subscription.getName(), createdResource.getSubscriptionInternalName()); } @Test - void testCreateSubscriptionWithNonExistentProject() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getSubscriptionsUrl(project)); - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - SubscriptionResource resource = getSubscriptionResource("sub12", project, topicResource); - VaradhiSubscription subscription = getUngroupedSubscription("sub12", project, vTopic); + void createSubscription_NonSuperUserWithIgnoreConstraints_ThrowsUnauthorizedException() + throws InterruptedException { + HttpRequest request = + createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT) + "?ignoreConstraints=true"); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + String errorMessage = "ignoreConstraints is restricted to super admins only."; - doReturn(vTopic).when(topicService).get(topicResource.getProject() + "." + topicResource.getName()); - doReturn(subscription).when(subscriptionFactory).get(any(), any(), any()); - String errMsg = "Project not found."; - doThrow(new ResourceNotFoundException(errMsg)).when(projectService).getCachedProject(project.getName()); + doThrow(new HttpException(HTTP_UNAUTHORIZED, errorMessage)).when(subscriptionService) + .createSubscription(any(), any(), any()); + + ErrorResponse response = sendRequestWithEntity(request, resource, 401, errorMessage, ErrorResponse.class); - ErrorResponse resp = sendRequestWithBody(request, resource, 404, errMsg, ErrorResponse.class); - assertEquals(errMsg, resp.reason()); + assertEquals(errorMessage, response.reason()); } @Test - void testCreateSubscriptionWithNonExistentTopic() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getSubscriptionsUrl(project)); - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - SubscriptionResource resource = getSubscriptionResource("sub12", project, topicResource); - VaradhiSubscription subscription = getUngroupedSubscription("sub12", project, vTopic); + void createSubscription_NonExistentProject_ThrowsNotFoundException() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + String errorMessage = "Project not found."; - doReturn(subscription).when(subscriptionFactory).get(any(), any(), any()); - doReturn(project).when(projectService).getCachedProject(project.getName()); - String errMsg = "Topic not found."; - doThrow(new ResourceNotFoundException(errMsg)).when(topicService) - .get(topicResource.getProject() + "." + topicResource.getName()); + doThrow(new ResourceNotFoundException(errorMessage)).when(projectService) + .getCachedProject(PROJECT.getName()); + + ErrorResponse response = sendRequestWithEntity(request, resource, 404, errorMessage, ErrorResponse.class); - ErrorResponse resp = sendRequestWithBody(request, resource, 404, errMsg, ErrorResponse.class); - assertEquals(errMsg, resp.reason()); + assertEquals(errorMessage, response.reason()); } @Test - void testSubscriptionCreateInconsistentProjectNameFailure() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getSubscriptionsUrl(project)); + void createSubscription_NonExistentTopic_ThrowsNotFoundException() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + String errorMessage = "Topic not found."; + + doThrow(new ResourceNotFoundException(errorMessage)).when(topicService) + .get(TOPIC_RESOURCE.getProject() + "." + TOPIC_RESOURCE.getName()); + + ErrorResponse response = sendRequestWithEntity(request, resource, 404, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void createSubscription_MismatchedProjectName_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + SubscriptionResource resource = + createSubscriptionResource( + "sub1", + Project.of("project2", "", "team1", "org1"), TOPIC_RESOURCE + ); + String errorMessage = "Project name mismatch between URL and request body."; + + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void createSubscription_ExceedingRetryLimit_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + RetryPolicy retryPolicy = createCustomRetryPolicy(4); SubscriptionResource resource = - getSubscriptionResource("sub1", Project.of("project2", "", "team1", "org1"), topicResource); + createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE, retryPolicy); + String errorMessage = "Only 3 retries are supported."; + + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); - String errMsg = "Project name mismatch between URL and request body."; - ErrorResponse resp = sendRequestWithBody(request, resource, 400, errMsg, ErrorResponse.class); - assertEquals(errMsg, resp.reason()); + assertEquals(errorMessage, response.reason()); } @Test - void testSubscriptionGet() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.GET, getSubscriptionUrl("sub12", project)); - SubscriptionResource resource = getSubscriptionResource("sub12", project, topicResource); + void createSubscription_UnsupportedProperties_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + resource.getProperties().put("unsupportedProperty", "value"); + String errorMessage = "Unsupported properties: unsupportedProperty"; - VaradhiSubscription subscription = getUngroupedSubscription("sub12", project, topicResource.toVaradhiTopic()); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - when(subscriptionService.getSubscription(captor.capture())).thenReturn(subscription); + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); - SubscriptionResource got = sendRequestWithoutBody(request, SubscriptionResource.class); - assertEquals(got.getName(), resource.getName()); - assertEquals(captor.getValue(), resource.getSubscriptionInternalName()); + assertEquals(errorMessage, response.reason()); } @Test - void testListSubscription() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.GET, getSubscriptionsUrl(project)); + void createSubscription_InvalidPropertyValues_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.POST, buildSubscriptionsUrl(PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + resource.getProperties().put("unsideline.api.message_count", "-10"); + String errorMessage = "Invalid value for property: unsideline.api.message_count"; - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - when(subscriptionService.getSubscriptionList(captor.capture())) + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void getSubscription_ValidSubscription_ReturnsSubscription() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.GET, buildSubscriptionUrl("sub12", PROJECT)); + SubscriptionResource expectedResource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + VaradhiSubscription subscription = + createUngroupedSubscription("sub12", PROJECT, TOPIC_RESOURCE.toVaradhiTopic()); + + when(subscriptionService.getSubscription(anyString())).thenReturn(subscription); + + SubscriptionResource actualResource = sendRequestWithoutPayload(request, SubscriptionResource.class); + + assertEquals(actualResource.getName(), expectedResource.getName()); + verify(subscriptionService).getSubscription("project1.sub12"); + } + + @Test + void listSubscriptions_ValidProject_ReturnsSubscriptionList() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.GET, buildSubscriptionsUrl(PROJECT)); + + when(subscriptionService.getSubscriptionList(PROJECT.getName())) .thenReturn(List.of("sub1", "sub2")) .thenReturn(List.of()); - List got = sendRequestWithoutBody(request, List.class); - assertEquals(List.of("sub1", "sub2"), got); - assertEquals(project.getName(), captor.getValue()); + List subscriptions = sendRequestWithoutPayload(request, List.class); + List subscriptions2 = sendRequestWithoutPayload(request, List.class); - List got2 = sendRequestWithoutBody(request, List.class); - assertEquals(List.of(), got2); + assertEquals(List.of("sub1", "sub2"), subscriptions); + assertEquals(List.of(), subscriptions2); + verify(subscriptionService, times(2)).getSubscriptionList(PROJECT.getName()); } @Test - void testSubscriptionDelete() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.DELETE, getSubscriptionUrl("sub1", project)); - SubscriptionResource resource = getSubscriptionResource("sub1", project, topicResource); + void deleteSubscription_SoftDelete_Success() throws InterruptedException { + HttpRequest request = + createRequest( + HttpMethod.DELETE, + buildSubscriptionUrl("sub1", PROJECT) + "?deletionType=SOFT_DELETE" + ); + + doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); + doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) + .deleteSubscription(anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + + sendRequestWithoutPayload(request, null); + + verify(subscriptionService, times(1)).deleteSubscription( + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + } + + @Test + void deleteSubscription_HardDelete_Success() throws InterruptedException { + HttpRequest request = + createRequest( + HttpMethod.DELETE, + buildSubscriptionUrl("sub1", PROJECT) + "?deletionType=HARD_DELETE" + ); + + doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); + doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) + .deleteSubscription(anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.HARD_DELETE)); + + sendRequestWithoutPayload(request, null); + + verify(subscriptionService, times(1)).deleteSubscription( + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.HARD_DELETE)); + } + + @Test + void deleteSubscription_NoDeletionType_UsesSoftDelete() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.DELETE, buildSubscriptionUrl("sub1", PROJECT)); + + doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); + doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) + .deleteSubscription( + anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); - doReturn(project).when(projectService).getCachedProject(project.getName()); - ArgumentCaptor captorSubName = ArgumentCaptor.forClass(String.class); - ArgumentCaptor captorProject = ArgumentCaptor.forClass(Project.class); + sendRequestWithoutPayload(request, null); + + verify(subscriptionService, times(1)).deleteSubscription( + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + } + + @Test + void deleteSubscription_InvalidDeletionType_UsesDefaultDeletionType() throws InterruptedException { + HttpRequest request = + createRequest( + HttpMethod.DELETE, + buildSubscriptionUrl("sub1", PROJECT) + "?deletionType=INVALID_TYPE" + ); + + doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) - .deleteSubscription(captorSubName.capture(), captorProject.capture(), any(), any()); + .deleteSubscription( + anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.DEFAULT)); - sendRequestWithoutBody(request, null); - assertEquals(captorSubName.getValue(), resource.getSubscriptionInternalName()); - assertEquals(captorProject.getValue().getName(), project.getName()); - verify(subscriptionService, times(1)).deleteSubscription(any(), any(), any(), any()); + sendRequestWithoutPayload(request, null); + + verify(subscriptionService, times(1)).deleteSubscription( + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.DEFAULT)); } @Test - void testSubscriptionUpdate() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.PUT, getSubscriptionUrl("sub1", project)); - SubscriptionResource resource = getSubscriptionResource("sub1", project, topicResource); + void updateSubscription_ValidRequest_ReturnsUpdatedSubscription() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.PUT, buildSubscriptionUrl("sub1", PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub1", PROJECT, TOPIC_RESOURCE); - VaradhiTopic vTopic = topicResource.toVaradhiTopic(); - doReturn(vTopic).when(topicService).get(topicResource.getProject() + "." + topicResource.getName()); + VaradhiTopic vTopic = TOPIC_RESOURCE.toVaradhiTopic(); + doReturn(vTopic).when(topicService).get(TOPIC_RESOURCE.getProject() + "." + TOPIC_RESOURCE.getName()); - VaradhiSubscription subscription = getUngroupedSubscription("sub1", project, vTopic); + VaradhiSubscription subscription = createUngroupedSubscription("sub1", PROJECT, vTopic); subscription.setVersion(2); doReturn(subscription).when(subscriptionFactory).get(any(), any(), any()); - ArgumentCaptor nameCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor versionCaptor = ArgumentCaptor.forClass(Integer.class); + when(subscriptionService.updateSubscription( - nameCaptor.capture(), versionCaptor.capture(), anyString(), anyBoolean(), any(), any(), any(), + stringCaptor.capture(), integerCaptor.capture(), anyString(), anyBoolean(), any(), any(), any(), any() )).thenReturn( CompletableFuture.completedFuture(subscription)); - SubscriptionResource updated = sendRequestWithBody(request, resource, SubscriptionResource.class); + SubscriptionResource updated = sendRequestWithEntity(request, resource, SubscriptionResource.class); + assertEquals(resource.getName(), updated.getName()); - assertEquals(resource.getSubscriptionInternalName(), nameCaptor.getValue()); - assertEquals(0, versionCaptor.getValue()); + assertEquals(resource.getSubscriptionInternalName(), stringCaptor.getValue()); + assertEquals(0, integerCaptor.getValue()); + } + + @Test + void updateSubscription_ExceedingRetryLimit_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.PUT, buildSubscriptionUrl("sub12", PROJECT)); + RetryPolicy retryPolicy = createCustomRetryPolicy(4); // Exceeding retry attempts + SubscriptionResource resource = + createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE, retryPolicy); + + String errorMessage = "Only 3 retries are supported."; + + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void updateSubscription_MismatchedProjectName_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.PUT, buildSubscriptionUrl("sub12", PROJECT)); + SubscriptionResource resource = + createSubscriptionResource( + "sub12", + Project.of("project2", "", "team1", "org1"), TOPIC_RESOURCE + ); + + String errorMessage = "Project name mismatch between URL and request body."; + + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void updateSubscription_NonSuperUserWithIgnoreConstraints_ThrowsUnauthorizedException() + throws InterruptedException { + HttpRequest request = + createRequest( + HttpMethod.PUT, + buildSubscriptionUrl("sub12", PROJECT) + "?ignoreConstraints=true" + ); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + + String errorMessage = "ignoreConstraints is restricted to super admins only."; + doThrow(new HttpException(HTTP_UNAUTHORIZED, errorMessage)).when(subscriptionService) + .updateSubscription(any(), anyInt(), anyString(), anyBoolean(), any(), any(), any(), any()); + + ErrorResponse response = sendRequestWithEntity(request, resource, 401, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void updateSubscription_UnsupportedProperties_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.PUT, buildSubscriptionUrl("sub12", PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + resource.getProperties().put("unsupportedProperty", "value"); + + String errorMessage = "Unsupported properties: unsupportedProperty"; + + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); } + @Test + void updateSubscription_InvalidPropertyValues_ThrowsBadRequest() throws InterruptedException { + HttpRequest request = createRequest(HttpMethod.PUT, buildSubscriptionUrl("sub12", PROJECT)); + SubscriptionResource resource = createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE); + resource.getProperties().put("unsideline.api.message_count", "-10"); + + String errorMessage = "Invalid value for property: unsideline.api.message_count"; + + ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + + assertEquals(errorMessage, response.reason()); + } + + @Test + void restoreSubscription_ValidRequest_ReturnsRestoredSubscription() throws InterruptedException { + HttpRequest request = createRequest( + HttpMethod.POST, + buildSubscriptionUrl("sub1", PROJECT) + "/restore" + ); + SubscriptionResource resource = createSubscriptionResource("sub1", PROJECT, TOPIC_RESOURCE); + VaradhiSubscription subscription = + createUngroupedSubscription("sub1", PROJECT, TOPIC_RESOURCE.toVaradhiTopic()); + + doReturn(CompletableFuture.completedFuture(subscription)).when(subscriptionService) + .restoreSubscription(any(), any()); + + SubscriptionResource restored = sendRequestWithoutPayload(request, SubscriptionResource.class); + + assertEquals(resource.getName(), restored.getName()); + verify(subscriptionService, times(1)).restoreSubscription(any(), any()); + } + + @Test + void startSubscription_ValidRequest_TriggersStart() throws InterruptedException { + HttpRequest request = createRequest( + HttpMethod.POST, + buildSubscriptionUrl("sub1", PROJECT) + "/start" + ); + + doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService).start(any(), any()); + + sendRequestWithoutPayload(request, null); + + verify(subscriptionService, times(1)).start(any(), any()); + } + + @Test + void stopSubscription_ValidRequest_TriggersStop() throws InterruptedException { + HttpRequest request = createRequest( + HttpMethod.POST, + buildSubscriptionUrl("sub1", PROJECT) + "/stop" + ); + + doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService).stop(any(), any()); + + sendRequestWithoutPayload(request, null); + + verify(subscriptionService, times(1)).stop(any(), any()); + } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java index 45384515..309ddb8a 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java @@ -1,7 +1,18 @@ package com.flipkart.varadhi.web.admin; import com.flipkart.varadhi.config.RestOptions; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.CodeRange; +import com.flipkart.varadhi.entities.ConsumptionPolicy; +import com.flipkart.varadhi.entities.DlqMessage; +import com.flipkart.varadhi.entities.Endpoint; +import com.flipkart.varadhi.entities.Offset; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.SubscriptionShards; +import com.flipkart.varadhi.entities.SubscriptionUnitShard; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.pulsar.entities.PulsarOffset; import com.flipkart.varadhi.services.ProjectService; import com.flipkart.varadhi.services.SubscriptionService; @@ -12,65 +23,91 @@ import com.flipkart.varadhi.web.entities.SubscriptionResource; import com.flipkart.varadhi.web.entities.TopicResource; import com.google.common.collect.ArrayListMultimap; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.net.URI; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; import static com.flipkart.varadhi.entities.StandardHeaders.GROUP_ID; import static com.flipkart.varadhi.entities.StandardHeaders.MESSAGE_ID; -import static org.mockito.Mockito.mock; public class SubscriptionTestBase extends WebTestBase { - private final Random r = new Random(); - private static final Endpoint endpoint = - new Endpoint.HttpEndpoint(URI.create("http://localhost:8080"), "GET", "", 500, 500, false); - private static final RetryPolicy retryPolicy = new RetryPolicy( + private final Random random = new Random(); + + private static final Endpoint DEFAULT_ENDPOINT = new Endpoint.HttpEndpoint( + URI.create("http://localhost:8080"), + "GET", "", 500, 500, false + ); + + private static final RetryPolicy DEFAULT_RETRY_POLICY = new RetryPolicy( new CodeRange[]{new CodeRange(500, 502)}, RetryPolicy.BackoffType.LINEAR, 1, 1, 1, 3 ); - private static final ConsumptionPolicy consumptionPolicy = new ConsumptionPolicy(10, 1, 1, false, 1, null); - private static final TopicCapacityPolicy capacityPolicy = new TopicCapacityPolicy(1, 10, 1); - private static final SubscriptionShards shards = new SubscriptionUnitShard(0, capacityPolicy, null, null, null); - private static final Map subscriptionDefaultProperties = + + private static final ConsumptionPolicy DEFAULT_CONSUMPTION_POLICY = new ConsumptionPolicy( + 10, 1, 1, + false, 1, null + ); + + private static final TopicCapacityPolicy DEFAULT_CAPACITY_POLICY = new TopicCapacityPolicy( + 1, 10, 1); + + private static final SubscriptionShards DEFAULT_SHARDS = new SubscriptionUnitShard( + 0, DEFAULT_CAPACITY_POLICY, null, null, null); + + private static final Map DEFAULT_SUBSCRIPTION_PROPERTIES = SubscriptionPropertyValidator.createPropertyDefaultValueProviders(new RestOptions()); - protected final Project project = Project.of("project1", "", "team1", "org1"); - protected final TopicResource topicResource = TopicResource.unGrouped("topic1", "project2", null); - SubscriptionService subscriptionService; - ProjectService projectService; - VaradhiTopicService topicService; - VaradhiSubscriptionFactory subscriptionFactory; - - protected String getSubscriptionsUrl(Project project) { - return String.join("/", "/projects", project.getName(), "subscriptions"); + + protected static final Project PROJECT = Project.of( + "project1", "", "team1", "org1"); + protected static final TopicResource TOPIC_RESOURCE = TopicResource.unGrouped( + "topic1", "project1", null); + + @Mock + protected SubscriptionService subscriptionService; + + @Mock + protected ProjectService projectService; + + @Mock + protected VaradhiTopicService topicService; + + @Mock + protected VaradhiSubscriptionFactory subscriptionFactory; + + @Override + public void setUp() throws InterruptedException { + super.setUp(); + MockitoAnnotations.openMocks(this); } - protected String getSubscriptionUrl(String subscriptionName, Project project) { - return String.join("/", getSubscriptionsUrl(project), subscriptionName); + protected String buildSubscriptionsUrl(Project project) { + return String.format("/projects/%s/subscriptions", project.getName()); } - public void setUp() throws InterruptedException { - super.setUp(); - subscriptionService = mock(SubscriptionService.class); - projectService = mock(ProjectService.class); - topicService = mock(VaradhiTopicService.class); - subscriptionFactory = mock(VaradhiSubscriptionFactory.class); + protected String buildSubscriptionUrl(String subscriptionName, Project project) { + return String.format("%s/%s", buildSubscriptionsUrl(project), subscriptionName); } - public static VaradhiSubscription getUngroupedSubscription( + public static VaradhiSubscription createUngroupedSubscription( String subscriptionName, Project project, VaradhiTopic topic ) { - return getSubscription(subscriptionName, false, project, topic); + return createSubscription(subscriptionName, false, project, topic); } - public static VaradhiSubscription getGroupedSubscription( + public static VaradhiSubscription createGroupedSubscription( String subscriptionName, Project project, VaradhiTopic topic ) { - return getSubscription(subscriptionName, true, project, topic); + return createSubscription(subscriptionName, true, project, topic); } - private static VaradhiSubscription getSubscription( + private static VaradhiSubscription createSubscription( String subscriptionName, boolean grouped, Project project, VaradhiTopic topic ) { return VaradhiSubscription.of( @@ -79,46 +116,69 @@ private static VaradhiSubscription getSubscription( topic.getName(), UUID.randomUUID().toString(), grouped, - endpoint, - retryPolicy, - consumptionPolicy, - shards, - subscriptionDefaultProperties + DEFAULT_ENDPOINT, + DEFAULT_RETRY_POLICY, + DEFAULT_CONSUMPTION_POLICY, + DEFAULT_SHARDS, + DEFAULT_SUBSCRIPTION_PROPERTIES ); } - - protected SubscriptionResource getSubscriptionResource( + protected SubscriptionResource createSubscriptionResource( String subscriptionName, Project project, TopicResource topic + ) { + return createSubscriptionResource(subscriptionName, project, topic, DEFAULT_RETRY_POLICY); + } + + protected SubscriptionResource createSubscriptionResource( + String subscriptionName, Project project, TopicResource topic, RetryPolicy retryPolicy ) { return SubscriptionResource.of( subscriptionName, project.getName(), topic.getName(), topic.getProject(), - "desc", + "Description", false, - endpoint, + DEFAULT_ENDPOINT, retryPolicy, - consumptionPolicy, + DEFAULT_CONSUMPTION_POLICY, new HashMap<>() ); } - protected DlqMessage getDlqMessage(int partitionId) { - ArrayListMultimap requestHeaders = ArrayListMultimap.create(); - requestHeaders.put(MESSAGE_ID, Arrays.toString(getRandomBytes(10))); - requestHeaders.put(GROUP_ID, Arrays.toString(getRandomBytes(10))); - int lId = r.nextInt(30) % 5000; - int eId = r.nextInt(30) % 40000; + protected DlqMessage createDlqMessage(int partitionId) { + var requestHeaders = ArrayListMultimap.create(); + requestHeaders.put(MESSAGE_ID, generateRandomHex(10)); + requestHeaders.put(GROUP_ID, generateRandomHex(10)); + + int lId = random.nextInt(5000); + int eId = random.nextInt(40000); Offset offset = PulsarOffset.of("mId:%d:%d:%d".formatted(lId, eId, partitionId)); - return new DlqMessage(getRandomBytes(100), requestHeaders, offset, partitionId); + + return new DlqMessage(generateRandomBytes(100), requestHeaders, offset, partitionId); + } + + private String generateRandomHex(int length) { + byte[] bytes = generateRandomBytes(length); + StringBuilder hexBuilder = new StringBuilder(); + for (byte b : bytes) { + hexBuilder.append(String.format("%02x", b)); + } + return hexBuilder.toString(); } - private byte[] getRandomBytes(int len) { - byte[] buf = new byte[len]; - r.nextBytes(buf); - return buf; + private byte[] generateRandomBytes(int length) { + byte[] buffer = new byte[length]; + random.nextBytes(buffer); + return buffer; } + protected RetryPolicy createCustomRetryPolicy(int retryAttempts) { + return new RetryPolicy( + new CodeRange[]{new CodeRange(500, 502)}, + RetryPolicy.BackoffType.LINEAR, + 1, 1, 1, retryAttempts + ); + } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/TeamHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/TeamHandlersTest.java index 2199d774..d675cd74 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/TeamHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/TeamHandlersTest.java @@ -82,24 +82,24 @@ public void testTeamCreate() throws InterruptedException { Team team1 = Team.of("team1", o1.getName()); doReturn(team1).when(teamService).createTeam(eq(team1)); - Team team1Created = sendRequestWithBody(request, team1, Team.class); + Team team1Created = sendRequestWithEntity(request, team1, Team.class); Assertions.assertEquals(team1, team1Created); verify(teamService, times(1)).createTeam(eq(team1)); String orgNotFoundError = String.format("Org(%s) not found.", team1.getOrg()); doThrow(new ResourceNotFoundException(orgNotFoundError)).when(teamService).createTeam(team1); - ErrorResponse response = sendRequestWithBody(request, team1, 404, orgNotFoundError, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity(request, team1, 404, orgNotFoundError, ErrorResponse.class); Assertions.assertEquals(orgNotFoundError, response.reason()); String duplicateOrgError = String.format("Team(%s) already exists. Team is unique with in Org.", team1.getName()); doThrow(new DuplicateResourceException(duplicateOrgError)).when(teamService).createTeam(team1); - response = sendRequestWithBody(request, team1, 409, duplicateOrgError, ErrorResponse.class); + response = sendRequestWithEntity(request, team1, 409, duplicateOrgError, ErrorResponse.class); Assertions.assertEquals(duplicateOrgError, response.reason()); String someInternalError = "Some random error"; doThrow(new MetaStoreException(someInternalError)).when(teamService).createTeam(team1); - response = sendRequestWithBody(request, team1, 500, someInternalError, ErrorResponse.class); + response = sendRequestWithEntity(request, team1, 500, someInternalError, ErrorResponse.class); Assertions.assertEquals(someInternalError, response.reason()); } @@ -111,14 +111,14 @@ public void testTeamGet() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.GET, getTeamUrl(team1)); doReturn(team1).when(teamService).getTeam(team1.getName(), team1.getOrg()); - Team team1Get = sendRequestWithoutBody(request, Team.class); + Team team1Get = sendRequestWithoutPayload(request, Team.class); Assertions.assertEquals(team1, team1Get); verify(teamService, times(1)).getTeam(team1.getName(), team1.getOrg()); String notFoundError = String.format("Team(%s) not found.", team1.getName()); doThrow(new ResourceNotFoundException(notFoundError)).when(teamService) .getTeam(team1.getName(), team1.getOrg()); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); } @@ -171,18 +171,18 @@ public void testTeamDelete() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.DELETE, getTeamUrl(team1)); doNothing().when(teamService).deleteTeam(team1.getName(), team1.getOrg()); - sendRequestWithoutBody(request, null); + sendRequestWithoutPayload(request, null); verify(teamService, times(1)).deleteTeam(team1.getName(), team1.getOrg()); String notFoundError = String.format("Team(%s) not found.", team1.getName()); doThrow(new ResourceNotFoundException(notFoundError)).when(teamService) .deleteTeam(team1.getName(), team1.getOrg()); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); String invalidOpError = String.format("Can not delete Team(%s) as it has associated Project(s).", team1.getName()); doThrow(new InvalidOperationForResourceException(invalidOpError)).when(teamService) .deleteTeam(team1.getName(), team1.getOrg()); - sendRequestWithoutBody(request, 409, invalidOpError); + sendRequestWithoutPayload(request, 409, invalidOpError); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java index 7ca64181..550e2e70 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java @@ -126,7 +126,7 @@ void createTopic_WithValidRequest_ShouldCreateTopicSuccessfully() throws Interru doReturn(varadhiTopic).when(varadhiTopicFactory).get(project, topicResource); - TopicResource createdTopic = sendRequestWithBody( + TopicResource createdTopic = sendRequestWithEntity( createRequest(HttpMethod.POST, getTopicsUrl(project)), topicResource, TopicResource.class @@ -174,7 +174,7 @@ void getTopic_WithValidRequest_ShouldReturnTopicSuccessfully() throws Interrupte doReturn(varadhiTopic).when(varadhiTopicService).get(varadhiTopicName); - TopicResource retrievedTopic = sendRequestWithoutBody( + TopicResource retrievedTopic = sendRequestWithoutPayload( createRequest(HttpMethod.GET, getTopicUrl(project)), TopicResource.class ); @@ -188,7 +188,7 @@ void listTopics_WithTopicsAvailable_ShouldReturnAllTopics() throws InterruptedEx doReturn(topics).when(varadhiTopicService).getVaradhiTopics(project.getName()); - List retrievedTopics = sendRequestWithoutBody( + List retrievedTopics = sendRequestWithoutPayload( createRequest(HttpMethod.GET, getTopicsUrl(project)), List.class ); @@ -200,7 +200,7 @@ void listTopics_WithTopicsAvailable_ShouldReturnAllTopics() throws InterruptedEx void listTopics_WithNoTopicsAvailable_ShouldReturnEmptyList() throws InterruptedException { doReturn(Collections.emptyList()).when(varadhiTopicService).getVaradhiTopics(project.getName()); - List retrievedTopics = sendRequestWithoutBody( + List retrievedTopics = sendRequestWithoutPayload( createRequest(HttpMethod.GET, getTopicsUrl(project)), List.class ); @@ -233,7 +233,7 @@ void restoreTopic_WithValidRequest_ShouldRestoreTopicSuccessfully() throws Inter HttpRequest request = createRequest(HttpMethod.POST, getTopicUrl(project) + "/restore"); doNothing().when(varadhiTopicService).restore(any()); - sendRequestWithoutBody(request, null); + sendRequestWithoutPayload(request, null); verify(varadhiTopicService).restore(any()); } @@ -248,7 +248,7 @@ private void verifyDeleteRequest(String deletionType, ResourceDeletionType expec HttpRequest request = createRequest(HttpMethod.DELETE, url); doNothing().when(varadhiTopicService).delete(any(), eq(expectedDeletionType)); - sendRequestWithoutBody(request, null); + sendRequestWithoutPayload(request, null); verify(varadhiTopicService).delete(any(), eq(expectedDeletionType)); } diff --git a/server/src/test/java/com/flipkart/varadhi/web/authz/IamPolicyHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/authz/IamPolicyHandlersTest.java index 519dfc70..ebe8bdd8 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/authz/IamPolicyHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/authz/IamPolicyHandlersTest.java @@ -85,13 +85,13 @@ void testDeleteIamPolicyRecord() throws Exception { createRequest(HttpMethod.DELETE, getOrgIamPolicyUrl(resourceId)); doNothing().when(iamPolicyService).deleteIamPolicy(eq(ResourceType.ORG), eq(resourceId)); - sendRequestWithoutBody(request, null); + sendRequestWithoutPayload(request, null); verify(iamPolicyService, times(1)).deleteIamPolicy(eq(ResourceType.ORG), eq(resourceId)); String notFoundError = String.format("IamPolicyRecord on resource(%s) not found.", resourceId); doThrow(new ResourceNotFoundException(notFoundError)).when(iamPolicyService) .deleteIamPolicy(ResourceType.ORG, resourceId); - sendRequestWithoutBody(request, 404, notFoundError); + sendRequestWithoutPayload(request, 404, notFoundError); } @Test @@ -111,7 +111,7 @@ void testSetOrgIamPolicy() throws InterruptedException { doReturn(policyRecord).when(iamPolicyService) .setIamPolicy(eq(ResourceType.ORG), eq(orgName), eq(assignmentUpdate)); - IamPolicyResponse response = sendRequestWithBody(request, assignmentUpdate, IamPolicyResponse.class); + IamPolicyResponse response = sendRequestWithEntity(request, assignmentUpdate, IamPolicyResponse.class); assertEquals(expected, response); verify(iamPolicyService, times(1)).setIamPolicy(eq(ResourceType.ORG), eq(orgName), eq(assignmentUpdate)); @@ -119,7 +119,7 @@ void testSetOrgIamPolicy() throws InterruptedException { doThrow(new MetaStoreException(someInternalError)).when(iamPolicyService) .setIamPolicy(eq(ResourceType.ORG), eq(orgName), eq(assignmentUpdate)); ErrorResponse errResponse = - sendRequestWithBody(request, assignmentUpdate, 500, someInternalError, ErrorResponse.class); + sendRequestWithEntity(request, assignmentUpdate, 500, someInternalError, ErrorResponse.class); assertEquals(someInternalError, errResponse.reason()); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/produce/BodyHandlerTest.java b/server/src/test/java/com/flipkart/varadhi/web/produce/BodyHandlerTest.java index f3d88dcb..375aa856 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/produce/BodyHandlerTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/produce/BodyHandlerTest.java @@ -49,25 +49,25 @@ public void testProduceWithForBodySize() throws InterruptedException { request.putHeader(MESSAGE_ID, messageId); request.putHeader(FORWARDED_FOR, "host1, host2"); payload = "0123456789".getBytes(); - String messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + String messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); payload = "0123456789012345678".getBytes(); - messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); payload = "01234567890123456789".getBytes(); - messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); payload = "012345678901234567890".getBytes(); - sendRequestWithByteBufferBody( + sendRequestWithPayload( request, payload, 413, "Entity too large.", ErrorResponse.class ); payload = "012345678901234567890123456789".getBytes(); - sendRequestWithByteBufferBody( + sendRequestWithPayload( request, payload, 413, "Entity too large.", ErrorResponse.class ); diff --git a/server/src/test/java/com/flipkart/varadhi/web/produce/HeaderValidationTest.java b/server/src/test/java/com/flipkart/varadhi/web/produce/HeaderValidationTest.java index 9598ba9f..a94266da 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/produce/HeaderValidationTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/produce/HeaderValidationTest.java @@ -62,11 +62,11 @@ public void testProduceWithValidHeaders() throws InterruptedException { request.putHeader(MESSAGE_ID, messageId); request.putHeader(FORWARDED_FOR, "host1, host2"); request.putHeader("x_header1", List.of("h1v1", "h1v2")); - String messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + String messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); request.putHeader("x_header2", "h2v1"); - messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); } @@ -75,7 +75,7 @@ public void testProduceWithHighHeaderKeySize() throws InterruptedException { request.putHeader(MESSAGE_ID, messageId); request.putHeader(FORWARDED_FOR, "host1, host2"); request.putHeader("x_header1_morethantwentycharsintotal", "value1"); - sendRequestWithByteBufferBody( + sendRequestWithPayload( request, payload, 400, "Header name x_header1_morethantwentycharsintotal exceeds allowed size.", ErrorResponse.class ); @@ -86,7 +86,7 @@ public void testProduceWithHighHeaderValueSize() throws InterruptedException { request.putHeader(MESSAGE_ID, messageId); request.putHeader(FORWARDED_FOR, "host1, host2"); request.putHeader("x_header1", "morethantwentycharsintotal"); - sendRequestWithByteBufferBody( + sendRequestWithPayload( request, payload, 400, "Value of Header x_header1 exceeds allowed size.", ErrorResponse.class ); @@ -99,7 +99,7 @@ public void testProduceWithHighHeaderNumbers() throws InterruptedException { request.putHeader("x_header1", "value1"); request.putHeader("x_header2", "value2"); request.putHeader("x_header3", "value3"); - sendRequestWithByteBufferBody( + sendRequestWithPayload( request, payload, 400, "More Varadhi specific headers specified than allowed max(4).", ErrorResponse.class ); @@ -111,7 +111,7 @@ public void testProduceWithMultiValueHeaderIsSingleHeader() throws InterruptedEx request.putHeader(FORWARDED_FOR, "host1, host2"); request.putHeader("x_header1", List.of("value1", "value2", "value3", "value4")); request.putHeader("x_header3", "value3"); - String messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + String messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/produce/ProduceHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/produce/ProduceHandlersTest.java index ffc010f8..6e2986e4 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/produce/ProduceHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/produce/ProduceHandlersTest.java @@ -74,7 +74,7 @@ public void testProduceAndDuplicateMessage() throws InterruptedException { request.putHeader("RandomHeader", "value1"); request.putHeader("x_header1", List.of("h1v1", "h1v2")); request.putHeader("X_HEADER2", "h2v1"); - String messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + String messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); Message capturedMessage = msgCapture.getValue(); REQUIRED_HEADERS.forEach(s -> Assertions.assertTrue(capturedMessage.hasHeader(s))); @@ -86,7 +86,7 @@ public void testProduceAndDuplicateMessage() throws InterruptedException { Assertions.assertTrue(capturedMessage.getHeaders("x_header1").contains("h1v2")); Assertions.assertTrue(capturedMessage.getHeaders("x_header2").contains("h2v1")); Assertions.assertFalse(capturedMessage.hasHeader("X_HEADER2")); - messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); verify(producerService, times(2)).produceToTopic(any(), eq(topicFullName), any()); } @@ -99,7 +99,7 @@ public void testProduceThrows() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.POST, topicPath); request.putHeader(MESSAGE_ID, messageId); - sendRequestWithByteBufferBody(request, payload, 404, exceptionMessage, ErrorResponse.class); + sendRequestWithPayload(request, payload, 404, exceptionMessage, ErrorResponse.class); } @Test @@ -122,7 +122,7 @@ record testData(int status, String message, TopicState state) { doReturn(CompletableFuture.completedFuture(result)).when(producerService) .produceToTopic(msgCapture.capture(), eq(topicFullName), any()); try { - sendRequestWithByteBufferBody(request, payload, d.status, d.message, ErrorResponse.class); + sendRequestWithPayload(request, payload, d.status, d.message, ErrorResponse.class); } catch (InterruptedException e) { Assertions.fail("Unexpected Interruped Exception."); } @@ -140,7 +140,8 @@ public void testProduceFailureResult() throws InterruptedException { ProduceResult.of(messageId, Result.of(new ProduceException(topicProduceFailureMsg))); doReturn(CompletableFuture.completedFuture(result)).when(producerService) .produceToTopic(msgCapture.capture(), eq(topicFullName), any()); - sendRequestWithByteBufferBody(request, payload, 500, + sendRequestWithPayload( + request, payload, 500, String.format("Produce failure from messaging stack for Topic/Queue. %s", topicProduceFailureMsg), ErrorResponse.class ); @@ -153,11 +154,11 @@ public void testProduceUnexpectedFailure() throws InterruptedException { .produceToTopic(msgCapture.capture(), eq(topicFullName), any()); HttpRequest request = createRequest(HttpMethod.POST, topicPath); request.putHeader(MESSAGE_ID, messageId); - sendRequestWithByteBufferBody(request, payload, 404, exceptionMessage, ErrorResponse.class); + sendRequestWithPayload(request, payload, 404, exceptionMessage, ErrorResponse.class); doReturn(CompletableFuture.failedFuture(new RuntimeException(exceptionMessage))).when(producerService) .produceToTopic(msgCapture.capture(), eq(topicFullName), any()); - sendRequestWithByteBufferBody(request, payload, 500, exceptionMessage, ErrorResponse.class); + sendRequestWithPayload(request, payload, 500, exceptionMessage, ErrorResponse.class); } @Test @@ -167,7 +168,7 @@ public void testProduceProduceException() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.POST, topicPath); request.putHeader(MESSAGE_ID, messageId); - sendRequestWithByteBufferBody(request, payload, 500, exceptionMessage, ErrorResponse.class); + sendRequestWithPayload(request, payload, 500, exceptionMessage, ErrorResponse.class); } @Test @@ -185,7 +186,7 @@ public void testProduceHeaderOrdering() throws InterruptedException { multimap.add("x_header1", "h1v3"); request.putHeaders(multimap); request.putHeader("X_HEADER2", "h2v1"); - String messageIdObtained = sendRequestWithByteBufferBody(request, payload, String.class); + String messageIdObtained = sendRequestWithPayload(request, payload, String.class); Assertions.assertEquals(messageId, messageIdObtained); String[] h1Values = msgCapture.getValue().getHeaders("x_header1").toArray(new String[]{}); Assertions.assertEquals("h1v1", h1Values[0]); @@ -202,7 +203,7 @@ public void testProduceForNonexistingProject() throws InterruptedException { ProduceResult result = ProduceResult.of(messageId, Result.of(new DummyProducer.DummyOffset(10))); doReturn(CompletableFuture.completedFuture(result)).when(producerService) .produceToTopic(msgCapture.capture(), eq(topicFullName), any()); - sendRequestWithByteBufferBody(request, payload, 404, "Project1 not found.", ErrorResponse.class + sendRequestWithPayload(request, payload, 404, "Project1 not found.", ErrorResponse.class ); } } From 48884b70dad7ac3c1b595f1101e2e0a57c5a28ff Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:13:08 +0530 Subject: [PATCH 6/7] Fix E2E Test Failure --- .../varadhi/web/v1/admin/SubscriptionHandlers.java | 4 ++-- .../varadhi/web/admin/SubscriptionHandlersTest.java | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java index 54997614..30fa3ef6 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java +++ b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java @@ -269,8 +269,8 @@ public void delete(RoutingContext ctx) { * @param ctx the routing context */ public void restore(RoutingContext ctx) { - ctx.handleResponse(subscriptionService.restoreSubscription(getSubscriptionFqn(ctx), ctx.getIdentityOrDefault()) - .thenApply(SubscriptionResource::from)); + ctx.handleResponse( + subscriptionService.restoreSubscription(getSubscriptionFqn(ctx), ctx.getIdentityOrDefault())); } /** diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java index 62b93fd5..da3aab10 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java @@ -430,16 +430,12 @@ void restoreSubscription_ValidRequest_ReturnsRestoredSubscription() throws Inter HttpMethod.POST, buildSubscriptionUrl("sub1", PROJECT) + "/restore" ); - SubscriptionResource resource = createSubscriptionResource("sub1", PROJECT, TOPIC_RESOURCE); - VaradhiSubscription subscription = - createUngroupedSubscription("sub1", PROJECT, TOPIC_RESOURCE.toVaradhiTopic()); - doReturn(CompletableFuture.completedFuture(subscription)).when(subscriptionService) + doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) .restoreSubscription(any(), any()); - SubscriptionResource restored = sendRequestWithoutPayload(request, SubscriptionResource.class); + sendRequestWithoutPayload(request, null); - assertEquals(resource.getName(), restored.getName()); verify(subscriptionService, times(1)).restoreSubscription(any(), any()); } From 514abe475249daf66a5780a066a1348929d275d9 Mon Sep 17 00:00:00 2001 From: Devanshu Garg <65565864+devanshu-garg-IN@users.noreply.github.com> Date: Sat, 1 Feb 2025 18:43:58 +0530 Subject: [PATCH 7/7] feat: enhance deletion and restoration logic with action codes and validation - Added message as an input query parameter in delete and restore requests (not mandatory). - Introduced Action Code as an enum to track who performed the action (User, Varadhi Admin, System). - Implemented logic to allow actions based on who performed the previous action. - Updated validation logic for topic deletion: - Soft deletion of a topic is allowed only if all subscriptions are soft deleted. - Hard deletion of a topic is allowed only if all subscriptions are hard deleted. - Added `includeInactive` flag to resource listing to include inactive resources. - Added unit tests to verify the new functionality. - Added a generic `LifecycleStatus` class to handle both topic and subscription lifecycle statuses. - Refactored existing code to use the new `LifecycleStatus` class. --- .../java/com/flipkart/varadhi/Constants.java | 4 +- .../controller/AssignmentManagerTest.java | 12 +- .../controller/ControllerApiMgrTest.java | 46 ++-- .../varadhi/controller/OperationMgrTest.java | 45 ++-- .../impl/LeastAssignedStrategyTests.java | 11 +- .../varadhi/entities/LifecycleStatus.java | 115 ++++++++++ .../entities/ResourceActionRequest.java | 19 ++ .../varadhi/entities/VaradhiSubscription.java | 88 +++----- .../varadhi/entities/VaradhiTopic.java | 61 ++--- .../varadhi/entities/auth/ResourceAction.java | 145 ++++++++---- .../varadhi/entities/LifecycleStatusTest.java | 68 ++++++ .../entities/ResourceActionRequestTest.java | 59 +++++ .../entities/VaradhiSubscriptionTest.java | 101 +++++---- .../varadhi/entities/VaradhiTopicTest.java | 44 ++-- .../entities/auth/ResourceActionTest.java | 62 ++++++ .../varadhi/entities/SubscriptionUtils.java | 206 +++++++++++++++-- .../services/ProducerServiceTests.java | 7 +- .../pulsar/PulsarProducerFactoryTest.java | 2 +- .../pulsar/PulsarStackProviderTest.java | 2 +- .../pulsar/entities/PulsarProducerTest.java | 2 +- .../services/PulsarTopicServiceTest.java | 16 +- .../pulsar/PulsarTopicServiceTest.java | 10 +- .../flipkart/varadhi/config/RestOptions.java | 2 +- .../varadhi/services/SubscriptionService.java | 65 ++++-- .../varadhi/services/VaradhiTopicService.java | 84 ++++--- .../utils/VaradhiSubscriptionFactory.java | 209 ++++++++++++++++-- .../varadhi/utils/VaradhiTopicFactory.java | 60 ++++- .../web/entities/SubscriptionResource.java | 117 ++++++++-- .../varadhi/web/entities/TopicResource.java | 83 ++++++- .../varadhi/web/routes/RouteDefinition.java | 4 + .../web/v1/admin/SubscriptionHandlers.java | 73 +++++- .../varadhi/web/v1/admin/TopicHandlers.java | 61 ++++- .../services/SubscriptionServiceTest.java | 163 +++++++++++--- .../services/VaradhiTopicServiceTest.java | 174 +++++++++++++-- .../utils/VaradhiSubscriptionFactoryTest.java | 111 ++++++++++ .../utils/VaradhiTopicFactoryTest.java | 116 +++++++--- .../com/flipkart/varadhi/web/WebTestBase.java | 13 +- .../web/admin/SubscriptionHandlersTest.java | 107 ++++++--- .../web/admin/SubscriptionTestBase.java | 21 +- .../varadhi/web/admin/TopicHandlersTest.java | 39 +++- .../entities/SubscriptionResourceTest.java | 74 +++++++ .../web/entities/TopicResourceTest.java | 78 +++++++ .../flipkart/varadhi/AuthZProviderTests.java | 8 +- .../java/com/flipkart/varadhi/E2EBase.java | 33 +-- .../java/com/flipkart/varadhi/OrgTests.java | 4 +- .../flipkart/varadhi/SubscriptionTests.java | 31 ++- .../java/com/flipkart/varadhi/TopicTests.java | 35 ++- 47 files changed, 2312 insertions(+), 578 deletions(-) create mode 100644 entities/src/main/java/com/flipkart/varadhi/entities/LifecycleStatus.java create mode 100644 entities/src/main/java/com/flipkart/varadhi/entities/ResourceActionRequest.java create mode 100644 entities/src/test/java/com/flipkart/varadhi/entities/LifecycleStatusTest.java create mode 100644 entities/src/test/java/com/flipkart/varadhi/entities/ResourceActionRequestTest.java create mode 100644 entities/src/test/java/com/flipkart/varadhi/entities/auth/ResourceActionTest.java create mode 100644 server/src/test/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactoryTest.java create mode 100644 server/src/test/java/com/flipkart/varadhi/web/entities/SubscriptionResourceTest.java create mode 100644 server/src/test/java/com/flipkart/varadhi/web/entities/TopicResourceTest.java diff --git a/common/src/main/java/com/flipkart/varadhi/Constants.java b/common/src/main/java/com/flipkart/varadhi/Constants.java index 8a3cb300..909c9786 100644 --- a/common/src/main/java/com/flipkart/varadhi/Constants.java +++ b/common/src/main/java/com/flipkart/varadhi/Constants.java @@ -21,7 +21,7 @@ public class Constants { public static final String USER_ID_HEADER = "x_user_id"; // Default topic capacity policy - public static final TopicCapacityPolicy DefaultTopicCapacity = new TopicCapacityPolicy(100, 400, 2); + public static final TopicCapacityPolicy DEFAULT_TOPIC_CAPACITY = new TopicCapacityPolicy(100, 400, 2); // System identity constant public static final String SYSTEM_IDENTITY = "System"; @@ -52,6 +52,8 @@ private PathParams() { public static class QueryParams { public static final String QUERY_PARAM_DELETION_TYPE = "deletionType"; public static final String QUERY_PARAM_IGNORE_CONSTRAINTS = "ignoreConstraints"; + public static final String QUERY_PARAM_INCLUDE_INACTIVE = "includeInactive"; + public static final String QUERY_PARAM_MESSAGE = "message"; private QueryParams() { // Private constructor to prevent instantiation diff --git a/controller/src/test/java/com/flipkart/varadhi/controller/AssignmentManagerTest.java b/controller/src/test/java/com/flipkart/varadhi/controller/AssignmentManagerTest.java index 54b4e350..c7b002c2 100644 --- a/controller/src/test/java/com/flipkart/varadhi/controller/AssignmentManagerTest.java +++ b/controller/src/test/java/com/flipkart/varadhi/controller/AssignmentManagerTest.java @@ -49,7 +49,7 @@ public void setup() { @Test public void testAssignShards() throws Exception { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List nodes = NodeProvider.getConsumerNodes(3); nodes.forEach(this::addConsumerNode); @@ -85,7 +85,7 @@ public void testAssignShards() throws Exception { @Test public void testAssignShards_StoreThrows() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List nodes = NodeProvider.getConsumerNodes(3); nodes.forEach(this::addConsumerNode); @@ -123,7 +123,7 @@ public void testAssignShards_StoreThrows() { @Test public void testUnAssignShards() throws Exception { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List nodes = NodeProvider.getConsumerNodes(3); nodes.forEach(this::addConsumerNode); @@ -183,7 +183,7 @@ public void testUnAssignShards() throws Exception { @Test public void testUnAssignShards_Throws() throws Exception { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List nodes = NodeProvider.getConsumerNodes(3); nodes.forEach(this::addConsumerNode); @@ -220,7 +220,7 @@ public void testUnAssignShards_Throws() throws Exception { @Test public void testReAssign() throws Exception { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List nodes = NodeProvider.getConsumerNodes(3); nodes.forEach(this::addConsumerNode); @@ -241,7 +241,7 @@ public void testReAssign() throws Exception { @Test public void testReAssign_Throws() throws Exception { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List nodes = NodeProvider.getConsumerNodes(3); nodes.forEach(this::addConsumerNode); diff --git a/controller/src/test/java/com/flipkart/varadhi/controller/ControllerApiMgrTest.java b/controller/src/test/java/com/flipkart/varadhi/controller/ControllerApiMgrTest.java index a6dfebb3..520b0e0a 100644 --- a/controller/src/test/java/com/flipkart/varadhi/controller/ControllerApiMgrTest.java +++ b/controller/src/test/java/com/flipkart/varadhi/controller/ControllerApiMgrTest.java @@ -102,7 +102,7 @@ public void testAddConsumerNodeWhenGetConsumerInfoThrows() { @Test public void testGetSubscriptionStatus() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -130,7 +130,7 @@ public void testGetSubscriptionStatus() { @Test public void testStartSubscription() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -149,7 +149,7 @@ public void testStartSubscription() { @Test public void testStartOfRunningSubscription() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -166,7 +166,7 @@ public void testStartOfRunningSubscription() { @Test public void testStartOfStartingSubscription() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -183,7 +183,7 @@ public void testStartOfStartingSubscription() { @Test public void testStartSubscription_SubscriptionStatusFailure() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); doReturn(sub1).when(metaStore).getSubscription(sub1.getName()); doThrow(new MetaStoreException("Failed to get assignments for sub.")).when( @@ -200,7 +200,7 @@ public void testStartSubscription_SubscriptionStatusFailure() { @Test public void testStartSubscriptionOneShardAlreadyStarted() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -220,7 +220,7 @@ public void testStartSubscriptionOneShardAlreadyStarted() { @Test public void testStartSubscriptionAllShardsAlreadyStarted() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -244,7 +244,7 @@ public void testStartSubscriptionAllShardsAlreadyStarted() { @Test public void testStartSubscriptionWhenConsumerStartFails() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -277,7 +277,7 @@ public void testStartSubscriptionWhenConsumerStartFails() { @Test public void testStartSubscriptionWhenConsumerStartShardThrows() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -303,7 +303,7 @@ public void testStartSubscriptionWhenConsumerStartShardThrows() { @Test public void testStopSubscriptionAssignmentExists() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -324,7 +324,7 @@ public void testStopSubscriptionAssignmentDoesNotExists() { //ideally shouldn't happen as Status would return appropriate state, but to ensure // code path doesn't fail for this case VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -346,7 +346,7 @@ public void testStopSubscriptionAssignmentDoesNotExists() { @Test public void testStopOfStoppedSubscription() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -363,7 +363,7 @@ public void testStopOfStoppedSubscription() { @Test public void testStartOfStoppingSubscription() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -380,7 +380,7 @@ public void testStartOfStoppingSubscription() { @Test public void testStopSubscription_SubscriptionStatusFailure() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); doReturn(sub1).when(metaStore).getSubscription(sub1.getName()); doThrow(new MetaStoreException("Failed to get assignments for sub.")).when( assignmentManager).getSubAssignments(sub1.getName()); @@ -396,7 +396,7 @@ public void testStopSubscription_SubscriptionStatusFailure() { @Test public void testStopSubscriptionOneShardAlreadyStopped() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -421,7 +421,7 @@ public void testStopSubscriptionOneShardAlreadyStopped() { @Test public void testStopSubscriptionWhenConsumerStopFails() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -450,7 +450,7 @@ public void testStopSubscriptionWhenConsumerStopFails() { @Test public void testStopSubscriptionWhenConsumerStopShardThrows() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); List consumerNodes = getConsumerNodes(3); List assignments = new ArrayList<>(); @@ -479,7 +479,7 @@ public void testStopSubscriptionWhenConsumerStopShardThrows() { @Test public void testUpdateShardOp() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); SubscriptionOperation subOp = OperationMgrTest.getStartOp(sub1); ShardOperation shardOp = OperationMgrTest.getShardStartOp(subOp.getId(), shards.get(0), sub1); @@ -493,7 +493,7 @@ public void testUpdateShardOp() { @Test public void testUpdateWhenUpdateShardOpThrows() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); SubscriptionOperation subOp = OperationMgrTest.getStartOp(sub1); ShardOperation shardOp = OperationMgrTest.getShardStartOp(subOp.getId(), shards.get(0), sub1); @@ -523,7 +523,7 @@ public void testGetPendingSubOps() { @Test public void testRetryOperation() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation subOp = OperationMgrTest.getStartOp(sub1); doReturn(sub1).when(metaStore).getSubscription(sub1.getName()); doNothing().when(operationMgr).enqueue(any(), any()); @@ -535,7 +535,7 @@ public void testRetryOperation() { @Test public void testConsumerNodeJoined() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(3).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); ConsumerNode node = NodeProvider.getConsumerNodes(1).get(0); doReturn(CompletableFuture.completedFuture(NodeProvider.getConsumerInfo(node, sub1, shards))).when(consumerApi) @@ -550,10 +550,10 @@ public void testConsumerNodeJoined() { @Test public void testConsumerNodeLeft() { VaradhiSubscription sub1 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards1 = SubscriptionUtils.shardsOf(sub1); VaradhiSubscription sub2 = - SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub2", "project1", "project1.topic1"); + SubscriptionUtils.builder().setNumShards(2).build("project1.sub2", "project1", "project1.topic1"); List shards2 = SubscriptionUtils.shardsOf(sub2); List nodes = NodeProvider.getConsumerNodes(3); diff --git a/controller/src/test/java/com/flipkart/varadhi/controller/OperationMgrTest.java b/controller/src/test/java/com/flipkart/varadhi/controller/OperationMgrTest.java index 80e9869a..d0746ac3 100644 --- a/controller/src/test/java/com/flipkart/varadhi/controller/OperationMgrTest.java +++ b/controller/src/test/java/com/flipkart/varadhi/controller/OperationMgrTest.java @@ -43,7 +43,7 @@ public void setup() { @Test public void scheduleAndExecuteOperation() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); operationMgr.enqueue(startOp, operation -> { @@ -65,7 +65,7 @@ public void scheduleAndExecuteOperation() { @Test public void orderedExecutionForSingleSubscription() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp1 = getStartOp(sub1); SubscriptionOperation stopOp1 = getStopOp(sub1); SubscriptionOperation startOp2 = getStartOp(sub1); @@ -109,7 +109,7 @@ public void orderedExecutionForSingleSubscription() { @Test public void duplicateOperationsAreIgnored() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); operationMgr.enqueue(startOp, operation -> { @@ -143,9 +143,9 @@ public void duplicateOperationsAreIgnored() { @Test public void parallelExecutionForDifferentSubs() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); - VaradhiSubscription sub2 = SubscriptionUtils.getBuilder().build("project1.sub2", "project1", "project1.topic1"); - VaradhiSubscription sub3 = SubscriptionUtils.getBuilder().build("project1.sub3", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub2 = SubscriptionUtils.builder().build("project1.sub2", "project1", "project1.topic1"); + VaradhiSubscription sub3 = SubscriptionUtils.builder().build("project1.sub3", "project1", "project1.topic1"); SubscriptionOperation startOp1 = getStartOp(sub1); SubscriptionOperation startOp2 = getStopOp(sub2); SubscriptionOperation startOp3 = getStartOp(sub3); @@ -203,8 +203,8 @@ public void parallelExecutionForDifferentSubs() { @Test public void updateOnNonTrackedTaskIsIgnored() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); - VaradhiSubscription sub2 = SubscriptionUtils.getBuilder().build("project1.sub2", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub2 = SubscriptionUtils.builder().build("project1.sub2", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); SubscriptionOperation startOp2 = getStartOp(sub1); SubscriptionOperation startOp3 = getStartOp(sub2); @@ -234,7 +234,7 @@ public void updateOnNonTrackedTaskIsIgnored() { @Test public void partialUpdatesDoesNotCompleteTask() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); operationMgr.enqueue(startOp, operation -> { @@ -261,7 +261,7 @@ public void partialUpdatesDoesNotCompleteTask() { @Test public void updateWaitingTaskIsIgnored() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); SubscriptionOperation stopOp = getStopOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); @@ -295,7 +295,7 @@ public void updateWaitingTaskIsIgnored() { @Test public void enqueueWhenOpExecutorThrows() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); ArgumentCaptor opCaptor = ArgumentCaptor.forClass(SubscriptionOperation.class); CountDownLatch updateLatch = new CountDownLatch(1); @@ -320,7 +320,7 @@ public void enqueueWhenOpExecutorThrows() { @Test public void enqueueWhenOpFailsWithException() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); ArgumentCaptor opCaptor = ArgumentCaptor.forClass(SubscriptionOperation.class); @@ -347,7 +347,7 @@ public void enqueueWhenOpFailsWithException() { @Test public void updateOpThrows() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); ArgumentCaptor opCaptor = ArgumentCaptor.forClass(SubscriptionOperation.class); @@ -377,7 +377,7 @@ public void updateOpThrows() { @Test public void saveFailureInExecutionRemovesPendingTask() { CountDownLatch executionLatch = new CountDownLatch(1); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); ArgumentCaptor opCaptor = ArgumentCaptor.forClass(SubscriptionOperation.class); @@ -402,7 +402,7 @@ public void saveFailureInExecutionRemovesPendingTask() { @Test public void saveFailureInUpdateRemovesPendingTask() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); CompletableFuture executionCalled = new CompletableFuture<>(); ArgumentCaptor opCaptor = ArgumentCaptor.forClass(SubscriptionOperation.class); @@ -423,7 +423,8 @@ public void saveFailureInUpdateRemovesPendingTask() { @Test public void updateOfShardOpUpdatesSubscriptionOp() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); SubscriptionOperation startSubOp = getStartOp(sub1); ShardOperation shard1Op = getShardStartOp(startSubOp.getId(), shards.get(0), sub1); @@ -453,7 +454,8 @@ public void updateOfShardOpUpdatesSubscriptionOp() { @Test public void testCreateAndEnqueue() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startSubOp = getStartOp(sub1); operationMgr.createAndEnqueue(startSubOp, operation -> CompletableFuture.completedFuture(null)); verify(opStore, times(1)).createSubOp(startSubOp); @@ -461,7 +463,8 @@ public void testCreateAndEnqueue() { @Test public void testCreateIfNeededAndExecute() { - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = + SubscriptionUtils.builder().setNumShards(2).build("project1.sub1", "project1", "project1.topic1"); List shards = SubscriptionUtils.shardsOf(sub1); SubscriptionOperation startSubOp = getStartOp(sub1); ShardOperation shard1Op = getShardStartOp(startSubOp.getId(), shards.get(0), sub1); @@ -479,7 +482,7 @@ public void testCreateIfNeededAndExecute() { public void failedOperationShouldBeRetried() { RetryPolicy retryPolicy = new RetryPolicy(1, 1, 1, 1); operationMgr = new OperationMgr(config.getMaxConcurrentOps(), opStore, retryPolicy); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); operationMgr.enqueue(startOp, operation -> CompletableFuture.completedFuture(null)); @@ -505,7 +508,7 @@ public void failedOperationShouldBeRetried() { public void failedOperationIsNotRetriedIfSubAlreadyHasPendingOp() { RetryPolicy retryPolicy = new RetryPolicy(1, 1, 1, 1); operationMgr = new OperationMgr(config.getMaxConcurrentOps(), opStore, retryPolicy); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); SubscriptionOperation stopOp = getStopOp(sub1); @@ -524,7 +527,7 @@ public void failedOperationIsNotRetriedIfSubAlreadyHasPendingOp() { public void subsequentOperationShouldClearPendingRetriesIfAny() { RetryPolicy retryPolicy = new RetryPolicy(1, 1, 1, 1); operationMgr = new OperationMgr(config.getMaxConcurrentOps(), opStore, retryPolicy); - VaradhiSubscription sub1 = SubscriptionUtils.getBuilder().build("project1.sub1", "project1", "project1.topic1"); + VaradhiSubscription sub1 = SubscriptionUtils.builder().build("project1.sub1", "project1", "project1.topic1"); SubscriptionOperation startOp = getStartOp(sub1); SubscriptionOperation stopOp = getStopOp(sub1); diff --git a/controller/src/test/java/com/flipkart/varadhi/controller/impl/LeastAssignedStrategyTests.java b/controller/src/test/java/com/flipkart/varadhi/controller/impl/LeastAssignedStrategyTests.java index 7640237a..479cc679 100644 --- a/controller/src/test/java/com/flipkart/varadhi/controller/impl/LeastAssignedStrategyTests.java +++ b/controller/src/test/java/com/flipkart/varadhi/controller/impl/LeastAssignedStrategyTests.java @@ -23,7 +23,8 @@ public void setUp() { @Test public void testAssign_NoConsumerNodes_ThrowsException() { - VaradhiSubscription subscription = SubscriptionUtils.getBuilder().build("sub1", "subProject1", "subProject1.topic"); + VaradhiSubscription subscription = + SubscriptionUtils.builder().build("sub1", "subProject1", "subProject1.topic"); List shards = SubscriptionUtils.shardsOf(subscription); List consumerNodes = Collections.emptyList(); assertThrows(CapacityException.class, () -> strategy.assign(shards, subscription, consumerNodes)); @@ -32,7 +33,7 @@ public void testAssign_NoConsumerNodes_ThrowsException() { @Test public void testAssign_EnoughResources_ReturnsAssignments() { VaradhiSubscription subscription = - SubscriptionUtils.getBuilder().setNumShards(2).build("sub1", "subProject1", "subProject1.topic"); + SubscriptionUtils.builder().setNumShards(2).build("sub1", "subProject1", "subProject1.topic"); List shards = SubscriptionUtils.shardsOf(subscription); List nodes = NodeProvider.getConsumerNodes(2); NodeCapacity initialCapacity = nodes.get(0).getAvailable().clone(); @@ -51,7 +52,7 @@ public void testAssign_EnoughResources_ReturnsAssignments() { @Test public void testAssign_NotEnoughResources_ThrowsException() { VaradhiSubscription subscription = - SubscriptionUtils.getBuilder().setNumShards(2).build("sub1", "subProject1", "subProject1.topic"); + SubscriptionUtils.builder().setNumShards(2).build("sub1", "subProject1", "subProject1.topic"); List shards = SubscriptionUtils.shardsOf(subscription); List nodes = NodeProvider.getConsumerNodes(2, NodeProvider.getNodeCapacity(400, 10000)); assertThrows(CapacityException.class, () -> strategy.assign(shards, subscription, nodes)); @@ -62,7 +63,7 @@ public void testAssign_NotEnoughResources_ThrowsException() { public void testAssign_NotEnoughDistinctNodes_ReusesNodes() { // Create two shards and one consumer node with enough resources VaradhiSubscription subscription = - SubscriptionUtils.getBuilder().setNumShards(3).setCapacity(new TopicCapacityPolicy(12000, 15000, 2)) + SubscriptionUtils.builder().setNumShards(3).setCapacity(new TopicCapacityPolicy(12000, 15000, 2)) .build("sub1", "subProject1", "subProject1.topic"); List shards = SubscriptionUtils.shardsOf(subscription); List nodes = new ArrayList<>(); @@ -84,7 +85,7 @@ public void testAssign_NotEnoughDistinctNodes_ReusesNodes() { public void testAssignReusesAllNodesWhenNoAvailableNodes() { // Create two shards and one consumer node with enough resources VaradhiSubscription subscription = - SubscriptionUtils.getBuilder().setNumShards(4).setCapacity(new TopicCapacityPolicy(12000, 16000, 2)) + SubscriptionUtils.builder().setNumShards(4).setCapacity(new TopicCapacityPolicy(12000, 16000, 2)) .build("sub1", "subProject1", "subProject1.topic"); List shards = SubscriptionUtils.shardsOf(subscription); List nodes = new ArrayList<>(); diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/LifecycleStatus.java b/entities/src/main/java/com/flipkart/varadhi/entities/LifecycleStatus.java new file mode 100644 index 00000000..6eb4a0ca --- /dev/null +++ b/entities/src/main/java/com/flipkart/varadhi/entities/LifecycleStatus.java @@ -0,0 +1,115 @@ +package com.flipkart.varadhi.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Represents the lifecycle status of an entity with its state, message, and action code. + * This class ensures consistency across different entity types such as topics and subscriptions. + */ +@Getter +@AllArgsConstructor(onConstructor = @__(@JsonCreator)) +public class LifecycleStatus { + + /** + * The current state of the entity. + */ + private State state; + + /** + * The message associated with the current state. + */ + private String message; + + /** + * The action code indicating the reason for the current state. + */ + private ActionCode actionCode; + + /** + * Constructs a new LifecycleStatus instance with the specified state and action code. + * The message is set to the default message of the state. + * + * @param state the state of the entity + * @param actionCode the action code indicating the reason for the state + */ + public LifecycleStatus(State state, ActionCode actionCode) { + this(state, state.getDefaultMessage(), actionCode); + } + + /** + * Updates the lifecycle status with the specified state, message, and action code. + * + * @param state the new state of the entity + * @param message the new message associated with the state + * @param actionCode the new action code indicating the reason for the state + */ + public void update(State state, String message, ActionCode actionCode) { + this.state = state; + this.message = message != null ? message : state.getDefaultMessage(); + this.actionCode = actionCode; + } + + /** + * Updates the lifecycle status with the specified state and message. + * The action code remains unchanged. + * + * @param state the new state of the entity + * @param message the new message associated with the state + */ + public void update(State state, String message) { + update(state, message, this.actionCode); + } + + /** + * Enum representing the possible states of an entity. + */ + @Getter + @RequiredArgsConstructor + public enum State { + CREATING("Creation in progress."), + CREATED("Successfully created."), + CREATE_FAILED("Creation failed."), + + UPDATING("Update in progress."), + UPDATED("Successfully updated."), + UPDATE_FAILED("Update failed."), + + DELETING("Deletion in progress."), + DELETED("Successfully deleted."), + DELETE_FAILED("Deletion failed."), + + ACTIVE("Currently active."), + INACTIVE("Currently inactive."); + + /** + * The default message associated with the state. + */ + private final String defaultMessage; + } + + /** + * Enum representing the action codes for entity actions. + */ + public enum ActionCode { +// USER_INITIATED_ACTION, // Action initiated directly by the user. +// USER_REQUESTED_ADMIN_ACTION, // Action requested by the user to be performed by an admin. +// ADMIN_FORCED_ACTION, // Action intentionally performed by an admin. +// SYSTEM_ACTION; // Action performed by the system due to policy. + + USER_ACTION, + ADMIN_ACTION, + SYSTEM_ACTION; + + /** + * Checks if the action is allowed to be performed by the user. + * + * @return true if the action is USER_ACTION, false otherwise. + */ + public boolean isUserAllowed() { + return this == USER_ACTION; + } + } +} diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/ResourceActionRequest.java b/entities/src/main/java/com/flipkart/varadhi/entities/ResourceActionRequest.java new file mode 100644 index 00000000..a1a17f79 --- /dev/null +++ b/entities/src/main/java/com/flipkart/varadhi/entities/ResourceActionRequest.java @@ -0,0 +1,19 @@ +package com.flipkart.varadhi.entities; + +import jakarta.validation.constraints.NotNull; + +/** + * Represents a request for an action on a resource, containing an action code and an optional message. + * + * @param actionCode the action code indicating the action to be performed, must not be null + * @param message an optional message associated with the action + */ +public record ResourceActionRequest(@NotNull LifecycleStatus.ActionCode actionCode, String message) + implements Validatable { + + public ResourceActionRequest { + if (actionCode == null) { + throw new NullPointerException("actionCode must not be null"); + } + } +} diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java index ea99b8c0..5102ba95 100644 --- a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java +++ b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiSubscription.java @@ -1,8 +1,9 @@ package com.flipkart.varadhi.entities; -import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import java.util.Map; @@ -22,7 +23,7 @@ public class VaradhiSubscription extends MetaStoreEntity { private RetryPolicy retryPolicy; private ConsumptionPolicy consumptionPolicy; private SubscriptionShards shards; - private Status status; + private LifecycleStatus status; private Map properties; private static final String SHARDS_ERROR = "Shards cannot be null or empty"; @@ -55,7 +56,7 @@ private VaradhiSubscription( RetryPolicy retryPolicy, ConsumptionPolicy consumptionPolicy, SubscriptionShards shards, - Status status, + LifecycleStatus status, Map properties ) { super(name, version); @@ -84,6 +85,7 @@ private VaradhiSubscription( * @param consumptionPolicy the consumption policy of the subscription * @param shards the shards of the subscription * @param properties the properties of the subscription + * @param actionCode the action code indicating the reason for the state * * @return a new VaradhiSubscription instance */ @@ -97,11 +99,12 @@ public static VaradhiSubscription of( RetryPolicy retryPolicy, ConsumptionPolicy consumptionPolicy, SubscriptionShards shards, - Map properties + Map properties, + LifecycleStatus.ActionCode actionCode ) { return new VaradhiSubscription( name, INITIAL_VERSION, project, topic, description, grouped, endpoint, retryPolicy, consumptionPolicy, - shards, new Status(State.CREATING), properties + shards, new LifecycleStatus(LifecycleStatus.State.CREATING, actionCode), properties ); } @@ -112,7 +115,7 @@ shards, new Status(State.CREATING), properties */ @JsonIgnore public boolean isWellProvisioned() { - return State.CREATED.equals(status.getState()); + return LifecycleStatus.State.CREATED.equals(status.getState()); } /** @@ -122,7 +125,7 @@ public boolean isWellProvisioned() { */ @JsonIgnore public boolean isActive() { - return !State.INACTIVE.equals(status.getState()); + return !LifecycleStatus.State.INACTIVE.equals(status.getState()); } /** @@ -131,14 +134,14 @@ public boolean isActive() { * @param message the failure message */ public void markCreateFailed(String message) { - updateStatus(State.CREATE_FAILED, message); + this.status.update(LifecycleStatus.State.CREATE_FAILED, message); } /** * Marks the subscription as created. */ public void markCreated() { - updateStatus(State.CREATED, null); + this.status.update(LifecycleStatus.State.CREATED, null); } /** @@ -147,28 +150,37 @@ public void markCreated() { * @param message the failure message */ public void markDeleteFailed(String message) { - updateStatus(State.DELETE_FAILED, message); + this.status.update(LifecycleStatus.State.DELETE_FAILED, message); } /** * Marks the subscription as deleting. + * + * @param actionCode the action code indicating why it's being deleted + * @param message the message for the action */ - public void markDeleting() { - updateStatus(State.DELETING, null); + public void markDeleting(LifecycleStatus.ActionCode actionCode, String message) { + this.status.update(LifecycleStatus.State.DELETING, message, actionCode); } /** * Marks the subscription as inactive. + * + * @param actionCode the action code indicating why it's inactive + * @param message the message for the action */ - public void markInactive() { - updateStatus(State.INACTIVE, null); + public void markInactive(LifecycleStatus.ActionCode actionCode, String message) { + this.status.update(LifecycleStatus.State.INACTIVE, message, actionCode); } /** * Restores the subscription to the created state. + * + * @param actionCode the action code indicating why it's restored + * @param message the message for the action */ - public void restore() { - updateStatus(State.CREATED, "Entity restored to created state."); + public void restore(LifecycleStatus.ActionCode actionCode, String message) { + this.status.update(LifecycleStatus.State.CREATED, message, actionCode); } /** @@ -237,46 +249,4 @@ private static Map validateProperties(Map proper } return properties; } - - /** - * Updates the status of the subscription. - * - * @param state the new state of the subscription - * @param message the status message - */ - private void updateStatus(State state, String message) { - status.setState(state); - status.setMessage(message); - } - - /** - * Enum representing the state of the subscription. - */ - public enum State { - CREATING, - CREATE_FAILED, - CREATED, - DELETING, - DELETE_FAILED, - INACTIVE - } - - /** - * Represents the status of the subscription. - */ - @Data - @AllArgsConstructor(onConstructor = @__(@JsonCreator)) - public static class Status { - String message; - State state; - - /** - * Constructs a new Status instance with the given state. - * - * @param state the state of the subscription - */ - public Status(State state) { - this.state = state; - } - } } diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java index 2721ada8..ac612309 100644 --- a/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java +++ b/entities/src/main/java/com/flipkart/varadhi/entities/VaradhiTopic.java @@ -17,7 +17,7 @@ public class VaradhiTopic extends AbstractTopic { private final Map internalTopics; private final boolean grouped; private final TopicCapacityPolicy capacity; - private Status status; + private final LifecycleStatus status; /** * Constructs a new VaradhiTopic instance. @@ -31,7 +31,7 @@ public class VaradhiTopic extends AbstractTopic { */ private VaradhiTopic( String name, int version, boolean grouped, TopicCapacityPolicy capacity, - Map internalTopics, Status status + Map internalTopics, LifecycleStatus status ) { super(name, version); this.grouped = grouped; @@ -43,22 +43,30 @@ private VaradhiTopic( /** * Creates a new VaradhiTopic instance. * - * @param project the project associated with the topic - * @param name the name of the topic - * @param grouped whether the topic is grouped - * @param capacity the capacity policy of the topic + * @param project the project associated with the topic + * @param name the name of the topic + * @param grouped whether the topic is grouped + * @param capacity the capacity policy of the topic + * @param actionCode the action code indicating the reason for the state + * * @return a new VaradhiTopic instance */ - public static VaradhiTopic of(String project, String name, boolean grouped, TopicCapacityPolicy capacity) { + public static VaradhiTopic of( + String project, String name, boolean grouped, TopicCapacityPolicy capacity, + LifecycleStatus.ActionCode actionCode + ) { return new VaradhiTopic( - buildTopicName(project, name), INITIAL_VERSION, grouped, capacity, new HashMap<>(), Status.ACTIVE); + buildTopicName(project, name), INITIAL_VERSION, grouped, capacity, new HashMap<>(), + new LifecycleStatus(LifecycleStatus.State.ACTIVE, actionCode) + ); } /** * Builds the topic name from the project name and topic name. * * @param projectName the name of the project - * @param topicName the name of the topic + * @param topicName the name of the topic + * * @return the constructed topic name */ public static String buildTopicName(String projectName, String topicName) { @@ -68,7 +76,7 @@ public static String buildTopicName(String projectName, String topicName) { /** * Adds an internal topic for a specific region. * - * @param region the region for the internal topic + * @param region the region for the internal topic * @param internalTopic the internal topic to add */ public void addInternalTopic(String region, InternalCompositeTopic internalTopic) { @@ -89,6 +97,7 @@ public String getProjectName() { * Retrieves the produce topic for a specific region. * * @param region the region for which to retrieve the produce topic + * * @return the produce topic for the specified region */ public InternalCompositeTopic getProduceTopicForRegion(String region) { @@ -96,17 +105,23 @@ public InternalCompositeTopic getProduceTopicForRegion(String region) { } /** - * Updates the status of the topic. + * Marks the topic as active. * - * @param newStatus the new status to set + * @param actionCode the action code indicating why the topic is being marked as active + * @param message the message for the action + */ + public void markActive(LifecycleStatus.ActionCode actionCode, String message) { + this.status.update(LifecycleStatus.State.ACTIVE, message, actionCode); + } + + /** + * Marks the topic as inactive. * - * @throws IllegalArgumentException if the new status is null + * @param actionCode the action code indicating why the topic is being marked as inactive + * @param message the message for the action */ - public void updateStatus(Status newStatus) { - if (newStatus == null) { - throw new IllegalArgumentException("Status cannot be null"); - } - this.status = newStatus; + public void markInactive(LifecycleStatus.ActionCode actionCode, String message) { + this.status.update(LifecycleStatus.State.INACTIVE, message, actionCode); } /** @@ -116,14 +131,6 @@ public void updateStatus(Status newStatus) { */ @JsonIgnore public boolean isActive() { - return this.status == Status.ACTIVE; - } - - /** - * Enum representing the status of the topic. - */ - public enum Status { - ACTIVE, - INACTIVE + return this.status.getState() == LifecycleStatus.State.ACTIVE; } } diff --git a/entities/src/main/java/com/flipkart/varadhi/entities/auth/ResourceAction.java b/entities/src/main/java/com/flipkart/varadhi/entities/auth/ResourceAction.java index 451106e4..3b2878eb 100644 --- a/entities/src/main/java/com/flipkart/varadhi/entities/auth/ResourceAction.java +++ b/entities/src/main/java/com/flipkart/varadhi/entities/auth/ResourceAction.java @@ -1,58 +1,119 @@ package com.flipkart.varadhi.entities.auth; import lombok.Getter; +import lombok.RequiredArgsConstructor; +/** + * Enum representing various resource actions in the system. + */ @Getter +@RequiredArgsConstructor public enum ResourceAction { - ORG_CREATE(ResourceType.ORG, "create"), - ORG_UPDATE(ResourceType.ORG, "update"), - ORG_DELETE(ResourceType.ORG, "delete"), - ORG_GET(ResourceType.ORG, "get"), - ORG_LIST(ResourceType.ROOT, "list"), - ORG_PROJECT_MIGRATE(ResourceType.ORG, "migrate"), - - TEAM_CREATE(ResourceType.TEAM, "create"), - TEAM_UPDATE(ResourceType.TEAM, "update"), - TEAM_DELETE(ResourceType.TEAM, "delete"), - TEAM_GET(ResourceType.TEAM, "get"), - TEAM_LIST(ResourceType.ORG, "list"), - - PROJECT_CREATE(ResourceType.PROJECT, "create"), - PROJECT_UPDATE(ResourceType.PROJECT, "update"), - PROJECT_DELETE(ResourceType.PROJECT, "delete"), - PROJECT_GET(ResourceType.PROJECT, "get"), - PROJECT_LIST(ResourceType.TEAM, "list"), - - TOPIC_CREATE(ResourceType.TOPIC, "create"), - TOPIC_GET(ResourceType.TOPIC, "get"), - TOPIC_DELETE(ResourceType.TOPIC, "delete"), - TOPIC_UPDATE(ResourceType.TOPIC, "update"), - TOPIC_LIST(ResourceType.PROJECT, "list"), - TOPIC_CONSUME(ResourceType.TOPIC, "consume"), - TOPIC_PRODUCE(ResourceType.TOPIC, "produce"), - - SUBSCRIPTION_CREATE(ResourceType.SUBSCRIPTION, "create"), - SUBSCRIPTION_GET(ResourceType.SUBSCRIPTION, "get"), - SUBSCRIPTION_LIST(ResourceType.PROJECT, "list"), - SUBSCRIPTION_DELETE(ResourceType.SUBSCRIPTION, "delete"), - SUBSCRIPTION_UPDATE(ResourceType.SUBSCRIPTION, "update"), - SUBSCRIPTION_SEEK(ResourceType.SUBSCRIPTION, "seek"), - - IAM_POLICY_GET(ResourceType.IAM_POLICY, "get"), - IAM_POLICY_SET(ResourceType.IAM_POLICY, "set"), - IAM_POLICY_DELETE(ResourceType.IAM_POLICY, "delete"); + /** + * Actions related to organization resources. + */ + ORG_CREATE(ResourceType.ORG, Action.CREATE), + ORG_UPDATE(ResourceType.ORG, Action.UPDATE), + ORG_DELETE(ResourceType.ORG, Action.DELETE), + ORG_GET(ResourceType.ORG, Action.GET), + ORG_LIST(ResourceType.ROOT, Action.LIST), + ORG_PROJECT_MIGRATE(ResourceType.ORG, Action.MIGRATE), + /** + * Actions related to team resources. + */ + TEAM_CREATE(ResourceType.TEAM, Action.CREATE), + TEAM_UPDATE(ResourceType.TEAM, Action.UPDATE), + TEAM_DELETE(ResourceType.TEAM, Action.DELETE), + TEAM_GET(ResourceType.TEAM, Action.GET), + TEAM_LIST(ResourceType.ORG, Action.LIST), + + /** + * Actions related to project resources. + */ + PROJECT_CREATE(ResourceType.PROJECT, Action.CREATE), + PROJECT_UPDATE(ResourceType.PROJECT, Action.UPDATE), + PROJECT_DELETE(ResourceType.PROJECT, Action.DELETE), + PROJECT_GET(ResourceType.PROJECT, Action.GET), + PROJECT_LIST(ResourceType.TEAM, Action.LIST), + + /** + * Actions related to topic resources. + */ + TOPIC_CREATE(ResourceType.TOPIC, Action.CREATE), + TOPIC_UPDATE(ResourceType.TOPIC, Action.UPDATE), + TOPIC_DELETE(ResourceType.TOPIC, Action.DELETE), + TOPIC_GET(ResourceType.TOPIC, Action.GET), + TOPIC_LIST(ResourceType.PROJECT, Action.LIST), + TOPIC_RESTORE(ResourceType.TOPIC, Action.RESTORE), + TOPIC_CONSUME(ResourceType.TOPIC, Action.CONSUME), + TOPIC_PRODUCE(ResourceType.TOPIC, Action.PRODUCE), + + /** + * Actions related to subscription resources. + */ + SUBSCRIPTION_CREATE(ResourceType.SUBSCRIPTION, Action.CREATE), + SUBSCRIPTION_UPDATE(ResourceType.SUBSCRIPTION, Action.UPDATE), + SUBSCRIPTION_DELETE(ResourceType.SUBSCRIPTION, Action.DELETE), + SUBSCRIPTION_GET(ResourceType.SUBSCRIPTION, Action.GET), + SUBSCRIPTION_LIST(ResourceType.PROJECT, Action.LIST), + SUBSCRIPTION_RESTORE(ResourceType.SUBSCRIPTION, Action.RESTORE), + SUBSCRIPTION_SEEK(ResourceType.SUBSCRIPTION, Action.SEEK), + + /** + * Actions related to IAM policy resources. + */ + IAM_POLICY_GET(ResourceType.IAM_POLICY, Action.GET), + IAM_POLICY_SET(ResourceType.IAM_POLICY, Action.SET), + IAM_POLICY_DELETE(ResourceType.IAM_POLICY, Action.DELETE); + + /** + * The type of resource associated with the action. + */ private final ResourceType resourceType; - private final String action; - ResourceAction(ResourceType type, String action) { - this.resourceType = type; - this.action = action; - } + /** + * The action to be performed on the resource. + */ + private final Action action; + /** + * Returns the string representation of the resource action. + * + * @return the string representation of the resource action + */ @Override public String toString() { return String.format("%s.%s", resourceType, action); } + + /** + * Enum representing all possible actions on resources. + */ + @Getter + @RequiredArgsConstructor + public enum Action { + CREATE("create"), + UPDATE("update"), + DELETE("delete"), + GET("get"), + LIST("list"), + MIGRATE("migrate"), + RESTORE("restore"), + CONSUME("consume"), + PRODUCE("produce"), + SEEK("seek"), + SET("set"); + + /** + * The name of the action. + */ + private final String actionName; + + @Override + public String toString() { + return actionName; + } + } } diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/LifecycleStatusTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/LifecycleStatusTest.java new file mode 100644 index 00000000..9836d9b9 --- /dev/null +++ b/entities/src/test/java/com/flipkart/varadhi/entities/LifecycleStatusTest.java @@ -0,0 +1,68 @@ +package com.flipkart.varadhi.entities; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LifecycleStatusTest { + + @Test + void constructor_SetsStateAndActionCode() { + LifecycleStatus status = + new LifecycleStatus(LifecycleStatus.State.CREATED, LifecycleStatus.ActionCode.SYSTEM_ACTION); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATED, status.getState()), + () -> assertEquals("Successfully created.", status.getMessage()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, status.getActionCode()) + ); + } + + @Test + void update_ChangesStateMessageAndActionCode() { + LifecycleStatus status = + new LifecycleStatus(LifecycleStatus.State.CREATING, LifecycleStatus.ActionCode.SYSTEM_ACTION); + status.update( + LifecycleStatus.State.CREATED, "Created successfully", + LifecycleStatus.ActionCode.USER_ACTION + ); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATED, status.getState()), + () -> assertEquals("Created successfully", status.getMessage()), + () -> assertEquals(LifecycleStatus.ActionCode.USER_ACTION, status.getActionCode()) + ); + } + + @Test + void update_ChangesStateAndMessageOnly() { + LifecycleStatus status = + new LifecycleStatus(LifecycleStatus.State.CREATING, LifecycleStatus.ActionCode.SYSTEM_ACTION); + status.update(LifecycleStatus.State.CREATED, "Created successfully"); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATED, status.getState()), + () -> assertEquals("Created successfully", status.getMessage()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, status.getActionCode()) + ); + } + + @Test + void update_NullMessageUsesDefaultMessage() { + LifecycleStatus status = + new LifecycleStatus(LifecycleStatus.State.CREATING, LifecycleStatus.ActionCode.SYSTEM_ACTION); + status.update(LifecycleStatus.State.CREATED, null); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATED, status.getState()), + () -> assertEquals("Successfully created.", status.getMessage()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, status.getActionCode()) + ); + } + + @Test + void actionCode_isUserAllowed_ReturnsCorrectValue() { + assertTrue(LifecycleStatus.ActionCode.USER_ACTION.isUserAllowed()); + assertFalse(LifecycleStatus.ActionCode.ADMIN_ACTION.isUserAllowed()); + assertFalse(LifecycleStatus.ActionCode.SYSTEM_ACTION.isUserAllowed()); + } +} diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/ResourceActionRequestTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/ResourceActionRequestTest.java new file mode 100644 index 00000000..67057f77 --- /dev/null +++ b/entities/src/test/java/com/flipkart/varadhi/entities/ResourceActionRequestTest.java @@ -0,0 +1,59 @@ +package com.flipkart.varadhi.entities; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ResourceActionRequestTest { + + @Test + void constructor_SetsActionCodeAndMessage() { + ResourceActionRequest request = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Test message" + ); + assertAll( + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, request.actionCode()), + () -> assertEquals("Test message", request.message()) + ); + } + + @Test + void constructor_NullMessage_SetsActionCodeOnly() { + ResourceActionRequest request = new ResourceActionRequest(LifecycleStatus.ActionCode.SYSTEM_ACTION, null); + assertAll( + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, request.actionCode()), + () -> assertNull(request.message()) + ); + } + + @Test + void constructor_NullActionCode_ThrowsException() { + assertThrows( + NullPointerException.class, () -> { + new ResourceActionRequest(null, "Test message"); + } + ); + } + + @Test + void actionCode_ReturnsCorrectActionCode() { + ResourceActionRequest request = new ResourceActionRequest( + LifecycleStatus.ActionCode.USER_ACTION, + "Test message" + ); + assertEquals(LifecycleStatus.ActionCode.USER_ACTION, request.actionCode()); + } + + @Test + void message_ReturnsCorrectMessage() { + ResourceActionRequest request = new ResourceActionRequest( + LifecycleStatus.ActionCode.USER_ACTION, + "Test message" + ); + assertEquals("Test message", request.message()); + } +} diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java index e357f25b..876a7575 100644 --- a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java +++ b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiSubscriptionTest.java @@ -5,9 +5,9 @@ import java.net.URI; import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -40,19 +40,22 @@ void createSubscription_Success() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") - ); - - assertEquals("sub1", subscription.getName()); - assertEquals("project1", subscription.getProject()); - assertEquals("topic1", subscription.getTopic()); - assertEquals("description", subscription.getDescription()); - assertTrue(subscription.isGrouped()); - assertNotNull(subscription.getEndpoint()); - assertNotNull(subscription.getRetryPolicy()); - assertNotNull(subscription.getConsumptionPolicy()); - assertNotNull(subscription.getShards()); - assertNotNull(subscription.getProperties()); + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + + assertAll( + () -> assertEquals("sub1", subscription.getName()), + () -> assertEquals("project1", subscription.getProject()), + () -> assertEquals("topic1", subscription.getTopic()), + () -> assertEquals("description", subscription.getDescription()), + () -> assertTrue(subscription.isGrouped()), + () -> assertNotNull(subscription.getEndpoint()), + () -> assertNotNull(subscription.getRetryPolicy()), + () -> assertNotNull(subscription.getConsumptionPolicy()), + () -> assertNotNull(subscription.getShards()), + () -> assertNotNull(subscription.getProperties()), + () -> assertEquals(LifecycleStatus.State.CREATING, subscription.getStatus().getState()) + ); } @Test @@ -62,7 +65,7 @@ void createSubscription_InvalidProject_ThrowsException() { VaradhiSubscription.of( "sub1", "", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); } ); @@ -77,7 +80,7 @@ void createSubscription_InvalidTopic_ThrowsException() { VaradhiSubscription.of( "sub1", "project1", "", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); } ); @@ -92,7 +95,7 @@ void createSubscription_NullShards_ThrowsException() { VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - null, Map.of("key", "value") + null, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); } ); @@ -107,7 +110,7 @@ void createSubscription_NullProperties_ThrowsException() { VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, null + DEFAULT_SHARDS, null, LifecycleStatus.ActionCode.SYSTEM_ACTION ); } ); @@ -120,12 +123,14 @@ void markCreateFailed_ChangesStateToCreateFailed() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); subscription.markCreateFailed("Creation failed"); - assertEquals(VaradhiSubscription.State.CREATE_FAILED, subscription.getStatus().getState()); - assertEquals("Creation failed", subscription.getStatus().getMessage()); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATE_FAILED, subscription.getStatus().getState()), + () -> assertEquals("Creation failed", subscription.getStatus().getMessage()) + ); } @Test @@ -133,12 +138,14 @@ void markCreated_ChangesStateToCreated() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); subscription.markCreated(); - assertEquals(VaradhiSubscription.State.CREATED, subscription.getStatus().getState()); - assertNull(subscription.getStatus().getMessage()); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATED, subscription.getStatus().getState()), + () -> assertEquals("Successfully created.", subscription.getStatus().getMessage()) + ); } @Test @@ -146,12 +153,14 @@ void markDeleteFailed_ChangesStateToDeleteFailed() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); subscription.markDeleteFailed("Deletion failed"); - assertEquals(VaradhiSubscription.State.DELETE_FAILED, subscription.getStatus().getState()); - assertEquals("Deletion failed", subscription.getStatus().getMessage()); + assertAll( + () -> assertEquals(LifecycleStatus.State.DELETE_FAILED, subscription.getStatus().getState()), + () -> assertEquals("Deletion failed", subscription.getStatus().getMessage()) + ); } @Test @@ -159,12 +168,14 @@ void markDeleting_ChangesStateToDeleting() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); - subscription.markDeleting(); - assertEquals(VaradhiSubscription.State.DELETING, subscription.getStatus().getState()); - assertNull(subscription.getStatus().getMessage()); + subscription.markDeleting(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Deleting"); + assertAll( + () -> assertEquals(LifecycleStatus.State.DELETING, subscription.getStatus().getState()), + () -> assertEquals("Deleting", subscription.getStatus().getMessage()) + ); } @Test @@ -172,12 +183,14 @@ void markInactive_ChangesStateToInactive() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); - subscription.markInactive(); - assertEquals(VaradhiSubscription.State.INACTIVE, subscription.getStatus().getState()); - assertNull(subscription.getStatus().getMessage()); + subscription.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive"); + assertAll( + () -> assertEquals(LifecycleStatus.State.INACTIVE, subscription.getStatus().getState()), + () -> assertEquals("Inactive", subscription.getStatus().getMessage()) + ); } @Test @@ -185,13 +198,15 @@ void restore_ChangesStateToCreated() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); - subscription.markInactive(); - subscription.restore(); - assertEquals(VaradhiSubscription.State.CREATED, subscription.getStatus().getState()); - assertEquals("Entity restored to created state.", subscription.getStatus().getMessage()); + subscription.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive"); + subscription.restore(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Restored"); + assertAll( + () -> assertEquals(LifecycleStatus.State.CREATED, subscription.getStatus().getState()), + () -> assertEquals("Restored", subscription.getStatus().getMessage()) + ); } @Test @@ -199,7 +214,7 @@ void getIntProperty_ReturnsCorrectValue() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "10") + DEFAULT_SHARDS, Map.of("key", "10"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); assertEquals(10, subscription.getIntProperty("key")); @@ -210,7 +225,7 @@ void getIntProperty_PropertyNotFound_ThrowsException() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "value") + DEFAULT_SHARDS, Map.of("key", "value"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); IllegalArgumentException exception = assertThrows( @@ -227,7 +242,7 @@ void getIntProperty_InvalidValue_ThrowsException() { VaradhiSubscription subscription = VaradhiSubscription.of( "sub1", "project1", "topic1", "description", true, DEFAULT_ENDPOINT, DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, - DEFAULT_SHARDS, Map.of("key", "invalid") + DEFAULT_SHARDS, Map.of("key", "invalid"), LifecycleStatus.ActionCode.SYSTEM_ACTION ); NumberFormatException exception = assertThrows( diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java index 979eeca6..39cdde87 100644 --- a/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java +++ b/entities/src/test/java/com/flipkart/varadhi/entities/VaradhiTopicTest.java @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class VaradhiTopicTest { @@ -25,7 +24,10 @@ public DummyStorageTopic(String name, int version) { } private VaradhiTopic createDefaultVaradhiTopic(boolean grouped) { - return VaradhiTopic.of(PROJECT_NAME, TOPIC_NAME, grouped, TOPIC_CAPACITY); + return VaradhiTopic.of( + PROJECT_NAME, TOPIC_NAME, grouped, TOPIC_CAPACITY, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); } @Test @@ -107,26 +109,40 @@ void getProduceTopicForRegion_WithUnknownRegion_ReturnsNull() { } @Test - void updateStatus_ChangesActiveState() { + void markActive_ChangesStatusToActive() { VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); - varadhiTopic.updateStatus(VaradhiTopic.Status.INACTIVE); - assertFalse(varadhiTopic.isActive(), "Inactive status update failed"); + varadhiTopic.markActive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Activated"); - varadhiTopic.updateStatus(VaradhiTopic.Status.ACTIVE); - assertTrue(varadhiTopic.isActive(), "Active status update failed"); + assertAll( + () -> assertTrue(varadhiTopic.isActive(), "Active status update failed"), + () -> assertEquals( + LifecycleStatus.State.ACTIVE, varadhiTopic.getStatus().getState(), + "Status state mismatch" + ), + () -> assertEquals( + LifecycleStatus.ActionCode.SYSTEM_ACTION, varadhiTopic.getStatus().getActionCode(), + "Action code mismatch" + ) + ); } @Test - void updateStatus_WithNull_ThrowsIllegalArgumentException() { + void markInactive_ChangesStatusToInactive() { VaradhiTopic varadhiTopic = createDefaultVaradhiTopic(false); - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> varadhiTopic.updateStatus(null), - "Null status should throw IllegalArgumentException" - ); + varadhiTopic.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Deactivated"); - assertEquals("Status cannot be null", exception.getMessage(), "Exception message mismatch"); + assertAll( + () -> assertFalse(varadhiTopic.isActive(), "Inactive status update failed"), + () -> assertEquals( + LifecycleStatus.State.INACTIVE, varadhiTopic.getStatus().getState(), + "Status state mismatch" + ), + () -> assertEquals( + LifecycleStatus.ActionCode.SYSTEM_ACTION, varadhiTopic.getStatus().getActionCode(), + "Action code mismatch" + ) + ); } } diff --git a/entities/src/test/java/com/flipkart/varadhi/entities/auth/ResourceActionTest.java b/entities/src/test/java/com/flipkart/varadhi/entities/auth/ResourceActionTest.java new file mode 100644 index 00000000..56f31218 --- /dev/null +++ b/entities/src/test/java/com/flipkart/varadhi/entities/auth/ResourceActionTest.java @@ -0,0 +1,62 @@ +package com.flipkart.varadhi.entities.auth; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResourceActionTest { + + @Test + void toString_ReturnsCorrectFormat() { + assertAll( + () -> assertEquals("varadhi.org.create", ResourceAction.ORG_CREATE.toString()), + () -> assertEquals("varadhi.team.update", ResourceAction.TEAM_UPDATE.toString()), + () -> assertEquals("varadhi.project.delete", ResourceAction.PROJECT_DELETE.toString()), + () -> assertEquals("varadhi.topic.get", ResourceAction.TOPIC_GET.toString()), + () -> assertEquals("varadhi.project.list", ResourceAction.SUBSCRIPTION_LIST.toString()), + () -> assertEquals("varadhi.iam_policy.set", ResourceAction.IAM_POLICY_SET.toString()) + ); + } + + @Test + void actionName_ReturnsCorrectActionName() { + assertAll( + () -> assertEquals("create", ResourceAction.Action.CREATE.toString()), + () -> assertEquals("update", ResourceAction.Action.UPDATE.toString()), + () -> assertEquals("delete", ResourceAction.Action.DELETE.toString()), + () -> assertEquals("get", ResourceAction.Action.GET.toString()), + () -> assertEquals("list", ResourceAction.Action.LIST.toString()), + () -> assertEquals("migrate", ResourceAction.Action.MIGRATE.toString()), + () -> assertEquals("restore", ResourceAction.Action.RESTORE.toString()), + () -> assertEquals("consume", ResourceAction.Action.CONSUME.toString()), + () -> assertEquals("produce", ResourceAction.Action.PRODUCE.toString()), + () -> assertEquals("seek", ResourceAction.Action.SEEK.toString()), + () -> assertEquals("set", ResourceAction.Action.SET.toString()) + ); + } + + @Test + void resourceType_ReturnsCorrectResourceType() { + assertAll( + () -> assertEquals(ResourceType.ORG, ResourceAction.ORG_CREATE.getResourceType()), + () -> assertEquals(ResourceType.TEAM, ResourceAction.TEAM_UPDATE.getResourceType()), + () -> assertEquals(ResourceType.PROJECT, ResourceAction.PROJECT_DELETE.getResourceType()), + () -> assertEquals(ResourceType.TOPIC, ResourceAction.TOPIC_GET.getResourceType()), + () -> assertEquals(ResourceType.PROJECT, ResourceAction.SUBSCRIPTION_LIST.getResourceType()), + () -> assertEquals(ResourceType.IAM_POLICY, ResourceAction.IAM_POLICY_SET.getResourceType()) + ); + } + + @Test + void action_ReturnsCorrectAction() { + assertAll( + () -> assertEquals(ResourceAction.Action.CREATE, ResourceAction.ORG_CREATE.getAction()), + () -> assertEquals(ResourceAction.Action.UPDATE, ResourceAction.TEAM_UPDATE.getAction()), + () -> assertEquals(ResourceAction.Action.DELETE, ResourceAction.PROJECT_DELETE.getAction()), + () -> assertEquals(ResourceAction.Action.GET, ResourceAction.TOPIC_GET.getAction()), + () -> assertEquals(ResourceAction.Action.LIST, ResourceAction.SUBSCRIPTION_LIST.getAction()), + () -> assertEquals(ResourceAction.Action.SET, ResourceAction.IAM_POLICY_SET.getAction()) + ); + } +} diff --git a/entities/src/testFixtures/java/com/flipkart/varadhi/entities/SubscriptionUtils.java b/entities/src/testFixtures/java/com/flipkart/varadhi/entities/SubscriptionUtils.java index dcb71422..6ebdb568 100644 --- a/entities/src/testFixtures/java/com/flipkart/varadhi/entities/SubscriptionUtils.java +++ b/entities/src/testFixtures/java/com/flipkart/varadhi/entities/SubscriptionUtils.java @@ -5,12 +5,35 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +/** + * Utility class for creating and managing Varadhi subscriptions. + */ public class SubscriptionUtils { + + private static final int DEFAULT_NUM_SHARDS = 2; + private static final int DEFAULT_QPS = 1000; + private static final int DEFAULT_THROUGHPUT_KBPS = 20000; + private static final int DEFAULT_READ_FANOUT = 2; + + /** + * Creates a default HTTP endpoint. + * + * @return the default HTTP endpoint + */ public static Endpoint getHttpEndpoint() { - return new Endpoint.HttpEndpoint(URI.create("http://localhost:8080"), "GET", "", 500, 500, false); + return new Endpoint.HttpEndpoint( + URI.create("http://localhost:8080"), "GET", "", + 500, 500, false + ); } + /** + * Creates a default retry policy. + * + * @return the default retry policy + */ public static RetryPolicy getRetryPolicy() { return new RetryPolicy( new CodeRange[]{new CodeRange(500, 502)}, @@ -19,18 +42,39 @@ public static RetryPolicy getRetryPolicy() { ); } + /** + * Creates a map of default subscription properties. + * + * @return the default subscription properties + */ public static Map getSubscriptionDefaultProperties() { - Map properties = new HashMap<>(); - properties.put("unsideline.api.message_count", "100"); - properties.put("unsideline.api.group_count", "20"); - properties.put("getmessages.api.messages_limit", "100"); - return properties; + return new HashMap<>(Map.of( + "unsideline.api.message_count", "100", + "unsideline.api.group_count", "20", + "getmessages.api.messages_limit", "100" + )); } + /** + * Creates a default consumption policy. + * + * @return the default consumption policy + */ public static ConsumptionPolicy getConsumptionPolicy() { - return new ConsumptionPolicy(10, 1, 1, false, 1, null); + return new ConsumptionPolicy( + 10, 1, 1, + false, 1, null + ); } + /** + * Creates subscription shards with the specified number of shards and capacity. + * + * @param numShards the number of shards + * @param shardCapacity the capacity of each shard + * + * @return the subscription shards + */ public static SubscriptionShards getShards(int numShards, TopicCapacityPolicy shardCapacity) { Map subShards = new HashMap<>(); for (int shardId = 0; shardId < numShards; shardId++) { @@ -39,22 +83,50 @@ public static SubscriptionShards getShards(int numShards, TopicCapacityPolicy sh return new SubscriptionMultiShard(subShards); } + /** + * Creates a subscription unit shard with the specified shard ID and capacity. + * + * @param shardId the shard ID + * @param capacity the capacity of the shard + * + * @return the subscription unit shard + */ public static SubscriptionUnitShard getShard(int shardId, TopicCapacityPolicy capacity) { return new SubscriptionUnitShard(shardId, capacity, null, null, null); } + /** + * Creates a topic capacity policy with the specified QPS and throughput. + * + * @param qps the queries per second + * @param throughputKbps the throughput in kilobits per second + * + * @return the topic capacity policy + */ public static TopicCapacityPolicy getCapacity(int qps, int throughputKbps) { - return getCapacity(qps, throughputKbps, 2); + return getCapacity(qps, throughputKbps, DEFAULT_READ_FANOUT); } + /** + * Creates a topic capacity policy with the specified QPS, throughput, and read fan-out. + * + * @param qps the queries per second + * @param throughputKbps the throughput in kilobits per second + * @param readFanOut the read fan-out + * + * @return the topic capacity policy + */ public static TopicCapacityPolicy getCapacity(int qps, int throughputKbps, int readFanOut) { return new TopicCapacityPolicy(qps, throughputKbps, readFanOut); } - public static Builder getBuilder() { - return new Builder(); - } - + /** + * Retrieves the list of subscription unit shards from the specified subscription. + * + * @param subscription the subscription + * + * @return the list of subscription unit shards + */ public static List shardsOf(VaradhiSubscription subscription) { List shards = new ArrayList<>(); for (int shardId = 0; shardId < subscription.getShards().getShardCount(); shardId++) { @@ -63,8 +135,20 @@ public static List shardsOf(VaradhiSubscription subscript return shards; } + /** + * Creates a new builder for constructing Varadhi subscriptions. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder class for constructing Varadhi subscriptions. + */ public static class Builder { - private int numShards = 2; + private int numShards = DEFAULT_NUM_SHARDS; private TopicCapacityPolicy subCapacity; private String description; private boolean isGrouped = false; @@ -74,57 +158,132 @@ public static class Builder { private SubscriptionShards shards; private final Map properties = getSubscriptionDefaultProperties(); + /** + * Sets the description for the subscription. + * + * @param description the description + * + * @return the builder + */ public Builder setDescription(String description) { this.description = description; return this; } - public Builder setIsGrouped(boolean isGrouped) { + /** + * Sets whether the subscription is grouped. + * + * @param isGrouped whether the subscription is grouped + * + * @return the builder + */ + public Builder setGrouped(boolean isGrouped) { this.isGrouped = isGrouped; return this; } + /** + * Sets the endpoint for the subscription. + * + * @param endpoint the endpoint + * + * @return the builder + */ public Builder setEndpoint(Endpoint endpoint) { this.endpoint = endpoint; return this; } + /** + * Sets the retry policy for the subscription. + * + * @param retryPolicy the retry policy + * + * @return the builder + */ public Builder setRetryPolicy(RetryPolicy retryPolicy) { this.retryPolicy = retryPolicy; return this; } + /** + * Sets the consumption policy for the subscription. + * + * @param consumptionPolicy the consumption policy + * + * @return the builder + */ public Builder setConsumptionPolicy(ConsumptionPolicy consumptionPolicy) { this.consumptionPolicy = consumptionPolicy; return this; } + /** + * Sets the shards for the subscription. + * + * @param shards the shards + * + * @return the builder + */ public Builder setShards(SubscriptionShards shards) { this.shards = shards; return this; } + /** + * Sets the capacity for the subscription. + * + * @param capacity the capacity + * + * @return the builder + */ public Builder setCapacity(TopicCapacityPolicy capacity) { this.subCapacity = capacity; return this; } + /** + * Sets the number of shards for the subscription. + * + * @param numShards the number of shards + * + * @return the builder + */ public Builder setNumShards(int numShards) { this.numShards = numShards; return this; } + + /** + * Sets a property for the subscription. + * + * @param property the property name + * @param value the property value + * + * @return the builder + */ public Builder setProperty(String property, String value) { this.properties.put(property, value); return this; } + /** + * Builds a Varadhi subscription with the specified parameters. + * + * @param name the subscription name + * @param subProject the subscription project + * @param subscribedTopic the subscribed topic + * + * @return the Varadhi subscription + */ public VaradhiSubscription build(String name, String subProject, String subscribedTopic) { - if (null == subCapacity) { - subCapacity = getCapacity(1000, 20000, 2); + if (subCapacity == null) { + subCapacity = getCapacity(DEFAULT_QPS, DEFAULT_THROUGHPUT_KBPS, DEFAULT_READ_FANOUT); } + if (shards == null) { double shardCapacityFactor = (double) 1 / numShards; - TopicCapacityPolicy shardCapacity = subCapacity.from(shardCapacityFactor, 2); + TopicCapacityPolicy shardCapacity = subCapacity.from(shardCapacityFactor, DEFAULT_READ_FANOUT); shards = getShards(numShards, shardCapacity); } @@ -132,14 +291,15 @@ public VaradhiSubscription build(String name, String subProject, String subscrib name, subProject, subscribedTopic, - description == null ? "Test Subscription " + name + "Subscribed to " + subscribedTopic : - description, + Optional.ofNullable(description) + .orElse("Test Subscription " + name + " Subscribed to " + subscribedTopic), isGrouped, - endpoint == null ? getHttpEndpoint() : endpoint, - retryPolicy == null ? getRetryPolicy() : retryPolicy, - consumptionPolicy == null ? getConsumptionPolicy() : consumptionPolicy, + Optional.ofNullable(endpoint).orElse(getHttpEndpoint()), + Optional.ofNullable(retryPolicy).orElse(getRetryPolicy()), + Optional.ofNullable(consumptionPolicy).orElse(getConsumptionPolicy()), shards, - properties + properties, + LifecycleStatus.ActionCode.SYSTEM_ACTION ); } } diff --git a/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java b/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java index 13fd2ab4..5261b332 100644 --- a/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java +++ b/producer/src/test/java/com/flipkart/varadhi/services/ProducerServiceTests.java @@ -252,7 +252,10 @@ public VaradhiTopic getTopic(String name, Project project, String region) { } public VaradhiTopic getTopic(TopicState state, String name, Project project, String region) { - VaradhiTopic topic = VaradhiTopic.of(project.getName(), name, false, null); + VaradhiTopic topic = VaradhiTopic.of( + project.getName(), name, false, null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); StorageTopic st = new DummyStorageTopic(topic.getName(), 0); InternalCompositeTopic ict = InternalCompositeTopic.of(st); ict.setTopicState(state); @@ -319,7 +322,7 @@ static class ResultCapture { public static class DummyStorageTopic extends StorageTopic { public DummyStorageTopic(String name, int version) { - super(name, version, Constants.DefaultTopicCapacity); + super(name, version, Constants.DEFAULT_TOPIC_CAPACITY); } } } diff --git a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarProducerFactoryTest.java b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarProducerFactoryTest.java index 0ddb0eef..227ca810 100644 --- a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarProducerFactoryTest.java +++ b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarProducerFactoryTest.java @@ -32,7 +32,7 @@ public void preTest() throws IOException { "pulsarAdminOptions:\n serviceHttpUrl: \"http://127.0.0.1:8081\"\npulsarClientOptions:\n serviceUrl: \"http://127.0.0.1:8081\"\n"; Path configFile = tempDir.resolve("pulsarConfig.yaml"); Files.write(configFile, yamlContent.getBytes()); - topic = PulsarStorageTopic.of("testTopic", 1, Constants.DefaultTopicCapacity); + topic = PulsarStorageTopic.of("testTopic", 1, Constants.DEFAULT_TOPIC_CAPACITY); pClient = mock(PulsarClient.class); builder = mock(ProducerBuilder.class); org.apache.pulsar.client.api.Producer producer = mock(org.apache.pulsar.client.api.Producer.class); diff --git a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarStackProviderTest.java b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarStackProviderTest.java index 8d5d29a5..c897cc48 100644 --- a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarStackProviderTest.java +++ b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/PulsarStackProviderTest.java @@ -74,7 +74,7 @@ public void testGetStorageTopicFactory_NotInitialized() { @Test public void testGetStorageTopicFactory_Initialized() { String topicName = "foobar"; - TopicCapacityPolicy capacity = Constants.DefaultTopicCapacity; + TopicCapacityPolicy capacity = Constants.DEFAULT_TOPIC_CAPACITY; InternalQueueCategory topicCategory = InternalQueueCategory.MAIN; pulsarStackProvider.init(messagingStackOptions, objectMapper); StorageTopicFactory storageTopicFactory = pulsarStackProvider.getStorageTopicFactory(); diff --git a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/entities/PulsarProducerTest.java b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/entities/PulsarProducerTest.java index fcf8833e..7aa2d345 100644 --- a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/entities/PulsarProducerTest.java +++ b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/entities/PulsarProducerTest.java @@ -49,7 +49,7 @@ public void preTest() throws PulsarClientException { messageBuilder = spy(new TypedMessageBuilderImpl(producer, Schema.BYTES)); doReturn(messageBuilder).when(producer).newMessage(); - policy = Constants.DefaultTopicCapacity; + policy = Constants.DEFAULT_TOPIC_CAPACITY; topic = PulsarStorageTopic.of("one.two.three.four", 1, policy); doReturn(topic.getName()).when(producer).getTopic(); diff --git a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/services/PulsarTopicServiceTest.java b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/services/PulsarTopicServiceTest.java index c82d5366..d24e979b 100644 --- a/pulsar/src/test/java/com/flipkart/varadhi/pulsar/services/PulsarTopicServiceTest.java +++ b/pulsar/src/test/java/com/flipkart/varadhi/pulsar/services/PulsarTopicServiceTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.jsontype.NamedType; import com.flipkart.varadhi.Constants; -import com.flipkart.varadhi.entities.Offset; import com.flipkart.varadhi.entities.Project; import com.flipkart.varadhi.pulsar.ClientProvider; import com.flipkart.varadhi.pulsar.PulsarTopicService; @@ -14,17 +13,10 @@ import com.flipkart.varadhi.spi.services.MessagingException; import com.flipkart.varadhi.utils.JsonMapper; import org.apache.pulsar.client.admin.*; -import org.apache.pulsar.client.api.MessageId; -import org.apache.pulsar.client.impl.MessageIdImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -58,7 +50,7 @@ public void setUp() { @Test public void testCreate() throws PulsarAdminException { - PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DEFAULT_TOPIC_CAPACITY); doThrow(new PulsarAdminException.NotFoundException(new RuntimeException(""), "topic not found", 409)).when( topics).getPartitionedTopicMetadata(topic.getName()); doNothing().when(topics).createPartitionedTopic(anyString(), eq(1)); @@ -68,7 +60,7 @@ public void testCreate() throws PulsarAdminException { @Test public void testCreate_PulsarAdminException() throws PulsarAdminException { - PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DEFAULT_TOPIC_CAPACITY); doThrow(new PulsarAdminException.NotFoundException(new RuntimeException(""), "topic not found", 409)).when( topics).getPartitionedTopicMetadata(topic.getName()); doThrow(PulsarAdminException.class).when(topics).createPartitionedTopic(anyString(), eq(1)); @@ -78,7 +70,7 @@ public void testCreate_PulsarAdminException() throws PulsarAdminException { @Test public void testCreate_ConflictException() throws PulsarAdminException { - PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DEFAULT_TOPIC_CAPACITY); doThrow(new PulsarAdminException.NotFoundException(new RuntimeException(""), "topic not found", 409)).when( topics).getPartitionedTopicMetadata(topic.getName()); doThrow(PulsarAdminException.class).when(topics).createPartitionedTopic(anyString(), eq(1)); @@ -98,7 +90,7 @@ public void testCreate_NewTenantNamespace() throws PulsarAdminException { String newTenant = "testTenantNew"; Project projectNew = Project.of("projectNew", "", "public", newTenant); String newNamespace = EntityHelper.getNamespace(newTenant, projectNew.getName()); - PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic topic = PulsarStorageTopic.of(TEST_TOPIC, 1, Constants.DEFAULT_TOPIC_CAPACITY); doThrow(new PulsarAdminException.NotFoundException(new RuntimeException(""), "topic not found", 409)).when( topics).getPartitionedTopicMetadata(topic.getName()); doNothing().when(topics).createPartitionedTopic(anyString(), eq(1)); diff --git a/pulsar/src/testE2E/java/com/flipkart/varadhi/pulsar/PulsarTopicServiceTest.java b/pulsar/src/testE2E/java/com/flipkart/varadhi/pulsar/PulsarTopicServiceTest.java index dc8a280c..ac228262 100644 --- a/pulsar/src/testE2E/java/com/flipkart/varadhi/pulsar/PulsarTopicServiceTest.java +++ b/pulsar/src/testE2E/java/com/flipkart/varadhi/pulsar/PulsarTopicServiceTest.java @@ -34,7 +34,7 @@ public void init() throws PulsarAdminException { @Test public void testCreateTopic() throws PulsarAdminException { String topicFQDN = getRandomTopicFQDN(); - PulsarStorageTopic pt = PulsarStorageTopic.of(topicFQDN, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic pt = PulsarStorageTopic.of(topicFQDN, 1, Constants.DEFAULT_TOPIC_CAPACITY); topicService.create(pt, project); validateTopicExists(topicFQDN); } @@ -42,7 +42,7 @@ public void testCreateTopic() throws PulsarAdminException { @Test public void testDuplicateTopicWithSameConfigAllowed() { String topicFQDN = getRandomTopicFQDN(); - PulsarStorageTopic pt = PulsarStorageTopic.of(topicFQDN, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic pt = PulsarStorageTopic.of(topicFQDN, 1, Constants.DEFAULT_TOPIC_CAPACITY); topicService.create(pt, project); topicService.create(pt, project); } @@ -50,8 +50,8 @@ public void testDuplicateTopicWithSameConfigAllowed() { @Test public void testDuplicateTopicWithDifferentConfigNotAllowed() { String topicFQDN = getRandomTopicFQDN(); - PulsarStorageTopic pt1 = PulsarStorageTopic.of(topicFQDN, 2, Constants.DefaultTopicCapacity); - PulsarStorageTopic pt2 = PulsarStorageTopic.of(topicFQDN, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic pt1 = PulsarStorageTopic.of(topicFQDN, 2, Constants.DEFAULT_TOPIC_CAPACITY); + PulsarStorageTopic pt2 = PulsarStorageTopic.of(topicFQDN, 1, Constants.DEFAULT_TOPIC_CAPACITY); topicService.create(pt1, project); MessagingException m = Assertions.assertThrows(MessagingException.class, () -> topicService.create(pt2, project)); @@ -65,7 +65,7 @@ public void testCreate_NewTenantNamespace() throws PulsarAdminException { String newNamespace = EntityHelper.getNamespace(newTenant, projectNew.getName()); String topicFQDN = getRandomTopicFQDN(); - PulsarStorageTopic pt = PulsarStorageTopic.of(topicFQDN, 1, Constants.DefaultTopicCapacity); + PulsarStorageTopic pt = PulsarStorageTopic.of(topicFQDN, 1, Constants.DEFAULT_TOPIC_CAPACITY); topicService.create(pt, projectNew); validateTopicExists(topicFQDN); validateTenantExists(newTenant); diff --git a/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java b/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java index ff02a8c7..c73ff7d7 100644 --- a/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java +++ b/server/src/main/java/com/flipkart/varadhi/config/RestOptions.java @@ -14,7 +14,7 @@ public class RestOptions { private String deployedRegion; @NotNull private String projectCacheBuilderSpec = "expireAfterWrite=3600s"; - private TopicCapacityPolicy defaultTopicCapacity = Constants.DefaultTopicCapacity; + private TopicCapacityPolicy defaultTopicCapacity = Constants.DEFAULT_TOPIC_CAPACITY; private boolean traceRequestEnabled = true; private int payloadSizeMax = PAYLOAD_SIZE_MAX; private int headersAllowedMax = HEADERS_ALLOWED_MAX; diff --git a/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java b/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java index d01d297e..7d1f2bd8 100644 --- a/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java +++ b/server/src/main/java/com/flipkart/varadhi/services/SubscriptionService.java @@ -1,7 +1,15 @@ package com.flipkart.varadhi.services; import com.flipkart.varadhi.core.cluster.ControllerRestApi; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.ConsumptionPolicy; +import com.flipkart.varadhi.entities.Endpoint; +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceActionRequest; +import com.flipkart.varadhi.entities.ResourceDeletionType; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.entities.cluster.SubscriptionOperation; import com.flipkart.varadhi.exceptions.InvalidOperationForResourceException; import com.flipkart.varadhi.exceptions.ResourceNotFoundException; @@ -42,12 +50,13 @@ public SubscriptionService( * Retrieves the list of subscription names for a given project. * * @param projectName the name of the project + * @param includeInactive flag to include inactive or soft-deleted subscriptions * * @return the list of subscription names */ - public List getSubscriptionList(String projectName) { + public List getSubscriptionList(String projectName, boolean includeInactive) { return metaStore.getSubscriptionNames(projectName).stream() - .filter(this::isActiveOrWellProvisionedByName) + .filter(subscriptionName -> includeInactive || isActiveOrWellProvisionedByName(subscriptionName)) .toList(); } @@ -157,16 +166,19 @@ public CompletableFuture updateSubscription( /** * Deletes a subscription. * - * @param subscriptionName the name of the subscription + * @param subscriptionName the name of the subscription to delete * @param subProject the project associated with the subscription * @param requestedBy the user requesting the deletion * @param deletionType the type of deletion (soft or hard) + * @param actionRequest the request containing the action code and message for the deletion * * @return a CompletableFuture representing the deletion operation + * + * @throws IllegalArgumentException if the subscription cannot be deleted in its current state */ public CompletableFuture deleteSubscription( String subscriptionName, Project subProject, String requestedBy, - ResourceDeletionType deletionType + ResourceDeletionType deletionType, ResourceActionRequest actionRequest ) { VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); @@ -178,9 +190,9 @@ public CompletableFuture deleteSubscription( } if (deletionType.equals(ResourceDeletionType.HARD_DELETE)) { - handleHardDelete(subscription, subProject); + handleHardDelete(subscription, subProject, actionRequest); } else { - handleSoftDelete(subscription); + handleSoftDelete(subscription, actionRequest); } }); } @@ -188,12 +200,16 @@ public CompletableFuture deleteSubscription( /** * Restores a subscription. * - * @param subscriptionName the name of the subscription + * @param subscriptionName the name of the subscription to restore * @param requestedBy the user requesting the restoration + * @param actionRequest the request containing the action code and message for the restoration * * @return a CompletableFuture representing the restored subscription + * + * @throws InvalidOperationForResourceException if the subscription is already active or if the restoration is not allowed */ - public CompletableFuture restoreSubscription(String subscriptionName, String requestedBy) { + public CompletableFuture restoreSubscription( + String subscriptionName, String requestedBy, ResourceActionRequest actionRequest) { VaradhiSubscription subscription = metaStore.getSubscription(subscriptionName); if (subscription.isActive()) { @@ -201,9 +217,19 @@ public CompletableFuture restoreSubscription(String subscri "Subscription '%s' is already active.".formatted(subscriptionName)); } + LifecycleStatus.ActionCode lastAction = subscription.getStatus().getActionCode(); + boolean isVaradhiAdmin = actionRequest.actionCode() == LifecycleStatus.ActionCode.SYSTEM_ACTION || + actionRequest.actionCode() == LifecycleStatus.ActionCode.ADMIN_ACTION; + + if (!lastAction.isUserAllowed() && !isVaradhiAdmin) { + throw new InvalidOperationForResourceException( + "Restoration denied. Only Varadhi Admin can restore this subscription." + ); + } + return controllerClient.getSubscriptionState(subscriptionName, requestedBy) .thenApply(state -> { - subscription.restore(); + subscription.restore(actionRequest.actionCode(), actionRequest.message()); metaStore.updateSubscription(subscription); log.info("Subscription '{}' restored successfully.", subscriptionName); return subscription; @@ -305,11 +331,15 @@ private CompletableFuture performSubscriptionOperation( /** * Handles the hard deletion of a subscription. * - * @param subscription the subscription to delete - * @param subProject the project associated with the subscription + * @param subscription the subscription to be hard-deleted + * @param subProject the project associated with the subscription + * @param actionRequest the request containing the action code and message for the deletion */ - private void handleHardDelete(VaradhiSubscription subscription, Project subProject) { - subscription.markDeleting(); + private void handleHardDelete( + VaradhiSubscription subscription, + Project subProject, ResourceActionRequest actionRequest + ) { + subscription.markDeleting(actionRequest.actionCode(), actionRequest.message()); metaStore.updateSubscription(subscription); try { @@ -327,10 +357,11 @@ private void handleHardDelete(VaradhiSubscription subscription, Project subProje /** * Handles the soft deletion of a subscription. * - * @param subscription the subscription to delete + * @param subscription the subscription to be soft-deleted + * @param actionRequest the request containing the action code and message for the deletion */ - private void handleSoftDelete(VaradhiSubscription subscription) { - subscription.markInactive(); + private void handleSoftDelete(VaradhiSubscription subscription, ResourceActionRequest actionRequest) { + subscription.markInactive(actionRequest.actionCode(), actionRequest.message()); metaStore.updateSubscription(subscription); log.info("Subscription '{}' marked inactive successfully.", subscription.getName()); } diff --git a/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java b/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java index b8e72ea0..fe0526b4 100644 --- a/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java +++ b/server/src/main/java/com/flipkart/varadhi/services/VaradhiTopicService.java @@ -1,8 +1,11 @@ package com.flipkart.varadhi.services; +import com.flipkart.varadhi.entities.LifecycleStatus; import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceActionRequest; import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.VaradhiSubscription; import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.exceptions.InvalidOperationForResourceException; import com.flipkart.varadhi.exceptions.ResourceNotFoundException; @@ -69,30 +72,32 @@ public VaradhiTopic get(String topicName) { /** * Deletes a Varadhi topic by its name. * - * @param topicName the name of the topic - * @param deletionType the type of deletion (hard or soft) + * @param topicName the name of the topic to delete + * @param deletionType the type of deletion (hard or soft) + * @param actionRequest the request containing the action code and message for the deletion */ - public void delete(String topicName, ResourceDeletionType deletionType) { + public void delete(String topicName, ResourceDeletionType deletionType, ResourceActionRequest actionRequest) { log.info("Deleting Varadhi topic: {}", topicName); // TODO: If the only topic in a namespace, also delete the namespace and tenant. Perform cleanup independently of the delete operation. VaradhiTopic varadhiTopic = metaStore.getTopic(topicName); - validateTopicForDeletion(topicName); + validateTopicForDeletion(topicName, deletionType); if (deletionType.equals(ResourceDeletionType.HARD_DELETE)) { handleHardDelete(varadhiTopic); } else { - handleSoftDelete(varadhiTopic); + handleSoftDelete(varadhiTopic, actionRequest); } } /** * Handles the soft deletion of a Varadhi topic. * - * @param varadhiTopic the Varadhi topic to soft-delete + * @param varadhiTopic the Varadhi topic to be soft-deleted + * @param actionRequest the request containing the action code and message for the soft deletion */ - public void handleSoftDelete(VaradhiTopic varadhiTopic) { + public void handleSoftDelete(VaradhiTopic varadhiTopic, ResourceActionRequest actionRequest) { log.info("Soft deleting Varadhi topic: {}", varadhiTopic.getName()); - varadhiTopic.updateStatus(VaradhiTopic.Status.INACTIVE); + varadhiTopic.markInactive(actionRequest.actionCode(), actionRequest.message()); metaStore.updateTopic(varadhiTopic); } @@ -117,11 +122,12 @@ public void handleHardDelete(VaradhiTopic varadhiTopic) { /** * Restores a deleted Varadhi topic. * - * @param topicName the name of the topic to restore + * @param topicName the name of the topic to restore + * @param actionRequest the request containing the action code and message for the restoration * - * @throws InvalidOperationForResourceException if the topic is not deleted + * @throws InvalidOperationForResourceException if the topic is not deleted or if the restoration is not allowed */ - public void restore(String topicName) { + public void restore(String topicName, ResourceActionRequest actionRequest) { log.info("Restoring Varadhi topic: {}", topicName); VaradhiTopic varadhiTopic = metaStore.getTopic(topicName); @@ -129,28 +135,50 @@ public void restore(String topicName) { if (varadhiTopic.isActive()) { throw new InvalidOperationForResourceException("Topic %s is not deleted.".formatted(topicName)); } - varadhiTopic.updateStatus(VaradhiTopic.Status.ACTIVE); + + LifecycleStatus.ActionCode lastAction = varadhiTopic.getStatus().getActionCode(); + boolean isVaradhiAdmin = actionRequest.actionCode() == LifecycleStatus.ActionCode.SYSTEM_ACTION || + actionRequest.actionCode() == LifecycleStatus.ActionCode.ADMIN_ACTION; + + if (!lastAction.isUserAllowed() && !isVaradhiAdmin) { + throw new InvalidOperationForResourceException( + "Restoration denied. Only Varadhi Admin can restore this topic." + ); + } + + varadhiTopic.markActive(actionRequest.actionCode(), actionRequest.message()); metaStore.updateTopic(varadhiTopic); } /** - * Validates if a topic can be deleted. + * Validates if a topic can be deleted based on the deletion type. * - * @param topicName the name of the topic to validate + * @param topicName the name of the topic to validate + * @param deletionType the type of deletion (SOFT_DELETE or HARD_DELETE) * - * @throws InvalidOperationForResourceException if the topic is being used by a subscription + * @throws InvalidOperationForResourceException if the topic cannot be deleted */ - private void validateTopicForDeletion(String topicName) { + private void validateTopicForDeletion(String topicName, ResourceDeletionType deletionType) { // TODO: Improve efficiency by avoiding a full scan of all subscriptions across projects. - List subscriptions = metaStore.getAllSubscriptionNames(); - boolean isTopicInUse = subscriptions.stream() + List subscriptions = metaStore.getAllSubscriptionNames().stream() .map(metaStore::getSubscription) - .anyMatch(subscription -> subscription.getTopic().equals(topicName)); + .filter(subscription -> subscription.getTopic().equals(topicName)) + .toList(); - if (isTopicInUse) { - throw new InvalidOperationForResourceException( - "Cannot delete topic as it is being used by a subscription." - ); + if (subscriptions.isEmpty()) { + return; + } + + boolean hasActiveSubscriptions = subscriptions.stream() + .anyMatch(subscription -> + subscription.getStatus().getState() == LifecycleStatus.State.CREATED); + + if (deletionType == ResourceDeletionType.SOFT_DELETE && hasActiveSubscriptions) { + throw new InvalidOperationForResourceException("Cannot delete topic as it has active subscriptions."); + } + + if (deletionType == ResourceDeletionType.HARD_DELETE) { + throw new InvalidOperationForResourceException("Cannot delete topic as it has existing subscriptions."); } } @@ -165,14 +193,16 @@ public boolean exists(String topicName) { } /** - * Retrieves a list of active Varadhi topics for a project. + * Retrieves a list of Varadhi topic names for a given project. * * @param projectName the name of the project - * @return a list of active Varadhi topic names + * @param includeInactive flag to include inactive or soft-deleted topics + * + * @return a list of Varadhi topic names */ - public List getVaradhiTopics(String projectName) { + public List getVaradhiTopics(String projectName, boolean includeInactive) { return metaStore.getTopicNames(projectName).stream() - .filter(topicName -> metaStore.getTopic(topicName).isActive()) + .filter(topicName -> includeInactive || metaStore.getTopic(topicName).isActive()) .toList(); } } diff --git a/server/src/main/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactory.java b/server/src/main/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactory.java index a8f5fcea..a98c70bb 100644 --- a/server/src/main/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactory.java +++ b/server/src/main/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactory.java @@ -1,7 +1,21 @@ package com.flipkart.varadhi.utils; +import com.flipkart.varadhi.entities.ConsumptionPolicy; +import com.flipkart.varadhi.entities.InternalCompositeSubscription; import com.flipkart.varadhi.entities.InternalQueueCategory; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.InternalQueueType; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.RetrySubscription; +import com.flipkart.varadhi.entities.StorageSubscription; +import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.SubscriptionMultiShard; +import com.flipkart.varadhi.entities.SubscriptionShards; +import com.flipkart.varadhi.entities.SubscriptionUnitShard; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.TopicPartitions; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.spi.services.StorageSubscriptionFactory; import com.flipkart.varadhi.spi.services.StorageTopicFactory; import com.flipkart.varadhi.spi.services.StorageTopicService; @@ -12,6 +26,9 @@ import java.util.List; import java.util.Map; +/** + * Factory class for creating Varadhi subscriptions. + */ @Slf4j public final class VaradhiSubscriptionFactory { private static final String NAME_SEPARATOR = "."; @@ -20,11 +37,20 @@ public final class VaradhiSubscriptionFactory { private static final String TOPIC_QUALIFIER = "it"; private static final String SHARD_QUALIFIER = "shard"; private static final int READ_FAN_OUT_FOR_INTERNAL_QUEUE = 1; + private final String deployedRegion; private final StorageSubscriptionFactory, StorageTopic> subscriptionFactory; private final StorageTopicFactory topicFactory; private final StorageTopicService topicService; + /** + * Constructs a VaradhiSubscriptionFactory. + * + * @param topicService The service for managing storage topics. + * @param subscriptionFactory The factory for creating storage subscriptions. + * @param topicFactory The factory for creating storage topics. + * @param deployedRegion The region where the subscription is deployed. + */ public VaradhiSubscriptionFactory( StorageTopicService topicService, StorageSubscriptionFactory, StorageTopic> subscriptionFactory, @@ -36,6 +62,15 @@ public VaradhiSubscriptionFactory( this.deployedRegion = deployedRegion; } + /** + * Creates a VaradhiSubscription from a SubscriptionResource. + * + * @param subscriptionResource The subscription resource. + * @param subProject The project associated with the subscription. + * @param topic The topic associated with the subscription. + * + * @return A VaradhiSubscription instance. + */ public VaradhiSubscription get(SubscriptionResource subscriptionResource, Project subProject, VaradhiTopic topic) { String subName = subscriptionResource.getSubscriptionInternalName(); SubscriptionShards shards = @@ -53,10 +88,22 @@ public VaradhiSubscription get(SubscriptionResource subscriptionResource, Projec subscriptionResource.getRetryPolicy(), subscriptionResource.getConsumptionPolicy(), shards, - subscriptionResource.getProperties() + subscriptionResource.getProperties(), + subscriptionResource.getActionCode() ); } + /** + * Gets the subscription shards for a given subscription. + * + * @param subName The name of the subscription. + * @param topic The topic associated with the subscription. + * @param subProject The project associated with the subscription. + * @param consumptionPolicy The consumption policy for the subscription. + * @param retryPolicy The retry policy for the subscription. + * + * @return A SubscriptionShards instance. + */ private SubscriptionShards getSubscriptionShards( String subName, VaradhiTopic topic, Project subProject, ConsumptionPolicy consumptionPolicy, RetryPolicy retryPolicy @@ -66,33 +113,39 @@ private SubscriptionShards getSubscriptionShards( topicService.shardTopic(subscribedStorageTopic, InternalQueueCategory.MAIN); int numShards = topicPartitions.size(); log.info("Planning {} shards for subscription {}.", topicPartitions.size(), subName); + TopicCapacityPolicy shardCapacity = getShardCapacity(topic.getCapacity(), numShards); if (numShards == 1) { - return getShard( - subName, 0, topicPartitions.get(0), shardCapacity, subProject, consumptionPolicy, retryPolicy); + return getSingleShard( + subName, 0, topicPartitions.getFirst(), shardCapacity, subProject, + consumptionPolicy, retryPolicy + ); } else { - Map subShards = new HashMap<>(); - for (int shardId = 0; shardId < numShards; shardId++) { - subShards.put( - shardId, getShard(subName, shardId, topicPartitions.get(shardId), shardCapacity, subProject, - consumptionPolicy, retryPolicy - )); - } - return new SubscriptionMultiShard(subShards); + return getMultiShard( + subName, topicPartitions, shardCapacity, subProject, consumptionPolicy, retryPolicy); } } - private TopicCapacityPolicy getShardCapacity(TopicCapacityPolicy topicCapacity, int shardCount) { - return topicCapacity.from((double) 1 / shardCount, topicCapacity.getReadFanOut()); - } - - private SubscriptionUnitShard getShard( + /** + * Gets a single subscription shard. + * + * @param subName The name of the subscription. + * @param shardId The ID of the shard. + * @param shardTopicPartition The topic partitions for the shard. + * @param capacity The capacity policy for the shard. + * @param subProject The project associated with the subscription. + * @param consumptionPolicy The consumption policy for the subscription. + * @param retryPolicy The retry policy for the subscription. + * + * @return A SubscriptionUnitShard instance. + */ + private SubscriptionUnitShard getSingleShard( String subName, int shardId, TopicPartitions shardTopicPartition, TopicCapacityPolicy capacity, Project subProject, ConsumptionPolicy consumptionPolicy, RetryPolicy retryPolicy ) { - //TODO::Take care of region. - //TODO::Storage Topic/Subscription names needs to be indexed with in Composite topic/subscription. + // TODO: Handle region-specific logic. + // TODO: Ensure that storage topic and subscription names are indexed within composite topics/subscriptions. InternalCompositeSubscription shardMainSub = getShardMainSub(subName, shardId, shardTopicPartition, subProject); RetrySubscription retrySub = getRetrySub(subName, shardId, subProject, capacity, consumptionPolicy, retryPolicy); @@ -101,6 +154,57 @@ private SubscriptionUnitShard getShard( return new SubscriptionUnitShard(shardId, capacity, shardMainSub, retrySub, dltSub); } + /** + * Gets multiple subscription shards. + * + * @param subName The name of the subscription. + * @param partitions The topic partitions for the shards. + * @param capacity The capacity policy for the shards. + * @param subProject The project associated with the subscription. + * @param consumptionPolicy The consumption policy for the subscription. + * @param retryPolicy The retry policy for the subscription. + * + * @return A SubscriptionMultiShard instance. + */ + private SubscriptionMultiShard getMultiShard( + String subName, List> partitions, + TopicCapacityPolicy capacity, Project subProject, ConsumptionPolicy consumptionPolicy, + RetryPolicy retryPolicy + ) { + Map subShards = new HashMap<>(); + for (int shardId = 0; shardId < partitions.size(); shardId++) { + subShards.put( + shardId, getSingleShard( + subName, shardId, partitions.get(shardId), capacity, subProject, + consumptionPolicy, retryPolicy + ) + ); + } + return new SubscriptionMultiShard(subShards); + } + + /** + * Gets the capacity policy for a shard. + * + * @param topicCapacity The capacity policy for the topic. + * @param shardCount The number of shards. + * + * @return A TopicCapacityPolicy instance. + */ + private TopicCapacityPolicy getShardCapacity(TopicCapacityPolicy topicCapacity, int shardCount) { + return topicCapacity.from((double) 1 / shardCount, topicCapacity.getReadFanOut()); + } + + /** + * Gets the main subscription for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * @param shardTopicPartition The topic partitions for the shard. + * @param project The project associated with the subscription. + * + * @return An InternalCompositeSubscription instance. + */ private InternalCompositeSubscription getShardMainSub( String subscriptionName, int shardId, TopicPartitions shardTopicPartition, Project project ) { @@ -109,6 +213,14 @@ private InternalCompositeSubscription getShardMainSub( return InternalCompositeSubscription.of(ss, new InternalQueueType.Main()); } + /** + * Gets the name of the main subscription for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * + * @return The name of the main subscription for the shard. + */ private String getShardMainSubName(String subscriptionName, int shardId) { return String.join( NAME_SEPARATOR, subscriptionName, SHARD_QUALIFIER, String.valueOf(shardId), @@ -116,6 +228,17 @@ private String getShardMainSubName(String subscriptionName, int shardId) { ); } + /** + * Gets the retry subscription for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * @param project The project associated with the subscription. + * @param capacity The capacity policy for the shard. + * @param consumptionPolicy The consumption policy for the subscription. + * @param retryPolicy The retry policy for the subscription. + * @return A RetrySubscription instance. + */ private RetrySubscription getRetrySub( String subscriptionName, int shardId, Project project, TopicCapacityPolicy capacity, ConsumptionPolicy consumptionPolicy, RetryPolicy retryPolicy @@ -135,6 +258,16 @@ private RetrySubscription getRetrySub( return new RetrySubscription(retrySubs); } + /** + * Gets the dead letter subscription for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * @param project The project associated with the subscription. + * @param capacity The capacity policy for the shard. + * @param consumptionPolicy The consumption policy for the subscription. + * @return An InternalCompositeSubscription instance. + */ private InternalCompositeSubscription getDltSub( String subscriptionName, int shardId, Project project, TopicCapacityPolicy capacity, ConsumptionPolicy consumptionPolicy @@ -150,11 +283,23 @@ private InternalCompositeSubscription getDltSub( ); } + /** + * Gets an internal subscription for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * @param queueType The type of the internal queue. + * @param queueIndex The index of the internal queue. + * @param project The project associated with the subscription. + * @param capacity The capacity policy for the shard. + * @param consumptionPolicy The consumption policy for the subscription. + * @return An InternalCompositeSubscription instance. + */ private InternalCompositeSubscription getInternalSub( String subscriptionName, int shardId, InternalQueueType queueType, int queueIndex, Project project, TopicCapacityPolicy capacity, ConsumptionPolicy consumptionPolicy ) { - // TODO::handle cases where retry and dlt topic might be on different projects. + // TODO: Address scenarios where Retry and DLT topics might belong to different projects. String itSubName = getInternalSubName(subscriptionName, shardId, queueType.getCategory(), queueIndex); String itTopicName = getInternalTopicName(subscriptionName, shardId, queueType.getCategory(), queueIndex); TopicCapacityPolicy errCapacity = @@ -164,14 +309,23 @@ private InternalCompositeSubscription getInternalSub( if (topicPartitions.size() != 1) { throw new IllegalArgumentException("Multi shard internal topics are unsupported for now."); } - StorageSubscription ss = subscriptionFactory.get(itSubName, topicPartitions.get(0), project); + StorageSubscription ss = subscriptionFactory.get(itSubName, topicPartitions.getFirst(), project); return InternalCompositeSubscription.of(ss, queueType); } + /** + * Gets the name of an internal subscription for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * @param queueCategory The category of the internal queue. + * @param index The index of the internal queue. + * @return The name of the internal subscription for the shard. + */ private String getInternalSubName( String subscriptionName, int shardId, InternalQueueCategory queueCategory, int index ) { - // is-.shard-.- + // Format: is-.shard-.- return String.join( NAME_SEPARATOR, String.join(PART_SEPARATOR, SUB_QUALIFIER, subscriptionName), @@ -180,10 +334,19 @@ private String getInternalSubName( ); } + /** + * Gets the name of an internal topic for a shard. + * + * @param subscriptionName The name of the subscription. + * @param shardId The ID of the shard. + * @param queueCategory The category of the internal queue. + * @param index The index of the internal queue. + * @return The name of the internal topic for the shard. + */ private String getInternalTopicName( String subscriptionName, int shardId, InternalQueueCategory queueCategory, int index ) { - // it-.shard-.- + // Format: it-.shard-.- return String.join( NAME_SEPARATOR, String.join(PART_SEPARATOR, TOPIC_QUALIFIER, subscriptionName), diff --git a/server/src/main/java/com/flipkart/varadhi/utils/VaradhiTopicFactory.java b/server/src/main/java/com/flipkart/varadhi/utils/VaradhiTopicFactory.java index a6611f5e..75fccfb6 100644 --- a/server/src/main/java/com/flipkart/varadhi/utils/VaradhiTopicFactory.java +++ b/server/src/main/java/com/flipkart/varadhi/utils/VaradhiTopicFactory.java @@ -1,22 +1,42 @@ package com.flipkart.varadhi.utils; - -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.InternalCompositeTopic; +import com.flipkart.varadhi.entities.InternalQueueCategory; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.spi.services.StorageTopicFactory; import com.flipkart.varadhi.web.entities.TopicResource; +import java.util.Optional; + +/** + * Factory class for creating Varadhi topics. + */ public class VaradhiTopicFactory { private final StorageTopicFactory topicFactory; private final TopicCapacityPolicy defaultTopicCapacityPolicy; - //TODO:: This is currently used to provide default value for primary region for the topic being created. - //This should come from TopicResource a part of Regional/HA/BCP-DR policy. Since those are not available - //use deploymentRegion as global single primary topic region as a workaround. + /** + * TODO: This field is currently used to provide a default value for the primary region of the topic being created. + * Ideally, this should be derived from the TopicResource as part of the Regional/HA/BCP-DR policy. + * Since those policies are not yet available, the deploymentRegion is used as a global single primary + * topic region as a temporary solution. + */ private final String deploymentRegion; + /** + * Constructs a new VaradhiTopicFactory instance. + * + * @param topicFactory the factory for creating storage topics + * @param deploymentRegion the default primary region for the topic + * @param defaultTopicCapacityPolicy the default capacity policy for the topic + */ public VaradhiTopicFactory( - StorageTopicFactory topicFactory, String deploymentRegion, + StorageTopicFactory topicFactory, + String deploymentRegion, TopicCapacityPolicy defaultTopicCapacityPolicy ) { this.topicFactory = topicFactory; @@ -24,19 +44,35 @@ public VaradhiTopicFactory( this.deploymentRegion = deploymentRegion; } + /** + * Creates a VaradhiTopic instance based on the provided project and topic resource. + * + * @param project the project associated with the topic + * @param topicResource the topic resource containing topic details + * + * @return the created VaradhiTopic instance + */ public VaradhiTopic get(Project project, TopicResource topicResource) { - if (null == topicResource.getCapacity()) { - topicResource.setCapacity(defaultTopicCapacityPolicy); - } - VaradhiTopic vt = topicResource.toVaradhiTopic(); - planDeployment(project, vt); - return vt; + topicResource.setCapacity( + Optional.ofNullable(topicResource.getCapacity()).orElse(defaultTopicCapacityPolicy) + ); + + VaradhiTopic varadhiTopic = topicResource.toVaradhiTopic(); + planDeployment(project, varadhiTopic); + return varadhiTopic; } + /** + * Plans the deployment of the VaradhiTopic by creating and associating an internal storage topic. + * + * @param project the project associated with the topic + * @param varadhiTopic the VaradhiTopic instance to be deployed + */ private void planDeployment(Project project, VaradhiTopic varadhiTopic) { StorageTopic storageTopic = topicFactory.getTopic( varadhiTopic.getName(), project, varadhiTopic.getCapacity(), InternalQueueCategory.MAIN); + varadhiTopic.addInternalTopic(deploymentRegion, InternalCompositeTopic.of(storageTopic)); } } diff --git a/server/src/main/java/com/flipkart/varadhi/web/entities/SubscriptionResource.java b/server/src/main/java/com/flipkart/varadhi/web/entities/SubscriptionResource.java index 17eb3377..8f45cab4 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/entities/SubscriptionResource.java +++ b/server/src/main/java/com/flipkart/varadhi/web/entities/SubscriptionResource.java @@ -1,44 +1,76 @@ package com.flipkart.varadhi.web.entities; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.ConsumptionPolicy; +import com.flipkart.varadhi.entities.Endpoint; +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.Validatable; +import com.flipkart.varadhi.entities.ValidateResource; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VersionedEntity; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.Setter; import java.util.HashMap; import java.util.Map; +/** + * Represents a subscription resource in Varadhi. + */ @Getter +@EqualsAndHashCode(callSuper = true) @ValidateResource(message = "Invalid Subscription name. Check naming constraints.", max = 64) public class SubscriptionResource extends VersionedEntity implements Validatable { @NotBlank - String project; + private final String project; @NotBlank - String topic; + private final String topic; @NotBlank - String topicProject; + private final String topicProject; @NotBlank - String description; + private final String description; - boolean grouped; + private final boolean grouped; @NotNull - Endpoint endpoint; + private final Endpoint endpoint; @NotNull - RetryPolicy retryPolicy; + private final RetryPolicy retryPolicy; @NotNull - ConsumptionPolicy consumptionPolicy; + private final ConsumptionPolicy consumptionPolicy; @NotNull - Map properties; - + private final Map properties; + + @Setter + private LifecycleStatus.ActionCode actionCode; + + /** + * Constructs a new SubscriptionResource. + * + * @param name The name of the subscription. + * @param version The version of the subscription. + * @param project The project associated with the subscription. + * @param topic The topic associated with the subscription. + * @param topicProject The project of the topic associated with the subscription. + * @param description The description of the subscription. + * @param grouped Indicates if the subscription is grouped. + * @param endpoint The endpoint associated with the subscription. + * @param retryPolicy The retry policy for the subscription. + * @param consumptionPolicy The consumption policy for the subscription. + * @param properties Additional properties for the subscription. + * @param actionCode The action code associated with the subscription. + */ private SubscriptionResource( String name, int version, @@ -50,7 +82,8 @@ private SubscriptionResource( Endpoint endpoint, RetryPolicy retryPolicy, ConsumptionPolicy consumptionPolicy, - Map properties + Map properties, + LifecycleStatus.ActionCode actionCode ) { super(name, version); this.project = project; @@ -61,9 +94,27 @@ private SubscriptionResource( this.endpoint = endpoint; this.retryPolicy = retryPolicy; this.consumptionPolicy = consumptionPolicy; - this.properties = null == properties ? new HashMap<>() : properties; + this.properties = properties == null ? new HashMap<>() : properties; + this.actionCode = actionCode; } + /** + * Creates a new SubscriptionResource instance. + * + * @param name The name of the subscription. + * @param project The project associated with the subscription. + * @param topic The topic associated with the subscription. + * @param topicProject The project of the topic associated with the subscription. + * @param description The description of the subscription. + * @param grouped Indicates if the subscription is grouped. + * @param endpoint The endpoint associated with the subscription. + * @param retryPolicy The retry policy for the subscription. + * @param consumptionPolicy The consumption policy for the subscription. + * @param properties Additional properties for the subscription. + * @param actionCode The action code associated with the subscription. + * + * @return A new SubscriptionResource instance. + */ public static SubscriptionResource of( String name, String project, @@ -74,26 +125,42 @@ public static SubscriptionResource of( Endpoint endpoint, RetryPolicy retryPolicy, ConsumptionPolicy consumptionPolicy, - Map properties + Map properties, + LifecycleStatus.ActionCode actionCode ) { return new SubscriptionResource( name, INITIAL_VERSION, project, topic, topicProject, description, grouped, endpoint, retryPolicy, - consumptionPolicy, properties + consumptionPolicy, properties, actionCode ); } + /** + * Builds the internal name for the subscription. + * + * @param project The project associated with the subscription. + * @param subsResourceName The name of the subscription resource. + * + * @return The internal name for the subscription. + */ public static String buildInternalName(String project, String subsResourceName) { return String.join(NAME_SEPARATOR, project, subsResourceName); } + /** + * Creates a SubscriptionResource from a VaradhiSubscription. + * + * @param subscription The VaradhiSubscription instance. + * + * @return A new SubscriptionResource instance. + */ public static SubscriptionResource from(VaradhiSubscription subscription) { - String[] subscriptionNameSegments = subscription.getName().split(NAME_SEPARATOR_REGEX); - String subscriptionProject = subscriptionNameSegments[0]; - String subscriptionName = subscriptionNameSegments[1]; + String[] subscriptionSegments = subscription.getName().split(NAME_SEPARATOR_REGEX); + String subscriptionProject = subscriptionSegments[0]; + String subscriptionName = subscriptionSegments[1]; - String[] topicNameSegments = subscription.getTopic().split(NAME_SEPARATOR_REGEX); - String topicProject = topicNameSegments[0]; - String topicName = topicNameSegments[1]; + String[] topicSegments = subscription.getTopic().split(NAME_SEPARATOR_REGEX); + String topicProject = topicSegments[0]; + String topicName = topicSegments[1]; SubscriptionResource subResource = of( subscriptionName, @@ -105,12 +172,18 @@ public static SubscriptionResource from(VaradhiSubscription subscription) { subscription.getEndpoint(), subscription.getRetryPolicy(), subscription.getConsumptionPolicy(), - subscription.getProperties() + subscription.getProperties(), + subscription.getStatus().getActionCode() ); subResource.setVersion(subscription.getVersion()); return subResource; } + /** + * Gets the internal name for the subscription. + * + * @return The internal name for the subscription. + */ @JsonIgnore public String getSubscriptionInternalName() { return buildInternalName(project, getName()); diff --git a/server/src/main/java/com/flipkart/varadhi/web/entities/TopicResource.java b/server/src/main/java/com/flipkart/varadhi/web/entities/TopicResource.java index f5c722cf..44e32f61 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/entities/TopicResource.java +++ b/server/src/main/java/com/flipkart/varadhi/web/entities/TopicResource.java @@ -1,55 +1,116 @@ package com.flipkart.varadhi.web.entities; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.Validatable; +import com.flipkart.varadhi.entities.ValidateResource; +import com.flipkart.varadhi.entities.VaradhiTopic; +import com.flipkart.varadhi.entities.VersionedEntity; +import jakarta.validation.constraints.NotBlank; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import java.util.HashMap; - +/** + * Represents a topic resource in the Varadhi system. + */ @Getter @EqualsAndHashCode(callSuper = true) @ValidateResource(message = "Invalid Topic name. Check naming constraints.", max = 64) public class TopicResource extends VersionedEntity implements Validatable { - private static final String RESOURCE_TYPE_NAME = "TopicResource"; + + @NotBlank private final String project; + private final boolean grouped; + @Setter private TopicCapacityPolicy capacity; + @Setter + private LifecycleStatus.ActionCode actionCode; + + /** + * Constructs a new TopicResource instance. + * + * @param name the name of the topic + * @param version the version of the topic + * @param project the project associated with the topic + * @param grouped whether the topic is grouped + * @param capacity the capacity policy of the topic + * @param actionCode the action code indicating reason behind the action performed on the topic + */ private TopicResource( String name, int version, String project, boolean grouped, - TopicCapacityPolicy capacity + TopicCapacityPolicy capacity, + LifecycleStatus.ActionCode actionCode ) { super(name, version); this.project = project; this.grouped = grouped; this.capacity = capacity; + this.actionCode = actionCode; } - public static TopicResource grouped(String name, String project, TopicCapacityPolicy capacity) { - return new TopicResource(name, INITIAL_VERSION, project, true, capacity); + /** + * Creates a new grouped TopicResource instance. + * + * @param name the name of the topic + * @param project the project associated with the topic + * @param capacity the capacity policy of the topic + * @param actionCode the action code indicating reason behind the action performed on the topic + * + * @return a new grouped TopicResource instance + */ + public static TopicResource grouped( + String name, String project, TopicCapacityPolicy capacity, LifecycleStatus.ActionCode actionCode) { + return new TopicResource(name, INITIAL_VERSION, project, true, capacity, actionCode); } - public static TopicResource unGrouped(String name, String project, TopicCapacityPolicy capacity) { - return new TopicResource(name, INITIAL_VERSION, project, false, capacity); + /** + * Creates a new ungrouped TopicResource instance. + * + * @param name the name of the topic + * @param project the project associated with the topic + * @param capacity the capacity policy of the topic + * @param actionCode the action code indicating reason behind the action performed on the topic + * + * @return a new ungrouped TopicResource instance + */ + public static TopicResource unGrouped( + String name, String project, TopicCapacityPolicy capacity, LifecycleStatus.ActionCode actionCode) { + return new TopicResource(name, INITIAL_VERSION, project, false, capacity, actionCode); } + /** + * Creates a TopicResource instance from a VaradhiTopic instance. + * + * @param varadhiTopic the VaradhiTopic instance + * + * @return a new TopicResource instance + */ public static TopicResource from(VaradhiTopic varadhiTopic) { String[] topicResourceInfo = varadhiTopic.getName().split(NAME_SEPARATOR_REGEX); + return new TopicResource( topicResourceInfo[1], varadhiTopic.getVersion(), topicResourceInfo[0], varadhiTopic.isGrouped(), - varadhiTopic.getCapacity() + varadhiTopic.getCapacity(), + varadhiTopic.getStatus().getActionCode() ); } + /** + * Converts this TopicResource instance to a VaradhiTopic instance. + * + * @return a new VaradhiTopic instance + */ public VaradhiTopic toVaradhiTopic() { - return VaradhiTopic.of(getProject(), getName(), isGrouped(), getCapacity()); + return VaradhiTopic.of(project, getName(), grouped, capacity, actionCode); } } diff --git a/server/src/main/java/com/flipkart/varadhi/web/routes/RouteDefinition.java b/server/src/main/java/com/flipkart/varadhi/web/routes/RouteDefinition.java index b180cc07..edacf01f 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/routes/RouteDefinition.java +++ b/server/src/main/java/com/flipkart/varadhi/web/routes/RouteDefinition.java @@ -68,6 +68,10 @@ public static Builder delete(String name, String path) { return new Builder(name, HttpMethod.DELETE, path); } + public static Builder patch(String name, String path) { + return new Builder(name, HttpMethod.PATCH, path); + } + @RequiredArgsConstructor public static class Builder { private final String name; diff --git a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java index 30fa3ef6..4fc28915 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java +++ b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/SubscriptionHandlers.java @@ -1,7 +1,15 @@ package com.flipkart.varadhi.web.v1.admin; import com.flipkart.varadhi.config.RestOptions; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.Hierarchies; +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceActionRequest; +import com.flipkart.varadhi.entities.ResourceDeletionType; +import com.flipkart.varadhi.entities.ResourceHierarchy; +import com.flipkart.varadhi.entities.RetryPolicy; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.entities.auth.ResourceType; import com.flipkart.varadhi.services.ProjectService; import com.flipkart.varadhi.services.SubscriptionService; @@ -20,17 +28,26 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import static com.flipkart.varadhi.Constants.CONTEXT_KEY_BODY; import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_PROJECT; import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_SUBSCRIPTION; import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_DELETION_TYPE; import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_IGNORE_CONSTRAINTS; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_INCLUDE_INACTIVE; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_MESSAGE; import static com.flipkart.varadhi.entities.Hierarchies.SubscriptionHierarchy; import static com.flipkart.varadhi.entities.Hierarchies.TopicHierarchy; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; -import static com.flipkart.varadhi.entities.auth.ResourceAction.*; +import static com.flipkart.varadhi.entities.auth.ResourceAction.SUBSCRIPTION_CREATE; +import static com.flipkart.varadhi.entities.auth.ResourceAction.SUBSCRIPTION_DELETE; +import static com.flipkart.varadhi.entities.auth.ResourceAction.SUBSCRIPTION_GET; +import static com.flipkart.varadhi.entities.auth.ResourceAction.SUBSCRIPTION_LIST; +import static com.flipkart.varadhi.entities.auth.ResourceAction.SUBSCRIPTION_RESTORE; +import static com.flipkart.varadhi.entities.auth.ResourceAction.SUBSCRIPTION_UPDATE; +import static com.flipkart.varadhi.entities.auth.ResourceAction.TOPIC_CONSUME; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; /** @@ -114,9 +131,9 @@ public List get() { .authorize(SUBSCRIPTION_DELETE) .build(this::getHierarchies, this::delete), RouteDefinition - .post("RestoreSubscription", "/:subscription/restore") + .patch("RestoreSubscription", "/:subscription/restore") .nonBlocking() - .authorize(SUBSCRIPTION_UPDATE) + .authorize(SUBSCRIPTION_RESTORE) .build(this::getHierarchies, this::restore), RouteDefinition .post("StartSubscription", "/:subscription/start") @@ -139,6 +156,10 @@ public List get() { */ public void setSubscription(RoutingContext ctx) { SubscriptionResource subscriptionResource = ctx.body().asValidatedPojo(SubscriptionResource.class); + String requestedBy = ctx.getIdentityOrDefault(); + LifecycleStatus.ActionCode actionCode = isVaradhiAdmin(requestedBy) ? LifecycleStatus.ActionCode.ADMIN_ACTION + : LifecycleStatus.ActionCode.USER_ACTION; + subscriptionResource.setActionCode(actionCode); ctx.put(CONTEXT_KEY_BODY, subscriptionResource); } @@ -183,10 +204,16 @@ public Map getHierarchies(RoutingContext ctx, b * Lists all subscriptions for a given project. * * @param ctx the routing context + * - includeInactive: query parameter to include inactive or soft-deleted subscriptions */ public void list(RoutingContext ctx) { String projectName = ctx.pathParam(PATH_PARAM_PROJECT); - List subscriptionNames = subscriptionService.getSubscriptionList(projectName); + boolean includeInactive = ctx.queryParam(QUERY_PARAM_INCLUDE_INACTIVE).stream() + .findFirst() + .map(Boolean::parseBoolean) + .orElse(false); + + List subscriptionNames = subscriptionService.getSubscriptionList(projectName, includeInactive); ctx.endApiWithResponse(subscriptionNames); } @@ -252,13 +279,15 @@ public void delete(RoutingContext ctx) { .map(ResourceDeletionType::fromValue) .findFirst() .orElse(ResourceDeletionType.SOFT_DELETE); + ResourceActionRequest actionRequest = createResourceActionRequest(ctx); ctx.handleResponse( subscriptionService.deleteSubscription( getSubscriptionFqn(ctx), projectService.getCachedProject(ctx.pathParam(PATH_PARAM_PROJECT)), ctx.getIdentityOrDefault(), - deletionType + deletionType, + actionRequest ) ); } @@ -269,8 +298,11 @@ public void delete(RoutingContext ctx) { * @param ctx the routing context */ public void restore(RoutingContext ctx) { + ResourceActionRequest actionRequest = createResourceActionRequest(ctx); + ctx.handleResponse( - subscriptionService.restoreSubscription(getSubscriptionFqn(ctx), ctx.getIdentityOrDefault())); + subscriptionService.restoreSubscription( + getSubscriptionFqn(ctx), ctx.getIdentityOrDefault(), actionRequest)); } /** @@ -417,4 +449,31 @@ private void validateProperties(Map properties, boolean usePermi } }); } + + /** + * Creates a resource action request from the routing context. + * + * @param ctx the routing context + * + * @return the resource action request + */ + private ResourceActionRequest createResourceActionRequest(RoutingContext ctx) { + String requestedBy = ctx.getIdentityOrDefault(); + LifecycleStatus.ActionCode actionCode = isVaradhiAdmin(requestedBy) ? LifecycleStatus.ActionCode.ADMIN_ACTION + : LifecycleStatus.ActionCode.USER_ACTION; + String message = ctx.queryParam(QUERY_PARAM_MESSAGE).stream().findFirst().orElse(""); + return new ResourceActionRequest(actionCode, message); + } + + /** + * Checks if the identity is a Varadhi admin. + * TODO: Replace with a call to isVaradhiAdmin(requestedBy) when authorization is implemented. + * + * @param identity the identity to check + * + * @return true if the identity is a Varadhi admin, false otherwise + */ + private boolean isVaradhiAdmin(String identity) { + return Objects.equals(identity, "varadhi-admin"); + } } diff --git a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java index 705da692..d0d842ce 100644 --- a/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java +++ b/server/src/main/java/com/flipkart/varadhi/web/v1/admin/TopicHandlers.java @@ -1,6 +1,8 @@ package com.flipkart.varadhi.web.v1.admin; +import com.flipkart.varadhi.entities.LifecycleStatus; import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceActionRequest; import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.ResourceHierarchy; import com.flipkart.varadhi.entities.VaradhiTopic; @@ -21,16 +23,23 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import static com.flipkart.varadhi.Constants.CONTEXT_KEY_BODY; import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_PROJECT; import static com.flipkart.varadhi.Constants.PathParams.PATH_PARAM_TOPIC; import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_DELETION_TYPE; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_INCLUDE_INACTIVE; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_MESSAGE; import static com.flipkart.varadhi.entities.Hierarchies.ProjectHierarchy; import static com.flipkart.varadhi.entities.Hierarchies.TopicHierarchy; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; -import static com.flipkart.varadhi.entities.auth.ResourceAction.*; +import static com.flipkart.varadhi.entities.auth.ResourceAction.TOPIC_CREATE; +import static com.flipkart.varadhi.entities.auth.ResourceAction.TOPIC_DELETE; +import static com.flipkart.varadhi.entities.auth.ResourceAction.TOPIC_GET; +import static com.flipkart.varadhi.entities.auth.ResourceAction.TOPIC_LIST; +import static com.flipkart.varadhi.entities.auth.ResourceAction.TOPIC_RESTORE; /** * Handler class for managing topics in the Varadhi. @@ -84,8 +93,8 @@ public List get() { RouteDefinition.get("ListTopics", "") .authorize(TOPIC_LIST) .build(this::getHierarchies, this::listTopics), - RouteDefinition.post("RestoreTopic", "/:topic/restore") - .authorize(TOPIC_UPDATE) + RouteDefinition.patch("RestoreTopic", "/:topic/restore") + .authorize(TOPIC_RESTORE) .build(this::getHierarchies, this::restore) ) ).get(); @@ -146,6 +155,10 @@ public void create(RoutingContext ctx) { // TODO: Consider implementing rollback mechanisms for failure scenarios and ≠≠ kind of semantics for all operations. String projectName = ctx.pathParam(PATH_PARAM_PROJECT); TopicResource topicResource = ctx.get(CONTEXT_KEY_BODY); + String requestedBy = ctx.getIdentityOrDefault(); + LifecycleStatus.ActionCode actionCode = isVaradhiAdmin(requestedBy) ? LifecycleStatus.ActionCode.ADMIN_ACTION + : LifecycleStatus.ActionCode.USER_ACTION; + topicResource.setActionCode(actionCode); validateProjectName(projectName, topicResource); @@ -171,8 +184,9 @@ public void delete(RoutingContext ctx) { .map(ResourceDeletionType::fromValue) .findFirst() .orElse(ResourceDeletionType.SOFT_DELETE); + ResourceActionRequest actionRequest = createResourceActionRequest(ctx); - varadhiTopicService.delete(getVaradhiTopicName(ctx), deletionType); + varadhiTopicService.delete(getVaradhiTopicName(ctx), deletionType, actionRequest); ctx.endApi(); } @@ -182,7 +196,9 @@ public void delete(RoutingContext ctx) { * @param ctx the routing context */ public void restore(RoutingContext ctx) { - varadhiTopicService.restore(getVaradhiTopicName(ctx)); + ResourceActionRequest actionRequest = createResourceActionRequest(ctx); + + varadhiTopicService.restore(getVaradhiTopicName(ctx), actionRequest); ctx.endApi(); } @@ -190,10 +206,16 @@ public void restore(RoutingContext ctx) { * Handles the GET request to list topics for a project. * * @param ctx the routing context + * - includeInactive: query parameter to include inactive or soft-deleted topics */ public void listTopics(RoutingContext ctx) { String projectName = ctx.pathParam(PATH_PARAM_PROJECT); - List topics = varadhiTopicService.getVaradhiTopics(projectName).stream() + boolean includeInactive = ctx.queryParam(QUERY_PARAM_INCLUDE_INACTIVE).stream() + .findFirst() + .map(Boolean::parseBoolean) + .orElse(false); + + List topics = varadhiTopicService.getVaradhiTopics(projectName, includeInactive).stream() .filter(topic -> topic.startsWith(projectName + NAME_SEPARATOR)) .map(topic -> topic.split(NAME_SEPARATOR_REGEX)[1]) .toList(); @@ -239,4 +261,31 @@ private void validateProjectName(String projectName, TopicResource topicResource private String buildTopicName(String projectName, String topicName) { return String.join(NAME_SEPARATOR, projectName, topicName); } + + /** + * Creates a resource action request from the routing context. + * + * @param ctx the routing context + * + * @return the resource action request + */ + private ResourceActionRequest createResourceActionRequest(RoutingContext ctx) { + String requestedBy = ctx.getIdentityOrDefault(); + LifecycleStatus.ActionCode actionCode = isVaradhiAdmin(requestedBy) ? LifecycleStatus.ActionCode.ADMIN_ACTION + : LifecycleStatus.ActionCode.USER_ACTION; + String message = ctx.queryParam(QUERY_PARAM_MESSAGE).stream().findFirst().orElse(""); + return new ResourceActionRequest(actionCode, message); + } + + /** + * Checks if the identity is a Varadhi admin. + * TODO: Replace with a call to isVaradhiAdmin(requestedBy) when authorization is implemented. + * + * @param identity the identity to check + * + * @return true if the identity is a Varadhi admin, false otherwise + */ + private boolean isVaradhiAdmin(String identity) { + return Objects.equals(identity, "varadhi-admin"); + } } diff --git a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java index 9ed92eb8..ea9b7edc 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/SubscriptionServiceTest.java @@ -12,8 +12,10 @@ import com.flipkart.varadhi.entities.InternalCompositeTopic; import com.flipkart.varadhi.entities.InternalQueueCategory; import com.flipkart.varadhi.entities.InternalQueueType; +import com.flipkart.varadhi.entities.LifecycleStatus; import com.flipkart.varadhi.entities.Org; import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceActionRequest; import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.RetryPolicy; import com.flipkart.varadhi.entities.RetrySubscription; @@ -128,8 +130,14 @@ void setUp() throws Exception { team = Team.of("Team", org.getName()); project1 = Project.of("Project1", "", team.getName(), team.getOrg()); project2 = Project.of("Project2", "", team.getName(), team.getOrg()); - unGroupedTopic = VaradhiTopic.of("UngroupedTopic", project1.getName(), false, null); - groupedTopic = VaradhiTopic.of("GroupedTopic", project2.getName(), true, null); + unGroupedTopic = VaradhiTopic.of( + "UngroupedTopic", project1.getName(), false, null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + groupedTopic = VaradhiTopic.of( + "GroupedTopic", project2.getName(), true, null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); subscription1 = createUngroupedSubscription("Sub1", project1, unGroupedTopic); subscription2 = createUngroupedSubscription("Sub2", project1, unGroupedTopic); @@ -171,7 +179,7 @@ void serializeDeserializeSubscription_Success() { 1, false, 1, null ); - TopicCapacityPolicy capacity = Constants.DefaultTopicCapacity; + TopicCapacityPolicy capacity = Constants.DEFAULT_TOPIC_CAPACITY; String region = "default"; TopicPlanner planner = new TopicPlanner(new PulsarConfig()); @@ -179,7 +187,10 @@ void serializeDeserializeSubscription_Success() { StorageTopicFactory topicFactory = new PulsarTopicFactory(planner); StorageTopicService topicService = new PulsarTopicService(null, planner); - VaradhiTopic topic = VaradhiTopic.of("GroupedTopic", project2.getName(), true, capacity); + VaradhiTopic topic = VaradhiTopic.of( + "GroupedTopic", project2.getName(), true, capacity, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); StorageTopic storageTopic = topicFactory.getTopic(topic.getName(), project2, capacity, InternalQueueCategory.MAIN); topic.addInternalTopic(region, InternalCompositeTopic.of(storageTopic)); @@ -194,7 +205,8 @@ void serializeDeserializeSubscription_Success() { endpoint, retryPolicy, consumptionPolicy, - SubscriptionPropertyValidator.createPropertyDefaultValueProviders(new RestOptions()) + SubscriptionPropertyValidator.createPropertyDefaultValueProviders(new RestOptions()), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); VaradhiSubscriptionFactory varadhiFactory = @@ -214,7 +226,7 @@ void serializeDeserializeSubscription_Success() { assertEquals(subscription.getRetryPolicy(), deserializedSubscription.getRetryPolicy()); assertEquals(subscription.getConsumptionPolicy(), deserializedSubscription.getConsumptionPolicy()); assertEquals(subscription.getShards().getShardCount(), deserializedSubscription.getShards().getShardCount()); - assertEquals(subscription.getStatus(), deserializedSubscription.getStatus()); + assertEquals(subscription.getStatus().getState(), deserializedSubscription.getStatus().getState()); assertEquals(subscription.getProperties(), deserializedSubscription.getProperties()); } @@ -258,10 +270,16 @@ unGroupedTopic, createUngroupedSubscription("Sub3", project2, unGroupedTopic), project2 ); - List actualProject1Subscriptions = subscriptionService.getSubscriptionList(project1.getName()); + List actualProject1Subscriptions = subscriptionService.getSubscriptionList( + project1.getName(), + false + ); assertEquals(List.of("Project1.Sub1", "Project1.Sub2"), actualProject1Subscriptions); - List actualProject2Subscriptions = subscriptionService.getSubscriptionList(project2.getName()); + List actualProject2Subscriptions = subscriptionService.getSubscriptionList( + project2.getName(), + false + ); assertEquals(List.of("Project2.Sub3"), actualProject2Subscriptions); } @@ -273,7 +291,7 @@ void getSubscriptionList_MetaStoreFailure_ThrowsException() { Exception exception = assertThrows( RuntimeException.class, - () -> subscriptionService.getSubscriptionList(projectName) + () -> subscriptionService.getSubscriptionList(projectName, false) ); assertEquals("MetaStore listing failed", exception.getMessage()); @@ -286,7 +304,7 @@ void getSubscriptionList_InactiveSubscriptionsAreFilteredOut() { subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); subscriptionService.createSubscription(unGroupedTopic, subscription2, project1); - subscription2.markInactive(); + subscription2.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive subscription"); varadhiMetaStore.updateSubscription(subscription2); when(varadhiMetaStore.getSubscriptionNames(project1.getName())) @@ -294,7 +312,7 @@ void getSubscriptionList_InactiveSubscriptionsAreFilteredOut() { when(varadhiMetaStore.getSubscription(subscription1.getName())).thenReturn(subscription1); when(varadhiMetaStore.getSubscription(subscription2.getName())).thenReturn(subscription2); - List actualSubscriptions = subscriptionService.getSubscriptionList(project1.getName()); + List actualSubscriptions = subscriptionService.getSubscriptionList(project1.getName(), false); assertEquals(List.of(subscription1.getName()), actualSubscriptions); verify(varadhiMetaStore, times(1)).getSubscriptionNames(project1.getName()); @@ -302,6 +320,24 @@ void getSubscriptionList_InactiveSubscriptionsAreFilteredOut() { verify(varadhiMetaStore, times(1)).getSubscription(subscription2.getName()); } + @Test + void listSubscriptions_IncludingInactive_ReturnsAllSubscriptions() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + subscriptionService.createSubscription(unGroupedTopic, subscription2, project1); + + subscription2.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive subscription"); + varadhiMetaStore.updateSubscription(subscription2); + + when(varadhiMetaStore.getSubscriptionNames(project1.getName())) + .thenReturn(List.of(subscription1.getName(), subscription2.getName())); + + List actualSubscriptions = subscriptionService.getSubscriptionList(project1.getName(), true); + + assertEquals(List.of(subscription1.getName(), subscription2.getName()), actualSubscriptions); + verify(varadhiMetaStore, times(1)).getSubscriptionNames(project1.getName()); + } + @Test void getSubscription_ExistingSubscription_ReturnsCorrectSubscription() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); @@ -327,7 +363,7 @@ void getSubscription_NonExistentSubscription_ThrowsException() { @Test void getSubscription_InactiveSubscription_ThrowsException() { subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); - subscription1.markInactive(); + subscription1.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive subscription"); varadhiMetaStore.updateSubscription(subscription1); when(varadhiMetaStore.getSubscription(subscription1.getName())).thenReturn(subscription1); @@ -415,7 +451,7 @@ void createSubscription_ProvisionFailure_SetsStateToCreateFailed() { ); assertEquals("Provision failed", exception.getMessage()); - assertEquals(VaradhiSubscription.State.CREATE_FAILED, subscription1.getStatus().getState()); + assertEquals(LifecycleStatus.State.CREATE_FAILED, subscription1.getStatus().getState()); verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); } @@ -554,9 +590,16 @@ void deleteSubscription_HardDelete_Success(VertxTestContext ctx) { CompletableFuture.completedFuture(SubscriptionState.forStopped()); doReturn(status).when(controllerRestApi).getSubscriptionState(subscriptionName, REQUESTED_BY); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Delete" + ); + Future.fromCompletionStage( subscriptionService.deleteSubscription( - subscriptionName, project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE)) + subscriptionName, project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE, + actionRequest + )) .onComplete(ctx.succeeding(result -> { ResourceNotFoundException exception = assertThrows( ResourceNotFoundException.class, @@ -576,14 +619,19 @@ void deleteSubscription_SoftDelete_UpdatesSubscriptionState() { CompletableFuture.completedFuture(SubscriptionState.forStopped()); doReturn(stoppedState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Delete" + ); + assertDoesNotThrow(() -> subscriptionService.deleteSubscription( subscription1.getName(), project1, REQUESTED_BY, - ResourceDeletionType.SOFT_DELETE + ResourceDeletionType.SOFT_DELETE, actionRequest ).get()); verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); VaradhiSubscription updatedSubscription = varadhiMetaStore.getSubscription(subscription1.getName()); - assertEquals(VaradhiSubscription.State.INACTIVE, updatedSubscription.getStatus().getState()); + assertEquals(LifecycleStatus.State.INACTIVE, updatedSubscription.getStatus().getState()); } @Test @@ -595,10 +643,17 @@ void deleteSubscription_ResourceNotStopped_ThrowsException() { CompletableFuture.completedFuture(SubscriptionState.forRunning()); doReturn(activeState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Delete" + ); + ExecutionException exception = assertThrows( ExecutionException.class, () -> subscriptionService.deleteSubscription( - subscription1.getName(), project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE).get() + subscription1.getName(), project1, + REQUESTED_BY, ResourceDeletionType.HARD_DELETE, actionRequest + ).get() ); Throwable cause = exception.getCause(); @@ -618,10 +673,17 @@ void deleteSubscription_MetaStoreFailure_ThrowsException() { doThrow(new RuntimeException("MetaStore deletion failed")).when(varadhiMetaStore) .deleteSubscription(subscription1.getName()); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Delete" + ); + ExecutionException exception = assertThrows( ExecutionException.class, () -> subscriptionService.deleteSubscription( - subscription1.getName(), project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE).get() + subscription1.getName(), project1, + REQUESTED_BY, ResourceDeletionType.HARD_DELETE, actionRequest + ).get() ); Throwable cause = exception.getCause(); @@ -640,10 +702,17 @@ void deleteSubscription_DeProvisionFailure_ThrowsException() { doReturn(stoppedState).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); doThrow(new RuntimeException("DeProvision failed")).when(shardProvisioner).deProvision(any(), any()); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Delete" + ); + Exception exception = assertThrows( ExecutionException.class, () -> subscriptionService.deleteSubscription( - subscription1.getName(), project1, REQUESTED_BY, ResourceDeletionType.HARD_DELETE).get() + subscription1.getName(), project1, + REQUESTED_BY, ResourceDeletionType.HARD_DELETE, actionRequest + ).get() ); Throwable cause = exception.getCause(); @@ -651,7 +720,7 @@ void deleteSubscription_DeProvisionFailure_ThrowsException() { assertEquals("DeProvision failed", cause.getMessage()); VaradhiSubscription updatedSubscription = varadhiMetaStore.getSubscription(subscription1.getName()); - assertEquals(VaradhiSubscription.State.DELETE_FAILED, updatedSubscription.getStatus().getState()); + assertEquals(LifecycleStatus.State.DELETE_FAILED, updatedSubscription.getStatus().getState()); verify(varadhiMetaStore, times(1)).updateSubscription(subscription1); } @@ -728,36 +797,73 @@ void restoreSubscription_Success() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); - subscription1.markInactive(); + subscription1.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive subscription"); varadhiMetaStore.updateSubscription(subscription1); CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); doReturn(status).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Restore" + ); + CompletableFuture result = - subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY); + subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY, actionRequest); assertDoesNotThrow(() -> { VaradhiSubscription restoredSubscription = result.get(); - assertEquals(VaradhiSubscription.State.CREATED, restoredSubscription.getStatus().getState()); + assertEquals(LifecycleStatus.State.CREATED, restoredSubscription.getStatus().getState()); }); verify(varadhiMetaStore, times(2)).updateSubscription(subscription1); } + @Test + void restoreSubscription_UserNotAllowed_ThrowsException() { + doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); + subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + + subscription1.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive subscription"); + varadhiMetaStore.updateSubscription(subscription1); + + CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); + doReturn(status).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); + + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.USER_ACTION, + "Restore" + ); + + InvalidOperationForResourceException exception = assertThrows( + InvalidOperationForResourceException.class, + () -> subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY, actionRequest) + .get() + ); + + String expectedMessage = "Restoration denied. Only Varadhi Admin can restore this subscription."; + assertEquals(expectedMessage, exception.getMessage()); + verify(varadhiMetaStore, times(2)).updateSubscription(subscription1); + } + @Test void restoreSubscription_MetaStoreFailure_ThrowsException() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); - subscription1.markInactive(); + subscription1.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "Inactive subscription"); varadhiMetaStore.updateSubscription(subscription1); CompletableFuture status = CompletableFuture.completedFuture(SubscriptionState.forStopped()); doReturn(status).when(controllerRestApi).getSubscriptionState(subscription1.getName(), REQUESTED_BY); doThrow(new RuntimeException("MetaStore update failed")).when(varadhiMetaStore).updateSubscription(any()); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Restore" + ); + CompletableFuture result = - subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY); + subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY, actionRequest); Exception exception = assertThrows(ExecutionException.class, result::get); Throwable cause = exception.getCause(); @@ -771,9 +877,14 @@ void restoreSubscription_AlreadyWellProvisioned_ThrowsException() { doReturn(unGroupedTopic).when(varadhiMetaStore).getTopic(unGroupedTopic.getName()); subscriptionService.createSubscription(unGroupedTopic, subscription1, project1); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "Restore" + ); + InvalidOperationForResourceException exception = assertThrows( InvalidOperationForResourceException.class, () -> { - subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY).get(); + subscriptionService.restoreSubscription(subscription1.getName(), REQUESTED_BY, actionRequest).get(); } ); diff --git a/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java b/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java index 8c7f7865..89399f11 100644 --- a/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java +++ b/server/src/test/java/com/flipkart/varadhi/services/VaradhiTopicServiceTest.java @@ -2,7 +2,9 @@ import com.flipkart.varadhi.Constants; import com.flipkart.varadhi.entities.InternalQueueCategory; +import com.flipkart.varadhi.entities.LifecycleStatus; import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.ResourceActionRequest; import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.StorageTopic; import com.flipkart.varadhi.entities.TopicCapacityPolicy; @@ -24,10 +26,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -44,7 +50,7 @@ class VaradhiTopicServiceTest { private static final String REGION = "local"; private static final String TOPIC_NAME = "testTopic"; - private static final TopicCapacityPolicy DEFAULT_CAPACITY_POLICY = Constants.DefaultTopicCapacity; + private static final TopicCapacityPolicy DEFAULT_CAPACITY_POLICY = Constants.DEFAULT_TOPIC_CAPACITY; @Mock private StorageTopicService storageTopicService; @@ -55,6 +61,12 @@ class VaradhiTopicServiceTest { @Mock private StorageTopicFactory storageTopicFactory; + @Mock + private VaradhiSubscription subscription; + + @Mock + private LifecycleStatus status; + @InjectMocks private VaradhiTopicService varadhiTopicService; @@ -124,7 +136,7 @@ void createVaradhiTopic_StorageTopicServiceFailure_ThrowsException() { void deleteVaradhiTopic_SuccessfulHardDelete() { VaradhiTopic varadhiTopic = mockDeleteSetup(); - varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE); + varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE, null); verify(storageTopicService, times(1)).delete(pulsarStorageTopic.getName(), project); verify(metaStore, times(1)).deleteTopic(varadhiTopic.getName()); @@ -137,7 +149,7 @@ void deleteVaradhiTopic_StorageTopicDoesNotExist_SuccessfulHardDelete() { when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); when(metaStore.getProject(project.getName())).thenReturn(project); - varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE); + varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE, null); verify(storageTopicService, times(1)).delete(pulsarStorageTopic.getName(), project); verify(metaStore, times(1)).deleteTopic(varadhiTopic.getName()); @@ -150,7 +162,7 @@ void deleteVaradhiTopic_MetaStoreFailure_ThrowsException() { Exception exception = assertThrows( VaradhiException.class, - () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE) + () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE, null) ); verify(storageTopicService, times(1)).delete(pulsarStorageTopic.getName(), project); @@ -162,16 +174,17 @@ void deleteVaradhiTopic_MetaStoreFailure_ThrowsException() { @Test void deleteVaradhiTopic_TopicInUse_ThrowsException() { VaradhiTopic varadhiTopic = createVaradhiTopicMock(); - VaradhiSubscription subscription = mock(VaradhiSubscription.class); when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); when(metaStore.getAllSubscriptionNames()).thenReturn(List.of("subscription1")); when(metaStore.getSubscription("subscription1")).thenReturn(subscription); when(subscription.getTopic()).thenReturn(varadhiTopic.getName()); + when(subscription.getStatus()).thenReturn(status); + when(status.getState()).thenReturn(LifecycleStatus.State.CREATED); Exception exception = assertThrows( InvalidOperationForResourceException.class, - () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE) + () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.HARD_DELETE, null) ); verify(metaStore, never()).deleteTopic(varadhiTopic.getName()); @@ -181,8 +194,12 @@ void deleteVaradhiTopic_TopicInUse_ThrowsException() { @Test void softDeleteVaradhiTopic_MetaStoreSuccess_UpdatesTopicStatus() { VaradhiTopic varadhiTopic = mockDeleteSetup(); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "message" + ); - varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.SOFT_DELETE); + varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.SOFT_DELETE, actionRequest); verify(metaStore, times(1)).updateTopic(varadhiTopic); assertFalse(varadhiTopic.isActive()); @@ -191,11 +208,15 @@ void softDeleteVaradhiTopic_MetaStoreSuccess_UpdatesTopicStatus() { @Test void softDeleteVaradhiTopic_MetaStoreFailure_ThrowsException() { VaradhiTopic varadhiTopic = mockDeleteSetup(); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, "message" + ); doThrow(new VaradhiException("MetaStore update failed")).when(metaStore).updateTopic(varadhiTopic); Exception exception = assertThrows( VaradhiException.class, - () -> varadhiTopicService.delete(varadhiTopic.getName(), ResourceDeletionType.SOFT_DELETE) + () -> varadhiTopicService.delete( + varadhiTopic.getName(), ResourceDeletionType.SOFT_DELETE, actionRequest) ); verify(metaStore, times(1)).updateTopic(varadhiTopic); @@ -203,13 +224,98 @@ void softDeleteVaradhiTopic_MetaStoreFailure_ThrowsException() { assertEquals("MetaStore update failed", exception.getMessage()); } + @Test + void validateTopicForDeletion_SoftDeleteWithInactiveSubscriptions_Success() throws Exception { + String topicName = "testTopic"; + + when(metaStore.getAllSubscriptionNames()).thenReturn(List.of("subscription1")); + when(metaStore.getSubscription("subscription1")).thenReturn(subscription); + when(subscription.getTopic()).thenReturn(topicName); + when(subscription.getStatus()).thenReturn(status); + when(status.getState()).thenReturn(LifecycleStatus.State.INACTIVE); + + Method method = VaradhiTopicService.class.getDeclaredMethod( + "validateTopicForDeletion", String.class, + ResourceDeletionType.class + ); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(varadhiTopicService, topicName, ResourceDeletionType.SOFT_DELETE)); + } + + @Test + void validateTopicForDeletion_SoftDeleteWithActiveSubscriptions_ThrowsException() throws Exception { + String topicName = "testTopic"; + + when(metaStore.getAllSubscriptionNames()).thenReturn(List.of("subscription1")); + when(metaStore.getSubscription("subscription1")).thenReturn(subscription); + when(subscription.getTopic()).thenReturn(topicName); + when(subscription.getStatus()).thenReturn(status); + when(status.getState()).thenReturn(LifecycleStatus.State.CREATED); + + Method method = VaradhiTopicService.class.getDeclaredMethod( + "validateTopicForDeletion", String.class, + ResourceDeletionType.class + ); + method.setAccessible(true); + + Exception exception = assertThrows( + InvocationTargetException.class, () -> + method.invoke(varadhiTopicService, topicName, ResourceDeletionType.SOFT_DELETE) + ); + assertInstanceOf(InvalidOperationForResourceException.class, exception.getCause()); + assertEquals("Cannot delete topic as it has active subscriptions.", exception.getCause().getMessage()); + } + + @Test + void validateTopicForDeletion_HardDeleteWithExistingSubscriptions_ThrowsException() throws Exception { + String topicName = "testTopic"; + + when(metaStore.getAllSubscriptionNames()).thenReturn(List.of("subscription1")); + when(metaStore.getSubscription("subscription1")).thenReturn(subscription); + when(subscription.getTopic()).thenReturn(topicName); + when(subscription.getStatus()).thenReturn(status); + when(status.getState()).thenReturn(LifecycleStatus.State.CREATED); + + Method method = VaradhiTopicService.class.getDeclaredMethod( + "validateTopicForDeletion", String.class, + ResourceDeletionType.class + ); + method.setAccessible(true); + + Exception exception = assertThrows( + InvocationTargetException.class, () -> + method.invoke(varadhiTopicService, topicName, ResourceDeletionType.HARD_DELETE) + ); + assertInstanceOf(InvalidOperationForResourceException.class, exception.getCause()); + assertEquals("Cannot delete topic as it has existing subscriptions.", exception.getCause().getMessage()); + } + + @Test + void validateTopicForDeletion_DeleteWithNoSubscriptions_Success() throws Exception { + when(metaStore.getAllSubscriptionNames()).thenReturn(List.of()); + + Method method = VaradhiTopicService.class.getDeclaredMethod( + "validateTopicForDeletion", String.class, + ResourceDeletionType.class + ); + method.setAccessible(true); + + assertDoesNotThrow(() -> method.invoke(varadhiTopicService, TOPIC_NAME, ResourceDeletionType.HARD_DELETE)); + assertDoesNotThrow(() -> method.invoke(varadhiTopicService, TOPIC_NAME, ResourceDeletionType.SOFT_DELETE)); + } + @Test void restoreVaradhiTopic_SuccessfulRestore() { VaradhiTopic varadhiTopic = createVaradhiTopicMock(); - varadhiTopic.updateStatus(VaradhiTopic.Status.INACTIVE); + varadhiTopic.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "message"); when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "message" + ); - varadhiTopicService.restore(varadhiTopic.getName()); + varadhiTopicService.restore(varadhiTopic.getName(), actionRequest); verify(metaStore, times(1)).updateTopic(varadhiTopic); Assertions.assertTrue(varadhiTopic.isActive()); @@ -219,10 +325,14 @@ void restoreVaradhiTopic_SuccessfulRestore() { void restoreVaradhiTopic_AlreadyActive_ThrowsException() { VaradhiTopic varadhiTopic = createVaradhiTopicMock(); when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.SYSTEM_ACTION, + "message" + ); Exception exception = assertThrows( InvalidOperationForResourceException.class, - () -> varadhiTopicService.restore(varadhiTopic.getName()) + () -> varadhiTopicService.restore(varadhiTopic.getName(), actionRequest) ); verify(metaStore, never()).updateTopic(varadhiTopic); @@ -230,6 +340,24 @@ void restoreVaradhiTopic_AlreadyActive_ThrowsException() { assertEquals("Topic default.testTopic is not deleted.", exception.getMessage()); } + @Test + void restoreVaradhiTopic_InvalidUser_ThrowsException() { + VaradhiTopic varadhiTopic = createVaradhiTopicMock(); + varadhiTopic.markInactive(LifecycleStatus.ActionCode.SYSTEM_ACTION, "message"); + when(metaStore.getTopic(varadhiTopic.getName())).thenReturn(varadhiTopic); + ResourceActionRequest actionRequest = new ResourceActionRequest( + LifecycleStatus.ActionCode.USER_ACTION, "message"); + + Exception exception = assertThrows( + InvalidOperationForResourceException.class, + () -> varadhiTopicService.restore(varadhiTopic.getName(), actionRequest) + ); + + verify(metaStore, never()).updateTopic(varadhiTopic); + assertEquals(InvalidOperationForResourceException.class, exception.getClass()); + assertEquals("Restoration denied. Only Varadhi Admin can restore this topic.", exception.getMessage()); + } + @Test void checkVaradhiTopicExists_TopicExists_ReturnsTrue() { VaradhiTopic varadhiTopic = createVaradhiTopicMock(); @@ -285,7 +413,7 @@ void getVaradhiTopicsForProject_ValidProject_ReturnsActiveTopics() { setupMockTopics(projectName, topicNames, topicStatuses); - List activeTopics = varadhiTopicService.getVaradhiTopics(projectName); + List activeTopics = varadhiTopicService.getVaradhiTopics(projectName, false); assertNotNull(activeTopics); assertEquals(2, activeTopics.size()); @@ -300,15 +428,33 @@ void getVaradhiTopicsForProject_MultipleStatuses_FiltersInactiveTopics() { setupMockTopics(projectName, topicNames, topicStatuses); - List activeTopics = varadhiTopicService.getVaradhiTopics(projectName); + List activeTopics = varadhiTopicService.getVaradhiTopics(projectName, false); assertNotNull(activeTopics); assertEquals(2, activeTopics.size()); assertEquals(List.of("topic1", "topic3"), activeTopics); } + @Test + void getVaradhiTopicsForProject_IncludingInactive_ReturnsAllTopics() { + String projectName = project.getName(); + List topicNames = List.of("topic1", "topic2", "topic3"); + List topicStatuses = List.of(true, false, true); + + setupMockTopics(projectName, topicNames, topicStatuses); + + List allTopics = varadhiTopicService.getVaradhiTopics(projectName, true); + + assertNotNull(allTopics); + assertEquals(3, allTopics.size()); + assertEquals(topicNames, allTopics); + } + private VaradhiTopic createVaradhiTopicMock() { - TopicResource topicResource = TopicResource.grouped(TOPIC_NAME, project.getName(), DEFAULT_CAPACITY_POLICY); + TopicResource topicResource = TopicResource.grouped( + TOPIC_NAME, project.getName(), DEFAULT_CAPACITY_POLICY, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); return varadhiTopicFactory.get(project, topicResource); } diff --git a/server/src/test/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactoryTest.java b/server/src/test/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactoryTest.java new file mode 100644 index 00000000..3c57e7c3 --- /dev/null +++ b/server/src/test/java/com/flipkart/varadhi/utils/VaradhiSubscriptionFactoryTest.java @@ -0,0 +1,111 @@ +package com.flipkart.varadhi.utils; + +import com.flipkart.varadhi.Constants; +import com.flipkart.varadhi.entities.InternalQueueCategory; +import com.flipkart.varadhi.entities.StorageSubscription; +import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.spi.services.StorageSubscriptionFactory; +import com.flipkart.varadhi.spi.services.StorageTopicFactory; +import com.flipkart.varadhi.spi.services.StorageTopicService; +import com.flipkart.varadhi.web.admin.SubscriptionTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class VaradhiSubscriptionFactoryTest extends SubscriptionTestBase { + + private static final String REGION = "local"; + private static final String SUBSCRIPTION_NAME = "testSubscription"; + private static final TopicCapacityPolicy CAPACITY_POLICY = Constants.DEFAULT_TOPIC_CAPACITY; + + @Mock + private StorageSubscriptionFactory, StorageTopic> subscriptionFactory; + + @Mock + private StorageTopicFactory topicFactory; + + @Mock + private StorageTopicService topicService; + + @InjectMocks + private VaradhiSubscriptionFactory varadhiSubscriptionFactory; + + @Override + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + varadhiSubscriptionFactory = + new VaradhiSubscriptionFactory(topicService, subscriptionFactory, topicFactory, REGION); + } + + @Test + void getShardCapacity_ValidInput_ReturnsCorrectCapacity() throws Exception { + Method method = + VaradhiSubscriptionFactory.class.getDeclaredMethod( + "getShardCapacity", TopicCapacityPolicy.class, + int.class + ); + method.setAccessible(true); + + TopicCapacityPolicy result = + (TopicCapacityPolicy) method.invoke(varadhiSubscriptionFactory, CAPACITY_POLICY, 2); + + assertNotNull(result); + assertEquals(CAPACITY_POLICY.getReadFanOut(), result.getReadFanOut()); + } + + @Test + void getShardMainSubName_ValidInput_ReturnsCorrectName() throws Exception { + Method method = + VaradhiSubscriptionFactory.class.getDeclaredMethod("getShardMainSubName", String.class, int.class); + method.setAccessible(true); + + String result = (String) method.invoke(varadhiSubscriptionFactory, SUBSCRIPTION_NAME, 0); + + assertEquals("testSubscription.shard.0.MAIN", result); + } + + @Test + void getInternalSubName_ValidInput_ReturnsCorrectName() throws Exception { + Method method = + VaradhiSubscriptionFactory.class.getDeclaredMethod( + "getInternalSubName", String.class, int.class, + InternalQueueCategory.class, int.class + ); + method.setAccessible(true); + + String result = + (String) method.invoke( + varadhiSubscriptionFactory, SUBSCRIPTION_NAME, 0, + InternalQueueCategory.MAIN, 0 + ); + + assertEquals("is-testSubscription.shard-0.MAIN-0", result); + } + + @Test + void getInternalTopicName_ValidInput_ReturnsCorrectName() throws Exception { + Method method = + VaradhiSubscriptionFactory.class.getDeclaredMethod( + "getInternalTopicName", String.class, int.class, + InternalQueueCategory.class, int.class + ); + method.setAccessible(true); + + String result = + (String) method.invoke( + varadhiSubscriptionFactory, SUBSCRIPTION_NAME, 0, + InternalQueueCategory.MAIN, 0 + ); + + assertEquals("it-testSubscription.shard-0.MAIN-0", result); + } +} diff --git a/server/src/test/java/com/flipkart/varadhi/utils/VaradhiTopicFactoryTest.java b/server/src/test/java/com/flipkart/varadhi/utils/VaradhiTopicFactoryTest.java index b0e0153a..5437c68f 100644 --- a/server/src/test/java/com/flipkart/varadhi/utils/VaradhiTopicFactoryTest.java +++ b/server/src/test/java/com/flipkart/varadhi/utils/VaradhiTopicFactoryTest.java @@ -1,59 +1,113 @@ package com.flipkart.varadhi.utils; import com.flipkart.varadhi.Constants; -import com.flipkart.varadhi.entities.*; +import com.flipkart.varadhi.entities.InternalCompositeTopic; +import com.flipkart.varadhi.entities.InternalQueueCategory; +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.StorageTopic; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.TopicState; +import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.pulsar.entities.PulsarStorageTopic; import com.flipkart.varadhi.spi.services.StorageTopicFactory; import com.flipkart.varadhi.web.entities.TopicResource; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; -import static org.mockito.Mockito.*; +import java.lang.reflect.Method; -public class VaradhiTopicFactoryTest { - private final String region = "local"; - private final String topicName = "testTopic"; - private VaradhiTopicFactory varadhiTopicFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class VaradhiTopicFactoryTest { + + private static final String REGION = "local"; + private static final String TOPIC_NAME = "testTopic"; + private static final TopicCapacityPolicy CAPACITY_POLICY = Constants.DEFAULT_TOPIC_CAPACITY; + + @Mock private StorageTopicFactory storageTopicFactory; + + @InjectMocks + private VaradhiTopicFactory varadhiTopicFactory; + private Project project; private String vTopicName; @BeforeEach public void setUp() { - storageTopicFactory = mock(StorageTopicFactory.class); - varadhiTopicFactory = new VaradhiTopicFactory(storageTopicFactory, region, Constants.DefaultTopicCapacity); + MockitoAnnotations.openMocks(this); + varadhiTopicFactory = new VaradhiTopicFactory(storageTopicFactory, REGION, Constants.DEFAULT_TOPIC_CAPACITY); + project = Project.of("default", "", "public", "public"); - vTopicName = String.format("%s.%s", project.getName(), topicName); - String pTopicName = - String.format("persistent://%s/%s", project.getOrg(), vTopicName); - TopicCapacityPolicy capacityPolicy = Constants.DefaultTopicCapacity; - PulsarStorageTopic pTopic = PulsarStorageTopic.of(pTopicName, 1, capacityPolicy); + vTopicName = String.format("%s.%s", project.getName(), TOPIC_NAME); + String pTopicName = String.format("persistent://%s/%s", project.getOrg(), vTopicName); + PulsarStorageTopic pTopic = PulsarStorageTopic.of(pTopicName, 1, CAPACITY_POLICY); + doReturn(pTopic).when(storageTopicFactory) - .getTopic(vTopicName, project, capacityPolicy, InternalQueueCategory.MAIN); + .getTopic(vTopicName, project, CAPACITY_POLICY, InternalQueueCategory.MAIN); } @Test - public void getTopic() { - TopicCapacityPolicy capacityPolicy = Constants.DefaultTopicCapacity; - TopicResource topicResource = TopicResource.grouped(topicName, project.getName(), capacityPolicy); + void get_WithValidTopicResource_ShouldReturnValidVaradhiTopic() { + TopicResource topicResource = TopicResource.grouped( + TOPIC_NAME, project.getName(), CAPACITY_POLICY, LifecycleStatus.ActionCode.SYSTEM_ACTION + ); VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - Assertions.assertNotNull(varadhiTopic); - InternalCompositeTopic it = varadhiTopic.getProduceTopicForRegion(region); - StorageTopic st = it.getTopicToProduce(); - Assertions.assertEquals(it.getTopicState(), TopicState.Producing); - Assertions.assertNotNull(st); - verify(storageTopicFactory, times(1)).getTopic(vTopicName, project, capacityPolicy, InternalQueueCategory.MAIN); + + assertNotNull(varadhiTopic); + InternalCompositeTopic internalTopic = varadhiTopic.getProduceTopicForRegion(REGION); + assertEquals(TopicState.Producing, internalTopic.getTopicState()); + assertNotNull(internalTopic.getTopicToProduce()); + + verify(storageTopicFactory, times(1)).getTopic( + vTopicName, project, CAPACITY_POLICY, InternalQueueCategory.MAIN); } @Test - public void getTopicWithDefaultCapacity() { - TopicCapacityPolicy capacityPolicy = Constants.DefaultTopicCapacity; - TopicResource topicResource = TopicResource.grouped(topicName, project.getName(), null); + void get_WhenNoCapacityPolicyProvided_ShouldUseDefaultCapacity() { + TopicResource topicResource = + TopicResource.grouped( + TOPIC_NAME, project.getName(), null, LifecycleStatus.ActionCode.SYSTEM_ACTION + ); VaradhiTopic varadhiTopic = varadhiTopicFactory.get(project, topicResource); - InternalCompositeTopic it = varadhiTopic.getProduceTopicForRegion(region); - PulsarStorageTopic pt = (PulsarStorageTopic) it.getTopicToProduce(); - Assertions.assertEquals(capacityPolicy.getThroughputKBps(), pt.getCapacity().getThroughputKBps()); - Assertions.assertEquals(capacityPolicy.getQps(), pt.getCapacity().getQps()); + InternalCompositeTopic internalTopic = varadhiTopic.getProduceTopicForRegion(REGION); + PulsarStorageTopic storageTopic = (PulsarStorageTopic) internalTopic.getTopicToProduce(); + + assertNotNull(storageTopic); + assertEquals(CAPACITY_POLICY, varadhiTopic.getCapacity()); + assertEquals(CAPACITY_POLICY.getThroughputKBps(), storageTopic.getCapacity().getThroughputKBps()); + assertEquals(CAPACITY_POLICY.getQps(), storageTopic.getCapacity().getQps()); + } + + @Test + void planDeployment_ValidVaradhiTopic_ShouldInvokeStorageTopicCreation() throws Exception { + TopicResource topicResource = + TopicResource.grouped( + TOPIC_NAME, project.getName(), Constants.DEFAULT_TOPIC_CAPACITY, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + VaradhiTopic varadhiTopic = topicResource.toVaradhiTopic(); + + Method planDeploymentMethod = + VaradhiTopicFactory.class.getDeclaredMethod("planDeployment", Project.class, VaradhiTopic.class); + planDeploymentMethod.setAccessible(true); + + planDeploymentMethod.invoke(varadhiTopicFactory, project, varadhiTopic); + + InternalCompositeTopic internalCompositeTopic = varadhiTopic.getProduceTopicForRegion(REGION); + assertNotNull(internalCompositeTopic); + assertEquals(TopicState.Producing, internalCompositeTopic.getTopicState()); + assertNotNull(internalCompositeTopic.getTopicToProduce()); + + verify(storageTopicFactory, times(1)).getTopic( + vTopicName, project, Constants.DEFAULT_TOPIC_CAPACITY, InternalQueueCategory.MAIN); } } diff --git a/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java b/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java index 1793c4d0..808db9f2 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java +++ b/server/src/test/java/com/flipkart/varadhi/web/WebTestBase.java @@ -25,10 +25,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import static io.vertx.core.http.HttpMethod.DELETE; -import static io.vertx.core.http.HttpMethod.GET; -import static io.vertx.core.http.HttpMethod.POST; -import static io.vertx.core.http.HttpMethod.PUT; +import static io.vertx.core.http.HttpMethod.*; import static java.net.HttpURLConnection.HTTP_OK; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -106,6 +103,8 @@ public HttpRequest createRequest(HttpMethod method, String path) { return webClient.delete(DEFAULT_PORT, DEFAULT_HOST, path); } else if (PUT == method) { return webClient.put(DEFAULT_PORT, DEFAULT_HOST, path); + } else if (PATCH == method) { + return webClient.patch(DEFAULT_PORT, DEFAULT_HOST, path); } else { throw new UnsupportedOperationException("Unsupported HTTP method"); } @@ -115,7 +114,11 @@ public R sendRequestWithPayload(HttpRequest request, byte[] payload, throws InterruptedException { HttpResponse response = sendRequest(request, payload); assertEquals(HTTP_OK, response.statusCode(), "Unexpected status code"); - return JsonMapper.jsonDeserialize(response.bodyAsString(), responseClass); + String responseBody = response.bodyAsString(); + if (responseBody == null || responseBody.isEmpty()) { + return null; + } + return JsonMapper.jsonDeserialize(responseBody, responseClass); } public R sendRequestWithPayload( diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java index da3aab10..cf0efff1 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionHandlersTest.java @@ -8,6 +8,7 @@ import com.flipkart.varadhi.entities.VaradhiTopic; import com.flipkart.varadhi.exceptions.ResourceNotFoundException; import com.flipkart.varadhi.web.ErrorResponse; +import com.flipkart.varadhi.web.Extensions; import com.flipkart.varadhi.web.entities.SubscriptionResource; import com.flipkart.varadhi.web.v1.admin.SubscriptionHandlers; import io.vertx.core.Handler; @@ -17,6 +18,7 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.handler.HttpException; +import lombok.experimental.ExtensionMethod; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,6 +42,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@ExtensionMethod({Extensions.RequestBodyExtension.class, Extensions.RoutingContextExtension.class}) class SubscriptionHandlersTest extends SubscriptionTestBase { private SubscriptionHandlers subscriptionHandlers; @@ -82,7 +85,7 @@ private void configureRoutes() { subscriptionHandlers::update, true ); createRoute( - HttpMethod.POST, "/projects/:project/subscriptions/:subscription/restore", + HttpMethod.PATCH, "/projects/:project/subscriptions/:subscription/restore", subscriptionHandlers::restore, false ); createRoute( @@ -138,7 +141,10 @@ void createSubscription_NonSuperUserWithIgnoreConstraints_ThrowsUnauthorizedExce doThrow(new HttpException(HTTP_UNAUTHORIZED, errorMessage)).when(subscriptionService) .createSubscription(any(), any(), any()); - ErrorResponse response = sendRequestWithEntity(request, resource, 401, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 401, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -152,7 +158,10 @@ void createSubscription_NonExistentProject_ThrowsNotFoundException() throws Inte doThrow(new ResourceNotFoundException(errorMessage)).when(projectService) .getCachedProject(PROJECT.getName()); - ErrorResponse response = sendRequestWithEntity(request, resource, 404, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 404, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -166,7 +175,10 @@ void createSubscription_NonExistentTopic_ThrowsNotFoundException() throws Interr doThrow(new ResourceNotFoundException(errorMessage)).when(topicService) .get(TOPIC_RESOURCE.getProject() + "." + TOPIC_RESOURCE.getName()); - ErrorResponse response = sendRequestWithEntity(request, resource, 404, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 404, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -181,7 +193,10 @@ void createSubscription_MismatchedProjectName_ThrowsBadRequest() throws Interrup ); String errorMessage = "Project name mismatch between URL and request body."; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -194,7 +209,10 @@ void createSubscription_ExceedingRetryLimit_ThrowsBadRequest() throws Interrupte createSubscriptionResource("sub12", PROJECT, TOPIC_RESOURCE, retryPolicy); String errorMessage = "Only 3 retries are supported."; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -206,7 +224,10 @@ void createSubscription_UnsupportedProperties_ThrowsBadRequest() throws Interrup resource.getProperties().put("unsupportedProperty", "value"); String errorMessage = "Unsupported properties: unsupportedProperty"; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -218,7 +239,10 @@ void createSubscription_InvalidPropertyValues_ThrowsBadRequest() throws Interrup resource.getProperties().put("unsideline.api.message_count", "-10"); String errorMessage = "Invalid value for property: unsideline.api.message_count"; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -242,7 +266,7 @@ void getSubscription_ValidSubscription_ReturnsSubscription() throws InterruptedE void listSubscriptions_ValidProject_ReturnsSubscriptionList() throws InterruptedException { HttpRequest request = createRequest(HttpMethod.GET, buildSubscriptionsUrl(PROJECT)); - when(subscriptionService.getSubscriptionList(PROJECT.getName())) + when(subscriptionService.getSubscriptionList(PROJECT.getName(), false)) .thenReturn(List.of("sub1", "sub2")) .thenReturn(List.of()); @@ -251,7 +275,23 @@ void listSubscriptions_ValidProject_ReturnsSubscriptionList() throws Interrupted assertEquals(List.of("sub1", "sub2"), subscriptions); assertEquals(List.of(), subscriptions2); - verify(subscriptionService, times(2)).getSubscriptionList(PROJECT.getName()); + verify(subscriptionService, times(2)) + .getSubscriptionList(PROJECT.getName(), false); + } + + @Test + void listSubscriptions_IncludingInactive_ReturnsAllSubscriptions() throws InterruptedException { + HttpRequest request = + createRequest(HttpMethod.GET, buildSubscriptionsUrl(PROJECT) + "?includeInactive=true"); + + when(subscriptionService.getSubscriptionList(PROJECT.getName(), true)) + .thenReturn(List.of("sub1", "sub2", "sub3")); + + List subscriptions = sendRequestWithoutPayload(request, List.class); + + assertEquals(List.of("sub1", "sub2", "sub3"), subscriptions); + verify(subscriptionService, times(1)) + .getSubscriptionList(PROJECT.getName(), true); } @Test @@ -264,12 +304,12 @@ void deleteSubscription_SoftDelete_Success() throws InterruptedException { doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) - .deleteSubscription(anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + .deleteSubscription(anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE), any()); sendRequestWithoutPayload(request, null); verify(subscriptionService, times(1)).deleteSubscription( - eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE), any()); } @Test @@ -282,12 +322,12 @@ void deleteSubscription_HardDelete_Success() throws InterruptedException { doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) - .deleteSubscription(anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.HARD_DELETE)); + .deleteSubscription(anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.HARD_DELETE), any()); sendRequestWithoutPayload(request, null); verify(subscriptionService, times(1)).deleteSubscription( - eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.HARD_DELETE)); + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.HARD_DELETE), any()); } @Test @@ -297,12 +337,12 @@ void deleteSubscription_NoDeletionType_UsesSoftDelete() throws InterruptedExcept doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) .deleteSubscription( - anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE), any()); sendRequestWithoutPayload(request, null); verify(subscriptionService, times(1)).deleteSubscription( - eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE)); + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.SOFT_DELETE), any()); } @Test @@ -316,12 +356,12 @@ void deleteSubscription_InvalidDeletionType_UsesDefaultDeletionType() throws Int doReturn(PROJECT).when(projectService).getCachedProject(PROJECT.getName()); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) .deleteSubscription( - anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.DEFAULT)); + anyString(), eq(PROJECT), any(), eq(ResourceDeletionType.DEFAULT), any()); sendRequestWithoutPayload(request, null); verify(subscriptionService, times(1)).deleteSubscription( - eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.DEFAULT)); + eq("project1.sub1"), eq(PROJECT), any(), eq(ResourceDeletionType.DEFAULT), any()); } @Test @@ -358,7 +398,10 @@ void updateSubscription_ExceedingRetryLimit_ThrowsBadRequest() throws Interrupte String errorMessage = "Only 3 retries are supported."; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -374,7 +417,10 @@ void updateSubscription_MismatchedProjectName_ThrowsBadRequest() throws Interrup String errorMessage = "Project name mismatch between URL and request body."; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -393,7 +439,10 @@ void updateSubscription_NonSuperUserWithIgnoreConstraints_ThrowsUnauthorizedExce doThrow(new HttpException(HTTP_UNAUTHORIZED, errorMessage)).when(subscriptionService) .updateSubscription(any(), anyInt(), anyString(), anyBoolean(), any(), any(), any(), any()); - ErrorResponse response = sendRequestWithEntity(request, resource, 401, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 401, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -406,7 +455,10 @@ void updateSubscription_UnsupportedProperties_ThrowsBadRequest() throws Interrup String errorMessage = "Unsupported properties: unsupportedProperty"; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -419,7 +471,10 @@ void updateSubscription_InvalidPropertyValues_ThrowsBadRequest() throws Interrup String errorMessage = "Invalid value for property: unsideline.api.message_count"; - ErrorResponse response = sendRequestWithEntity(request, resource, 400, errorMessage, ErrorResponse.class); + ErrorResponse response = sendRequestWithEntity( + request, resource, 400, errorMessage, + ErrorResponse.class + ); assertEquals(errorMessage, response.reason()); } @@ -427,16 +482,16 @@ void updateSubscription_InvalidPropertyValues_ThrowsBadRequest() throws Interrup @Test void restoreSubscription_ValidRequest_ReturnsRestoredSubscription() throws InterruptedException { HttpRequest request = createRequest( - HttpMethod.POST, + HttpMethod.PATCH, buildSubscriptionUrl("sub1", PROJECT) + "/restore" ); doReturn(CompletableFuture.completedFuture(null)).when(subscriptionService) - .restoreSubscription(any(), any()); + .restoreSubscription(any(), any(), any()); sendRequestWithoutPayload(request, null); - verify(subscriptionService, times(1)).restoreSubscription(any(), any()); + verify(subscriptionService, times(1)).restoreSubscription(any(), any(), any()); } @Test diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java index 309ddb8a..08d9de40 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/SubscriptionTestBase.java @@ -1,18 +1,7 @@ package com.flipkart.varadhi.web.admin; import com.flipkart.varadhi.config.RestOptions; -import com.flipkart.varadhi.entities.CodeRange; -import com.flipkart.varadhi.entities.ConsumptionPolicy; -import com.flipkart.varadhi.entities.DlqMessage; -import com.flipkart.varadhi.entities.Endpoint; -import com.flipkart.varadhi.entities.Offset; -import com.flipkart.varadhi.entities.Project; -import com.flipkart.varadhi.entities.RetryPolicy; -import com.flipkart.varadhi.entities.SubscriptionShards; -import com.flipkart.varadhi.entities.SubscriptionUnitShard; -import com.flipkart.varadhi.entities.TopicCapacityPolicy; -import com.flipkart.varadhi.entities.VaradhiSubscription; -import com.flipkart.varadhi.entities.VaradhiTopic; +import com.flipkart.varadhi.entities.*; import com.flipkart.varadhi.pulsar.entities.PulsarOffset; import com.flipkart.varadhi.services.ProjectService; import com.flipkart.varadhi.services.SubscriptionService; @@ -67,7 +56,7 @@ public class SubscriptionTestBase extends WebTestBase { protected static final Project PROJECT = Project.of( "project1", "", "team1", "org1"); protected static final TopicResource TOPIC_RESOURCE = TopicResource.unGrouped( - "topic1", "project1", null); + "topic1", "project1", null, LifecycleStatus.ActionCode.SYSTEM_ACTION); @Mock protected SubscriptionService subscriptionService; @@ -120,7 +109,8 @@ private static VaradhiSubscription createSubscription( DEFAULT_RETRY_POLICY, DEFAULT_CONSUMPTION_POLICY, DEFAULT_SHARDS, - DEFAULT_SUBSCRIPTION_PROPERTIES + DEFAULT_SUBSCRIPTION_PROPERTIES, + LifecycleStatus.ActionCode.SYSTEM_ACTION ); } @@ -143,7 +133,8 @@ protected SubscriptionResource createSubscriptionResource( DEFAULT_ENDPOINT, retryPolicy, DEFAULT_CONSUMPTION_POLICY, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); } diff --git a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java index 550e2e70..891954aa 100644 --- a/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java +++ b/server/src/test/java/com/flipkart/varadhi/web/admin/TopicHandlersTest.java @@ -1,6 +1,7 @@ package com.flipkart.varadhi.web.admin; import com.flipkart.varadhi.Constants; +import com.flipkart.varadhi.entities.LifecycleStatus; import com.flipkart.varadhi.entities.Project; import com.flipkart.varadhi.entities.ResourceDeletionType; import com.flipkart.varadhi.entities.VaradhiTopic; @@ -9,6 +10,7 @@ import com.flipkart.varadhi.utils.JsonMapper; import com.flipkart.varadhi.utils.VaradhiTopicFactory; import com.flipkart.varadhi.web.ErrorResponse; +import com.flipkart.varadhi.web.Extensions; import com.flipkart.varadhi.web.RequestTelemetryConfigurator; import com.flipkart.varadhi.web.SpanProvider; import com.flipkart.varadhi.web.WebTestBase; @@ -21,6 +23,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.HttpResponse; +import lombok.experimental.ExtensionMethod; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +44,7 @@ import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; +@ExtensionMethod({Extensions.RequestBodyExtension.class, Extensions.RoutingContextExtension.class}) class TopicHandlersTest extends WebTestBase { private static final String TOPIC_NAME = "topic1"; @@ -104,7 +108,7 @@ private void setupRoutes() { router.delete("/projects/:project/topics/:topic") .handler(wrapBlocking(topicHandlers::delete)); - router.post("/projects/:project/topics/:topic/restore") + router.patch("/projects/:project/topics/:topic/restore") .handler(wrapBlocking(topicHandlers::restore)); setupFailureHandlers(); @@ -186,7 +190,7 @@ void getTopic_WithValidRequest_ShouldReturnTopicSuccessfully() throws Interrupte void listTopics_WithTopicsAvailable_ShouldReturnAllTopics() throws InterruptedException { List topics = List.of(String.join(".", project.getName(), TOPIC_NAME)); - doReturn(topics).when(varadhiTopicService).getVaradhiTopics(project.getName()); + doReturn(topics).when(varadhiTopicService).getVaradhiTopics(project.getName(), false); List retrievedTopics = sendRequestWithoutPayload( createRequest(HttpMethod.GET, getTopicsUrl(project)), @@ -196,9 +200,23 @@ void listTopics_WithTopicsAvailable_ShouldReturnAllTopics() throws InterruptedEx assertEquals(topics.size(), retrievedTopics.size()); } + @Test + void listTopics_WithIncludeInactive_ShouldReturnAllTopicsIncludingInactive() throws InterruptedException { + List topics = List.of(String.join(".", project.getName(), TOPIC_NAME)); + + doReturn(topics).when(varadhiTopicService).getVaradhiTopics(project.getName(), true); + + List retrievedTopics = sendRequestWithoutPayload( + createRequest(HttpMethod.GET, getTopicsUrl(project) + "?includeInactive=true"), + List.class + ); + + assertEquals(topics.size(), retrievedTopics.size()); + } + @Test void listTopics_WithNoTopicsAvailable_ShouldReturnEmptyList() throws InterruptedException { - doReturn(Collections.emptyList()).when(varadhiTopicService).getVaradhiTopics(project.getName()); + doReturn(Collections.emptyList()).when(varadhiTopicService).getVaradhiTopics(project.getName(), false); List retrievedTopics = sendRequestWithoutPayload( createRequest(HttpMethod.GET, getTopicsUrl(project)), @@ -230,12 +248,12 @@ void deleteTopic_WithInvalidDeletionType_ShouldDefaultToDefault() throws Interru @Test void restoreTopic_WithValidRequest_ShouldRestoreTopicSuccessfully() throws InterruptedException { - HttpRequest request = createRequest(HttpMethod.POST, getTopicUrl(project) + "/restore"); - doNothing().when(varadhiTopicService).restore(any()); + HttpRequest request = createRequest(HttpMethod.PATCH, getTopicUrl(project) + "/restore"); + doNothing().when(varadhiTopicService).restore(any(), any()); sendRequestWithoutPayload(request, null); - verify(varadhiTopicService).restore(any()); + verify(varadhiTopicService).restore(any(), any()); } private void verifyDeleteRequest(String deletionType, ResourceDeletionType expectedDeletionType) @@ -246,11 +264,11 @@ private void verifyDeleteRequest(String deletionType, ResourceDeletionType expec } HttpRequest request = createRequest(HttpMethod.DELETE, url); - doNothing().when(varadhiTopicService).delete(any(), eq(expectedDeletionType)); + doNothing().when(varadhiTopicService).delete(any(), eq(expectedDeletionType), any()); sendRequestWithoutPayload(request, null); - verify(varadhiTopicService).delete(any(), eq(expectedDeletionType)); + verify(varadhiTopicService).delete(any(), eq(expectedDeletionType), any()); } private void assertErrorResponse(HttpResponse response, String expectedReason) { @@ -259,7 +277,10 @@ private void assertErrorResponse(HttpResponse response, String expectedR } private TopicResource getTopicResource(Project project) { - return TopicResource.grouped(TOPIC_NAME, project.getName(), Constants.DefaultTopicCapacity); + return TopicResource.grouped( + TOPIC_NAME, project.getName(), Constants.DEFAULT_TOPIC_CAPACITY, + LifecycleStatus.ActionCode.USER_ACTION + ); } private String getTopicsUrl(Project project) { diff --git a/server/src/test/java/com/flipkart/varadhi/web/entities/SubscriptionResourceTest.java b/server/src/test/java/com/flipkart/varadhi/web/entities/SubscriptionResourceTest.java new file mode 100644 index 00000000..7db2bcd2 --- /dev/null +++ b/server/src/test/java/com/flipkart/varadhi/web/entities/SubscriptionResourceTest.java @@ -0,0 +1,74 @@ +package com.flipkart.varadhi.web.entities; + +import com.flipkart.varadhi.Constants; +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.VaradhiSubscription; +import com.flipkart.varadhi.entities.VaradhiTopic; +import com.flipkart.varadhi.web.admin.SubscriptionTestBase; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class SubscriptionResourceTest extends SubscriptionTestBase { + + @Test + void of_CreatesSubscriptionResource() { + SubscriptionResource subscriptionResource = createSubscriptionResource( + "subscriptionName", PROJECT, TOPIC_RESOURCE + ); + assertAll( + () -> assertEquals("subscriptionName", subscriptionResource.getName()), + () -> assertEquals(PROJECT.getName(), subscriptionResource.getProject()), + () -> assertEquals(TOPIC_RESOURCE.getName(), subscriptionResource.getTopic()), + () -> assertEquals(TOPIC_RESOURCE.getProject(), subscriptionResource.getTopicProject()), + () -> assertEquals("Description", subscriptionResource.getDescription()), + () -> assertFalse(subscriptionResource.isGrouped()), + () -> assertNotNull(subscriptionResource.getEndpoint()), + () -> assertNotNull(subscriptionResource.getRetryPolicy()), + () -> assertNotNull(subscriptionResource.getConsumptionPolicy()), + () -> assertNotNull(subscriptionResource.getProperties()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, subscriptionResource.getActionCode()) + ); + } + + @Test + void from_CreatesSubscriptionResourceFromVaradhiSubscription() { + VaradhiSubscription varadhiSubscription = createUngroupedSubscription( + "subscriptionName", PROJECT, VaradhiTopic.of( + "project1.topic1", "topic1", false, Constants.DEFAULT_TOPIC_CAPACITY, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ) + ); + SubscriptionResource subscriptionResource = SubscriptionResource.from(varadhiSubscription); + assertAll( + () -> assertEquals("subscriptionName", subscriptionResource.getName()), + () -> assertEquals(PROJECT.getName(), subscriptionResource.getProject()), + () -> assertEquals("topic1", subscriptionResource.getTopic()), + () -> assertEquals("project1", subscriptionResource.getTopicProject()), + () -> assertEquals(varadhiSubscription.getDescription(), subscriptionResource.getDescription()), + () -> assertFalse(subscriptionResource.isGrouped()), + () -> assertNotNull(subscriptionResource.getEndpoint()), + () -> assertNotNull(subscriptionResource.getRetryPolicy()), + () -> assertNotNull(subscriptionResource.getConsumptionPolicy()), + () -> assertNotNull(subscriptionResource.getProperties()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, subscriptionResource.getActionCode()) + ); + } + + @Test + void buildInternalName_ReturnsCorrectInternalName() { + String internalName = SubscriptionResource.buildInternalName(PROJECT.getName(), "subscriptionName"); + assertEquals("project1.subscriptionName", internalName); + } + + @Test + void getSubscriptionInternalName_ReturnsCorrectInternalName() { + SubscriptionResource subscriptionResource = createSubscriptionResource( + "subscriptionName", PROJECT, TOPIC_RESOURCE + ); + assertEquals("project1.subscriptionName", subscriptionResource.getSubscriptionInternalName()); + } +} diff --git a/server/src/test/java/com/flipkart/varadhi/web/entities/TopicResourceTest.java b/server/src/test/java/com/flipkart/varadhi/web/entities/TopicResourceTest.java new file mode 100644 index 00000000..05b647fd --- /dev/null +++ b/server/src/test/java/com/flipkart/varadhi/web/entities/TopicResourceTest.java @@ -0,0 +1,78 @@ +package com.flipkart.varadhi.web.entities; + +import com.flipkart.varadhi.entities.LifecycleStatus; +import com.flipkart.varadhi.entities.TopicCapacityPolicy; +import com.flipkart.varadhi.entities.VaradhiTopic; +import org.junit.jupiter.api.Test; + +import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TopicResourceTest { + + @Test + void grouped_CreatesGroupedTopicResource() { + TopicResource topicResource = TopicResource.grouped( + "topicName", "projectName", new TopicCapacityPolicy(), + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + assertAll( + () -> assertEquals("topicName", topicResource.getName()), + () -> assertEquals("projectName", topicResource.getProject()), + () -> assertTrue(topicResource.isGrouped()), + () -> assertNotNull(topicResource.getCapacity()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, topicResource.getActionCode()) + ); + } + + @Test + void unGrouped_CreatesUngroupedTopicResource() { + TopicResource topicResource = TopicResource.unGrouped( + "topicName", "projectName", new TopicCapacityPolicy(), + LifecycleStatus.ActionCode.USER_ACTION + ); + assertAll( + () -> assertEquals("topicName", topicResource.getName()), + () -> assertEquals("projectName", topicResource.getProject()), + () -> assertFalse(topicResource.isGrouped()), + () -> assertNotNull(topicResource.getCapacity()), + () -> assertEquals(LifecycleStatus.ActionCode.USER_ACTION, topicResource.getActionCode()) + ); + } + + @Test + void from_CreatesTopicResourceFromVaradhiTopic() { + VaradhiTopic varadhiTopic = VaradhiTopic.of( + "projectName", "topicName", true, new TopicCapacityPolicy(), + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + TopicResource topicResource = TopicResource.from(varadhiTopic); + assertAll( + () -> assertEquals("topicName", topicResource.getName()), + () -> assertEquals("projectName", topicResource.getProject()), + () -> assertTrue(topicResource.isGrouped()), + () -> assertNotNull(topicResource.getCapacity()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, topicResource.getActionCode()) + ); + } + + @Test + void toVaradhiTopic_ConvertsToVaradhiTopic() { + TopicResource topicResource = TopicResource.grouped( + "topicName", "projectName", new TopicCapacityPolicy(), + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + VaradhiTopic varadhiTopic = topicResource.toVaradhiTopic(); + assertAll( + () -> assertEquals("topicName", varadhiTopic.getName().split(NAME_SEPARATOR_REGEX)[1]), + () -> assertEquals("projectName", varadhiTopic.getName().split(NAME_SEPARATOR_REGEX)[0]), + () -> assertTrue(varadhiTopic.isGrouped()), + () -> assertNotNull(varadhiTopic.getCapacity()), + () -> assertEquals(LifecycleStatus.ActionCode.SYSTEM_ACTION, varadhiTopic.getStatus().getActionCode()) + ); + } +} diff --git a/server/src/testE2E/java/com/flipkart/varadhi/AuthZProviderTests.java b/server/src/testE2E/java/com/flipkart/varadhi/AuthZProviderTests.java index 43241a8c..1620e207 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/AuthZProviderTests.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/AuthZProviderTests.java @@ -1,6 +1,7 @@ package com.flipkart.varadhi; import com.flipkart.varadhi.auth.DefaultAuthorizationProvider; +import com.flipkart.varadhi.entities.LifecycleStatus; import com.flipkart.varadhi.spi.authz.AuthorizationOptions; import com.flipkart.varadhi.entities.Org; import com.flipkart.varadhi.entities.Project; @@ -52,7 +53,10 @@ public static void setup(VertxTestContext testContext) throws IOException, Inter fkTeamRocket = Team.of("team_rocket", oPublic.getName()); fkTeamAsh = Team.of("team_ash", oPublic.getName()); fkDefault = Project.of("default", "", fkTeamRocket.getName(), oPublic.getName()); - fkTopic001 = TopicResource.unGrouped("topic001",fkDefault.getName(), null); + fkTopic001 = TopicResource.unGrouped( + "topic001", fkDefault.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); makeCreateRequest(getOrgsUri(), oPublic, 200); makeCreateRequest(getTeamsUri(oPublic.getName()), fkTeamRocket, 200); makeCreateRequest(getTeamsUri(oPublic.getName()), fkTeamAsh, 200); @@ -117,7 +121,7 @@ private static void setupProvider(Checkpoint checkpoint) throws IOException { private static ConcurrentHashMap policyCleanupHandlers = new ConcurrentHashMap<>(); private static String getIamPolicyUri(String resourceUri) { - return String.join("/", VaradhiBaseUri, "v1", resourceUri, "policy"); + return String.join("/", VARADHI_BASE_URI, "v1", resourceUri, "policy"); } private static void bootstrapRoleBindings() { diff --git a/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java b/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java index 46353614..3b2084ef 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/E2EBase.java @@ -1,10 +1,8 @@ package com.flipkart.varadhi; import com.fasterxml.jackson.databind.ObjectMapper; -import com.flipkart.varadhi.entities.Org; -import com.flipkart.varadhi.entities.Project; +import com.flipkart.varadhi.entities.*; import com.flipkart.varadhi.web.entities.SubscriptionResource; -import com.flipkart.varadhi.entities.Team; import com.flipkart.varadhi.utils.JsonMapper; import com.flipkart.varadhi.web.ErrorResponse; import jakarta.ws.rs.client.Client; @@ -21,18 +19,19 @@ import java.util.List; +import static com.flipkart.varadhi.Constants.QueryParams.QUERY_PARAM_DELETION_TYPE; import static com.flipkart.varadhi.Constants.USER_ID_HEADER; import static com.flipkart.varadhi.entities.VersionedEntity.NAME_SEPARATOR_REGEX; public class E2EBase { - protected static final String VaradhiBaseUri = "http://localhost:18488"; - private static final int ConnectTimeoutMs = 10 * 1000; - private static final int ReadTimeoutMs = 60 * 1000; + protected static final String VARADHI_BASE_URI = "http://localhost:18488"; + private static final int CONNECT_TIMEOUT_MS = 10 * 1000; + private static final int READ_TIMEOUT_MS = 60 * 1000; public static final String SUPER_USER = "thanos"; static String getOrgsUri() { - return String.format("%s/v1/orgs", VaradhiBaseUri); + return String.format("%s/v1/orgs", VARADHI_BASE_URI); } static String getOrgUri(Org org) { @@ -52,14 +51,13 @@ static String getProjectListUri(String orgName, String teamName) { } static String getProjectCreateUri() { - return String.join("/", VaradhiBaseUri, "v1", "projects"); + return String.join("/", VARADHI_BASE_URI, "v1", "projects"); } static String getProjectUri(Project project) { return String.join("/", getProjectCreateUri(), project.getName()); } - static String getTopicsUri(Project project) { return String.join("/", getProjectUri(project), "topics"); } @@ -134,7 +132,7 @@ static void cleanupProject(Project project) { } static void cleanupTopic(String topicName, Project project) { - makeDeleteRequest(getTopicsUri(project, topicName), "HARD_DELETE", 200); + makeDeleteRequest(getTopicsUri(project, topicName), ResourceDeletionType.HARD_DELETE.toString(), 200); } // this method traverses the resource hierarchy and clean-ups all subscriptions on the matching topics @@ -153,7 +151,10 @@ static void cleanupSubscriptionsOnTopics(List topicNames, String project SubscriptionResource res = makeGetRequest(getSubscriptionsUri(project, sub), SubscriptionResource.class, 200); if (topicNames.contains(res.getTopic()) && projectName.equals(res.getTopicProject())) { - makeDeleteRequest(getSubscriptionsUri(project, sub), "HARD_DELETE", 200); + makeDeleteRequest( + getSubscriptionsUri(project, sub), ResourceDeletionType.HARD_DELETE.toString(), + 200 + ); } }); }); @@ -164,14 +165,16 @@ static void cleanupSubscriptionsOnTopics(List topicNames, String project static void cleanupSubscriptionsOnProject(Project project) { getSubscriptions(makeListRequest(getSubscriptionsUri(project), 200)).forEach( s -> makeDeleteRequest( - getSubscriptionsUri(project, s.split(NAME_SEPARATOR_REGEX)[1]), "HARD_DELETE", 200)); + getSubscriptionsUri(project, s.split(NAME_SEPARATOR_REGEX)[1]), + ResourceDeletionType.HARD_DELETE.toString(), 200 + )); } static Client getClient() { ClientConfig clientConfig = new ClientConfig().register(new ObjectMapperContextResolver()); Client client = ClientBuilder.newClient(clientConfig); - client.property(ClientProperties.CONNECT_TIMEOUT, ConnectTimeoutMs); - client.property(ClientProperties.READ_TIMEOUT, ReadTimeoutMs); + client.property(ClientProperties.CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS); + client.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS); return client; } @@ -302,7 +305,7 @@ static Response makeHttpDeleteRequest(String targetUrl) { static Response makeHttpDeleteRequest(String targetUrl, String deletionType) { return getClient() .target(targetUrl) - .queryParam("deletionType", deletionType) + .queryParam(QUERY_PARAM_DELETION_TYPE, deletionType) .request(MediaType.APPLICATION_JSON_TYPE) .header(USER_ID_HEADER, SUPER_USER) .delete(); diff --git a/server/src/testE2E/java/com/flipkart/varadhi/OrgTests.java b/server/src/testE2E/java/com/flipkart/varadhi/OrgTests.java index 3931ae35..b2fc365d 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/OrgTests.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/OrgTests.java @@ -13,7 +13,6 @@ public class OrgTests extends E2EBase { Org org1; Org org2; - @BeforeEach public void setup() { org1 = Org.of("org10"); @@ -44,8 +43,7 @@ public void testOrgCreation() { makeDeleteRequest(getOrgUri(org1), 200); makeGetRequest(getOrgUri(org1), 404, String.format("Org(%s) not found.", org1.getName()), true); List orgList2 = getOrgs(makeListRequest(getOrgsUri(), 200)); - Assertions.assertTrue(!orgList2.contains(org1)); + Assertions.assertFalse(orgList2.contains(org1)); Assertions.assertTrue(orgList2.contains(org2)); } - } diff --git a/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java b/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java index 8db5d4b5..ed359cf0 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/SubscriptionTests.java @@ -35,8 +35,14 @@ public static void setup() { o1 = Org.of("public"); o1t1 = Team.of("team1", o1.getName()); o1t1p1 = Project.of("default", "", o1t1.getName(), o1t1.getOrg()); - p1t1 = TopicResource.unGrouped("topic1", o1t1p1.getName(), null); - p1t2 = TopicResource.grouped("topic2", o1t1p1.getName(), null); + p1t1 = TopicResource.unGrouped( + "topic1", o1t1p1.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + p1t2 = TopicResource.grouped( + "topic2", o1t1p1.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); makeCreateRequest(getOrgsUri(), o1, 200); makeCreateRequest(getTeamsUri(o1t1.getOrg()), o1t1, 200); makeCreateRequest(getProjectCreateUri(), o1t1p1, 200); @@ -70,7 +76,8 @@ void createSubscription() { endpoint, retryPolicy, consumptionPolicy, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); SubscriptionResource r = makeCreateRequest(getSubscriptionsUri(o1t1p1), sub, 200); assertSubscriptionEquals(sub, r); @@ -97,7 +104,8 @@ void updateSubscription() { endpoint, retryPolicy, consumptionPolicy, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); makeCreateRequest(getSubscriptionsUri(o1t1p1), sub, 200); SubscriptionResource created = @@ -112,7 +120,8 @@ void updateSubscription() { created.getEndpoint(), created.getRetryPolicy(), created.getConsumptionPolicy(), - created.getProperties() + created.getProperties(), + created.getActionCode() ); //create subscription executes update internally. update.setVersion(1); @@ -137,7 +146,8 @@ void createSubscriptionWithValidationFailure() { endpoint, retryPolicy, consumptionPolicy, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); makeCreateRequest( getSubscriptionsUri(o1t1p1), shortName, 400, "Invalid Subscription name. Check naming constraints.", @@ -154,7 +164,8 @@ void createSubscriptionWithValidationFailure() { endpoint, retryPolicy, consumptionPolicy, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); makeCreateRequest( getSubscriptionsUri(Project.of("some_proj", "desc", "someteam", "org")), projectNotExist, 404, @@ -171,7 +182,8 @@ void createSubscriptionWithValidationFailure() { endpoint, retryPolicy, consumptionPolicy, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); makeCreateRequest( getSubscriptionsUri(o1t1p1), topicNotExist, 404, @@ -188,7 +200,8 @@ void createSubscriptionWithValidationFailure() { endpoint, retryPolicy, consumptionPolicy, - new HashMap<>() + new HashMap<>(), + LifecycleStatus.ActionCode.SYSTEM_ACTION ); makeCreateRequest( getSubscriptionsUri(o1t1p1), groupedOnUnGroupTopic, 400, diff --git a/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java b/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java index e76ca7c0..37c8299d 100644 --- a/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java +++ b/server/src/testE2E/java/com/flipkart/varadhi/TopicTests.java @@ -1,8 +1,6 @@ package com.flipkart.varadhi; -import com.flipkart.varadhi.entities.Org; -import com.flipkart.varadhi.entities.Project; -import com.flipkart.varadhi.entities.Team; +import com.flipkart.varadhi.entities.*; import com.flipkart.varadhi.web.entities.TopicResource; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -46,11 +44,13 @@ public static void tearDown() { cleanupOrgs(List.of(org1, org2)); } - @Test public void createTopic() { String topicName = "test-topic-1"; - TopicResource topic = TopicResource.unGrouped(topicName, o1t1Project1.getName(), null); + TopicResource topic = TopicResource.unGrouped( + topicName, o1t1Project1.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); TopicResource r = makeCreateRequest(getTopicsUri(o1t1Project1), topic, 200); Assertions.assertEquals(topic.getVersion(), r.getVersion()); Assertions.assertEquals(topic.getName(), r.getName()); @@ -62,16 +62,21 @@ public void createTopic() { "Topic '%s' already exists.", String.join(NAME_SEPARATOR, topic.getProject(), topic.getName()) ); + makeCreateRequest(getTopicsUri(o1t1Project1), topic, 409, errorDuplicateTopic, true); makeGetRequest(getTopicsUri(o1t1Project1, topicName), TopicResource.class, 200); - makeDeleteRequest(getTopicsUri(o1t1Project1, topicName), "HARD_DELETE", 200); + makeDeleteRequest(getTopicsUri(o1t1Project1, topicName), ResourceDeletionType.HARD_DELETE.toString(), 200); } @Test public void createTopicWithValidationFailure() { String topicName = "ab"; - TopicResource topic = TopicResource.unGrouped(topicName, o1t1Project1.getName(), null); + TopicResource topic = TopicResource.unGrouped( + topicName, o1t1Project1.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); String errorValidationTopic = "Invalid Topic name. Check naming constraints."; + makeCreateRequest(getTopicsUri(o1t1Project1), topic, 400, errorValidationTopic, true); List topics = getTopics(makeListRequest(getTopicsUri(o1t1Project1), 200)); @@ -81,8 +86,14 @@ public void createTopicWithValidationFailure() { @Test public void createTopicsWithMultiTenancy() { String topicName = "test-topic-2"; - TopicResource topic1 = TopicResource.unGrouped(topicName, o1t1Project1.getName(), null); - TopicResource topic2 = TopicResource.unGrouped(topicName, o2t1Project1.getName(), null); + TopicResource topic1 = TopicResource.unGrouped( + topicName, o1t1Project1.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); + TopicResource topic2 = TopicResource.unGrouped( + topicName, o2t1Project1.getName(), null, + LifecycleStatus.ActionCode.SYSTEM_ACTION + ); TopicResource r1 = makeCreateRequest(getTopicsUri(o1t1Project1), topic1, 200); TopicResource r2 = makeCreateRequest(getTopicsUri(o2t1Project1), topic2, 200); @@ -95,7 +106,9 @@ public void createTopicsWithMultiTenancy() { Assertions.assertEquals(topic2.getName(), r2.getName()); Assertions.assertEquals(topic2.getProject(), r2.getProject()); - makeDeleteRequest(getTopicsUri(o1t1Project1, topic1.getName()), "HARD_DELETE", 200); - makeDeleteRequest(getTopicsUri(o2t1Project1, topic2.getName()), "HARD_DELETE", 200); + makeDeleteRequest( + getTopicsUri(o1t1Project1, topic1.getName()), ResourceDeletionType.HARD_DELETE.toString(), 200); + makeDeleteRequest( + getTopicsUri(o2t1Project1, topic2.getName()), ResourceDeletionType.HARD_DELETE.toString(), 200); } }