Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { expect, test } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config';
import { SERVICE_TYPE } from '../../constant/service';
import {
CERT_FILE,
lookerFormDetails,
Expand All @@ -23,9 +24,11 @@ import {
supersetFormDetails3,
supersetFormDetails4,
} from '../../constant/serviceForm';
import { MessagingServiceClass } from '../../support/entity/service/MessagingServiceClass';
import { UserClass } from '../../support/user/UserClass';
import { createNewPage, redirectToHomePage, uuid } from '../../utils/common';
import { waitForAllLoadersToDisappear } from '../../utils/entity';
import { visitServiceDetailsPage } from '../../utils/service';
import { fillSupersetFormDetails } from '../../utils/serviceFormUtils';

const SERVICE_NAMES = {
Expand Down Expand Up @@ -453,5 +456,103 @@ test.describe(
);
});
});

// Regression coverage for issue #25434:
// Clearing `schemaRegistryTopicSuffixName` on a Kafka connection must
// send an empty string to the backend instead of dropping the field,
// so the cleared value is persisted on reload.
test.describe('Kafka', () => {
const kafkaService = new MessagingServiceClass(
`pw-kafka-empty-suffix-${uuid()}`
);

test.beforeAll(
'Create Kafka service with suffix set',
async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await kafkaService.create(apiContext);
await kafkaService.patch(apiContext, [
{
op: 'add',
path: '/connection/config/schemaRegistryURL',
value: 'http://localhost:8081',
},
{
op: 'add',
path: '/connection/config/schemaRegistryTopicSuffixName',
value: '-value',
},
]);
await afterAction();
}
);

test.afterAll('Cleanup Kafka service', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
await kafkaService.delete(apiContext);
await afterAction();
});

test('should persist empty schemaRegistryTopicSuffixName when the field is cleared', async ({
page,
}) => {
await visitServiceDetailsPage(
page,
{
name: kafkaService.entity.name,
type: SERVICE_TYPE.Messaging,
},
false,
false
);

await page.getByRole('tab', { name: 'Connection' }).click();
await page.getByTestId('edit-connection-button').click();
await waitForAllLoadersToDisappear(page);

const suffixInput = page.locator(
String.raw`#root\/schemaRegistryTopicSuffixName`
);

await expect(suffixInput).toHaveValue('-value');
await suffixInput.clear();
await expect(suffixInput).toHaveValue('');

const patchResponse = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/services/messagingServices') &&
response.request().method() === 'PATCH'
);

await page.getByTestId('submit-btn').click();
await page.getByTestId('submit-btn').click();

const patch = await patchResponse;
const patchBody = patch.request().postDataJSON() as Array<{
op: string;
path: string;
value?: unknown;
}>;

const suffixOp = patchBody.find(
(op) => op.path === '/connection/config/schemaRegistryTopicSuffixName'
);

// The fix must send an explicit empty string, not drop the field
// (which would leave the stale "-value" on the server).
expect(suffixOp).toBeDefined();
expect(suffixOp?.value).toBe('');

await waitForAllLoadersToDisappear(page);

// Reopen the edit form and verify the cleared value persisted.
await page.getByTestId('edit-connection-button').click();
await waitForAllLoadersToDisappear(page);

await expect(
page.locator(String.raw`#root\/schemaRegistryTopicSuffixName`)
).toHaveValue('');
});
});
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2025 Collate.
* Licensed 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.
*/

jest.mock(
'../jsons/connectionSchemas/connections/messaging/kafkaConnection.json',
() => ({}),
{ virtual: true }
);
jest.mock(
'../jsons/connectionSchemas/connections/messaging/redpandaConnection.json',
() => ({}),
{ virtual: true }
);
jest.mock(
'../jsons/connectionSchemas/connections/messaging/customMessagingConnection.json',
() => ({}),
{ virtual: true }
);
jest.mock(
'../jsons/connectionSchemas/connections/messaging/kinesisConnection.json',
() => ({}),
{ virtual: true }
);
jest.mock(
'../jsons/connectionSchemas/connections/messaging/pubSubConnection.json',
() => ({}),
{ virtual: true }
);

import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant';
import { MessagingServiceType } from '../generated/entity/services/messagingService';
import { getMessagingConfig } from './MessagingServiceUtils';

describe('MessagingServiceUtils', () => {
it('Kafka uiSchema should include ui:emptyValue for schemaRegistryTopicSuffixName', () => {
const config = getMessagingConfig(MessagingServiceType.Kafka);

expect(config.uiSchema).toMatchObject({
...COMMON_UI_SCHEMA,
schemaRegistryTopicSuffixName: {
'ui:emptyValue': '',
},
});
});

it('Redpanda uiSchema should include ui:emptyValue for schemaRegistryTopicSuffixName', () => {
const config = getMessagingConfig(MessagingServiceType.Redpanda);

expect(config.uiSchema).toMatchObject({
...COMMON_UI_SCHEMA,
schemaRegistryTopicSuffixName: {
'ui:emptyValue': '',
},
});
});

it('non-broker services should not include schemaRegistryTopicSuffixName uiSchema', () => {
const config = getMessagingConfig(MessagingServiceType.Kinesis);

expect(config.uiSchema).not.toHaveProperty('schemaRegistryTopicSuffixName');
});

it('getMessagingConfig should return only common UI schema for invalid types', () => {
const config = getMessagingConfig('' as MessagingServiceType);

expect(config.uiSchema).toEqual({ ...COMMON_UI_SCHEMA });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,26 @@ export const getBrokers = (config: MessagingConnection['config']) => {
return isUndefined(retVal) ? '--' : retVal;
};

const SCHEMA_REGISTRY_SUFFIX_UI_SCHEMA = {
schemaRegistryTopicSuffixName: {
'ui:emptyValue': '',
},
};

export const getMessagingConfig = (type: MessagingServiceType) => {
let schema = {};
const uiSchema = { ...COMMON_UI_SCHEMA };

switch (type) {
case MessagingServiceType.Kafka:
schema = kafkaConnection;
Object.assign(uiSchema, SCHEMA_REGISTRY_SUFFIX_UI_SCHEMA);

break;

case MessagingServiceType.Redpanda:
schema = redpandaConnection;
Object.assign(uiSchema, SCHEMA_REGISTRY_SUFFIX_UI_SCHEMA);

break;

Expand Down
Loading