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');
+    });
+  });
 });

Reply via email to