From 350a0362db17dd7b705506ac4edf1e23d98eef23 Mon Sep 17 00:00:00 2001 From: liuhy Date: Sun, 26 Jul 2026 00:54:14 -0700 Subject: [PATCH] [Studio] Return consumer mock copies --- web/src/services/consumerService.test.ts | 83 ++++++++++++++++++++++++ web/src/services/consumerService.ts | 40 ++++++++++-- 2 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 web/src/services/consumerService.test.ts diff --git a/web/src/services/consumerService.test.ts b/web/src/services/consumerService.test.ts new file mode 100644 index 00000000..603861d1 --- /dev/null +++ b/web/src/services/consumerService.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { + createConsumerGroup, + getConsumerGroup, + getConsumerProgress, + getConsumerSubscriptions, + listConsumerGroups, +} from './consumerService'; + +vi.mock('../config', () => ({ + API_BASE_URL: '/api', + USE_MOCK: true, +})); + +describe('consumer service mock data', () => { + it('returns copied consumer group rows', async () => { + const first = await listConsumerGroups({ search: 'cg-order-notify' }); + expect(first[0].name).toBe('cg-order-notify'); + + first[0].name = 'mutated-group'; + first[0].subscribedTopics.push('mutated-topic'); + first[0].instances[0].topicLag['order-create'] = 999999; + + const second = await listConsumerGroups({ search: 'cg-order-notify' }); + expect(second[0].name).toBe('cg-order-notify'); + expect(second[0].subscribedTopics).not.toContain('mutated-topic'); + expect(second[0].instances[0].topicLag['order-create']).toBe(180); + expect(second[0]).not.toBe(first[0]); + expect(second[0].instances[0]).not.toBe(first[0].instances[0]); + }); + + it('returns copied consumer group details', async () => { + const first = await getConsumerGroup('cg-order-notify'); + first.instances[0].subscribedTopics.push('mutated-topic'); + + const second = await getConsumerGroup('cg-order-notify'); + expect(second.instances[0].subscribedTopics).not.toContain('mutated-topic'); + expect(second.instances[0]).not.toBe(first.instances[0]); + }); + + it('returns copied progress and subscription rows', async () => { + const firstProgress = await getConsumerProgress('cg-order-notify'); + const firstSubscriptions = await getConsumerSubscriptions('cg-order-notify'); + firstProgress[0].broker = 'mutated-broker'; + firstSubscriptions[0].topic = 'mutated-topic'; + + const secondProgress = await getConsumerProgress('cg-order-notify'); + const secondSubscriptions = await getConsumerSubscriptions('cg-order-notify'); + expect(secondProgress[0].broker).not.toBe('mutated-broker'); + expect(secondSubscriptions[0].topic).not.toBe('mutated-topic'); + expect(secondProgress[0]).not.toBe(firstProgress[0]); + expect(secondSubscriptions[0]).not.toBe(firstSubscriptions[0]); + }); + + it('returns a copy after creating consumer groups', async () => { + const created = await createConsumerGroup({ + name: 'cg-created-copy-test', + subscribedTopics: ['created-topic'], + }); + created.subscribedTopics.push('mutated-topic'); + + const detail = await getConsumerGroup('cg-created-copy-test'); + expect(detail.subscribedTopics).toEqual(['created-topic']); + expect(detail).not.toBe(created); + }); +}); diff --git a/web/src/services/consumerService.ts b/web/src/services/consumerService.ts index 34bee234..c1859dce 100644 --- a/web/src/services/consumerService.ts +++ b/web/src/services/consumerService.ts @@ -12,6 +12,30 @@ import { mockConsumerGroups, mockQueueProgress, mockSubscriptions } from '../moc const consumerGroupsState = mockConsumerGroups as unknown as ConsumerGroup[]; +function copyConsumerInstance(instance: ConsumerGroup['instances'][number]): ConsumerGroup['instances'][number] { + return { + ...instance, + subscribedTopics: [...instance.subscribedTopics], + topicLag: { ...instance.topicLag }, + }; +} + +function copyConsumerGroup(group: ConsumerGroup): ConsumerGroup { + return { + ...group, + subscribedTopics: [...group.subscribedTopics], + instances: group.instances.map(copyConsumerInstance), + }; +} + +function copyQueueProgress(progress: QueueProgress): QueueProgress { + return { ...progress }; +} + +function copySubscription(subscription: SubscriptionEntry): SubscriptionEntry { + return { ...subscription }; +} + export async function listConsumerGroups(params?: ConsumerGroupQuery): Promise { if (USE_MOCK) { let result = [...consumerGroupsState]; @@ -20,13 +44,15 @@ export async function listConsumerGroups(params?: ConsumerGroupQuery): Promise g.name.toLowerCase().includes(kw)); } - return result; + return result.map(copyConsumerGroup); } return metadataApi.listConsumerGroups(params); } export async function getConsumerProgress(name: string): Promise { - if (USE_MOCK) return (mockQueueProgress[name] as unknown as QueueProgress[]) ?? []; + if (USE_MOCK) { + return ((mockQueueProgress[name] as unknown as QueueProgress[]) ?? []).map(copyQueueProgress); + } return metadataApi.getConsumerProgress(name); } @@ -34,13 +60,15 @@ export async function getConsumerGroup(name: string): Promise item.name === name); if (!group) throw new Error(`Consumer group not found: ${name}`); - return group as unknown as ConsumerGroupDetail; + return copyConsumerGroup(group as unknown as ConsumerGroupDetail) as ConsumerGroupDetail; } return metadataApi.getConsumerGroup(name); } export async function getConsumerSubscriptions(name: string): Promise { - if (USE_MOCK) return (mockSubscriptions[name] as unknown as SubscriptionEntry[]) ?? []; + if (USE_MOCK) { + return ((mockSubscriptions[name] as unknown as SubscriptionEntry[]) ?? []).map(copySubscription); + } return metadataApi.getConsumerSubscriptions(name); } @@ -62,9 +90,9 @@ export async function createConsumerGroup(data: Partial): Promise updatedAt: now, delaySeconds: 0, instances: [], - }; + } as ConsumerGroup; mockConsumerGroups.unshift(group as never); - return group as ConsumerGroup; + return copyConsumerGroup(group); } return metadataApi.createConsumerGroup(data); }