airbnb/superset

View on GitHub
superset-frontend/src/SqlLab/actions/sqlLab.test.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * 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.
 */
/* eslint no-unused-expressions: 0 */
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import shortid from 'shortid';
import { waitFor } from '@testing-library/react';
import * as uiCore from '@superset-ui/core';
import * as actions from 'src/SqlLab/actions/sqlLab';
import { LOG_EVENT } from 'src/logger/actions';
import {
  defaultQueryEditor,
  query,
  initialState,
  queryId,
} from 'src/SqlLab/fixtures';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

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 request = actions.fetchQueryResults(query);
      return request(dispatch);
    };

    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(3);

      fetchMock.post(
        runQueryEndpoint,
        {
          throws: {
            message: 'error text',
            timeout: true,
            statusText: 'timeout',
          },
        },
        { overwriteRoutes: true },
      );

      const store = mockStore({});
      const expectedActionTypes = [
        actions.START_QUERY,
        LOG_EVENT,
        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.error_details).toContain(
          'Issue 1000',
        );
        expect(actions[2].payload.eventData.error_details).toContain(
          'Issue 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', () => {
    let stub;
    beforeEach(() => {
      stub = sinon.stub(shortid, 'generate').returns('abcd');
    });
    afterEach(() => {
      stub.restore();
    });

    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', () => {
    let stub;
    beforeEach(() => {
      stub = sinon.stub(shortid, 'generate').returns('abcd');
    });
    afterEach(() => {
      stub.restore();
    });

    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',
            templateParams: undefined,
          },
        },
      ];
      const request = actions.cloneQueryToNewTab(query, true);
      return request(store.dispatch, store.getState).then(() => {
        expect(store.getActions()).toEqual(expectedActions);
      });
    });
  });

  describe('addQueryEditor', () => {
    let stub;
    beforeEach(() => {
      stub = sinon.stub(shortid, 'generate').returns('abcd');
    });
    afterEach(() => {
      stub.restore();
    });

    it('creates new query editor', () => {
      expect.assertions(1);

      const store = mockStore(initialState);
      const expectedActions = [
        {
          type: actions.ADD_QUERY_EDITOR,
          queryEditor,
        },
      ];
      const request = actions.addQueryEditor(defaultQueryEditor);
      return request(store.dispatch, store.getState).then(() => {
        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',
              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,
            },
          },
        ];
        const request = actions.addNewQueryEditor();
        return request(store.dispatch, store.getState).then(() => {
          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, {});

    let isFeatureEnabledMock;

    beforeEach(() => {
      isFeatureEnabledMock = jest
        .spyOn(uiCore, 'isFeatureEnabled')
        .mockImplementation(
          feature => feature === 'SQLLAB_BACKEND_PERSISTENCE',
        );
    });

    afterEach(() => {
      isFeatureEnabledMock.mockRestore();
    });

    afterEach(fetchMock.resetHistory);

    describe('addQueryEditor', () => {
      it('updates the tab state in the backend', () => {
        expect.assertions(2);

        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.ADD_QUERY_EDITOR,
            queryEditor: { ...queryEditor, id: '1', loaded: true },
          },
        ];
        return store.dispatch(actions.addQueryEditor(queryEditor)).then(() => {
          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
        });
      });
    });

    describe('setActiveQueryEditor', () => {
      it('updates the tab state in the backend', () => {
        expect.assertions(2);

        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.SET_ACTIVE_QUERY_EDITOR,
            queryEditor,
          },
        ];
        return store
          .dispatch(actions.setActiveQueryEditor(queryEditor))
          .then(() => {
            expect(store.getActions()).toEqual(expectedActions);
            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
          });
      });
    });

    describe('removeQueryEditor', () => {
      it('updates the tab state in the backend', () => {
        expect.assertions(2);

        const store = mockStore({});
        const expectedActions = [
          {
            type: actions.REMOVE_QUERY_EDITOR,
            queryEditor,
          },
        ];
        return store
          .dispatch(actions.removeQueryEditor(queryEditor))
          .then(() => {
            expect(store.getActions()).toEqual(expectedActions);
            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
          });
      });
    });

    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);
          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', () => {
          const backendPersistenceOffMock = jest
            .spyOn(uiCore, 'isFeatureEnabled')
            .mockImplementation(
              feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
            );

          const store = mockStore(initialState);
          const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
          request(store.dispatch, store.getState);

          expect(store.getActions()).toEqual(expectedActions);
          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
          backendPersistenceOffMock.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', () => {
      it('updates and runs data preview query when configured', () => {
        expect.assertions(3);

        const results = {
          data: mockBigNumber,
          query: { sqlEditorId: 'null', dbId: 1 },
          query_id: 'efgh',
        };
        fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
          overwriteRoutes: true,
        });

        const tableName = 'table';
        const catalogName = null;
        const schemaName = 'schema';
        const store = mockStore({
          ...initialState,
          sqlLab: {
            ...initialState.sqlLab,
            databases: {
              1: { disable_data_preview: false },
            },
          },
        });
        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);
        });
      });
    });

    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('migrateQueryEditorFromLocalStorage', () => {
      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 tables = [
          { id: 'one', dataPreviewQueryId: 'previewOne' },
          { id: 'two', dataPreviewQueryId: 'previewTwo' },
        ];
        const queries = [
          { ...query, id: 'previewOne' },
          { ...query, id: 'previewTwo' },
        ];
        const store = mockStore({});
        const oldQueryEditor = { ...queryEditor, inLocalStorage: true };
        const expectedActions = [
          {
            type: actions.MIGRATE_QUERY_EDITOR,
            oldQueryEditor,
            // new qe has a different id
            newQueryEditor: {
              ...oldQueryEditor,
              id: '1',
              inLocalStorage: false,
            },
          },
          {
            type: actions.MIGRATE_TAB_HISTORY,
            newId: '1',
            oldId: 'abcd',
          },
          {
            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.migrateQueryEditorFromLocalStorage(
              oldQueryEditor,
              tables,
              queries,
            ),
          )
          .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);
          });
      });
    });
  });
});