diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java index 75044b84..4daf96aa 100644 --- a/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java +++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/CreateCertDTO.java @@ -16,6 +16,8 @@ */ package com.rocketmq.studio.cluster.k8s; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,10 +30,16 @@ @NoArgsConstructor @AllArgsConstructor public class CreateCertDTO { + @NotBlank(message = "name is required") private String name; + @NotBlank(message = "namespace is required") private String namespace; + @NotBlank(message = "cluster is required") private String cluster; + @NotBlank(message = "type is required") + @Pattern(regexp = "TLS|mTLS|ServiceAccount", message = "type must be one of TLS, mTLS, ServiceAccount") private String type; + @NotBlank(message = "issuer is required") private String issuer; private List san; } diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java index e6790771..ae3f490f 100644 --- a/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java +++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/DeleteCertDTO.java @@ -16,6 +16,7 @@ */ package com.rocketmq.studio.cluster.k8s; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -26,5 +27,6 @@ @NoArgsConstructor @AllArgsConstructor public class DeleteCertDTO { + @NotBlank(message = "id is required") private String id; } diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java index 43bc0f8c..6aa5ae49 100644 --- a/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java +++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/K8sCertController.java @@ -17,6 +17,7 @@ package com.rocketmq.studio.cluster.k8s; import com.rocketmq.studio.common.domain.Result; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -39,22 +40,22 @@ public Result> listCerts() { } @PostMapping("/create") - public Result createCert(@RequestBody CreateCertDTO command) { + public Result createCert(@Valid @RequestBody CreateCertDTO command) { return Result.ok(k8sCertService.createCert(command)); } @PostMapping("/update") - public Result updateCert(@RequestBody UpdateCertDTO command) { + public Result updateCert(@Valid @RequestBody UpdateCertDTO command) { return Result.ok(k8sCertService.updateCert(command)); } @PostMapping("/renew") - public Result renewCert(@RequestBody RenewCertDTO command) { + public Result renewCert(@Valid @RequestBody RenewCertDTO command) { return Result.ok(k8sCertService.renewCert(command)); } @PostMapping("/delete") - public Result deleteCert(@RequestBody DeleteCertDTO command) { + public Result deleteCert(@Valid @RequestBody DeleteCertDTO command) { k8sCertService.deleteCert(command); return Result.ok(); } diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java index d0877113..9a30924a 100644 --- a/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java +++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/RenewCertDTO.java @@ -16,6 +16,7 @@ */ package com.rocketmq.studio.cluster.k8s; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -26,5 +27,6 @@ @NoArgsConstructor @AllArgsConstructor public class RenewCertDTO { + @NotBlank(message = "id is required") private String id; } diff --git a/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java b/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java index f46b0a0d..c5a0dca3 100644 --- a/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java +++ b/server/src/main/java/com/rocketmq/studio/cluster/k8s/UpdateCertDTO.java @@ -16,6 +16,8 @@ */ package com.rocketmq.studio.cluster.k8s; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,10 +30,12 @@ @NoArgsConstructor @AllArgsConstructor public class UpdateCertDTO { + @NotBlank(message = "id is required") private String id; private String name; private String namespace; private String cluster; + @Pattern(regexp = "TLS|mTLS|ServiceAccount", message = "type must be one of TLS, mTLS, ServiceAccount") private String type; private String issuer; private List san; diff --git a/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java index 69cca770..ac49a161 100644 --- a/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java +++ b/server/src/test/java/com/rocketmq/studio/cluster/k8s/K8sCertControllerTest.java @@ -33,6 +33,7 @@ import java.util.List; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -138,6 +139,89 @@ void createCertShouldAcceptMinimalCommand() throws Exception { .andExpect(jsonPath("$.data.id").value("cert-min")); } + @Test + void createCertShouldRejectMissingName() throws Exception { + mockMvc.perform(post("/api/k8s-certs/create") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "namespace": "default", + "cluster": "test-cluster", + "type": "TLS", + "issuer": "vault" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value("name is required")); + + verifyNoInteractions(k8sCertService); + } + + @Test + void createCertShouldRejectUnsupportedType() throws Exception { + mockMvc.perform(post("/api/k8s-certs/create") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "new-cert", + "namespace": "default", + "cluster": "test-cluster", + "type": "PEM", + "issuer": "vault" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value("type must be one of TLS, mTLS, ServiceAccount")); + + verifyNoInteractions(k8sCertService); + } + + @Test + void updateCertShouldRejectMissingId() throws Exception { + mockMvc.perform(post("/api/k8s-certs/update") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "updated-cert" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value("id is required")); + + verifyNoInteractions(k8sCertService); + } + + @Test + void renewCertShouldRejectBlankId() throws Exception { + mockMvc.perform(post("/api/k8s-certs/renew") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "id": " " + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value("id is required")); + + verifyNoInteractions(k8sCertService); + } + + @Test + void deleteCertShouldRejectMissingId() throws Exception { + mockMvc.perform(post("/api/k8s-certs/delete") + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400)) + .andExpect(jsonPath("$.message").value("id is required")); + + verifyNoInteractions(k8sCertService); + } + private K8sCertVO buildCert(String id, String name, CertType type, CertStatus status) { K8sCertVO cert = K8sCertVO.builder() .name(name) diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx new file mode 100644 index 00000000..0e7381d6 --- /dev/null +++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx @@ -0,0 +1,98 @@ +/* + * 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. + */ + +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { App } from 'antd'; +import type { K8sCertInfo } from '../../../api/cluster'; +import { listK8sCerts } from '../../../services/clusterService'; +import K8sCertsPage from '../certs'; + +vi.mock('../../../services/clusterService', () => ({ + createK8sCert: vi.fn(), + deleteK8sCert: vi.fn(), + listK8sCerts: vi.fn(), + renewK8sCert: vi.fn(), + updateK8sCert: vi.fn(), +})); + +const certs: K8sCertInfo[] = [ + { + id: 'cert-prod', + name: 'rocketmq-prod-tls', + namespace: 'rocketmq', + cluster: 'prod-cluster', + type: 'TLS', + issuer: 'kubernetes-ca', + notBefore: '2026-01-01T00:00:00Z', + notAfter: '2027-01-01T00:00:00Z', + status: 'valid', + daysRemaining: 365, + san: ['broker.prod.example.com'], + }, + { + id: 'cert-staging', + name: 'rocketmq-staging-tls', + namespace: 'rocketmq', + cluster: 'staging-cluster', + type: 'TLS', + issuer: 'kubernetes-ca', + notBefore: '2026-01-01T00:00:00Z', + notAfter: '2027-01-01T00:00:00Z', + status: 'valid', + daysRemaining: 365, + san: ['broker.staging.example.com'], + }, +]; + +beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}); + +describe('K8sCertsPage', () => { + beforeEach(() => { + vi.mocked(listK8sCerts).mockResolvedValue(certs); + }); + + it('trims certificate search text before filtering', async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await screen.findByText('rocketmq-prod-tls'); + await user.type(screen.getByPlaceholderText('搜索证书名称或集群'), ' prod-cluster {enter}'); + + expect(screen.getByText('rocketmq-prod-tls')).toBeInTheDocument(); + expect(screen.queryByText('rocketmq-staging-tls')).not.toBeInTheDocument(); + }); +}); diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx index dfa0da2c..a9408b13 100644 --- a/web/src/pages/cluster/certs.tsx +++ b/web/src/pages/cluster/certs.tsx @@ -129,11 +129,12 @@ const K8sCertsPage = () => { } }; + const normalizedCertSearch = certSearch.trim().toLowerCase(); const filteredCerts = certs.filter((cert) => { const matchSearch = - !certSearch || - cert.name.toLowerCase().includes(certSearch.toLowerCase()) || - cert.cluster.toLowerCase().includes(certSearch.toLowerCase()); + !normalizedCertSearch || + cert.name.toLowerCase().includes(normalizedCertSearch) || + cert.cluster.toLowerCase().includes(normalizedCertSearch); const matchType = !certTypeFilter || cert.type === certTypeFilter; return matchSearch && matchType; });