diff --git a/web/src/services/clusterService.test.ts b/web/src/services/clusterService.test.ts new file mode 100644 index 00000000..a858a727 --- /dev/null +++ b/web/src/services/clusterService.test.ts @@ -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. + */ + +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('../config', () => ({ + USE_MOCK: true, + API_BASE_URL: '/api', +})); + +import { getCluster, listClusters } from './clusterService'; + +describe('clusterService mock clusters', () => { + it('returns defensive copies from cluster detail reads', async () => { + const cluster = await getCluster('cluster-prod'); + const originalBrokerStatus = cluster.brokers[0].status; + const originalProxyConnections = cluster.proxies[0].connections; + const originalNameServerAddr = cluster.nameServers[0].addr; + const originalFlushDiskType = cluster.config.flushDiskType; + const originalFirstTps = cluster.tpsHistory[0]; + + cluster.brokers[0].status = 'offline'; + cluster.proxies[0].connections = 0; + cluster.nameServers[0].addr = '127.0.0.1:9876'; + cluster.config.flushDiskType = 'ASYNC_FLUSH'; + cluster.tpsHistory[0] = 0; + + const fresh = await getCluster('cluster-prod'); + + expect(fresh.brokers[0].status).toBe(originalBrokerStatus); + expect(fresh.proxies[0].connections).toBe(originalProxyConnections); + expect(fresh.nameServers[0].addr).toBe(originalNameServerAddr); + expect(fresh.config.flushDiskType).toBe(originalFlushDiskType); + expect(fresh.tpsHistory[0]).toBe(originalFirstTps); + }); + + it('does not share nested references between list and detail reads', async () => { + const [listed] = await listClusters(); + const detail = await getCluster(listed.id); + + expect(detail).toEqual(listed); + expect(detail).not.toBe(listed); + expect(detail.brokers).not.toBe(listed.brokers); + expect(detail.brokers[0]).not.toBe(listed.brokers[0]); + expect(detail.proxies).not.toBe(listed.proxies); + expect(detail.nameServers).not.toBe(listed.nameServers); + expect(detail.config).not.toBe(listed.config); + expect(detail.tpsHistory).not.toBe(listed.tpsHistory); + }); +}); diff --git a/web/src/services/clusterService.ts b/web/src/services/clusterService.ts index d758dc6a..fd9fcc75 100644 --- a/web/src/services/clusterService.ts +++ b/web/src/services/clusterService.ts @@ -8,33 +8,37 @@ const mockCertStore: K8sCertInfo[] = mockK8sCerts.map((cert) => ({ san: [...cert.san], })); +function copyCluster(cluster: ClusterInfo): ClusterInfo { + return { + id: cluster.id, + name: cluster.name, + nsClusterName: cluster.nsClusterName, + type: cluster.type, + endpoint: cluster.endpoint, + status: cluster.status, + version: cluster.version, + brokers: cluster.brokers.map((broker) => ({ ...broker })), + proxies: cluster.proxies.map((proxy) => ({ ...proxy })), + nameServers: cluster.nameServers.map((nameServer) => ({ ...nameServer })), + config: { ...cluster.config }, + topicCount: cluster.topicCount, + groupCount: cluster.groupCount, + tpsHistory: [...cluster.tpsHistory], + }; +} + export async function listClusters(): Promise { if (USE_MOCK) { - return clusters.map((c) => ({ - id: c.id, - name: c.name, - nsClusterName: c.nsClusterName, - type: c.type, - endpoint: c.endpoint, - status: c.status, - version: c.version, - brokers: c.brokers.map((broker) => ({ ...broker })), - proxies: c.proxies.map((proxy) => ({ ...proxy })), - nameServers: c.nameServers.map((nameServer) => ({ ...nameServer })), - config: { ...c.config }, - topicCount: c.topicCount, - groupCount: c.groupCount, - tpsHistory: [...c.tpsHistory], - })); + return clusters.map(copyCluster); } return clusterApi.listClusters(); } -export async function getCluster(id: string) { +export async function getCluster(id: string): Promise { if (USE_MOCK) { - const c = clusters.find((c) => c.id === id); - if (!c) throw new Error('Cluster not found'); - return c; + const cluster = clusters.find((item) => item.id === id); + if (!cluster) throw new Error('Cluster not found'); + return copyCluster(cluster); } return clusterApi.getCluster(id); }