diff --git a/web/src/services/topicService.test.ts b/web/src/services/topicService.test.ts new file mode 100644 index 00000000..e07f2454 --- /dev/null +++ b/web/src/services/topicService.test.ts @@ -0,0 +1,59 @@ +/* + * 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'; +import { getTopicConsumers, getTopicRoutes, listTopics } from './topicService'; + +vi.mock('../config', () => ({ + API_BASE_URL: '/api', + USE_MOCK: true, +})); + +describe('topic service mock data', () => { + it('returns copied topic rows', async () => { + const first = await listTopics({ search: 'order-create' }); + expect(first[0].name).toBe('order-create'); + + first[0].name = 'mutated-topic'; + + const second = await listTopics({ search: 'order-create' }); + expect(second[0].name).toBe('order-create'); + expect(second[0]).not.toBe(first[0]); + }); + + it('returns copied topic route rows', async () => { + const first = await getTopicRoutes('order-create'); + expect(first[0].brokerName).toBe('broker-a-0'); + + first[0].brokerName = 'mutated-broker'; + + const second = await getTopicRoutes('order-create'); + expect(second[0].brokerName).toBe('broker-a-0'); + expect(second[0]).not.toBe(first[0]); + }); + + it('returns copied topic consumer rows', async () => { + const first = await getTopicConsumers('order-create'); + expect(first[0].group).toBe('GID_order_service'); + + first[0].group = 'mutated-group'; + + const second = await getTopicConsumers('order-create'); + expect(second[0].group).toBe('GID_order_service'); + expect(second[0]).not.toBe(first[0]); + }); +}); diff --git a/web/src/services/topicService.ts b/web/src/services/topicService.ts index 39d25cc1..78cc8c46 100644 --- a/web/src/services/topicService.ts +++ b/web/src/services/topicService.ts @@ -10,6 +10,11 @@ import type { } from '../api/metadata'; import { topics as mockTopics, topicRoutes, topicConsumers } from '../mock/topics'; +const cloneTopic = (topic: Topic): Topic => ({ ...topic }); +const cloneRoutes = (routes: BrokerRoute[]): BrokerRoute[] => routes.map((route) => ({ ...route })); +const cloneConsumers = (consumers: ConsumerGroupInfo[]): ConsumerGroupInfo[] => + consumers.map((consumer) => ({ ...consumer })); + export async function listTopics(params?: TopicQuery): Promise { if (USE_MOCK) { let result = [...mockTopics]; @@ -19,7 +24,7 @@ export async function listTopics(params?: TopicQuery): Promise { } if (params?.type) result = result.filter((t) => t.type === params.type); if (params?.clusterId) result = result.filter((t) => t.clusterId === params.clusterId); - return result as unknown as Topic[]; + return (result as unknown as Topic[]).map(cloneTopic); } return metadataApi.listTopics(params); } @@ -35,7 +40,7 @@ export async function createTopic(data: Partial): Promise { consumerGroupCount: 0, } as unknown as Topic; mockTopics.unshift(topic as never); - return topic; + return cloneTopic(topic); } return metadataApi.createTopic(data); } @@ -45,7 +50,7 @@ export async function updateTopic(data: Partial): Promise { const idx = mockTopics.findIndex((t) => t.name === data.name); if (idx < 0) throw new Error(`Topic not found: ${data.name}`); Object.assign(mockTopics[idx], data, { updatedAt: new Date().toISOString() }); - return mockTopics[idx] as unknown as Topic; + return cloneTopic(mockTopics[idx] as unknown as Topic); } return metadataApi.updateTopic(data); } @@ -67,12 +72,12 @@ export async function batchDeleteTopics(names: string[]): Promise { } export async function getTopicRoutes(name: string): Promise { - if (USE_MOCK) return (topicRoutes[name] as unknown as BrokerRoute[]) ?? []; + if (USE_MOCK) return cloneRoutes((topicRoutes[name] as unknown as BrokerRoute[]) ?? []); return metadataApi.getTopicRoutes(name); } export async function getTopicConsumers(name: string): Promise { - if (USE_MOCK) return (topicConsumers[name] as unknown as ConsumerGroupInfo[]) ?? []; + if (USE_MOCK) return cloneConsumers((topicConsumers[name] as unknown as ConsumerGroupInfo[]) ?? []); return metadataApi.getTopicConsumers(name); }