| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import sinon from 'sinon'; |
| import fetchMock from 'fetch-mock'; |
| import configureMockStore from 'redux-mock-store'; |
| import thunk from 'redux-thunk'; |
| import { waitFor } from 'spec/helpers/testing-library'; |
| import * as actions from 'src/SqlLab/actions/sqlLab'; |
| import { LOG_EVENT } from 'src/logger/actions'; |
| import { |
| defaultQueryEditor, |
| query, |
| initialState, |
| queryId, |
| } from 'src/SqlLab/fixtures'; |
| import { SupersetClient, isFeatureEnabled } from '@superset-ui/core'; |
| import { ADD_TOAST } from 'src/components/MessageToasts/actions'; |
| import { ToastType } from '../../components/MessageToasts/types'; |
| |
| const middlewares = [thunk]; |
| const mockStore = configureMockStore(middlewares); |
| |
| jest.mock('nanoid', () => ({ |
| nanoid: () => 'abcd', |
| })); |
| |
| afterAll(() => { |
| jest.resetAllMocks(); |
| }); |
| |
| jest.mock('@superset-ui/core', () => ({ |
| ...jest.requireActual('@superset-ui/core'), |
| isFeatureEnabled: jest.fn(), |
| })); |
| |
| describe('getUpToDateQuery', () => { |
| test('should return the up to date query editor state', () => { |
| const outOfUpdatedQueryEditor = { |
| ...defaultQueryEditor, |
| schema: null, |
| sql: 'SELECT ...', |
| }; |
| const queryEditor = { |
| ...defaultQueryEditor, |
| sql: 'SELECT * FROM table', |
| }; |
| const state = { |
| sqlLab: { |
| queryEditors: [queryEditor], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| expect(actions.getUpToDateQuery(state, outOfUpdatedQueryEditor)).toEqual( |
| queryEditor, |
| ); |
| }); |
| }); |
| |
| describe('async actions', () => { |
| const mockBigNumber = '9223372036854775807'; |
| const queryEditor = { |
| ...defaultQueryEditor, |
| id: 'abcd', |
| autorun: false, |
| latestQueryId: null, |
| sql: 'SELECT *\nFROM\nWHERE', |
| name: 'Untitled Query 1', |
| }; |
| |
| let dispatch; |
| |
| beforeEach(() => { |
| dispatch = sinon.spy(); |
| }); |
| |
| afterEach(() => fetchMock.resetHistory()); |
| |
| const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*'; |
| fetchMock.get( |
| fetchQueryEndpoint, |
| JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }), |
| ); |
| |
| const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; |
| fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`); |
| |
| describe('saveQuery', () => { |
| const saveQueryEndpoint = 'glob:*/api/v1/saved_query/'; |
| fetchMock.post(saveQueryEndpoint, { results: { json: {} } }); |
| |
| const makeRequest = () => { |
| const request = actions.saveQuery(query, queryId); |
| return request(dispatch, () => initialState); |
| }; |
| |
| it('posts to the correct url', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| return store.dispatch(actions.saveQuery(query, queryId)).then(() => { |
| expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| it('posts the correct query object', () => { |
| const store = mockStore(initialState); |
| return store.dispatch(actions.saveQuery(query, queryId)).then(() => { |
| const call = fetchMock.calls(saveQueryEndpoint)[0]; |
| const formData = JSON.parse(call[1].body); |
| const mappedQueryToServer = actions.convertQueryToServer(query); |
| |
| Object.keys(mappedQueryToServer).forEach(key => { |
| expect(formData[key]).toBeDefined(); |
| }); |
| }); |
| }); |
| |
| it('calls 3 dispatch actions', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.callCount).toBe(2); |
| }); |
| }); |
| |
| it('calls QUERY_EDITOR_SAVED after making a request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.args[0][0].type).toBe(actions.QUERY_EDITOR_SAVED); |
| }); |
| }); |
| |
| it('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| const expectedActionTypes = [ |
| actions.QUERY_EDITOR_SAVED, |
| actions.QUERY_EDITOR_SET_TITLE, |
| ]; |
| return store.dispatch(actions.saveQuery(query)).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| }); |
| |
| describe('formatQuery', () => { |
| const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/'; |
| const expectedSql = 'SELECT 1'; |
| fetchMock.post(formatQueryEndpoint, { result: expectedSql }); |
| |
| test('posts to the correct url', async () => { |
| const store = mockStore(initialState); |
| store.dispatch(actions.formatQuery(query, queryId)); |
| await waitFor(() => |
| expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1), |
| ); |
| expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL); |
| expect(store.getActions()[0].sql).toBe(expectedSql); |
| }); |
| }); |
| |
| describe('fetchQueryResults', () => { |
| const makeRequest = () => { |
| const store = mockStore(initialState); |
| const request = actions.fetchQueryResults(query); |
| return request(dispatch, store.getState); |
| }; |
| |
| it('makes the fetch request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| it('calls requestQueryResults', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.args[0][0].type).toBe(actions.REQUEST_QUERY_RESULTS); |
| }); |
| }); |
| |
| it.skip('parses large number result without losing precision', () => |
| makeRequest().then(() => { |
| expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1); |
| expect(dispatch.callCount).toBe(2); |
| expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( |
| mockBigNumber, |
| ); |
| })); |
| |
| it('calls querySuccess on fetch success', () => { |
| expect.assertions(1); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [ |
| actions.REQUEST_QUERY_RESULTS, |
| actions.QUERY_SUCCESS, |
| ]; |
| return store.dispatch(actions.fetchQueryResults(query)).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| |
| it('calls queryFailed on fetch error', () => { |
| expect.assertions(1); |
| |
| fetchMock.get( |
| fetchQueryEndpoint, |
| { throws: { message: 'error text' } }, |
| { overwriteRoutes: true }, |
| ); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [ |
| actions.REQUEST_QUERY_RESULTS, |
| actions.QUERY_FAILED, |
| ]; |
| return store.dispatch(actions.fetchQueryResults(query)).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| }); |
| |
| describe('runQuery without query params', () => { |
| const makeRequest = () => { |
| const request = actions.runQuery(query); |
| return request(dispatch, () => initialState); |
| }; |
| |
| it('makes the fetch request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| it('calls startQuery', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.args[0][0].type).toBe(actions.START_QUERY); |
| }); |
| }); |
| |
| it.skip('parses large number result without losing precision', () => |
| makeRequest().then(() => { |
| expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); |
| expect(dispatch.callCount).toBe(2); |
| expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( |
| mockBigNumber, |
| ); |
| })); |
| |
| it('calls querySuccess on fetch success', () => { |
| expect.assertions(1); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [actions.START_QUERY, actions.QUERY_SUCCESS]; |
| const { dispatch } = store; |
| const request = actions.runQuery(query); |
| return request(dispatch, () => initialState).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| }); |
| }); |
| |
| it('calls queryFailed on fetch error and logs the error details', () => { |
| expect.assertions(2); |
| |
| fetchMock.post( |
| runQueryEndpoint, |
| { |
| throws: { |
| message: 'error text', |
| timeout: true, |
| statusText: 'timeout', |
| }, |
| }, |
| { overwriteRoutes: true }, |
| ); |
| |
| const store = mockStore({}); |
| const expectedActionTypes = [ |
| actions.START_QUERY, |
| LOG_EVENT, |
| actions.QUERY_FAILED, |
| ]; |
| const { dispatch } = store; |
| const request = actions.runQuery(query); |
| return request(dispatch, () => initialState).then(() => { |
| const actions = store.getActions(); |
| expect(actions.map(a => a.type)).toEqual(expectedActionTypes); |
| expect(actions[1].payload.eventData.issue_codes).toEqual([1000, 1001]); |
| }); |
| }); |
| }); |
| |
| describe('runQuery with query params', () => { |
| const { location } = window; |
| |
| beforeAll(() => { |
| delete window.location; |
| window.location = new URL('http://localhost/sqllab/?foo=bar'); |
| }); |
| |
| afterAll(() => { |
| delete window.location; |
| window.location = location; |
| }); |
| |
| const makeRequest = () => { |
| const request = actions.runQuery(query); |
| return request(dispatch, () => initialState); |
| }; |
| |
| it('makes the fetch request', async () => { |
| const runQueryEndpointWithParams = |
| 'glob:*/api/v1/sqllab/execute/?foo=bar'; |
| fetchMock.post( |
| runQueryEndpointWithParams, |
| `{ "data": ${mockBigNumber} }`, |
| ); |
| await makeRequest().then(() => { |
| expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1); |
| }); |
| }); |
| }); |
| |
| describe('reRunQuery', () => { |
| it('creates new query with a new id', () => { |
| const id = 'id'; |
| const state = { |
| sqlLab: { |
| tabHistory: [id], |
| queryEditors: [{ id, name: 'Dummy query editor' }], |
| unsavedQueryEditor: {}, |
| }, |
| }; |
| const store = mockStore(state); |
| const request = actions.reRunQuery(query); |
| request(store.dispatch, store.getState); |
| expect(store.getActions()[0].query.id).toEqual('abcd'); |
| }); |
| }); |
| |
| describe('postStopQuery', () => { |
| const stopQueryEndpoint = 'glob:*/api/v1/query/stop'; |
| fetchMock.post(stopQueryEndpoint, {}); |
| const baseQuery = { |
| ...query, |
| id: 'test_foo', |
| }; |
| |
| const makeRequest = () => { |
| const request = actions.postStopQuery(baseQuery); |
| return request(dispatch); |
| }; |
| |
| it('makes the fetch request', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(fetchMock.calls(stopQueryEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| it('calls stopQuery', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| expect(dispatch.getCall(0).args[0].type).toBe(actions.STOP_QUERY); |
| }); |
| }); |
| |
| it('sends the correct data', () => { |
| expect.assertions(1); |
| |
| return makeRequest().then(() => { |
| const call = fetchMock.calls(stopQueryEndpoint)[0]; |
| const body = JSON.parse(call[1].body); |
| expect(body.client_id).toBe(baseQuery.id); |
| }); |
| }); |
| }); |
| |
| describe('cloneQueryToNewTab', () => { |
| it('creates new query editor', () => { |
| expect.assertions(1); |
| |
| const id = 'id'; |
| const state = { |
| sqlLab: { |
| tabHistory: [id], |
| queryEditors: [{ id, name: 'out of updated title' }], |
| unsavedQueryEditor: { |
| id, |
| name: 'Dummy query editor', |
| }, |
| }, |
| }; |
| const store = mockStore(state); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| name: 'Copy of Dummy query editor', |
| dbId: 1, |
| catalog: query.catalog, |
| schema: query.schema, |
| autorun: true, |
| sql: 'SELECT * FROM something', |
| queryLimit: undefined, |
| maxRow: undefined, |
| id: 'abcd', |
| immutableId: 'abcd', |
| templateParams: undefined, |
| inLocalStorage: true, |
| loaded: true, |
| }, |
| }, |
| ]; |
| const request = actions.cloneQueryToNewTab(query, true); |
| request(store.dispatch, store.getState); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('popSavedQuery', () => { |
| const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get'); |
| const store = mockStore({}); |
| |
| const mockSavedQueryApiResponse = { |
| catalog: null, |
| changed_by: { |
| first_name: 'Superset', |
| id: 1, |
| last_name: 'Admin', |
| }, |
| changed_on: '2024-12-28T20:06:14.246743', |
| changed_on_delta_humanized: '8 days ago', |
| created_by: { |
| first_name: 'Superset', |
| id: 1, |
| last_name: 'Admin', |
| }, |
| database: { |
| database_name: 'examples', |
| id: 2, |
| }, |
| description: '', |
| id: 1, |
| label: 'Query 1', |
| schema: 'public', |
| sql: 'SELECT * FROM channels', |
| sql_tables: [ |
| { |
| catalog: null, |
| schema: null, |
| table: 'channels', |
| }, |
| ], |
| template_parameters: null, |
| }; |
| |
| const makeRequest = id => { |
| const request = actions.popSavedQuery(id); |
| const { dispatch } = store; |
| |
| return request(dispatch, () => initialState); |
| }; |
| |
| beforeEach(() => { |
| supersetClientGetSpy.mockClear(); |
| store.clearActions(); |
| }); |
| |
| afterAll(() => { |
| supersetClientGetSpy.mockRestore(); |
| }); |
| |
| it('calls API endpint with correct params', async () => { |
| supersetClientGetSpy.mockResolvedValue({ |
| json: { result: mockSavedQueryApiResponse }, |
| }); |
| |
| await makeRequest(123); |
| |
| expect(supersetClientGetSpy).toHaveBeenCalledWith({ |
| endpoint: '/api/v1/saved_query/123', |
| }); |
| }); |
| |
| it('dispatches addQueryEditor with correct params on successful API call', async () => { |
| supersetClientGetSpy.mockResolvedValue({ |
| json: { result: mockSavedQueryApiResponse }, |
| }); |
| |
| const expectedParams = { |
| name: 'Query 1', |
| dbId: 2, |
| catalog: null, |
| schema: 'public', |
| sql: 'SELECT * FROM channels', |
| templateParams: null, |
| remoteId: 1, |
| }; |
| |
| await makeRequest(1); |
| |
| const addQueryEditorAction = store |
| .getActions() |
| .find(action => action.type === actions.ADD_QUERY_EDITOR); |
| |
| expect(addQueryEditorAction).toBeTruthy(); |
| expect(addQueryEditorAction?.queryEditor).toEqual( |
| expect.objectContaining(expectedParams), |
| ); |
| }); |
| |
| it('should dispatch addDangerToast on API error', async () => { |
| supersetClientGetSpy.mockResolvedValue(new Error()); |
| |
| await makeRequest(1); |
| |
| const addToastAction = store |
| .getActions() |
| .find(action => action.type === ADD_TOAST); |
| |
| expect(addToastAction).toBeTruthy(); |
| expect(addToastAction?.payload?.toastType).toBe(ToastType.Danger); |
| }); |
| }); |
| |
| describe('addQueryEditor', () => { |
| it('creates new query editor', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| ...queryEditor, |
| immutableId: 'abcd', |
| inLocalStorage: true, |
| loaded: true, |
| }, |
| }, |
| ]; |
| store.dispatch(actions.addQueryEditor(defaultQueryEditor)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| describe('addNewQueryEditor', () => { |
| it('creates new query editor with new tab name', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| unsavedQueryEditor: { |
| id: defaultQueryEditor.id, |
| name: 'Untitled Query 6', |
| }, |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| id: 'abcd', |
| immutableId: 'abcd', |
| sql: expect.stringContaining('SELECT ...'), |
| name: `Untitled Query 7`, |
| dbId: defaultQueryEditor.dbId, |
| catalog: defaultQueryEditor.catalog, |
| schema: defaultQueryEditor.schema, |
| autorun: false, |
| queryLimit: |
| defaultQueryEditor.queryLimit || |
| initialState.common.conf.DEFAULT_SQLLAB_LIMIT, |
| inLocalStorage: true, |
| loaded: true, |
| }, |
| }, |
| ]; |
| const request = actions.addNewQueryEditor(); |
| request(store.dispatch, store.getState); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| }); |
| |
| it('set current query editor', () => { |
| expect.assertions(1); |
| |
| const store = mockStore(initialState); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: defaultQueryEditor, |
| }, |
| ]; |
| store.dispatch(actions.setActiveQueryEditor(defaultQueryEditor)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| describe('swithQueryEditor', () => { |
| it('switch to the next tab editor', () => { |
| const store = mockStore(initialState); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: initialState.sqlLab.queryEditors[1], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor()); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| it('switch to the first tab editor once it reaches the rightmost tab', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [ |
| initialState.sqlLab.queryEditors[ |
| initialState.sqlLab.queryEditors.length - 1 |
| ].id, |
| ], |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: initialState.sqlLab.queryEditors[0], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor()); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| it('switch to the previous tab editor', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [initialState.sqlLab.queryEditors[1].id], |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: initialState.sqlLab.queryEditors[0], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor(true)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| |
| it('switch to the last tab editor once it reaches the leftmost tab', () => { |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| tabHistory: [initialState.sqlLab.queryEditors[0].id], |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.SET_ACTIVE_QUERY_EDITOR, |
| queryEditor: |
| initialState.sqlLab.queryEditors[ |
| initialState.sqlLab.queryEditors.length - 1 |
| ], |
| }, |
| ]; |
| store.dispatch(actions.switchQueryEditor(true)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('backend sync', () => { |
| const updateTabStateEndpoint = 'glob:*/tabstateview/*'; |
| fetchMock.put(updateTabStateEndpoint, {}); |
| fetchMock.delete(updateTabStateEndpoint, {}); |
| fetchMock.post(updateTabStateEndpoint, JSON.stringify({ id: 1 })); |
| |
| const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*'; |
| fetchMock.put(updateTableSchemaEndpoint, {}); |
| fetchMock.delete(updateTableSchemaEndpoint, {}); |
| fetchMock.post(updateTableSchemaEndpoint, JSON.stringify({ id: 1 })); |
| |
| const getTableMetadataEndpoint = |
| 'glob:**/api/v1/database/*/table_metadata/*'; |
| fetchMock.get(getTableMetadataEndpoint, {}); |
| const getExtraTableMetadataEndpoint = |
| 'glob:**/api/v1/database/*/table_metadata/extra/*'; |
| fetchMock.get(getExtraTableMetadataEndpoint, {}); |
| |
| beforeEach(() => { |
| isFeatureEnabled.mockImplementation( |
| feature => feature === 'SQLLAB_BACKEND_PERSISTENCE', |
| ); |
| }); |
| |
| afterEach(() => { |
| isFeatureEnabled.mockRestore(); |
| }); |
| |
| afterEach(() => fetchMock.resetHistory()); |
| |
| describe('addQueryEditor', () => { |
| it('creates the tab state in the local storage', () => { |
| expect.assertions(2); |
| |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.ADD_QUERY_EDITOR, |
| queryEditor: { |
| ...queryEditor, |
| id: 'abcd', |
| immutableId: 'abcd', |
| loaded: true, |
| inLocalStorage: true, |
| }, |
| }, |
| ]; |
| store.dispatch(actions.addQueryEditor(queryEditor)); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); |
| }); |
| }); |
| |
| describe('removeQueryEditor', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_QUERY_EDITOR, |
| queryEditor, |
| }, |
| ]; |
| store.dispatch(actions.removeQueryEditor(queryEditor)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetDb', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const dbId = 42; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SETDB, |
| queryEditor, |
| dbId, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetDb(queryEditor, dbId)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetCatalog', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const catalog = 'public'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_CATALOG, |
| queryEditor, |
| catalog, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetCatalog(queryEditor, catalog)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetSchema', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const schema = 'schema'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_SCHEMA, |
| queryEditor, |
| schema, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetSchema(queryEditor, schema)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetAutorun', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const autorun = true; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_AUTORUN, |
| queryEditor, |
| autorun, |
| }, |
| ]; |
| store.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun)); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetTitle', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const name = 'name'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_TITLE, |
| queryEditor, |
| name, |
| }, |
| ]; |
| store.dispatch( |
| actions.queryEditorSetTitle(queryEditor, name, queryEditor.id), |
| ); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetAndSaveSql', () => { |
| const sql = 'SELECT * '; |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_SQL, |
| queryEditor, |
| sql, |
| }, |
| ]; |
| describe('with backend persistence flag on', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(2); |
| |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| queryEditors: [queryEditor], |
| }, |
| }); |
| const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); |
| return request(store.dispatch, store.getState).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1); |
| }); |
| }); |
| }); |
| describe('with backend persistence flag off', () => { |
| it('does not update the tab state in the backend', () => { |
| isFeatureEnabled.mockImplementation( |
| feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), |
| ); |
| |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| queryEditors: [queryEditor], |
| }, |
| }); |
| const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); |
| request(store.dispatch, store.getState); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); |
| isFeatureEnabled.mockRestore(); |
| }); |
| }); |
| }); |
| |
| describe('queryEditorSetQueryLimit', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const queryLimit = 10; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_QUERY_LIMIT, |
| queryEditor, |
| queryLimit, |
| }, |
| ]; |
| store.dispatch( |
| actions.queryEditorSetQueryLimit(queryEditor, queryLimit), |
| ); |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('queryEditorSetTemplateParams', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(1); |
| |
| const templateParams = '{"foo": "bar"}'; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS, |
| queryEditor, |
| templateParams, |
| }, |
| ]; |
| store.dispatch( |
| actions.queryEditorSetTemplateParams(queryEditor, templateParams), |
| ); |
| |
| expect(store.getActions()).toEqual(expectedActions); |
| }); |
| }); |
| |
| describe('addTable', () => { |
| it('dispatches table state from unsaved change', () => { |
| const tableName = 'table'; |
| const catalogName = null; |
| const schemaName = 'schema'; |
| const expectedDbId = 473892; |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| unsavedQueryEditor: { |
| id: query.id, |
| dbId: expectedDbId, |
| }, |
| }, |
| }); |
| const request = actions.addTable( |
| query, |
| tableName, |
| catalogName, |
| schemaName, |
| ); |
| request(store.dispatch, store.getState); |
| expect(store.getActions()[0]).toEqual( |
| expect.objectContaining({ |
| table: expect.objectContaining({ |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| dbId: expectedDbId, |
| }), |
| }), |
| ); |
| }); |
| }); |
| |
| describe('syncTable', () => { |
| it('updates the table schema state in the backend', () => { |
| expect.assertions(4); |
| |
| const tableName = 'table'; |
| const schemaName = 'schema'; |
| const store = mockStore(initialState); |
| const expectedActionTypes = [ |
| actions.MERGE_TABLE, // syncTable |
| ]; |
| const request = actions.syncTable(query, tableName, schemaName); |
| return request(store.dispatch, store.getState).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| expect(store.getActions()[0].prepend).toBeFalsy(); |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); |
| |
| // tab state is not updated, since no query was run |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); |
| }); |
| }); |
| }); |
| |
| describe('runTablePreviewQuery', () => { |
| const results = { |
| data: mockBigNumber, |
| query: { sqlEditorId: 'null', dbId: 1 }, |
| query_id: 'efgh', |
| }; |
| const tableName = 'table'; |
| const catalogName = null; |
| const schemaName = 'schema'; |
| const store = mockStore({ |
| ...initialState, |
| sqlLab: { |
| ...initialState.sqlLab, |
| databases: { |
| 1: { disable_data_preview: false }, |
| }, |
| }, |
| }); |
| |
| beforeEach(() => { |
| fetchMock.post(runQueryEndpoint, JSON.stringify(results), { |
| overwriteRoutes: true, |
| }); |
| }); |
| |
| afterEach(() => { |
| store.clearActions(); |
| fetchMock.resetHistory(); |
| }); |
| |
| it('updates and runs data preview query when configured', () => { |
| expect.assertions(3); |
| |
| const expectedActionTypes = [ |
| actions.MERGE_TABLE, // addTable (data preview) |
| actions.START_QUERY, // runQuery (data preview) |
| actions.QUERY_SUCCESS, // querySuccess |
| ]; |
| const request = actions.runTablePreviewQuery({ |
| dbId: 1, |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| }); |
| return request(store.dispatch, store.getState).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); |
| // tab state is not updated, since the query is a data preview |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); |
| }); |
| }); |
| |
| it('runs data preview query only', () => { |
| const expectedActionTypes = [ |
| actions.START_QUERY, // runQuery (data preview) |
| actions.QUERY_SUCCESS, // querySuccess |
| ]; |
| const request = actions.runTablePreviewQuery( |
| { |
| dbId: 1, |
| name: tableName, |
| catalog: catalogName, |
| schema: schemaName, |
| }, |
| true, |
| ); |
| return request(store.dispatch, store.getState).then(() => { |
| expect(store.getActions().map(a => a.type)).toEqual( |
| expectedActionTypes, |
| ); |
| expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); |
| // tab state is not updated, since the query is a data preview |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); |
| }); |
| }); |
| }); |
| |
| describe('expandTable', () => { |
| it('updates the table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const table = { id: 1 }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.EXPAND_TABLE, |
| table, |
| }, |
| ]; |
| return store.dispatch(actions.expandTable(table)).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); |
| }); |
| }); |
| }); |
| |
| describe('collapseTable', () => { |
| it('updates the table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const table = { id: 1 }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.COLLAPSE_TABLE, |
| table, |
| }, |
| ]; |
| return store.dispatch(actions.collapseTable(table)).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); |
| }); |
| }); |
| }); |
| |
| describe('removeTables', () => { |
| it('updates the table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const table = { id: 1, initialized: true }; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_TABLES, |
| tables: [table], |
| }, |
| ]; |
| return store.dispatch(actions.removeTables([table])).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); |
| }); |
| }); |
| |
| it('deletes multiple tables and updates the table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const tables = [ |
| { id: 1, initialized: true }, |
| { id: 2, initialized: true }, |
| ]; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_TABLES, |
| tables, |
| }, |
| ]; |
| return store.dispatch(actions.removeTables(tables)).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2); |
| }); |
| }); |
| |
| it('only updates the initialized table schema state in the backend', () => { |
| expect.assertions(2); |
| |
| const tables = [{ id: 1 }, { id: 2, initialized: true }]; |
| const store = mockStore({}); |
| const expectedActions = [ |
| { |
| type: actions.REMOVE_TABLES, |
| tables, |
| }, |
| ]; |
| return store.dispatch(actions.removeTables(tables)).then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); |
| }); |
| }); |
| }); |
| |
| describe('syncQueryEditor', () => { |
| it('updates the tab state in the backend', () => { |
| expect.assertions(3); |
| |
| const results = { |
| data: mockBigNumber, |
| query: { sqlEditorId: 'null' }, |
| query_id: 'efgh', |
| }; |
| fetchMock.post(runQueryEndpoint, JSON.stringify(results), { |
| overwriteRoutes: true, |
| }); |
| |
| const oldQueryEditor = { ...queryEditor, inLocalStorage: true }; |
| const tables = [ |
| { |
| id: 'one', |
| dataPreviewQueryId: 'previewOne', |
| queryEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| { |
| id: 'two', |
| dataPreviewQueryId: 'previewTwo', |
| queryEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| ]; |
| const queries = [ |
| { |
| ...query, |
| id: 'previewOne', |
| sqlEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| { |
| ...query, |
| id: 'previewTwo', |
| sqlEditorId: oldQueryEditor.id, |
| inLocalStorage: true, |
| }, |
| ]; |
| const store = mockStore({ |
| sqlLab: { |
| queries, |
| tables, |
| }, |
| }); |
| const expectedActions = [ |
| { |
| type: actions.MIGRATE_QUERY_EDITOR, |
| oldQueryEditor, |
| // new qe has a different id |
| newQueryEditor: { |
| ...oldQueryEditor, |
| tabViewId: '1', |
| inLocalStorage: false, |
| loaded: true, |
| }, |
| }, |
| { |
| type: actions.MIGRATE_TABLE, |
| oldTable: tables[0], |
| // new table has a different id and points to new query editor |
| newTable: { ...tables[0], id: 1, queryEditorId: '1' }, |
| }, |
| { |
| type: actions.MIGRATE_TABLE, |
| oldTable: tables[1], |
| // new table has a different id and points to new query editor |
| newTable: { ...tables[1], id: 1, queryEditorId: '1' }, |
| }, |
| { |
| type: actions.MIGRATE_QUERY, |
| queryId: 'previewOne', |
| queryEditorId: '1', |
| }, |
| { |
| type: actions.MIGRATE_QUERY, |
| queryId: 'previewTwo', |
| queryEditorId: '1', |
| }, |
| ]; |
| return store |
| .dispatch(actions.syncQueryEditor(oldQueryEditor)) |
| .then(() => { |
| expect(store.getActions()).toEqual(expectedActions); |
| expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(3); |
| |
| // query editor has 2 tables loaded in the schema viewer |
| expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2); |
| }); |
| }); |
| }); |
| }); |
| }); |