Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
fix: fix: handle incorrect response for operations
  • Loading branch information
astandrik committed Jun 26, 2025
commit 5c27d60dd3f463f48e504571180fdb646e76d7fa
18 changes: 17 additions & 1 deletion src/store/reducers/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import {api} from './api';

const DEFAULT_PAGE_SIZE = 20;

// Validate and normalize the response to ensure it has proper structure
function validateOperationListResponse(data: TOperationList): TOperationList {
// If operations array is missing, return empty operations and stop pagination
if (!Array.isArray(data.operations)) {
return {
...data,
operations: [],
// Stop pagination by setting next_page_token to '0' (no more pages)
next_page_token: '0',
};
}
return data;
}

export const operationsApi = api.injectEndpoints({
endpoints: (build) => ({
getOperationList: build.infiniteQuery<
Expand All @@ -33,7 +47,9 @@ export const operationsApi = api.injectEndpoints({
page_token: pageParam,
};
const data = await window.api.operation.getOperationList(params, {signal});
return {data};
// Validate and normalize the response
const validatedData = validateOperationListResponse(data);
return {data: validatedData};
} catch (error) {
return {error};
}
Expand Down
94 changes: 94 additions & 0 deletions tests/suites/tenant/diagnostics/tabs/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {Diagnostics, DiagnosticsTab} from '../Diagnostics';

import {
setupEmptyOperationsMock,
setupMalformedOperationsMock,
setupOperation403Mock,
setupOperationNetworkErrorMock,
setupOperationsMock,
setupPartialMalformedOperationsMock,
} from './operationsMocks';

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

test('handles malformed response without operations array', async ({page}) => {
// Setup malformed response mock (returns status SUCCESS but no operations array)
await setupMalformedOperationsMock(page);

const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
};

const tenantPageInstance = new TenantPage(page);
await tenantPageInstance.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);
await diagnostics.clickTab(DiagnosticsTab.Operations);

// Wait for table to be visible
await diagnostics.operations.waitForTableVisible();
await diagnostics.operations.waitForDataLoad();

// Verify empty state is shown
const isEmptyVisible = await diagnostics.operations.isEmptyStateVisible();
expect(isEmptyVisible).toBe(true);

// Verify no data rows
const rowCount = await diagnostics.operations.getRowCount();
expect(rowCount).toBeLessThanOrEqual(1);

// Verify operations count is 0
const operationsCount = await diagnostics.operations.getOperationsCount();
expect(operationsCount).toBe(0);

// Wait to ensure no infinite refetching occurs
await page.waitForTimeout(3000);

// Verify the count is still 0 (no infinite refetching)
const finalOperationsCount = await diagnostics.operations.getOperationsCount();
expect(finalOperationsCount).toBe(0);
});

test('stops pagination when receiving malformed response after valid data', async ({page}) => {
// Setup mock that returns valid data first, then malformed response
await setupPartialMalformedOperationsMock(page);

const pageQueryParams = {
schema: tenantName,
database: tenantName,
tenantPage: 'diagnostics',
};

const tenantPageInstance = new TenantPage(page);
await tenantPageInstance.goto(pageQueryParams);

const diagnostics = new Diagnostics(page);
await diagnostics.clickTab(DiagnosticsTab.Operations);

// Wait for initial data to load
await diagnostics.operations.waitForTableVisible();
await diagnostics.operations.waitForDataLoad();

// Verify initial page loaded (should have 20 operations)
const initialOperationsCount = await diagnostics.operations.getOperationsCount();
expect(initialOperationsCount).toBe(20);

// Verify first row data
const firstRowData = await diagnostics.operations.getRowData(0);
expect(firstRowData['Operation ID']).toBeTruthy();

// Scroll to bottom to trigger next page load
await diagnostics.operations.scrollToBottom();

// Wait a bit for potential loading
await page.waitForTimeout(2000);

// Check if loading more appears and disappears
const isLoadingVisible = await diagnostics.operations.isLoadingMoreVisible();
if (isLoadingVisible) {
await diagnostics.operations.waitForLoadingMoreToDisappear();
}

// Verify the count remains at 20 (malformed response didn't add more)
const finalOperationsCount = await diagnostics.operations.getOperationsCount();
expect(finalOperationsCount).toBe(20);

// Wait to ensure no infinite refetching occurs
await page.waitForTimeout(3000);

// Verify the count is still 20
const stillFinalCount = await diagnostics.operations.getOperationsCount();
expect(stillFinalCount).toBe(20);
});

test('loads all operations when scrolling to the bottom multiple times', async ({page}) => {
// Setup mocks with 80 operations (4 pages of 20)
await setupOperationsMock(page, {totalOperations: 80});
Expand Down
60 changes: 60 additions & 0 deletions tests/suites/tenant/diagnostics/tabs/operationsMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,63 @@ export const setupOperationNetworkErrorMock = async (page: Page) => {
await route.abort('failed');
});
};

export const setupMalformedOperationsMock = async (page: Page) => {
await page.route(`${backend}/operation/list*`, async (route) => {
// Return a response with status SUCCESS but missing operations array
const response = {
status: 'SUCCESS',
next_page_token: '1',
// operations array is missing
};

await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
};

export const setupPartialMalformedOperationsMock = async (page: Page) => {
let requestCount = 0;

await page.route(`${backend}/operation/list*`, async (route) => {
requestCount++;

// First request returns valid data
if (requestCount === 1) {
const operations = generateBuildIndexOperations(0, 20);
const response = {
next_page_token: '1',
status: 'SUCCESS',
operations,
};

await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
} else {
// Subsequent requests return malformed response
const response = {
status: 'SUCCESS',
next_page_token: '2',
// operations array is missing
};

await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
}
});
};
Loading