Skip to content

Commit 5c27d60

Browse files
committed
fix: fix: handle incorrect response for operations
1 parent 8ff173e commit 5c27d60

File tree

3 files changed

+171
-1
lines changed

3 files changed

+171
-1
lines changed

src/store/reducers/operations.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ import {api} from './api';
1010

1111
const DEFAULT_PAGE_SIZE = 20;
1212

13+
// Validate and normalize the response to ensure it has proper structure
14+
function validateOperationListResponse(data: TOperationList): TOperationList {
15+
// If operations array is missing, return empty operations and stop pagination
16+
if (!Array.isArray(data.operations)) {
17+
return {
18+
...data,
19+
operations: [],
20+
// Stop pagination by setting next_page_token to '0' (no more pages)
21+
next_page_token: '0',
22+
};
23+
}
24+
return data;
25+
}
26+
1327
export const operationsApi = api.injectEndpoints({
1428
endpoints: (build) => ({
1529
getOperationList: build.infiniteQuery<
@@ -33,7 +47,9 @@ export const operationsApi = api.injectEndpoints({
3347
page_token: pageParam,
3448
};
3549
const data = await window.api.operation.getOperationList(params, {signal});
36-
return {data};
50+
// Validate and normalize the response
51+
const validatedData = validateOperationListResponse(data);
52+
return {data: validatedData};
3753
} catch (error) {
3854
return {error};
3955
}

tests/suites/tenant/diagnostics/tabs/operations.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {Diagnostics, DiagnosticsTab} from '../Diagnostics';
66

77
import {
88
setupEmptyOperationsMock,
9+
setupMalformedOperationsMock,
910
setupOperation403Mock,
1011
setupOperationNetworkErrorMock,
1112
setupOperationsMock,
13+
setupPartialMalformedOperationsMock,
1214
} from './operationsMocks';
1315

1416
test.describe('Operations Tab - Infinite Query', () => {
@@ -192,6 +194,98 @@ test.describe('Operations Tab - Infinite Query', () => {
192194
expect(errorDescription.toLowerCase()).toContain('network');
193195
});
194196

197+
test('handles malformed response without operations array', async ({page}) => {
198+
// Setup malformed response mock (returns status SUCCESS but no operations array)
199+
await setupMalformedOperationsMock(page);
200+
201+
const pageQueryParams = {
202+
schema: tenantName,
203+
database: tenantName,
204+
tenantPage: 'diagnostics',
205+
};
206+
207+
const tenantPageInstance = new TenantPage(page);
208+
await tenantPageInstance.goto(pageQueryParams);
209+
210+
const diagnostics = new Diagnostics(page);
211+
await diagnostics.clickTab(DiagnosticsTab.Operations);
212+
213+
// Wait for table to be visible
214+
await diagnostics.operations.waitForTableVisible();
215+
await diagnostics.operations.waitForDataLoad();
216+
217+
// Verify empty state is shown
218+
const isEmptyVisible = await diagnostics.operations.isEmptyStateVisible();
219+
expect(isEmptyVisible).toBe(true);
220+
221+
// Verify no data rows
222+
const rowCount = await diagnostics.operations.getRowCount();
223+
expect(rowCount).toBeLessThanOrEqual(1);
224+
225+
// Verify operations count is 0
226+
const operationsCount = await diagnostics.operations.getOperationsCount();
227+
expect(operationsCount).toBe(0);
228+
229+
// Wait to ensure no infinite refetching occurs
230+
await page.waitForTimeout(3000);
231+
232+
// Verify the count is still 0 (no infinite refetching)
233+
const finalOperationsCount = await diagnostics.operations.getOperationsCount();
234+
expect(finalOperationsCount).toBe(0);
235+
});
236+
237+
test('stops pagination when receiving malformed response after valid data', async ({page}) => {
238+
// Setup mock that returns valid data first, then malformed response
239+
await setupPartialMalformedOperationsMock(page);
240+
241+
const pageQueryParams = {
242+
schema: tenantName,
243+
database: tenantName,
244+
tenantPage: 'diagnostics',
245+
};
246+
247+
const tenantPageInstance = new TenantPage(page);
248+
await tenantPageInstance.goto(pageQueryParams);
249+
250+
const diagnostics = new Diagnostics(page);
251+
await diagnostics.clickTab(DiagnosticsTab.Operations);
252+
253+
// Wait for initial data to load
254+
await diagnostics.operations.waitForTableVisible();
255+
await diagnostics.operations.waitForDataLoad();
256+
257+
// Verify initial page loaded (should have 20 operations)
258+
const initialOperationsCount = await diagnostics.operations.getOperationsCount();
259+
expect(initialOperationsCount).toBe(20);
260+
261+
// Verify first row data
262+
const firstRowData = await diagnostics.operations.getRowData(0);
263+
expect(firstRowData['Operation ID']).toBeTruthy();
264+
265+
// Scroll to bottom to trigger next page load
266+
await diagnostics.operations.scrollToBottom();
267+
268+
// Wait a bit for potential loading
269+
await page.waitForTimeout(2000);
270+
271+
// Check if loading more appears and disappears
272+
const isLoadingVisible = await diagnostics.operations.isLoadingMoreVisible();
273+
if (isLoadingVisible) {
274+
await diagnostics.operations.waitForLoadingMoreToDisappear();
275+
}
276+
277+
// Verify the count remains at 20 (malformed response didn't add more)
278+
const finalOperationsCount = await diagnostics.operations.getOperationsCount();
279+
expect(finalOperationsCount).toBe(20);
280+
281+
// Wait to ensure no infinite refetching occurs
282+
await page.waitForTimeout(3000);
283+
284+
// Verify the count is still 20
285+
const stillFinalCount = await diagnostics.operations.getOperationsCount();
286+
expect(stillFinalCount).toBe(20);
287+
});
288+
195289
test('loads all operations when scrolling to the bottom multiple times', async ({page}) => {
196290
// Setup mocks with 80 operations (4 pages of 20)
197291
await setupOperationsMock(page, {totalOperations: 80});

tests/suites/tenant/diagnostics/tabs/operationsMocks.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,63 @@ export const setupOperationNetworkErrorMock = async (page: Page) => {
253253
await route.abort('failed');
254254
});
255255
};
256+
257+
export const setupMalformedOperationsMock = async (page: Page) => {
258+
await page.route(`${backend}/operation/list*`, async (route) => {
259+
// Return a response with status SUCCESS but missing operations array
260+
const response = {
261+
status: 'SUCCESS',
262+
next_page_token: '1',
263+
// operations array is missing
264+
};
265+
266+
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
267+
268+
await route.fulfill({
269+
status: 200,
270+
contentType: 'application/json',
271+
body: JSON.stringify(response),
272+
});
273+
});
274+
};
275+
276+
export const setupPartialMalformedOperationsMock = async (page: Page) => {
277+
let requestCount = 0;
278+
279+
await page.route(`${backend}/operation/list*`, async (route) => {
280+
requestCount++;
281+
282+
// First request returns valid data
283+
if (requestCount === 1) {
284+
const operations = generateBuildIndexOperations(0, 20);
285+
const response = {
286+
next_page_token: '1',
287+
status: 'SUCCESS',
288+
operations,
289+
};
290+
291+
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
292+
293+
await route.fulfill({
294+
status: 200,
295+
contentType: 'application/json',
296+
body: JSON.stringify(response),
297+
});
298+
} else {
299+
// Subsequent requests return malformed response
300+
const response = {
301+
status: 'SUCCESS',
302+
next_page_token: '2',
303+
// operations array is missing
304+
};
305+
306+
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
307+
308+
await route.fulfill({
309+
status: 200,
310+
contentType: 'application/json',
311+
body: JSON.stringify(response),
312+
});
313+
}
314+
});
315+
};

0 commit comments

Comments
 (0)