This is an automated email from the ASF dual-hosted git repository. elizabeth pushed a commit to branch csp-frame in repository https://gitbox.apache.org/repos/asf/superset.git
commit 7186f92d38b8d089503e189e2c74d068a65b37a3 Author: Elizabeth Thompson <[email protected]> AuthorDate: Fri Oct 17 11:47:04 2025 -0700 test: add comprehensive unhappy path tests for export Add tests for error scenarios including: - Network errors and logging - 404 errors when resource not found - Empty response handling - Download failure with proper cleanup - Malformed Content-Disposition header - Missing headers fallback - Empty IDs array edge case These tests ensure proper error handling and cleanup in all failure modes. --- superset-frontend/src/utils/export.test.ts | 120 +++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/superset-frontend/src/utils/export.test.ts b/superset-frontend/src/utils/export.test.ts index bfe936bdcd..c6900d5362 100644 --- a/superset-frontend/src/utils/export.test.ts +++ b/superset-frontend/src/utils/export.test.ts @@ -276,3 +276,123 @@ test('handles various resource types', async () => { expect(doneMock).toHaveBeenCalledTimes(5); }); + +test('handles network errors and logs them', async () => { + const networkError = new Error('Network request failed'); + (SupersetClient.get as jest.Mock).mockRejectedValue(networkError); + + const doneMock = jest.fn(); + + await expect( + handleResourceExport('dashboard', [1], doneMock), + ).rejects.toThrow('Network request failed'); + + expect(logging.error).toHaveBeenCalledWith( + 'Resource export failed:', + networkError, + ); + expect(doneMock).toHaveBeenCalled(); +}); + +test('handles 404 errors when resource not found', async () => { + const notFoundError = new Error('Not found'); + (SupersetClient.get as jest.Mock).mockRejectedValue(notFoundError); + + const doneMock = jest.fn(); + + await expect( + handleResourceExport('dashboard', [999], doneMock), + ).rejects.toThrow('Not found'); + + expect(doneMock).toHaveBeenCalled(); +}); + +test('handles empty response from server', async () => { + const emptyBlob = new Blob([], { type: 'application/zip' }); + mockResponse = { + headers: new Headers({ + 'Content-Disposition': 'attachment; filename="empty.zip"', + }), + blob: jest.fn().mockResolvedValue(emptyBlob), + } as unknown as Response; + (SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse); + + const doneMock = jest.fn(); + await handleResourceExport('dashboard', [1], doneMock); + + expect(window.URL.createObjectURL).toHaveBeenCalledWith(emptyBlob); + expect(doneMock).toHaveBeenCalled(); +}); + +test('cleans up blob URL even when download fails', async () => { + const mockAnchor = document.createElement('a'); + mockAnchor.click = jest.fn().mockImplementation(() => { + throw new Error('Click failed'); + }); + + createElementSpy.mockRestore(); + createElementSpy = jest + .spyOn(document, 'createElement') + .mockReturnValue(mockAnchor); + + const doneMock = jest.fn(); + + await expect( + handleResourceExport('dashboard', [1], doneMock), + ).rejects.toThrow('Click failed'); + + // Verify cleanup still happens + expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url'); + expect(doneMock).toHaveBeenCalled(); +}); + +test('handles malformed Content-Disposition header', async () => { + mockResponse = { + headers: new Headers({ + 'Content-Disposition': 'not-a-valid-header', + }), + blob: jest.fn().mockResolvedValue(mockBlob), + } as unknown as Response; + (SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse); + + (contentDisposition.parse as jest.Mock).mockImplementationOnce(() => { + throw new Error('Parse error'); + }); + + const doneMock = jest.fn(); + await handleResourceExport('dataset', [5], doneMock); + + // Should fall back to default filename + const anchor = document.createElement('a'); + expect(anchor.download).toBe('dataset_export.zip'); + expect(logging.warn).toHaveBeenCalledWith( + 'Failed to parse Content-Disposition header:', + expect.any(Error), + ); +}); + +test('handles missing headers object', async () => { + mockResponse = { + headers: new Headers(), + blob: jest.fn().mockResolvedValue(mockBlob), + } as unknown as Response; + (SupersetClient.get as jest.Mock).mockResolvedValue(mockResponse); + + const doneMock = jest.fn(); + await handleResourceExport('chart', [7], doneMock); + + const anchor = document.createElement('a'); + expect(anchor.download).toBe('chart_export.zip'); + expect(doneMock).toHaveBeenCalled(); +}); + +test('handles export with empty IDs array', async () => { + const doneMock = jest.fn(); + await handleResourceExport('dashboard', [], doneMock); + + expect(SupersetClient.get).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/v1/dashboard/export/?q=!()', + }), + ); +});
