You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2025/12/19 22:59:42 UTC
(superset) 08/10: test(streaming-export): add edge case tests for URL prefix guard
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 <jo...@preset.io>
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 <no...@anthropic.com>
---
.../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');
+ });
+ });
});