kafka-ui/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts

924 lines
24 KiB
TypeScript

import {
MessageSchemaSourceEnum,
SortOrder,
TopicColumnsToSort,
ConfigSource,
} from 'generated-sources';
import reducer, {
clearTopicsMessages,
setTopicsSearch,
setTopicsOrderBy,
fetchTopicConsumerGroups,
fetchTopicMessageSchema,
recreateTopic,
createTopic,
deleteTopic,
fetchTopicsList,
fetchTopicDetails,
fetchTopicConfig,
updateTopic,
updateTopicPartitionsCount,
updateTopicReplicationFactor,
deleteTopics,
} from 'redux/reducers/topics/topicsSlice';
import {
createTopicPayload,
createTopicResponsePayload,
} from 'components/Topics/New/__test__/fixtures';
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
import fetchMock from 'fetch-mock-jest';
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
import { getTypeAndPayload } from 'lib/testHelpers';
import {
alertAdded,
showSuccessAlert,
} from 'redux/reducers/alerts/alertsSlice';
const topic = {
name: 'topic',
id: 'id',
};
const messageSchema = {
key: {
name: 'key',
source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
schema: `{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/myURI.schema.json",
"title": "TestRecord",
"type": "object",
"additionalProperties": false,
"properties": {
"f1": {
"type": "integer"
},
"f2": {
"type": "string"
},
"schema": {
"type": "string"
}
}
}
`,
},
value: {
name: 'value',
source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
schema: `{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/myURI1.schema.json",
"title": "TestRecord",
"type": "object",
"additionalProperties": false,
"properties": {
"f1": {
"type": "integer"
},
"f2": {
"type": "string"
},
"schema": {
"type": "string"
}
}
}
`,
},
};
const config = [
{
name: 'compression.type',
value: 'producer',
defaultValue: 'producer',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'compression.type',
value: 'producer',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'compression.type',
value: 'producer',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
];
const details = {
name: 'local',
internal: false,
partitionCount: 1,
replicationFactor: 1,
replicas: 1,
inSyncReplicas: 1,
segmentSize: 0,
segmentCount: 0,
cleanUpPolicy: 'DELETE',
partitions: [
{
partition: 0,
leader: 1,
replicas: [{ broker: 1, leader: false, inSync: true }],
offsetMax: 0,
offsetMin: 0,
},
],
bytesInPerSec: 0.1,
bytesOutPerSec: 0.1,
};
let state = {
byName: {
[topic.name]: topic,
},
allNames: [topic.name],
messages: [],
totalPages: 1,
search: '',
orderBy: null,
sortOrder: SortOrder.ASC,
consumerGroups: [],
};
const clusterName = 'local';
describe('topics Slice', () => {
describe('topics reducer', () => {
describe('fetch topic details', () => {
it('fetchTopicDetails/fulfilled', () => {
expect(
reducer(state, {
type: fetchTopicDetails.fulfilled,
payload: {
clusterName,
topicName: topic.name,
topicDetails: details,
},
})
).toEqual({
...state,
byName: {
[topic.name]: {
...topic,
...details,
},
},
allNames: [topic.name],
});
});
});
describe('fetch topics', () => {
it('fetchTopicsList/fulfilled', () => {
expect(
reducer(state, {
type: fetchTopicsList.fulfilled,
payload: { clusterName, topicName: topic.name },
})
).toEqual({
...state,
byName: { topic },
allNames: [topic.name],
});
});
});
describe('fetch topic config', () => {
it('fetchTopicConfig/fulfilled', () => {
expect(
reducer(state, {
type: fetchTopicConfig.fulfilled,
payload: {
clusterName,
topicName: topic.name,
topicConfig: config,
},
})
).toEqual({
...state,
byName: {
[topic.name]: {
...topic,
config: config.map((conf) => ({ ...conf })),
},
},
allNames: [topic.name],
});
});
});
describe('update topic', () => {
it('updateTopic/fulfilled', () => {
const updatedTopic = {
name: 'topic',
id: 'id',
partitions: 1,
};
expect(
reducer(state, {
type: updateTopic.fulfilled,
payload: {
clusterName,
topicName: topic.name,
topic: updatedTopic,
},
})
).toEqual({
...state,
byName: {
[topic.name]: {
...updatedTopic,
},
},
});
});
});
describe('delete topic', () => {
it('deleteTopic/fulfilled', () => {
expect(
reducer(state, {
type: deleteTopic.fulfilled,
payload: { clusterName, topicName: topic.name },
})
).toEqual({
...state,
byName: {},
allNames: [],
});
});
it('clearTopicsMessages/fulfilled', () => {
expect(
reducer(state, {
type: clearTopicsMessages.fulfilled,
payload: { clusterName, topicName: topic.name },
})
).toEqual({
...state,
messages: [],
});
});
it('recreateTopic/fulfilled', () => {
expect(
reducer(state, {
type: recreateTopic.fulfilled,
payload: { topic, topicName: topic.name },
})
).toEqual({
...state,
byName: {
[topic.name]: topic,
},
});
});
});
describe('create topics', () => {
it('createTopic/fulfilled', () => {
expect(
reducer(state, {
type: createTopic.fulfilled,
payload: { clusterName, data: createTopicPayload },
})
).toEqual({
...state,
});
});
});
describe('search topics', () => {
it('setTopicsSearch', () => {
expect(
reducer(state, {
type: setTopicsSearch,
payload: 'test',
})
).toEqual({
...state,
search: 'test',
});
});
});
describe('order topics', () => {
it('setTopicsOrderBy', () => {
expect(
reducer(state, {
type: setTopicsOrderBy,
payload: TopicColumnsToSort.NAME,
})
).toEqual({
...state,
orderBy: TopicColumnsToSort.NAME,
});
});
});
describe('topic consumer groups', () => {
it('fetchTopicConsumerGroups/fulfilled', () => {
expect(
reducer(state, {
type: fetchTopicConsumerGroups.fulfilled,
payload: {
clusterName,
topicName: topic.name,
consumerGroups: consumerGroupPayload,
},
})
).toEqual({
...state,
byName: {
[topic.name]: {
...topic,
...consumerGroupPayload,
},
},
});
});
});
describe('message sending', () => {
it('fetchTopicMessageSchema/fulfilled', () => {
state = {
byName: {
[topic.name]: topic,
},
allNames: [topic.name],
messages: [],
totalPages: 1,
search: '',
orderBy: null,
sortOrder: SortOrder.ASC,
consumerGroups: [],
};
expect(
reducer(state, {
type: fetchTopicMessageSchema.fulfilled,
payload: { topicName: topic.name, schema: messageSchema },
}).byName
).toEqual({
[topic.name]: { ...topic, messageSchema },
});
});
});
});
describe('Thunks', () => {
const store = mockStoreCreator;
const topicName = topic.name;
afterEach(() => {
fetchMock.restore();
store.clearActions();
});
describe('fetchTopicsList', () => {
const topicResponse = {
pageCount: 1,
topics: [createTopicResponsePayload],
};
it('fetchTopicsList/fulfilled', async () => {
fetchMock.getOnce(`/api/clusters/${clusterName}/topics`, topicResponse);
await store.dispatch(fetchTopicsList({ clusterName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicsList.pending.type },
{
type: fetchTopicsList.fulfilled.type,
payload: { ...topicResponse },
},
]);
});
it('fetchTopicsList/rejected', async () => {
fetchMock.getOnce(`/api/clusters/${clusterName}/topics`, 404);
await store.dispatch(fetchTopicsList({ clusterName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicsList.pending.type },
{
type: fetchTopicsList.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics`,
message: undefined,
},
},
]);
});
});
describe('fetchTopicDetails', () => {
it('fetchTopicDetails/fulfilled', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
details
);
await store.dispatch(fetchTopicDetails({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicDetails.pending.type },
{
type: fetchTopicDetails.fulfilled.type,
payload: { topicDetails: { ...details }, topicName },
},
]);
});
it('fetchTopicDetails/rejected', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
404
);
await store.dispatch(fetchTopicDetails({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicDetails.pending.type },
{
type: fetchTopicDetails.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}`,
message: undefined,
},
},
]);
});
});
describe('fetchTopicConfig', () => {
it('fetchTopicConfig/fulfilled', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/topics/${topicName}/config`,
config
);
await store.dispatch(fetchTopicConfig({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicConfig.pending.type },
{
type: fetchTopicConfig.fulfilled.type,
payload: {
topicConfig: config,
topicName,
},
},
]);
});
it('fetchTopicConfig/rejected', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/topics/${topicName}/config`,
404
);
await store.dispatch(fetchTopicConfig({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicConfig.pending.type },
{
type: fetchTopicConfig.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}/config`,
message: undefined,
},
},
]);
});
});
describe('deleteTopic', () => {
it('deleteTopic/fulfilled', async () => {
fetchMock.deleteOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
topicName
);
await store.dispatch(deleteTopic({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: deleteTopic.pending.type },
{
type: deleteTopic.fulfilled.type,
payload: { topicName },
},
]);
});
it('deleteTopic/rejected', async () => {
fetchMock.deleteOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
404
);
await store.dispatch(deleteTopic({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: deleteTopic.pending.type },
{
type: deleteTopic.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}`,
message: undefined,
},
},
]);
});
});
describe('deleteTopics', () => {
it('deleteTopics/fulfilled', async () => {
fetchMock.delete(`/api/clusters/${clusterName}/topics/${topicName}`, [
topicName,
'topic2',
]);
await store.dispatch(
deleteTopics({ clusterName, topicNames: [topicName, 'topic2'] })
);
expect(getTypeAndPayload(store)).toEqual([
{ type: deleteTopics.pending.type },
{ type: deleteTopic.pending.type },
{ type: deleteTopic.pending.type },
{ type: deleteTopics.fulfilled.type },
]);
});
});
describe('recreateTopic', () => {
const recreateResponse = {
cleanUpPolicy: 'DELETE',
inSyncReplicas: 1,
internal: false,
name: topicName,
partitionCount: 1,
partitions: undefined,
replicas: 1,
replicationFactor: 1,
segmentCount: 0,
segmentSize: 0,
underReplicatedPartitions: undefined,
};
it('recreateTopic/fulfilled', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
recreateResponse
);
await store.dispatch(recreateTopic({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: recreateTopic.pending.type },
{
type: recreateTopic.fulfilled.type,
payload: { [topicName]: { ...recreateResponse } },
},
]);
});
it('recreateTopic/rejected', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
404
);
await store.dispatch(recreateTopic({ clusterName, topicName }));
expect(getTypeAndPayload(store)).toEqual([
{ type: recreateTopic.pending.type },
{
type: recreateTopic.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}`,
message: undefined,
},
},
]);
});
});
describe('fetchTopicConsumerGroups', () => {
const consumerGroups = [
{
groupId: 'groupId1',
members: 0,
topics: 1,
simple: false,
partitionAssignor: '',
coordinator: {
id: 1,
port: undefined,
host: 'host',
},
messagesBehind: undefined,
state: undefined,
},
{
groupId: 'groupId2',
members: 0,
topics: 1,
simple: false,
partitionAssignor: '',
coordinator: {
id: 1,
port: undefined,
host: 'host',
},
messagesBehind: undefined,
state: undefined,
},
];
it('fetchTopicConsumerGroups/fulfilled', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
consumerGroups
);
await store.dispatch(
fetchTopicConsumerGroups({ clusterName, topicName })
);
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicConsumerGroups.pending.type },
{
type: fetchTopicConsumerGroups.fulfilled.type,
payload: { consumerGroups, topicName },
},
]);
});
it('fetchTopicConsumerGroups/rejected', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
404
);
await store.dispatch(
fetchTopicConsumerGroups({ clusterName, topicName })
);
expect(getTypeAndPayload(store)).toEqual([
{ type: fetchTopicConsumerGroups.pending.type },
{
type: fetchTopicConsumerGroups.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}/consumer-groups`,
message: undefined,
},
},
]);
});
});
describe('updateTopicPartitionsCount', () => {
const RealDate = Date.now;
beforeAll(() => {
global.Date.now = jest.fn(() =>
new Date('2019-04-07T10:20:30Z').getTime()
);
});
afterAll(() => {
global.Date.now = RealDate;
});
it('updateTopicPartitionsCount/fulfilled', async () => {
fetchMock.patchOnce(
`/api/clusters/${clusterName}/topics/${topicName}/partitions`,
{ message: 'success' }
);
await store.dispatch(
updateTopicPartitionsCount({
clusterName,
topicName,
partitions: 1,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: updateTopicPartitionsCount.pending.type },
{ type: showSuccessAlert.pending.type },
{
type: alertAdded.type,
payload: {
id: 'message-topic-local-1',
title: '',
type: 'success',
createdAt: global.Date.now(),
message: 'Number of partitions successfully increased!',
},
},
{ type: fetchTopicDetails.pending.type },
{ type: showSuccessAlert.fulfilled.type },
{
type: updateTopicPartitionsCount.fulfilled.type,
},
]);
});
it('updateTopicPartitionsCount/rejected', async () => {
fetchMock.patchOnce(
`/api/clusters/${clusterName}/topics/${topicName}/partitions`,
404
);
await store.dispatch(
updateTopicPartitionsCount({
clusterName,
topicName,
partitions: 1,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: updateTopicPartitionsCount.pending.type },
{
type: updateTopicPartitionsCount.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
message: undefined,
},
},
]);
});
});
describe('updateTopicReplicationFactor', () => {
it('updateTopicReplicationFactor/fulfilled', async () => {
fetchMock.patchOnce(
`/api/clusters/${clusterName}/topics/${topicName}/replications`,
{ message: 'success' }
);
await store.dispatch(
updateTopicReplicationFactor({
clusterName,
topicName,
replicationFactor: 1,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: updateTopicReplicationFactor.pending.type },
{
type: updateTopicReplicationFactor.fulfilled.type,
},
]);
});
it('updateTopicReplicationFactor/rejected', async () => {
fetchMock.patchOnce(
`/api/clusters/${clusterName}/topics/${topicName}/replications`,
404
);
await store.dispatch(
updateTopicReplicationFactor({
clusterName,
topicName,
replicationFactor: 1,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: updateTopicReplicationFactor.pending.type },
{
type: updateTopicReplicationFactor.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}/replications`,
message: undefined,
},
},
]);
});
});
describe('createTopic', () => {
const newTopic = {
name: 'newTopic',
partitions: 0,
replicationFactor: 0,
minInsyncReplicas: 0,
cleanupPolicy: 'DELETE',
retentionMs: 1,
retentionBytes: 1,
maxMessageBytes: 1,
customParams: [
{
name: '',
value: '',
},
],
};
it('createTopic/fulfilled', async () => {
fetchMock.postOnce(`/api/clusters/${clusterName}/topics`, {
message: 'success',
});
await store.dispatch(
createTopic({
clusterName,
data: newTopic,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: createTopic.pending.type },
{
type: createTopic.fulfilled.type,
},
]);
});
it('createTopic/rejected', async () => {
fetchMock.postOnce(`/api/clusters/${clusterName}/topics`, 404);
await store.dispatch(
createTopic({
clusterName,
data: newTopic,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: createTopic.pending.type },
{
type: createTopic.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics`,
message: undefined,
},
},
]);
});
});
describe('updateTopic', () => {
const updateTopicResponse = {
name: topicName,
partitions: 0,
replicationFactor: 0,
minInsyncReplicas: 0,
cleanupPolicy: 'DELETE',
retentionMs: 0,
retentionBytes: 0,
maxMessageBytes: 0,
customParams: {
byIndex: {},
allIndexes: [],
},
};
it('updateTopic/fulfilled', async () => {
fetchMock.patchOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
createTopicResponsePayload
);
await store.dispatch(
updateTopic({
clusterName,
topicName,
form: updateTopicResponse,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: updateTopic.pending.type },
{
type: updateTopic.fulfilled.type,
payload: { [topicName]: { ...createTopicResponsePayload } },
},
]);
});
it('updateTopic/rejected', async () => {
fetchMock.patchOnce(
`/api/clusters/${clusterName}/topics/${topicName}`,
404
);
await store.dispatch(
updateTopic({
clusterName,
topicName,
form: updateTopicResponse,
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: updateTopic.pending.type },
{
type: updateTopic.rejected.type,
payload: {
status: 404,
statusText: 'Not Found',
url: `/api/clusters/${clusterName}/topics/${topicName}`,
message: undefined,
},
},
]);
});
});
describe('clearTopicsMessages', () => {
it('clearTopicsMessages/fulfilled', async () => {
fetchMock.deleteOnce(
`/api/clusters/${clusterName}/topics/${topicName}/messages`,
[topicName, 'topic2']
);
await store.dispatch(
clearTopicsMessages({
clusterName,
topicNames: [topicName, 'topic2'],
})
);
expect(getTypeAndPayload(store)).toEqual([
{ type: clearTopicsMessages.pending.type },
{ type: clearTopicsMessages.fulfilled.type },
]);
});
});
});
});