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
commit 95f7661de34e33a1a8f52b8f302b2f4775cfe133 Author: lizhimins <[email protected]> AuthorDate: Tue Jul 7 17:55:00 2026 +0800 feat: implement backend ops and settings modules - Dashboard with statistics (DashboardController) - Alert rule management (AlertRuleController, SystemAlertController) - Audit log recording (AuditController) - AI assistant with SSE streaming (AiController) - System settings management (SettingsController) - Data source configuration and testing - In-memory repository implementations - Unit tests for all services and controllers --- .../com/rocketmq/studio/ops/ai/AiCommandDTO.java | 37 ++++ .../com/rocketmq/studio/ops/ai/AiController.java | 52 +++++ .../rocketmq/studio/ops/ai/AiExecuteResultVO.java | 31 +++ .../java/com/rocketmq/studio/ops/ai/AiService.java | 63 +++++++ .../java/com/rocketmq/studio/ops/ai/AiToolVO.java | 32 ++++ .../java/com/rocketmq/studio/ops/ai/ChatDTO.java | 33 ++++ .../com/rocketmq/studio/ops/ai/LlmGateway.java | 26 +++ .../com/rocketmq/studio/ops/ai/LlmGatewayStub.java | 62 ++++++ .../com/rocketmq/studio/ops/ai/McpServerImpl.java | 64 +++++++ .../rocketmq/studio/ops/ai/McpServerRegistry.java | 25 +++ .../rocketmq/studio/ops/alert/AlertRepository.java | 34 ++++ .../studio/ops/alert/AlertRuleController.java | 64 +++++++ .../com/rocketmq/studio/ops/alert/AlertRuleVO.java | 42 +++++ .../rocketmq/studio/ops/alert/AlertService.java | 93 +++++++++ .../studio/ops/alert/InMemoryAlertRepository.java | 77 ++++++++ .../studio/ops/alert/SystemAlertController.java | 54 ++++++ .../rocketmq/studio/ops/alert/SystemAlertVO.java | 38 ++++ .../rocketmq/studio/ops/audit/AuditController.java | 57 ++++++ .../rocketmq/studio/ops/audit/AuditRecordVO.java | 41 ++++ .../rocketmq/studio/ops/audit/AuditRepository.java | 29 +++ .../rocketmq/studio/ops/audit/AuditService.java | 76 ++++++++ .../studio/ops/audit/InMemoryAuditRepository.java | 67 +++++++ .../studio/ops/dashboard/ClusterOverviewVO.java | 46 +++++ .../studio/ops/dashboard/DashboardController.java | 37 ++++ .../studio/ops/dashboard/DashboardDataVO.java | 34 ++++ .../studio/ops/dashboard/DashboardProvider.java | 23 +++ .../ops/dashboard/DashboardProviderStub.java | 99 ++++++++++ .../studio/ops/dashboard/DashboardService.java | 35 ++++ .../studio/ops/dashboard/DashboardStatsVO.java | 41 ++++ .../studio/settings/DataSourceTestDTO.java | 32 ++++ .../studio/settings/DataSourceTestResultVO.java | 31 +++ .../com/rocketmq/studio/settings/DataSourceVO.java | 35 ++++ .../studio/settings/GeneralSettingsVO.java | 39 ++++ .../settings/InMemorySettingsRepository.java | 77 ++++++++ .../studio/settings/SettingsController.java | 73 +++++++ .../studio/settings/SettingsRepository.java | 36 ++++ .../rocketmq/studio/settings/SettingsService.java | 77 ++++++++ .../com/rocketmq/studio/ops/ai/AiServiceTest.java | 184 ++++++++++++++++++ .../studio/ops/alert/AlertServiceTest.java | 209 +++++++++++++++++++++ .../studio/ops/audit/AuditServiceTest.java | 205 ++++++++++++++++++++ .../ops/dashboard/DashboardControllerTest.java | 118 ++++++++++++ .../studio/ops/dashboard/DashboardServiceTest.java | 113 +++++++++++ .../studio/settings/SettingsControllerTest.java | 196 +++++++++++++++++++ .../studio/settings/SettingsServiceTest.java | 174 +++++++++++++++++ 44 files changed, 3011 insertions(+) diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiCommandDTO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiCommandDTO.java new file mode 100644 index 0000000..b0a0f6c --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiCommandDTO.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiCommandDTO { + private String command; + private String mode; + private String model; + private String conversationId; + private String prompt; + private Map<String, Object> context; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiController.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiController.java new file mode 100644 index 0000000..27ce146 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiController.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.List; + +@RestController +@RequestMapping("/api/ai") +@RequiredArgsConstructor +public class AiController { + + private final AiService aiService; + + @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter chat(@RequestBody ChatDTO request) { + return aiService.chat(request); + } + + @PostMapping("/execute") + public Result<AiExecuteResultVO> execute(@RequestBody AiCommandDTO command) { + return Result.ok(aiService.execute(command)); + } + + @GetMapping("/tools") + public Result<List<AiToolVO>> listTools() { + return Result.ok(aiService.listTools()); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiExecuteResultVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiExecuteResultVO.java new file mode 100644 index 0000000..8f8cdcd --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiExecuteResultVO.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiExecuteResultVO { + private boolean success; + private String result; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiService.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiService.java new file mode 100644 index 0000000..b14479a --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiService.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AiService { + + private final LlmGateway llmGateway; + private final McpServerRegistry mcpServerRegistry; + + + public SseEmitter chat(ChatDTO request) { + log.info("Chat request received: mode={}, conversationId={}", request.getMode(), request.getConversationId()); + return llmGateway.chat(request); + } + + + public AiExecuteResultVO execute(AiCommandDTO command) { + log.info("Executing AI command: {}", command.getCommand()); + try { + String result = llmGateway.execute(command); + return AiExecuteResultVO.builder() + .success(true) + .result(result) + .build(); + } catch (Exception e) { + log.error("Failed to execute AI command", e); + return AiExecuteResultVO.builder() + .success(false) + .result("Error: " + e.getMessage()) + .build(); + } + } + + + public List<AiToolVO> listTools() { + log.debug("Listing available AI tools"); + return mcpServerRegistry.listTools(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/AiToolVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/AiToolVO.java new file mode 100644 index 0000000..fc2dbe5 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/AiToolVO.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiToolVO { + private String name; + private String description; + private Object parameters; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/ChatDTO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/ChatDTO.java new file mode 100644 index 0000000..d3f0893 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/ChatDTO.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChatDTO { + private String message; + private String mode; + private String model; + private String conversationId; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGateway.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGateway.java new file mode 100644 index 0000000..257d970 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGateway.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface LlmGateway { + + SseEmitter chat(ChatDTO request); + + String execute(AiCommandDTO command); +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGatewayStub.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGatewayStub.java new file mode 100644 index 0000000..97266f1 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmGatewayStub.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Slf4j +@Component +public class LlmGatewayStub implements LlmGateway { + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Override + public SseEmitter chat(ChatDTO request) { + SseEmitter emitter = new SseEmitter(60_000L); + + executor.execute(() -> { + try { + String[] tokens = ("This is a stub response for: " + request.getMessage()).split(" "); + for (String token : tokens) { + emitter.send(SseEmitter.event() + .name("message") + .data("{\"text\": \"" + token + " \"}")); + Thread.sleep(100); + } + emitter.send(SseEmitter.event().name("done").data("[DONE]")); + emitter.complete(); + } catch (IOException | InterruptedException e) { + log.error("Error streaming stub response", e); + emitter.completeWithError(e); + } + }); + + return emitter; + } + + @Override + public String execute(AiCommandDTO command) { + log.info("Stub executing AI command: {}", command.getCommand()); + return "Command executed successfully (stub)"; + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerImpl.java b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerImpl.java new file mode 100644 index 0000000..50aaa45 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerImpl.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class McpServerImpl implements McpServerRegistry { + + @Override + public List<AiToolVO> listTools() { + log.debug("Listing available MCP tools (stub)"); + + Map<String, Object> queryParams = new HashMap<>(); + queryParams.put("type", "object"); + queryParams.put("properties", Collections.singletonMap("query", + Collections.singletonMap("type", "string"))); + + Map<String, Object> brokerParams = new HashMap<>(); + brokerParams.put("type", "object"); + brokerParams.put("properties", Collections.singletonMap("brokerName", + Collections.singletonMap("type", "string"))); + + return Arrays.asList( + AiToolVO.builder() + .name("query_metrics") + .description("Query RocketMQ metrics from Prometheus") + .parameters(queryParams) + .build(), + AiToolVO.builder() + .name("list_brokers") + .description("List all RocketMQ brokers in the cluster") + .parameters(Collections.emptyMap()) + .build(), + AiToolVO.builder() + .name("diagnose_broker") + .description("Diagnose issues with a specific broker") + .parameters(brokerParams) + .build() + ); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerRegistry.java b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerRegistry.java new file mode 100644 index 0000000..24b5fff --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/McpServerRegistry.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + + +import java.util.List; + +public interface McpServerRegistry { + + List<AiToolVO> listTools(); +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRepository.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRepository.java new file mode 100644 index 0000000..958abc4 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRepository.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + + +import java.util.List; + +public interface AlertRepository { + List<AlertRuleVO> findAllRules(); + + AlertRuleVO saveRule(AlertRuleVO rule); + + void deleteRule(String id); + + List<SystemAlertVO> findAlerts(String level); + + SystemAlertVO saveAlert(SystemAlertVO alert); + + int deleteAcknowledgedAlerts(); +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleController.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleController.java new file mode 100644 index 0000000..c8ea802 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleController.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/alert-rules") +@RequiredArgsConstructor +public class AlertRuleController { + + private final AlertService alertService; + + @GetMapping + public Result<List<AlertRuleVO>> listRules() { + return Result.ok(alertService.listRules()); + } + + @PostMapping("/create") + public Result<AlertRuleVO> createRule(@RequestBody AlertRuleVO rule) { + return Result.ok(alertService.createRule(rule)); + } + + @PostMapping("/update") + public Result<AlertRuleVO> updateRule(@RequestBody AlertRuleVO rule) { + return Result.ok(alertService.updateRule(rule)); + } + + @PostMapping("/toggle") + public Result<AlertRuleVO> toggleRule(@RequestBody Map<String, Object> request) { + String id = (String) request.get("id"); + boolean enabled = (Boolean) request.get("enabled"); + return Result.ok(alertService.toggleRule(id, enabled)); + } + + @PostMapping("/delete") + public Result<Void> deleteRule(@RequestBody Map<String, String> request) { + alertService.deleteRule(request.get("id")); + return Result.ok(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleVO.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleVO.java new file mode 100644 index 0000000..377b457 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertRuleVO.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AlertRuleVO { + private String id; + private String name; + private String metric; + private String operator; + private double threshold; + private String thresholdUnit; + private String duration; + private List<String> channels; + private boolean enabled; + private String lastTriggered; + private String description; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java new file mode 100644 index 0000000..7336481 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/AlertService.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AlertService { + + private final AlertRepository alertRepository; + + + public List<AlertRuleVO> listRules() { + log.info("Listing all alert rules"); + return alertRepository.findAllRules(); + } + + + public AlertRuleVO createRule(AlertRuleVO rule) { + log.info("Creating alert rule: {}", rule.getName()); + rule.setId(UUID.randomUUID().toString()); + return alertRepository.saveRule(rule); + } + + + public AlertRuleVO updateRule(AlertRuleVO rule) { + log.info("Updating alert rule: {}", rule.getId()); + return alertRepository.saveRule(rule); + } + + + public AlertRuleVO toggleRule(String id, boolean enabled) { + log.info("Toggling alert rule id={}, enabled={}", id, enabled); + List<AlertRuleVO> rules = alertRepository.findAllRules(); + AlertRuleVO rule = rules.stream() + .filter(r -> r.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new com.rocketmq.studio.common.exception.BusinessException(404, "Alert rule not found: " + id)); + rule.setEnabled(enabled); + return alertRepository.saveRule(rule); + } + + + public void deleteRule(String id) { + log.info("Deleting alert rule id={}", id); + alertRepository.deleteRule(id); + } + + + public List<SystemAlertVO> listAlerts(String level) { + log.info("Listing system alerts, level={}", level); + return alertRepository.findAlerts(level); + } + + + public SystemAlertVO acknowledgeAlert(String id) { + log.info("Acknowledging system alert id={}", id); + List<SystemAlertVO> alerts = alertRepository.findAlerts(null); + SystemAlertVO alert = alerts.stream() + .filter(a -> a.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new com.rocketmq.studio.common.exception.BusinessException(404, "System alert not found: " + id)); + alert.setAcknowledged(true); + return alertRepository.saveAlert(alert); + } + + + public int clearAcknowledged() { + log.info("Clearing acknowledged system alerts"); + return alertRepository.deleteAcknowledgedAlerts(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/InMemoryAlertRepository.java b/server/src/main/java/com/rocketmq/studio/ops/alert/InMemoryAlertRepository.java new file mode 100644 index 0000000..d254728 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/InMemoryAlertRepository.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class InMemoryAlertRepository implements AlertRepository { + + private final Map<String, AlertRuleVO> rules = new ConcurrentHashMap<>(); + private final Map<String, SystemAlertVO> alerts = new ConcurrentHashMap<>(); + + @Override + public List<AlertRuleVO> findAllRules() { + return new ArrayList<>(rules.values()); + } + + @Override + public AlertRuleVO saveRule(AlertRuleVO rule) { + rules.put(rule.getId(), rule); + log.debug("Saved alert rule id={}", rule.getId()); + return rule; + } + + @Override + public void deleteRule(String id) { + rules.remove(id); + log.debug("Deleted alert rule id={}", id); + } + + @Override + public List<SystemAlertVO> findAlerts(String level) { + return alerts.values().stream() + .filter(a -> level == null || level.equalsIgnoreCase(a.getLevel().name())) + .collect(Collectors.toList()); + } + + @Override + public SystemAlertVO saveAlert(SystemAlertVO alert) { + alerts.put(alert.getId(), alert); + log.debug("Saved system alert id={}", alert.getId()); + return alert; + } + + @Override + public int deleteAcknowledgedAlerts() { + List<String> toRemove = alerts.values().stream() + .filter(SystemAlertVO::isAcknowledged) + .map(SystemAlertVO::getId) + .collect(Collectors.toList()); + toRemove.forEach(alerts::remove); + log.debug("Cleared {} acknowledged system alerts", toRemove.size()); + return toRemove.size(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertController.java b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertController.java new file mode 100644 index 0000000..2394944 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertController.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +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/system-alerts") +@RequiredArgsConstructor +public class SystemAlertController { + + private final AlertService alertService; + + @GetMapping + public Result<List<SystemAlertVO>> listAlerts( + @RequestParam(required = false) String level) { + return Result.ok(alertService.listAlerts(level)); + } + + @PostMapping("/acknowledge") + public Result<SystemAlertVO> acknowledgeAlert(@RequestBody Map<String, String> request) { + return Result.ok(alertService.acknowledgeAlert(request.get("id"))); + } + + @PostMapping("/clear-acknowledged") + public Result<Map<String, Integer>> clearAcknowledged() { + int cleared = alertService.clearAcknowledged(); + return Result.ok(Map.of("cleared", cleared)); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertVO.java b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertVO.java new file mode 100644 index 0000000..9141e4e --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/alert/SystemAlertVO.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import com.rocketmq.studio.common.domain.enums.AlertLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SystemAlertVO { + private String id; + private AlertLevel level; + private String title; + private String description; + private LocalDateTime time; + private boolean acknowledged; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditController.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditController.java new file mode 100644 index 0000000..70b4292 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditController.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.audit; + +import com.rocketmq.studio.common.domain.PageResult; +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/audit-logs") +@RequiredArgsConstructor +public class AuditController { + + private final AuditService auditService; + + @GetMapping + public Result<PageResult<AuditRecordVO>> queryLogs( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String search, + @RequestParam(required = false) String operationType, + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate, + @RequestParam(required = false) String result) { + return Result.ok(auditService.queryLogs(page, pageSize, search, + operationType, startDate, endDate, result)); + } + + @PostMapping("/cleanup") + public Result<Map<String, Integer>> cleanupLogs(@RequestBody Map<String, Integer> request) { + int beforeDays = request.getOrDefault("beforeDays", 30); + int deleted = auditService.cleanupLogs(beforeDays); + return Result.ok(Map.of("deleted", deleted)); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRecordVO.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRecordVO.java new file mode 100644 index 0000000..0f13abd --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRecordVO.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.audit; + +import com.rocketmq.studio.common.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class AuditRecordVO extends BaseEntity { + private LocalDateTime timestamp; + private String operator; + private String operationType; + private String target; + private String detail; + private String ipAddress; + private String result; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRepository.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRepository.java new file mode 100644 index 0000000..6f12fe7 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditRepository.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.audit; + + +import java.time.LocalDateTime; +import java.util.List; + +public interface AuditRepository { + List<AuditRecordVO> findAll(String search, String operationType, + LocalDateTime startDate, LocalDateTime endDate, + String result); + + int deleteBefore(LocalDateTime cutoff); +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/AuditService.java b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditService.java new file mode 100644 index 0000000..f5968b8 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/audit/AuditService.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.audit; + +import com.rocketmq.studio.common.domain.PageResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuditService { + + private final AuditRepository auditRepository; + + + public PageResult<AuditRecordVO> queryLogs(int page, int pageSize, String search, + String operationType, String startDate, + String endDate, String result) { + log.info("Querying audit logs, page={}, pageSize={}, search={}, operationType={}, result={}", + page, pageSize, search, operationType, result); + + LocalDateTime start = parseDate(startDate, true); + LocalDateTime end = parseDate(endDate, false); + + List<AuditRecordVO> allRecords = auditRepository.findAll(search, operationType, start, end, result); + long total = allRecords.size(); + + int fromIndex = Math.min((page - 1) * pageSize, allRecords.size()); + int toIndex = Math.min(fromIndex + pageSize, allRecords.size()); + List<AuditRecordVO> pageRecords = allRecords.subList(fromIndex, toIndex); + + return PageResult.of(pageRecords, total, page, pageSize); + } + + + public int cleanupLogs(int beforeDays) { + log.info("Cleaning up audit logs older than {} days", beforeDays); + LocalDateTime cutoff = LocalDateTime.now().minusDays(beforeDays); + return auditRepository.deleteBefore(cutoff); + } + + private LocalDateTime parseDate(String dateStr, boolean startOfDay) { + if (dateStr == null || dateStr.isEmpty()) { + return null; + } + try { + LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE); + return startOfDay ? date.atStartOfDay() : date.atTime(LocalTime.MAX); + } catch (Exception e) { + log.warn("Failed to parse date: {}", dateStr, e); + return null; + } + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/audit/InMemoryAuditRepository.java b/server/src/main/java/com/rocketmq/studio/ops/audit/InMemoryAuditRepository.java new file mode 100644 index 0000000..f4c2955 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/audit/InMemoryAuditRepository.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.audit; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class InMemoryAuditRepository implements AuditRepository { + + private final Map<String, AuditRecordVO> records = new ConcurrentHashMap<>(); + + @Override + public List<AuditRecordVO> findAll(String search, String operationType, + LocalDateTime startDate, LocalDateTime endDate, + String result) { + return records.values().stream() + .filter(r -> search == null || search.isEmpty() + || r.getDetail().toLowerCase().contains(search.toLowerCase()) + || r.getOperator().toLowerCase().contains(search.toLowerCase()) + || r.getTarget().toLowerCase().contains(search.toLowerCase())) + .filter(r -> operationType == null || operationType.isEmpty() + || operationType.equals(r.getOperationType())) + .filter(r -> startDate == null || r.getTimestamp() != null && !r.getTimestamp().isBefore(startDate)) + .filter(r -> endDate == null || r.getTimestamp() != null && !r.getTimestamp().isAfter(endDate)) + .filter(r -> result == null || result.isEmpty() || result.equals(r.getResult())) + .sorted((a, b) -> { + if (a.getTimestamp() == null || b.getTimestamp() == null) { + return 0; + } + return b.getTimestamp().compareTo(a.getTimestamp()); + }) + .collect(Collectors.toList()); + } + + @Override + public int deleteBefore(LocalDateTime cutoff) { + List<String> toRemove = records.values().stream() + .filter(r -> r.getTimestamp() != null && r.getTimestamp().isBefore(cutoff)) + .map(AuditRecordVO::getId) + .collect(Collectors.toList()); + toRemove.forEach(records::remove); + log.debug("Deleted {} audit records before {}", toRemove.size(), cutoff); + return toRemove.size(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java new file mode 100644 index 0000000..9a20a86 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/ClusterOverviewVO.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import com.rocketmq.studio.common.domain.enums.ClusterStatus; +import com.rocketmq.studio.common.domain.enums.ClusterType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ClusterOverviewVO { + private String id; + private String name; + private ClusterType type; + private ClusterStatus status; + private int brokers; + private int proxies; + private int topics; + private int groups; + private int tpsIn; + private int tpsOut; + private String version; + private List<Integer> throughput; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardController.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardController.java new file mode 100644 index 0000000..9f8f67e --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardController.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/dashboard") +@RequiredArgsConstructor +public class DashboardController { + + private final DashboardService dashboardService; + + @GetMapping + public Result<DashboardDataVO> getDashboard() { + return Result.ok(dashboardService.getDashboard()); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardDataVO.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardDataVO.java new file mode 100644 index 0000000..f3bf8fe --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardDataVO.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DashboardDataVO { + private DashboardStatsVO stats; + private List<ClusterOverviewVO> clusters; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProvider.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProvider.java new file mode 100644 index 0000000..0464a37 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProvider.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + + +public interface DashboardProvider { + DashboardDataVO getDashboardData(); +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProviderStub.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProviderStub.java new file mode 100644 index 0000000..3964ed9 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardProviderStub.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import com.rocketmq.studio.common.domain.enums.ClusterStatus; +import com.rocketmq.studio.common.domain.enums.ClusterType; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +@Component +public class DashboardProviderStub implements DashboardProvider { + + @Override + public DashboardDataVO getDashboardData() { + DashboardStatsVO stats = DashboardStatsVO.builder() + .totalClusters(3) + .healthyClusters(2) + .totalBrokers(12) + .totalProxies(6) + .totalNameServers(6) + .totalTopics(156) + .totalConsumerGroups(89) + .totalMessagesToday(12_500_000L) + .messagesPerSecond(14_467L) + .tpsIn(7_200L) + .tpsOut(7_267L) + .build(); + + List<ClusterOverviewVO> clusters = List.of( + ClusterOverviewVO.builder() + .id("cluster-1") + .name("production-cluster") + .type(ClusterType.V5_PROXY_CLUSTER) + .status(ClusterStatus.healthy) + .brokers(6) + .proxies(3) + .topics(80) + .groups(45) + .tpsIn(4200) + .tpsOut(4300) + .version("5.1.4") + .throughput(Arrays.asList(3200, 3500, 4100, 4200, 3800, 4000, + 4500, 4200, 3900, 4100, 4300, 4200)) + .build(), + ClusterOverviewVO.builder() + .id("cluster-2") + .name("staging-cluster") + .type(ClusterType.V5_PROXY_LOCAL) + .status(ClusterStatus.warning) + .brokers(4) + .proxies(2) + .topics(50) + .groups(30) + .tpsIn(2000) + .tpsOut(1967) + .version("5.1.4") + .throughput(Arrays.asList(1800, 1900, 2100, 2000, 1700, 2200, + 2000, 1800, 1900, 2100, 2000, 1967)) + .build(), + ClusterOverviewVO.builder() + .id("cluster-3") + .name("dev-cluster") + .type(ClusterType.V4_DIRECT) + .status(ClusterStatus.healthy) + .brokers(2) + .proxies(1) + .topics(26) + .groups(14) + .tpsIn(1000) + .tpsOut(1000) + .version("4.9.8") + .throughput(Arrays.asList(800, 900, 1100, 1000, 950, 1050, + 1000, 900, 1100, 1000, 950, 1000)) + .build() + ); + + return DashboardDataVO.builder() + .stats(stats) + .clusters(clusters) + .build(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardService.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardService.java new file mode 100644 index 0000000..9924b45 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardService.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DashboardService { + + private final DashboardProvider dashboardProvider; + + public DashboardDataVO getDashboard() { + log.debug("Fetching dashboard data"); + return dashboardProvider.getDashboardData(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardStatsVO.java b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardStatsVO.java new file mode 100644 index 0000000..cd22539 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/dashboard/DashboardStatsVO.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DashboardStatsVO { + private int totalClusters; + private int healthyClusters; + private int totalBrokers; + private int totalProxies; + private int totalNameServers; + private int totalTopics; + private int totalConsumerGroups; + private long totalMessagesToday; + private long messagesPerSecond; + private long tpsIn; + private long tpsOut; +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestDTO.java b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestDTO.java new file mode 100644 index 0000000..b17b8dc --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestDTO.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataSourceTestDTO { + private String url; + private String type; + private String auth; +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestResultVO.java b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestResultVO.java new file mode 100644 index 0000000..f842fae --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/DataSourceTestResultVO.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataSourceTestResultVO { + private boolean success; + private String message; +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/DataSourceVO.java b/server/src/main/java/com/rocketmq/studio/settings/DataSourceVO.java new file mode 100644 index 0000000..c2d1164 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/DataSourceVO.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataSourceVO { + private String key; + private String name; + private String type; + private String url; + private String auth; + private String status; +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java new file mode 100644 index 0000000..8dd3bd6 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/GeneralSettingsVO.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GeneralSettingsVO { + private String theme; + private boolean compact; + private boolean desktopNotify; + private boolean notifySound; + private int sessionTimeout; + private boolean requireLogin; + private String llmProvider; + private String apiKey; + private String model; + private String baseUrl; +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/InMemorySettingsRepository.java b/server/src/main/java/com/rocketmq/studio/settings/InMemorySettingsRepository.java new file mode 100644 index 0000000..d3c01e4 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/InMemorySettingsRepository.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class InMemorySettingsRepository implements SettingsRepository { + + private GeneralSettingsVO generalSettings = GeneralSettingsVO.builder() + .theme("system") + .compact(false) + .desktopNotify(true) + .notifySound(false) + .sessionTimeout(30) + .requireLogin(false) + .llmProvider("openai") + .apiKey("") + .model("gpt-4") + .baseUrl("") + .build(); + + private final Map<String, DataSourceVO> dataSources = new ConcurrentHashMap<>(); + + @Override + public GeneralSettingsVO loadGeneralSettings() { + return generalSettings; + } + + @Override + public void saveGeneralSettings(GeneralSettingsVO settings) { + this.generalSettings = settings; + } + + @Override + public List<DataSourceVO> findAllDataSources() { + return new ArrayList<>(dataSources.values()); + } + + @Override + public DataSourceVO saveDataSource(DataSourceVO dataSource) { + dataSources.put(dataSource.getKey(), dataSource); + return dataSource; + } + + @Override + public void deleteDataSource(String key) { + dataSources.remove(key); + } + + @Override + public Optional<DataSourceVO> findDataSourceByKey(String key) { + return Optional.ofNullable(dataSources.get(key)); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java b/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java new file mode 100644 index 0000000..a9a9123 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsController.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import com.rocketmq.studio.common.domain.Result; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/settings") +@RequiredArgsConstructor +public class SettingsController { + + private final SettingsService settingsService; + + @GetMapping("/general") + public Result<GeneralSettingsVO> getGeneralSettings() { + return Result.ok(settingsService.getGeneralSettings()); + } + + @PostMapping("/general/save") + public Result<Void> saveGeneralSettings(@RequestBody GeneralSettingsVO settings) { + settingsService.saveGeneralSettings(settings); + return Result.ok(); + } + + @GetMapping("/datasources") + public Result<List<DataSourceVO>> listDataSources() { + return Result.ok(settingsService.listDataSources()); + } + + @PostMapping("/datasources/create") + public Result<DataSourceVO> createDataSource(@RequestBody DataSourceVO dataSource) { + return Result.ok(settingsService.createDataSource(dataSource)); + } + + @PostMapping("/datasources/update") + public Result<DataSourceVO> updateDataSource(@RequestBody DataSourceVO dataSource) { + return Result.ok(settingsService.updateDataSource(dataSource)); + } + + @PostMapping("/datasources/delete") + public Result<Void> deleteDataSource(@RequestParam String key) { + settingsService.deleteDataSource(key); + return Result.ok(); + } + + @PostMapping("/datasources/test") + public Result<DataSourceTestResultVO> testDataSource(@RequestBody DataSourceTestDTO request) { + return Result.ok(settingsService.testDataSource(request)); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/SettingsRepository.java b/server/src/main/java/com/rocketmq/studio/settings/SettingsRepository.java new file mode 100644 index 0000000..70e08a7 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsRepository.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + + +import java.util.List; +import java.util.Optional; + +public interface SettingsRepository { + + GeneralSettingsVO loadGeneralSettings(); + + void saveGeneralSettings(GeneralSettingsVO settings); + + List<DataSourceVO> findAllDataSources(); + + DataSourceVO saveDataSource(DataSourceVO dataSource); + + void deleteDataSource(String key); + + Optional<DataSourceVO> findDataSourceByKey(String key); +} diff --git a/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java b/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java new file mode 100644 index 0000000..1f12876 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/settings/SettingsService.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SettingsService { + + private final SettingsRepository settingsRepository; + + + public GeneralSettingsVO getGeneralSettings() { + log.debug("Loading general settings"); + return settingsRepository.loadGeneralSettings(); + } + + + public void saveGeneralSettings(GeneralSettingsVO settings) { + log.info("Saving general settings"); + settingsRepository.saveGeneralSettings(settings); + } + + + public List<DataSourceVO> listDataSources() { + log.debug("Listing all data sources"); + return settingsRepository.findAllDataSources(); + } + + + public DataSourceVO createDataSource(DataSourceVO dataSource) { + log.info("Creating data source: {}", dataSource.getName()); + return settingsRepository.saveDataSource(dataSource); + } + + + public DataSourceVO updateDataSource(DataSourceVO dataSource) { + log.info("Updating data source: {}", dataSource.getKey()); + return settingsRepository.saveDataSource(dataSource); + } + + + public void deleteDataSource(String key) { + log.info("Deleting data source: {}", key); + settingsRepository.deleteDataSource(key); + } + + + public DataSourceTestResultVO testDataSource(DataSourceTestDTO request) { + log.info("Testing data source connection: url={}, type={}", request.getUrl(), request.getType()); + // Stub: always return success for now + return DataSourceTestResultVO.builder() + .success(true) + .message("Connection successful") + .build(); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/ai/AiServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/ai/AiServiceTest.java new file mode 100644 index 0000000..ac46c7d --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/ai/AiServiceTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.ai; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AiServiceTest { + + @Mock + private LlmGateway llmGateway; + + @Mock + private McpServerRegistry mcpServerRegistry; + + @InjectMocks + private AiService aiService; + + @Test + void chatShouldReturnSseEmitterFromGateway() { + ChatDTO request = ChatDTO.builder() + .message("What is the broker status?") + .mode("chat") + .model("gpt-4") + .conversationId("conv-1") + .build(); + SseEmitter mockEmitter = new SseEmitter(); + when(llmGateway.chat(request)).thenReturn(mockEmitter); + + SseEmitter result = aiService.chat(request); + + assertThat(result).isSameAs(mockEmitter); + verify(llmGateway).chat(request); + } + + @Test + void chatShouldPassRequestDirectlyToGateway() { + ChatDTO request = ChatDTO.builder() + .message("List all topics") + .mode("agent") + .build(); + SseEmitter mockEmitter = new SseEmitter(); + when(llmGateway.chat(any(ChatDTO.class))).thenReturn(mockEmitter); + + aiService.chat(request); + + verify(llmGateway).chat(request); + } + + @Test + void executeShouldReturnSuccessResult() { + AiCommandDTO command = AiCommandDTO.builder() + .command("list_topics") + .mode("agent") + .model("gpt-4") + .prompt("List all topics in the cluster") + .build(); + when(llmGateway.execute(command)).thenReturn("Found 5 topics: topic-a, topic-b, topic-c, topic-d, topic-e"); + + AiExecuteResultVO result = aiService.execute(command); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getResult()).contains("Found 5 topics"); + verify(llmGateway).execute(command); + } + + @Test + void executeShouldReturnFailureWhenGatewayThrows() { + AiCommandDTO command = AiCommandDTO.builder() + .command("delete_topic") + .mode("agent") + .prompt("Delete topic-x") + .build(); + when(llmGateway.execute(command)).thenThrow(new RuntimeException("Permission denied")); + + AiExecuteResultVO result = aiService.execute(command); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getResult()).contains("Error: Permission denied"); + } + + @Test + void executeShouldHandleNullMessageInException() { + AiCommandDTO command = AiCommandDTO.builder().command("bad_cmd").build(); + when(llmGateway.execute(command)).thenThrow(new RuntimeException()); + + AiExecuteResultVO result = aiService.execute(command); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getResult()).startsWith("Error:"); + } + + @Test + void executeShouldPassContextToGateway() { + Map<String, Object> context = new HashMap<>(); + context.put("clusterId", "cluster-1"); + context.put("namespace", "default"); + AiCommandDTO command = AiCommandDTO.builder() + .command("query_metrics") + .mode("agent") + .context(context) + .build(); + when(llmGateway.execute(command)).thenReturn("CPU: 45%, Memory: 72%"); + + AiExecuteResultVO result = aiService.execute(command); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getResult()).contains("CPU: 45%"); + } + + @Test + void listToolsShouldReturnAllTools() { + AiToolVO tool1 = AiToolVO.builder().name("list_topics").description("List all topics").build(); + AiToolVO tool2 = AiToolVO.builder().name("query_metrics").description("Query cluster metrics").build(); + AiToolVO tool3 = AiToolVO.builder().name("send_message").description("Send a test message").build(); + when(mcpServerRegistry.listTools()).thenReturn(Arrays.asList(tool1, tool2, tool3)); + + List<AiToolVO> result = aiService.listTools(); + + assertThat(result).hasSize(3); + assertThat(result.get(0).getName()).isEqualTo("list_topics"); + assertThat(result.get(0).getDescription()).isEqualTo("List all topics"); + assertThat(result.get(1).getName()).isEqualTo("query_metrics"); + assertThat(result.get(2).getName()).isEqualTo("send_message"); + } + + @Test + void listToolsShouldReturnEmptyListWhenNoTools() { + when(mcpServerRegistry.listTools()).thenReturn(Collections.emptyList()); + + List<AiToolVO> result = aiService.listTools(); + + assertThat(result).isEmpty(); + } + + @Test + void listToolsShouldIncludeParameters() { + Map<String, Object> params = new HashMap<>(); + params.put("clusterId", "string"); + params.put("limit", "integer"); + AiToolVO tool = AiToolVO.builder() + .name("list_topics") + .description("List all topics") + .parameters(params) + .build(); + when(mcpServerRegistry.listTools()).thenReturn(List.of(tool)); + + List<AiToolVO> result = aiService.listTools(); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getParameters()).isNotNull(); + assertThat(result.get(0).getParameters()).isInstanceOf(Map.class); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java new file mode 100644 index 0000000..2fd408c --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/alert/AlertServiceTest.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.alert; + +import com.rocketmq.studio.common.domain.enums.AlertLevel; +import com.rocketmq.studio.common.exception.BusinessException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AlertServiceTest { + + @Mock + private AlertRepository alertRepository; + + @InjectMocks + private AlertService alertService; + + @Test + void listRulesShouldReturnAllRules() { + AlertRuleVO rule1 = AlertRuleVO.builder().id("1").name("High CPU").metric("cpu_usage") + .operator(">").threshold(90.0).enabled(true).build(); + AlertRuleVO rule2 = AlertRuleVO.builder().id("2").name("Low Disk").metric("disk_free") + .operator("<").threshold(10.0).enabled(false).build(); + when(alertRepository.findAllRules()).thenReturn(Arrays.asList(rule1, rule2)); + + List<AlertRuleVO> result = alertService.listRules(); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getName()).isEqualTo("High CPU"); + assertThat(result.get(0).isEnabled()).isTrue(); + assertThat(result.get(1).getName()).isEqualTo("Low Disk"); + assertThat(result.get(1).isEnabled()).isFalse(); + } + + @Test + void listRulesShouldReturnEmptyListWhenNoRules() { + when(alertRepository.findAllRules()).thenReturn(Collections.emptyList()); + + List<AlertRuleVO> result = alertService.listRules(); + + assertThat(result).isEmpty(); + } + + @Test + void createRuleShouldAssignId() { + AlertRuleVO input = AlertRuleVO.builder().name("New Rule").metric("tps") + .operator(">").threshold(1000.0).build(); + when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AlertRuleVO result = alertService.createRule(input); + + assertThat(result.getId()).isNotNull().isNotEmpty(); + assertThat(result.getName()).isEqualTo("New Rule"); + assertThat(result.getMetric()).isEqualTo("tps"); + verify(alertRepository).saveRule(result); + } + + @Test + void createRuleShouldGenerateUniqueIds() { + AlertRuleVO input1 = AlertRuleVO.builder().name("Rule 1").build(); + AlertRuleVO input2 = AlertRuleVO.builder().name("Rule 2").build(); + when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AlertRuleVO result1 = alertService.createRule(input1); + AlertRuleVO result2 = alertService.createRule(input2); + + assertThat(result1.getId()).isNotEqualTo(result2.getId()); + } + + @Test + void toggleRuleShouldEnableRule() { + AlertRuleVO existing = AlertRuleVO.builder().id("rule-1").name("CPU Alert").enabled(false).build(); + when(alertRepository.findAllRules()).thenReturn(List.of(existing)); + when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AlertRuleVO result = alertService.toggleRule("rule-1", true); + + assertThat(result.isEnabled()).isTrue(); + verify(alertRepository).saveRule(result); + } + + @Test + void toggleRuleShouldDisableRule() { + AlertRuleVO existing = AlertRuleVO.builder().id("rule-1").name("CPU Alert").enabled(true).build(); + when(alertRepository.findAllRules()).thenReturn(List.of(existing)); + when(alertRepository.saveRule(any(AlertRuleVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AlertRuleVO result = alertService.toggleRule("rule-1", false); + + assertThat(result.isEnabled()).isFalse(); + } + + @Test + void toggleRuleShouldThrowWhenRuleNotFound() { + when(alertRepository.findAllRules()).thenReturn(Collections.emptyList()); + + assertThatThrownBy(() -> alertService.toggleRule("non-existent", true)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("Alert rule not found: non-existent"); + } + + @Test + void deleteRuleShouldCallRepository() { + doNothing().when(alertRepository).deleteRule("rule-1"); + + alertService.deleteRule("rule-1"); + + verify(alertRepository).deleteRule("rule-1"); + } + + @Test + void listAlertsShouldReturnAlertsForLevel() { + SystemAlertVO alert1 = SystemAlertVO.builder().id("a1").level(AlertLevel.error) + .title("Broker Down").acknowledged(false).build(); + SystemAlertVO alert2 = SystemAlertVO.builder().id("a2").level(AlertLevel.error) + .title("High Latency").acknowledged(false).build(); + when(alertRepository.findAlerts("error")).thenReturn(Arrays.asList(alert1, alert2)); + + List<SystemAlertVO> result = alertService.listAlerts("error"); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getLevel()).isEqualTo(AlertLevel.error); + assertThat(result.get(0).getTitle()).isEqualTo("Broker Down"); + verify(alertRepository).findAlerts("error"); + } + + @Test + void listAlertsShouldReturnAllAlertsWhenLevelIsNull() { + SystemAlertVO alert = SystemAlertVO.builder().id("a1").level(AlertLevel.warning) + .title("Slow Consumer").build(); + when(alertRepository.findAlerts(null)).thenReturn(List.of(alert)); + + List<SystemAlertVO> result = alertService.listAlerts(null); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getLevel()).isEqualTo(AlertLevel.warning); + verify(alertRepository).findAlerts(null); + } + + @Test + void acknowledgeAlertShouldSetAcknowledgedTrue() { + SystemAlertVO existing = SystemAlertVO.builder().id("a1").level(AlertLevel.error) + .title("Broker Down").acknowledged(false).build(); + when(alertRepository.findAlerts(null)).thenReturn(List.of(existing)); + when(alertRepository.saveAlert(any(SystemAlertVO.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + SystemAlertVO result = alertService.acknowledgeAlert("a1"); + + assertThat(result.isAcknowledged()).isTrue(); + verify(alertRepository).saveAlert(result); + } + + @Test + void acknowledgeAlertShouldThrowWhenAlertNotFound() { + when(alertRepository.findAlerts(null)).thenReturn(Collections.emptyList()); + + assertThatThrownBy(() -> alertService.acknowledgeAlert("non-existent")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("System alert not found: non-existent"); + } + + @Test + void clearAcknowledgedShouldReturnDeletedCount() { + when(alertRepository.deleteAcknowledgedAlerts()).thenReturn(3); + + int result = alertService.clearAcknowledged(); + + assertThat(result).isEqualTo(3); + verify(alertRepository).deleteAcknowledgedAlerts(); + } + + @Test + void clearAcknowledgedShouldReturnZeroWhenNoneAcknowledged() { + when(alertRepository.deleteAcknowledgedAlerts()).thenReturn(0); + + int result = alertService.clearAcknowledged(); + + assertThat(result).isZero(); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/audit/AuditServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/audit/AuditServiceTest.java new file mode 100644 index 0000000..e044632 --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/audit/AuditServiceTest.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.ops.audit; + +import com.rocketmq.studio.common.domain.PageResult; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +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.when; + +@ExtendWith(MockitoExtension.class) +class AuditServiceTest { + + @Mock + private AuditRepository auditRepository; + + @InjectMocks + private AuditService auditService; + + @Test + void queryLogsShouldReturnFirstPage() { + AuditRecordVO r1 = AuditRecordVO.builder() + .operator("admin").operationType("CREATE").target("topic-a").result("SUCCESS").build(); + AuditRecordVO r2 = AuditRecordVO.builder() + .operator("admin").operationType("DELETE").target("topic-b").result("SUCCESS").build(); + AuditRecordVO r3 = AuditRecordVO.builder() + .operator("user1").operationType("UPDATE").target("topic-c").result("FAILURE").build(); + when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull())) + .thenReturn(Arrays.asList(r1, r2, r3)); + + PageResult<AuditRecordVO> result = auditService.queryLogs(1, 2, null, null, null, null, null); + + assertThat(result.getItems()).hasSize(2); + assertThat(result.getTotal()).isEqualTo(3); + assertThat(result.getPage()).isEqualTo(1); + assertThat(result.getSize()).isEqualTo(2); + assertThat(result.getItems().get(0).getOperationType()).isEqualTo("CREATE"); + assertThat(result.getItems().get(1).getOperationType()).isEqualTo("DELETE"); + } + + @Test + void queryLogsShouldReturnSecondPage() { + AuditRecordVO r1 = AuditRecordVO.builder().operationType("CREATE").build(); + AuditRecordVO r2 = AuditRecordVO.builder().operationType("DELETE").build(); + AuditRecordVO r3 = AuditRecordVO.builder().operationType("UPDATE").build(); + when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull())) + .thenReturn(Arrays.asList(r1, r2, r3)); + + PageResult<AuditRecordVO> result = auditService.queryLogs(2, 2, null, null, null, null, null); + + assertThat(result.getItems()).hasSize(1); + assertThat(result.getTotal()).isEqualTo(3); + assertThat(result.getPage()).isEqualTo(2); + assertThat(result.getItems().get(0).getOperationType()).isEqualTo("UPDATE"); + } + + @Test + void queryLogsShouldReturnEmptyPageWhenNoRecords() { + when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull())) + .thenReturn(Collections.emptyList()); + + PageResult<AuditRecordVO> result = auditService.queryLogs(1, 10, null, null, null, null, null); + + assertThat(result.getItems()).isEmpty(); + assertThat(result.getTotal()).isZero(); + assertThat(result.getPage()).isEqualTo(1); + assertThat(result.getSize()).isEqualTo(10); + } + + @Test + void queryLogsShouldReturnEmptyWhenPageExceedsTotal() { + AuditRecordVO r1 = AuditRecordVO.builder().operationType("CREATE").build(); + when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull())) + .thenReturn(List.of(r1)); + + PageResult<AuditRecordVO> result = auditService.queryLogs(5, 10, null, null, null, null, null); + + assertThat(result.getItems()).isEmpty(); + assertThat(result.getTotal()).isEqualTo(1); + } + + @Test + void queryLogsShouldPassSearchFilterToRepository() { + when(auditRepository.findAll(eq("topic-a"), isNull(), isNull(), isNull(), isNull())) + .thenReturn(Collections.emptyList()); + + auditService.queryLogs(1, 10, "topic-a", null, null, null, null); + + verify(auditRepository).findAll(eq("topic-a"), isNull(), isNull(), isNull(), isNull()); + } + + @Test + void queryLogsShouldPassOperationTypeFilterToRepository() { + when(auditRepository.findAll(isNull(), eq("CREATE"), isNull(), isNull(), isNull())) + .thenReturn(Collections.emptyList()); + + auditService.queryLogs(1, 10, null, "CREATE", null, null, null); + + verify(auditRepository).findAll(isNull(), eq("CREATE"), isNull(), isNull(), isNull()); + } + + @Test + void queryLogsShouldPassResultFilterToRepository() { + when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), eq("SUCCESS"))) + .thenReturn(Collections.emptyList()); + + auditService.queryLogs(1, 10, null, null, null, null, "SUCCESS"); + + verify(auditRepository).findAll(isNull(), isNull(), isNull(), isNull(), eq("SUCCESS")); + } + + @Test + void queryLogsShouldParseDateRange() { + when(auditRepository.findAll(isNull(), isNull(), any(LocalDateTime.class), any(LocalDateTime.class), isNull())) + .thenReturn(Collections.emptyList()); + + auditService.queryLogs(1, 10, null, null, "2025-01-01", "2025-01-31", null); + + ArgumentCaptor<LocalDateTime> startCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + ArgumentCaptor<LocalDateTime> endCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + verify(auditRepository).findAll(isNull(), isNull(), startCaptor.capture(), endCaptor.capture(), isNull()); + + assertThat(startCaptor.getValue()).isEqualTo(LocalDateTime.of(2025, 1, 1, 0, 0, 0)); + assertThat(endCaptor.getValue().getYear()).isEqualTo(2025); + assertThat(endCaptor.getValue().getMonthValue()).isEqualTo(1); + assertThat(endCaptor.getValue().getDayOfMonth()).isEqualTo(31); + assertThat(endCaptor.getValue().getHour()).isEqualTo(23); + assertThat(endCaptor.getValue().getMinute()).isEqualTo(59); + } + + @Test + void queryLogsShouldHandleInvalidDateFormat() { + when(auditRepository.findAll(isNull(), isNull(), isNull(), isNull(), isNull())) + .thenReturn(Collections.emptyList()); + + PageResult<AuditRecordVO> result = auditService.queryLogs(1, 10, null, null, "invalid-date", null, null); + + assertThat(result).isNotNull(); + assertThat(result.getItems()).isEmpty(); + } + + @Test + void cleanupLogsShouldDeleteOldRecords() { + when(auditRepository.deleteBefore(any(LocalDateTime.class))).thenReturn(42); + + int result = auditService.cleanupLogs(30); + + assertThat(result).isEqualTo(42); + ArgumentCaptor<LocalDateTime> captor = ArgumentCaptor.forClass(LocalDateTime.class); + verify(auditRepository).deleteBefore(captor.capture()); + + LocalDateTime cutoff = captor.getValue(); + LocalDateTime expected = LocalDateTime.now().minusDays(30); + assertThat(cutoff).isCloseTo(expected, org.assertj.core.api.Assertions.within(2, java.time.temporal.ChronoUnit.SECONDS)); + } + + @Test + void cleanupLogsShouldReturnZeroWhenNoOldRecords() { + when(auditRepository.deleteBefore(any(LocalDateTime.class))).thenReturn(0); + + int result = auditService.cleanupLogs(90); + + assertThat(result).isZero(); + } + + @Test + void queryLogsShouldHandleAllFiltersTogether() { + when(auditRepository.findAll(eq("admin"), eq("DELETE"), any(LocalDateTime.class), + any(LocalDateTime.class), eq("FAILURE"))) + .thenReturn(Collections.emptyList()); + + auditService.queryLogs(1, 10, "admin", "DELETE", "2025-06-01", "2025-06-30", "FAILURE"); + + verify(auditRepository).findAll(eq("admin"), eq("DELETE"), any(LocalDateTime.class), + any(LocalDateTime.class), eq("FAILURE")); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardControllerTest.java b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardControllerTest.java new file mode 100644 index 0000000..41fa848 --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardControllerTest.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import com.rocketmq.studio.common.domain.enums.ClusterStatus; +import com.rocketmq.studio.common.domain.enums.ClusterType; +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.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(DashboardController.class) +@AutoConfigureMockMvc(addFilters = false) +class DashboardControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private DashboardService dashboardService; + + @Test + void getDashboardShouldReturnStatsAndClusters() throws Exception { + DashboardStatsVO stats = DashboardStatsVO.builder() + .totalClusters(2) + .healthyClusters(2) + .totalBrokers(6) + .totalProxies(2) + .totalNameServers(4) + .totalTopics(80) + .totalConsumerGroups(30) + .totalMessagesToday(500_000L) + .messagesPerSecond(250L) + .tpsIn(150L) + .tpsOut(100L) + .build(); + + ClusterOverviewVO cluster = ClusterOverviewVO.builder() + .id("c1") + .name("prod-cluster") + .type(ClusterType.V5_PROXY_CLUSTER) + .status(ClusterStatus.healthy) + .brokers(3) + .proxies(1) + .topics(40) + .groups(15) + .tpsIn(100) + .tpsOut(80) + .version("5.1.0") + .throughput(List.of(5, 10, 15)) + .build(); + + DashboardDataVO data = DashboardDataVO.builder() + .stats(stats) + .clusters(List.of(cluster)) + .build(); + + when(dashboardService.getDashboard()).thenReturn(data); + + mockMvc.perform(get("/api/dashboard")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.data.stats.totalClusters").value(2)) + .andExpect(jsonPath("$.data.stats.healthyClusters").value(2)) + .andExpect(jsonPath("$.data.stats.totalBrokers").value(6)) + .andExpect(jsonPath("$.data.stats.totalTopics").value(80)) + .andExpect(jsonPath("$.data.stats.messagesPerSecond").value(250)) + .andExpect(jsonPath("$.data.clusters").isArray()) + .andExpect(jsonPath("$.data.clusters[0].id").value("c1")) + .andExpect(jsonPath("$.data.clusters[0].name").value("prod-cluster")) + .andExpect(jsonPath("$.data.clusters[0].status").value("healthy")) + .andExpect(jsonPath("$.data.clusters[0].version").value("5.1.0")); + + verify(dashboardService).getDashboard(); + } + + @Test + void getDashboardShouldReturnEmptyClustersWhenNoneExist() throws Exception { + DashboardDataVO data = DashboardDataVO.builder() + .stats(DashboardStatsVO.builder().totalClusters(0).build()) + .clusters(List.of()) + .build(); + + when(dashboardService.getDashboard()).thenReturn(data); + + mockMvc.perform(get("/api/dashboard")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.stats.totalClusters").value(0)) + .andExpect(jsonPath("$.data.clusters").isArray()) + .andExpect(jsonPath("$.data.clusters").isEmpty()); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardServiceTest.java new file mode 100644 index 0000000..33dbe48 --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/dashboard/DashboardServiceTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rocketmq.studio.ops.dashboard; + +import com.rocketmq.studio.common.domain.enums.ClusterStatus; +import com.rocketmq.studio.common.domain.enums.ClusterType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DashboardServiceTest { + + @Mock + private DashboardProvider dashboardProvider; + + @InjectMocks + private DashboardService dashboardService; + + @Test + void getDashboardShouldReturnStatsAndClusters() { + DashboardStatsVO stats = DashboardStatsVO.builder() + .totalClusters(3) + .healthyClusters(2) + .totalBrokers(9) + .totalProxies(3) + .totalNameServers(6) + .totalTopics(120) + .totalConsumerGroups(45) + .totalMessagesToday(1_000_000L) + .messagesPerSecond(500L) + .tpsIn(300L) + .tpsOut(200L) + .build(); + + ClusterOverviewVO cluster = ClusterOverviewVO.builder() + .id("cluster-1") + .name("production") + .type(ClusterType.V5_PROXY_CLUSTER) + .status(ClusterStatus.healthy) + .brokers(3) + .proxies(1) + .topics(50) + .groups(20) + .tpsIn(100) + .tpsOut(80) + .version("5.1.0") + .throughput(List.of(10, 20, 30)) + .build(); + + DashboardDataVO mockData = DashboardDataVO.builder() + .stats(stats) + .clusters(List.of(cluster)) + .build(); + + when(dashboardProvider.getDashboardData()).thenReturn(mockData); + + DashboardDataVO result = dashboardService.getDashboard(); + + assertThat(result).isNotNull(); + assertThat(result.getStats()).isNotNull(); + assertThat(result.getStats().getTotalClusters()).isEqualTo(3); + assertThat(result.getStats().getHealthyClusters()).isEqualTo(2); + assertThat(result.getStats().getTotalBrokers()).isEqualTo(9); + assertThat(result.getStats().getTotalTopics()).isEqualTo(120); + assertThat(result.getStats().getMessagesPerSecond()).isEqualTo(500L); + + assertThat(result.getClusters()).hasSize(1); + assertThat(result.getClusters().get(0).getName()).isEqualTo("production"); + assertThat(result.getClusters().get(0).getStatus()).isEqualTo(ClusterStatus.healthy); + + verify(dashboardProvider).getDashboardData(); + } + + @Test + void getDashboardShouldDelegateToProvider() { + DashboardDataVO emptyData = DashboardDataVO.builder() + .stats(DashboardStatsVO.builder().build()) + .clusters(List.of()) + .build(); + + when(dashboardProvider.getDashboardData()).thenReturn(emptyData); + + DashboardDataVO result = dashboardService.getDashboard(); + + assertThat(result).isNotNull(); + assertThat(result.getClusters()).isEmpty(); + verify(dashboardProvider).getDashboardData(); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java b/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java new file mode 100644 index 0000000..42a3c0e --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/settings/SettingsControllerTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +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.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +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; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(SettingsController.class) +class SettingsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private SettingsService settingsService; + + @Test + void getGeneralSettingsShouldReturnSettings() throws Exception { + GeneralSettingsVO settings = GeneralSettingsVO.builder() + .theme("dark") + .compact(true) + .desktopNotify(true) + .notifySound(false) + .sessionTimeout(30) + .requireLogin(true) + .llmProvider("openai") + .apiKey("sk-xxx") + .model("gpt-4") + .baseUrl("https://api.openai.com") + .build(); + when(settingsService.getGeneralSettings()).thenReturn(settings); + + mockMvc.perform(get("/api/settings/general")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.message", is("success"))) + .andExpect(jsonPath("$.data.theme", is("dark"))) + .andExpect(jsonPath("$.data.compact", is(true))) + .andExpect(jsonPath("$.data.desktopNotify", is(true))) + .andExpect(jsonPath("$.data.notifySound", is(false))) + .andExpect(jsonPath("$.data.sessionTimeout", is(30))) + .andExpect(jsonPath("$.data.requireLogin", is(true))) + .andExpect(jsonPath("$.data.llmProvider", is("openai"))) + .andExpect(jsonPath("$.data.model", is("gpt-4"))); + } + + @Test + void saveGeneralSettingsShouldReturnSuccess() throws Exception { + GeneralSettingsVO settings = GeneralSettingsVO.builder() + .theme("light") + .compact(false) + .sessionTimeout(60) + .build(); + doNothing().when(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class)); + + mockMvc.perform(post("/api/settings/general/save") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(settings))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data").doesNotExist()); + + verify(settingsService).saveGeneralSettings(any(GeneralSettingsVO.class)); + } + + @Test + void listDataSourcesShouldReturnAllSources() throws Exception { + DataSourceVO ds1 = DataSourceVO.builder().key("ds-1").name("Production").type("rocketmq") + .url("prod:9876").status("connected").build(); + DataSourceVO ds2 = DataSourceVO.builder().key("ds-2").name("Staging").type("rocketmq") + .url("staging:9876").status("disconnected").build(); + when(settingsService.listDataSources()).thenReturn(Arrays.asList(ds1, ds2)); + + mockMvc.perform(get("/api/settings/datasources")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data", hasSize(2))) + .andExpect(jsonPath("$.data[0].key", is("ds-1"))) + .andExpect(jsonPath("$.data[0].name", is("Production"))) + .andExpect(jsonPath("$.data[0].status", is("connected"))) + .andExpect(jsonPath("$.data[1].key", is("ds-2"))) + .andExpect(jsonPath("$.data[1].name", is("Staging"))); + } + + @Test + void listDataSourcesShouldReturnEmptyList() throws Exception { + when(settingsService.listDataSources()).thenReturn(Collections.emptyList()); + + mockMvc.perform(get("/api/settings/datasources")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data", hasSize(0))); + } + + @Test + void createDataSourceShouldReturnCreatedSource() throws Exception { + DataSourceVO input = DataSourceVO.builder().name("New DS").type("rocketmq") + .url("new-host:9876").build(); + DataSourceVO created = DataSourceVO.builder().key("ds-new").name("New DS").type("rocketmq") + .url("new-host:9876").status("connected").build(); + when(settingsService.createDataSource(any(DataSourceVO.class))).thenReturn(created); + + mockMvc.perform(post("/api/settings/datasources/create") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(input))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data.key", is("ds-new"))) + .andExpect(jsonPath("$.data.name", is("New DS"))) + .andExpect(jsonPath("$.data.status", is("connected"))); + } + + @Test + void updateDataSourceShouldReturnUpdatedSource() throws Exception { + DataSourceVO input = DataSourceVO.builder().key("ds-1").name("Updated DS").type("rocketmq") + .url("updated:9876").build(); + when(settingsService.updateDataSource(any(DataSourceVO.class))).thenReturn(input); + + mockMvc.perform(post("/api/settings/datasources/update") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(input))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data.key", is("ds-1"))) + .andExpect(jsonPath("$.data.name", is("Updated DS"))); + } + + @Test + void deleteDataSourceShouldReturnSuccess() throws Exception { + doNothing().when(settingsService).deleteDataSource("ds-1"); + + mockMvc.perform(post("/api/settings/datasources/delete") + .param("key", "ds-1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))); + + verify(settingsService).deleteDataSource("ds-1"); + } + + @Test + void testDataSourceShouldReturnTestResult() throws Exception { + DataSourceTestDTO request = DataSourceTestDTO.builder() + .url("localhost:9876") + .type("rocketmq") + .build(); + DataSourceTestResultVO testResult = DataSourceTestResultVO.builder() + .success(true) + .message("Connection successful") + .build(); + when(settingsService.testDataSource(any(DataSourceTestDTO.class))).thenReturn(testResult); + + mockMvc.perform(post("/api/settings/datasources/test") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code", is(200))) + .andExpect(jsonPath("$.data.success", is(true))) + .andExpect(jsonPath("$.data.message", is("Connection successful"))); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java b/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java new file mode 100644 index 0000000..331197e --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/settings/SettingsServiceTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rocketmq.studio.settings; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SettingsServiceTest { + + @Mock + private SettingsRepository settingsRepository; + + @InjectMocks + private SettingsService settingsService; + + @Test + void getGeneralSettingsShouldReturnCurrentSettings() { + GeneralSettingsVO settings = GeneralSettingsVO.builder() + .theme("dark") + .compact(true) + .desktopNotify(true) + .notifySound(false) + .sessionTimeout(30) + .requireLogin(true) + .llmProvider("openai") + .model("gpt-4") + .build(); + when(settingsRepository.loadGeneralSettings()).thenReturn(settings); + + GeneralSettingsVO result = settingsService.getGeneralSettings(); + + assertThat(result.getTheme()).isEqualTo("dark"); + assertThat(result.isCompact()).isTrue(); + assertThat(result.isDesktopNotify()).isTrue(); + assertThat(result.isNotifySound()).isFalse(); + assertThat(result.getSessionTimeout()).isEqualTo(30); + assertThat(result.isRequireLogin()).isTrue(); + assertThat(result.getLlmProvider()).isEqualTo("openai"); + assertThat(result.getModel()).isEqualTo("gpt-4"); + } + + @Test + void saveGeneralSettingsShouldDelegateToRepository() { + GeneralSettingsVO settings = GeneralSettingsVO.builder() + .theme("light") + .compact(false) + .sessionTimeout(60) + .build(); + doNothing().when(settingsRepository).saveGeneralSettings(settings); + + settingsService.saveGeneralSettings(settings); + + verify(settingsRepository).saveGeneralSettings(settings); + } + + @Test + void listDataSourcesShouldReturnAllSources() { + DataSourceVO ds1 = DataSourceVO.builder().key("ds-1").name("Production").type("rocketmq") + .url("localhost:9876").status("connected").build(); + DataSourceVO ds2 = DataSourceVO.builder().key("ds-2").name("Staging").type("rocketmq") + .url("staging:9876").status("disconnected").build(); + when(settingsRepository.findAllDataSources()).thenReturn(Arrays.asList(ds1, ds2)); + + List<DataSourceVO> result = settingsService.listDataSources(); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getName()).isEqualTo("Production"); + assertThat(result.get(0).getStatus()).isEqualTo("connected"); + assertThat(result.get(1).getName()).isEqualTo("Staging"); + assertThat(result.get(1).getStatus()).isEqualTo("disconnected"); + } + + @Test + void listDataSourcesShouldReturnEmptyListWhenNoSources() { + when(settingsRepository.findAllDataSources()).thenReturn(Collections.emptyList()); + + List<DataSourceVO> result = settingsService.listDataSources(); + + assertThat(result).isEmpty(); + } + + @Test + void createDataSourceShouldDelegateToRepository() { + DataSourceVO input = DataSourceVO.builder().name("New DS").type("rocketmq") + .url("new-host:9876").build(); + DataSourceVO saved = DataSourceVO.builder().key("ds-new").name("New DS").type("rocketmq") + .url("new-host:9876").status("connected").build(); + when(settingsRepository.saveDataSource(any(DataSourceVO.class))).thenReturn(saved); + + DataSourceVO result = settingsService.createDataSource(input); + + assertThat(result.getKey()).isEqualTo("ds-new"); + assertThat(result.getName()).isEqualTo("New DS"); + assertThat(result.getStatus()).isEqualTo("connected"); + verify(settingsRepository).saveDataSource(input); + } + + @Test + void updateDataSourceShouldDelegateToRepository() { + DataSourceVO input = DataSourceVO.builder().key("ds-1").name("Updated DS").type("rocketmq") + .url("updated-host:9876").build(); + when(settingsRepository.saveDataSource(any(DataSourceVO.class))).thenReturn(input); + + DataSourceVO result = settingsService.updateDataSource(input); + + assertThat(result.getKey()).isEqualTo("ds-1"); + assertThat(result.getName()).isEqualTo("Updated DS"); + verify(settingsRepository).saveDataSource(input); + } + + @Test + void deleteDataSourceShouldDelegateToRepository() { + doNothing().when(settingsRepository).deleteDataSource("ds-1"); + + settingsService.deleteDataSource("ds-1"); + + verify(settingsRepository).deleteDataSource("ds-1"); + } + + @Test + void testConnectionShouldReturnSuccess() { + DataSourceTestDTO request = DataSourceTestDTO.builder() + .url("localhost:9876") + .type("rocketmq") + .build(); + + DataSourceTestResultVO result = settingsService.testDataSource(request); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getMessage()).isEqualTo("Connection successful"); + } + + @Test + void testConnectionShouldReturnSuccessForAnyInput() { + DataSourceTestDTO request = DataSourceTestDTO.builder() + .url("invalid-host:9999") + .type("unknown") + .auth("bad-auth") + .build(); + + DataSourceTestResultVO result = settingsService.testDataSource(request); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getMessage()).isEqualTo("Connection successful"); + } +}
