This is an automated email from the ASF dual-hosted git repository. jli pushed a commit to branch fix-app-prefix-export in repository https://gitbox.apache.org/repos/asf/superset.git
commit b89caf6f715aca8146ce298c70a6018f4a9d0f86 Author: Joe Li <[email protected]> AuthorDate: Fri Dec 19 09:29:37 2025 -0800 test(streaming-export): add edge case tests for URL prefix guard Adds tests to lock down URL prefix guard behavior: - Relative URLs without leading slash pass through unchanged (caller responsibility) - Absolute URLs (http/https) pass through unchanged - Legacy API path (useLegacyApi=true) correctly prefixes via getURIDirectory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- .../useStreamingExport.test.ts | 61 ++++++++++++++++++++++ .../src/explore/exploreUtils/exportChart.test.ts | 37 ++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/components/StreamingExportModal/useStreamingExport.test.ts b/superset-frontend/src/components/StreamingExportModal/useStreamingExport.test.ts index 5297576e9b..5f2bbb41f5 100644 --- a/superset-frontend/src/components/StreamingExportModal/useStreamingExport.test.ts +++ b/superset-frontend/src/components/StreamingExportModal/useStreamingExport.test.ts @@ -325,4 +325,65 @@ describe('URL prefix guard', () => { expect.any(Object), ); }); + + test('leaves relative URL without leading slash unchanged (caller responsibility)', async () => { + // URLs without leading slash are treated as non-relative and passed through. + // Callers should always use absolute paths starting with / + const appRoot = '/superset'; + applicationRoot.mockReturnValue(appRoot); + makeUrl.mockImplementation((path: string) => `${appRoot}${path}`); + + const mockFetch = createMockFetch(); + global.fetch = mockFetch; + + const { result } = renderHook(() => useStreamingExport()); + + act(() => { + result.current.startExport({ + url: 'api/v1/sqllab/export_streaming/', + payload: { client_id: 'test-id' }, + exportType: 'csv', + }); + }); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + // URL without leading slash is passed through unchanged - guard only handles /paths + expect(mockFetch).toHaveBeenCalledWith( + 'api/v1/sqllab/export_streaming/', + expect.any(Object), + ); + }); + + test('leaves absolute URLs (http/https) unchanged', async () => { + // Absolute URLs should pass through without modification + const appRoot = '/superset'; + applicationRoot.mockReturnValue(appRoot); + makeUrl.mockImplementation((path: string) => `${appRoot}${path}`); + + const mockFetch = createMockFetch(); + global.fetch = mockFetch; + + const { result } = renderHook(() => useStreamingExport()); + + act(() => { + result.current.startExport({ + url: 'https://external.example.com/api/export/', + payload: { client_id: 'test-id' }, + exportType: 'csv', + }); + }); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + // Absolute URLs should not be modified + expect(mockFetch).toHaveBeenCalledWith( + 'https://external.example.com/api/export/', + expect.any(Object), + ); + }); }); diff --git a/superset-frontend/src/explore/exploreUtils/exportChart.test.ts b/superset-frontend/src/explore/exploreUtils/exportChart.test.ts index 9189b844d7..ae6de113a1 100644 --- a/superset-frontend/src/explore/exploreUtils/exportChart.test.ts +++ b/superset-frontend/src/explore/exploreUtils/exportChart.test.ts @@ -41,7 +41,6 @@ jest.mock('@superset-ui/core', () => ({ const { ensureAppRoot } = jest.requireMock('src/utils/pathUtils'); - describe('exportChart URL prefix for streaming export', () => { beforeEach(() => { jest.clearAllMocks(); @@ -144,4 +143,40 @@ describe('exportChart URL prefix for streaming export', () => { ); }); }); + + describe('legacy API endpoint (useLegacyApi=true)', () => { + // Legacy API uses getExploreUrl() -> getURIDirectory() -> ensureAppRoot() + // This test ensures the legacy path also correctly prefixes URLs + const { getChartMetadataRegistry } = jest.requireMock('@superset-ui/core'); + + test('passes prefixed URL for legacy viz type with app root configured', async () => { + const appRoot = '/superset'; + ensureAppRoot.mockImplementation((path: string) => `${appRoot}${path}`); + + // Configure mock to return useLegacyApi: true + getChartMetadataRegistry.mockReturnValue({ + get: jest + .fn() + .mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }), + }); + + const onStartStreamingExport = jest.fn(); + const legacyFormData = { + datasource: '1__table', + viz_type: 'legacy_viz', + }; + + await exportChart({ + formData: legacyFormData, + resultFormat: 'csv', + onStartStreamingExport: onStartStreamingExport as unknown as null, + }); + + expect(onStartStreamingExport).toHaveBeenCalledTimes(1); + const callArgs = onStartStreamingExport.mock.calls[0][0]; + // Legacy path uses getURIDirectory which calls ensureAppRoot + expect(callArgs.url).toContain(appRoot); + expect(callArgs.exportType).toBe('csv'); + }); + }); });
