This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new d36729f2 fix: validate instance resource action requests (#634)
d36729f2 is described below
commit d36729f23afa6bf871c9a056da74fe59075bd195
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 07:08:53 2026 -0700
fix: validate instance resource action requests (#634)
* [Studio] Validate instance resource action requests
* fix: validate instance delete requests
---
.../studio/instance/InstanceController.java | 6 +-
...pdateDTO.java => InstanceDeleteRequestDTO.java} | 16 ++--
.../studio/instance/dlq/DLQController.java | 11 +--
.../DLQResendRequestDTO.java} | 19 +++--
.../rocketmq/studio/instance/dlq/DLQService.java | 25 ++++++
.../instance/group/ConsumerGroupController.java | 8 +-
.../ResetConsumerOffsetDTO.java} | 24 ++++--
...eTopicTTLUpdateDTO.java => DeleteTopicDTO.java} | 14 +++-
.../studio/instance/topic/LiteTopicController.java | 3 +-
.../instance/topic/LiteTopicTTLUpdateDTO.java | 4 +
.../studio/instance/topic/TopicController.java | 6 +-
.../studio/instance/InstanceControllerTest.java | 25 ++++++
.../studio/instance/dlq/DLQControllerTest.java | 38 +++++++++
.../studio/instance/dlq/DLQServiceTest.java | 34 ++++++++
.../group/ConsumerGroupControllerTest.java | 98 ++++++++++++++++++++++
.../instance/topic/LiteTopicControllerTest.java | 32 +++++++
.../studio/instance/topic/TopicControllerTest.java | 38 +++++++++
17 files changed, 363 insertions(+), 38 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
index 29dcb83d..e5db352a 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceController.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.instance;
import org.apache.rocketmq.studio.common.domain.Result;
import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -28,7 +29,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
-import java.util.Map;
@RestController
@RequestMapping("/api/instances")
@@ -55,8 +55,8 @@ public class InstanceController {
}
@PostMapping("/delete")
- public Result<Void> deleteInstance(@RequestBody Map<String, String> body) {
- instanceService.deleteInstance(body.get("id"));
+ public Result<Void> deleteInstance(@Valid @RequestBody
InstanceDeleteRequestDTO request) {
+ instanceService.deleteInstance(request.getId());
return Result.ok();
}
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceDeleteRequestDTO.java
similarity index 71%
copy from
server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/instance/InstanceDeleteRequestDTO.java
index a511c336..418bf53f 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceDeleteRequestDTO.java
@@ -14,13 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.instance;
-package org.apache.rocketmq.studio.instance.topic;
-
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class LiteTopicTTLUpdateDTO {
- private String topicPattern;
- private Long newTTL;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InstanceDeleteRequestDTO {
+ @NotBlank(message = "id is required")
+ private String id;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQController.java
index 8d59c608..4015e1de 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQController.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.instance.dlq;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -26,7 +27,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
-import java.util.Map;
@RestController
@RequestMapping("/api/dlq")
@@ -41,12 +41,9 @@ public class DLQController {
}
@PostMapping("/resend")
- public Result<Void> resendMessages(@RequestBody Map<String, Object>
request) {
- String groupName = (String) request.get("groupName");
- Long startTime = request.get("startTime") != null ? ((Number)
request.get("startTime")).longValue() : null;
- Long endTime = request.get("endTime") != null ? ((Number)
request.get("endTime")).longValue() : null;
- String targetTopic = (String) request.get("targetTopic");
- dlqService.resendMessages(groupName, startTime, endTime, targetTopic);
+ public Result<Void> resendMessages(@Valid @RequestBody DLQResendRequestDTO
request) {
+ dlqService.resendMessages(
+ request.getGroupName(), request.getStartTime(),
request.getEndTime(), request.getTargetTopic());
return Result.ok();
}
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQResendRequestDTO.java
similarity index 65%
copy from
server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQResendRequestDTO.java
index a511c336..b91e1f48 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQResendRequestDTO.java
@@ -14,13 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.instance.dlq;
-package org.apache.rocketmq.studio.instance.topic;
-
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class LiteTopicTTLUpdateDTO {
- private String topicPattern;
- private Long newTTL;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DLQResendRequestDTO {
+ @NotBlank(message = "groupName is required")
+ private String groupName;
+ private Long startTime;
+ private Long endTime;
+ private String targetTopic;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
index 42f7ce77..a829c07f 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQService.java
@@ -16,9 +16,12 @@
*/
package org.apache.rocketmq.studio.instance.dlq;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
import java.util.List;
@@ -35,7 +38,29 @@ public class DLQService {
}
public void resendMessages(String groupName, Long startTime, Long endTime,
String targetTopic) {
+ validateResendRequest(groupName, startTime, endTime);
log.info("Resending DLQ messages: group={}, targetTopic={}",
groupName, targetTopic);
dlqProvider.resendMessages(groupName, startTime, endTime, targetTopic);
}
+
+ private void validateResendRequest(String groupName, Long startTime, Long
endTime) {
+ if (!StringUtils.hasText(groupName)) {
+ throw new BusinessException(HttpStatus.BAD_REQUEST.value(),
"groupName is required");
+ }
+ if ((startTime == null) != (endTime == null)) {
+ throw new BusinessException(
+ HttpStatus.BAD_REQUEST.value(), "startTime and endTime
must be provided together");
+ }
+ if (startTime == null) {
+ return;
+ }
+ if (startTime <= 0 || endTime <= 0) {
+ throw new BusinessException(
+ HttpStatus.BAD_REQUEST.value(), "startTime and endTime
must be positive");
+ }
+ if (endTime < startTime) {
+ throw new BusinessException(
+ HttpStatus.BAD_REQUEST.value(), "endTime must not be
earlier than startTime");
+ }
+ }
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
index 9ef78e34..10644eb3 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupController.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.instance.group;
import org.apache.rocketmq.studio.instance.topic.MetadataService;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -80,11 +81,8 @@ public class ConsumerGroupController {
}
@PostMapping("/reset-offset")
- public Result<Void> resetOffset(@RequestBody Map<String, Object> request) {
- String name = (String) request.get("name");
- long timestamp = ((Number) request.get("timestamp")).longValue();
- String topic = (String) request.get("topic");
- metadataService.resetOffset(name, timestamp, topic);
+ public Result<Void> resetOffset(@Valid @RequestBody ResetConsumerOffsetDTO
request) {
+ metadataService.resetOffset(request.getName(), request.getTimestamp(),
request.getTopic());
return Result.ok();
}
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
similarity index 58%
copy from
server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
index a511c336..b7f5265c 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/group/ResetConsumerOffsetDTO.java
@@ -14,13 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.instance.group;
-package org.apache.rocketmq.studio.instance.topic;
-
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class LiteTopicTTLUpdateDTO {
- private String topicPattern;
- private Long newTTL;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ResetConsumerOffsetDTO {
+ @NotBlank(message = "name is required")
+ private String name;
+
+ @NotNull(message = "timestamp is required")
+ @Positive(message = "timestamp must be positive")
+ private Long timestamp;
+
+ private String topic;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
similarity index 75%
copy from
server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
index a511c336..50f256c7 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/DeleteTopicDTO.java
@@ -14,13 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.apache.rocketmq.studio.instance.topic;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class LiteTopicTTLUpdateDTO {
- private String topicPattern;
- private Long newTTL;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeleteTopicDTO {
+ @NotBlank(message = "name is required")
+ private String name;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicController.java
index d9ed7f5c..ff98e498 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicController.java
@@ -18,6 +18,7 @@
package org.apache.rocketmq.studio.instance.topic;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -49,7 +50,7 @@ public class LiteTopicController {
}
@PostMapping("/extendTTL")
- public Result<Void> extendTTL(@RequestBody LiteTopicTTLUpdateDTO request) {
+ public Result<Void> extendTTL(@Valid @RequestBody LiteTopicTTLUpdateDTO
request) {
liteTopicService.extendTTL(request.getTopicPattern(),
request.getNewTTL());
return Result.ok();
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
index a511c336..206948f4 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/LiteTopicTTLUpdateDTO.java
@@ -17,10 +17,14 @@
package org.apache.rocketmq.studio.instance.topic;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;
import lombok.Data;
@Data
public class LiteTopicTTLUpdateDTO {
+ @NotBlank(message = "topicPattern is required")
private String topicPattern;
+ @Positive(message = "newTTL must be positive")
private Long newTTL;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
index 3948d1e1..07c059d2 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/TopicController.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.instance.topic;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -27,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
-import java.util.Map;
@RestController
@RequestMapping("/api/topics")
@@ -55,8 +55,8 @@ public class TopicController {
}
@PostMapping("/delete")
- public Result<Void> deleteTopic(@RequestBody Map<String, String> request) {
- metadataService.deleteTopic(request.get("name"));
+ public Result<Void> deleteTopic(@Valid @RequestBody DeleteTopicDTO
request) {
+ metadataService.deleteTopic(request.getName());
return Result.ok();
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
index 3c868d84..37f14fdf 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceControllerTest.java
@@ -36,6 +36,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -175,6 +176,30 @@ class InstanceControllerTest {
verify(instanceService).deleteInstance("inst-1");
}
+ @Test
+ void deleteInstanceShouldRejectBlankId() throws Exception {
+ mockMvc.perform(post("/api/instances/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
" "))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(instanceService);
+ }
+
+ @Test
+ void deleteInstanceShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/instances/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of())))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(instanceService);
+ }
+
private InstanceVO buildInstance(String id, String name, InstanceType
type, String endpoint) {
InstanceVO instance = InstanceVO.builder()
.name(name)
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
index 7dad065b..6c23be58 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQControllerTest.java
@@ -33,6 +33,7 @@ import java.util.Map;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -122,4 +123,41 @@ class DLQControllerTest {
verify(dlqService).resendMessages(
eq("test-group"), isNull(), isNull(), eq("target-topic"));
}
+
+ @Test
+ void resendMessagesShouldRejectMissingGroupName() throws Exception {
+ Map<String, Object> body = Map.of(
+ "startTime", 1000,
+ "endTime", 2000,
+ "targetTopic", "target-topic"
+ );
+
+ mockMvc.perform(post("/api/dlq/resend")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("groupName is
required"));
+
+ verifyNoInteractions(dlqService);
+ }
+
+ @Test
+ void resendMessagesShouldRejectInvalidTimeType() throws Exception {
+ Map<String, Object> body = Map.of(
+ "groupName", "test-group",
+ "startTime", "invalid",
+ "endTime", 2000,
+ "targetTopic", "target-topic"
+ );
+
+ mockMvc.perform(post("/api/dlq/resend")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("Invalid request
body"));
+
+ verifyNoInteractions(dlqService);
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
index 9c120c8c..9dfeff4c 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/dlq/DLQServiceTest.java
@@ -26,7 +26,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -87,4 +89,36 @@ class DLQServiceTest {
verify(dlqProvider).resendMessages("group-1", null, null,
"target-topic");
}
+
+ @Test
+ void resendMessagesShouldRejectBlankGroupName() {
+ assertThatThrownBy(() -> dlqService.resendMessages(" ", 1000L, 2000L,
"target-topic"))
+ .hasMessage("groupName is required");
+
+ verifyNoInteractions(dlqProvider);
+ }
+
+ @Test
+ void resendMessagesShouldRejectPartialTimeRange() {
+ assertThatThrownBy(() -> dlqService.resendMessages("group-1", 1000L,
null, "target-topic"))
+ .hasMessage("startTime and endTime must be provided together");
+
+ verifyNoInteractions(dlqProvider);
+ }
+
+ @Test
+ void resendMessagesShouldRejectNonPositiveTimeRange() {
+ assertThatThrownBy(() -> dlqService.resendMessages("group-1", 0L,
2000L, "target-topic"))
+ .hasMessage("startTime and endTime must be positive");
+
+ verifyNoInteractions(dlqProvider);
+ }
+
+ @Test
+ void resendMessagesShouldRejectReversedTimeRange() {
+ assertThatThrownBy(() -> dlqService.resendMessages("group-1", 2000L,
1000L, "target-topic"))
+ .hasMessage("endTime must not be earlier than startTime");
+
+ verifyNoInteractions(dlqProvider);
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
index d743daa2..4dbd2d60 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/group/ConsumerGroupControllerTest.java
@@ -17,19 +17,26 @@
package org.apache.rocketmq.studio.instance.group;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.rocketmq.studio.instance.topic.MetadataService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.Map;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -40,6 +47,9 @@ class ConsumerGroupControllerTest {
@Autowired
private MockMvc mockMvc;
+ @Autowired
+ private ObjectMapper objectMapper;
+
@MockBean
private MetadataService metadataService;
@@ -76,4 +86,92 @@ class ConsumerGroupControllerTest {
.andExpect(jsonPath("$.data.threads[0].stackTrace[0]")
.value("org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService.run"));
}
+
+ @Test
+ void resetOffsetShouldPassValidatedRequest() throws Exception {
+ Map<String, Object> body = Map.of(
+ "name", "cg-orders",
+ "topic", "orders",
+ "timestamp", 1784246400000L
+ );
+
+ mockMvc.perform(post("/api/groups/reset-offset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.message").value("success"));
+
+ verify(metadataService).resetOffset(eq("cg-orders"),
eq(1784246400000L), eq("orders"));
+ }
+
+ @Test
+ void resetOffsetShouldRejectMissingName() throws Exception {
+ Map<String, Object> body = Map.of(
+ "topic", "orders",
+ "timestamp", 1784246400000L
+ );
+
+ mockMvc.perform(post("/api/groups/reset-offset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("name is required"));
+
+ verifyNoInteractions(metadataService);
+ }
+
+ @Test
+ void resetOffsetShouldRejectMissingTimestamp() throws Exception {
+ Map<String, Object> body = Map.of(
+ "name", "cg-orders",
+ "topic", "orders"
+ );
+
+ mockMvc.perform(post("/api/groups/reset-offset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("timestamp is
required"));
+
+ verifyNoInteractions(metadataService);
+ }
+
+ @Test
+ void resetOffsetShouldRejectNonPositiveTimestamp() throws Exception {
+ Map<String, Object> body = Map.of(
+ "name", "cg-orders",
+ "topic", "orders",
+ "timestamp", 0L
+ );
+
+ mockMvc.perform(post("/api/groups/reset-offset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("timestamp must be
positive"));
+
+ verifyNoInteractions(metadataService);
+ }
+
+ @Test
+ void resetOffsetShouldRejectInvalidTimestampType() throws Exception {
+ Map<String, Object> body = Map.of(
+ "name", "cg-orders",
+ "topic", "orders",
+ "timestamp", "invalid"
+ );
+
+ mockMvc.perform(post("/api/groups/reset-offset")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(body)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("Invalid request
body"));
+
+ verifyNoInteractions(metadataService);
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
index d2184fcf..8c1609f4 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/LiteTopicControllerTest.java
@@ -30,6 +30,7 @@ import java.util.List;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -124,4 +125,35 @@ class LiteTopicControllerTest {
verify(liteTopicService).extendTTL(eq("chat/{sessionId}"),
eq(7_200_000L));
}
+
+ @Test
+ void extendTTLShouldRejectMissingTopicPattern() throws Exception {
+ LiteTopicTTLUpdateDTO request = new LiteTopicTTLUpdateDTO();
+ request.setNewTTL(7_200_000L);
+
+ mockMvc.perform(post("/api/liteTopic/extendTTL")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("topicPattern is
required"));
+
+ verifyNoInteractions(liteTopicService);
+ }
+
+ @Test
+ void extendTTLShouldRejectNonPositiveTTL() throws Exception {
+ LiteTopicTTLUpdateDTO request = new LiteTopicTTLUpdateDTO();
+ request.setTopicPattern("chat/{sessionId}");
+ request.setNewTTL(0L);
+
+ mockMvc.perform(post("/api/liteTopic/extendTTL")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("newTTL must be
positive"));
+
+ verifyNoInteractions(liteTopicService);
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
index eb8aa7e6..27c7bc16 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/topic/TopicControllerTest.java
@@ -27,11 +27,13 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
+import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -130,4 +132,40 @@ class TopicControllerTest {
.andExpect(jsonPath("$.data.msgId").value("msg-001"))
.andExpect(jsonPath("$.data.offsetMsgId").value("offset-001"));
}
+
+ @Test
+ void deleteTopicShouldReturnSuccess() throws Exception {
+ mockMvc.perform(post("/api/topics/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("name", "test-topic"))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.message").value("success"));
+
+ verify(metadataService).deleteTopic("test-topic");
+ }
+
+ @Test
+ void deleteTopicShouldRejectMissingName() throws Exception {
+ mockMvc.perform(post("/api/topics/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("name is required"));
+
+ verifyNoInteractions(metadataService);
+ }
+
+ @Test
+ void deleteTopicShouldRejectBlankName() throws Exception {
+ mockMvc.perform(post("/api/topics/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("name", " "))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("name is required"));
+
+ verifyNoInteractions(metadataService);
+ }
}