airbnb/caravel

View on GitHub
superset-frontend/src/middleware/asyncEvent.test.ts

Summary

Maintainability
A
0 mins
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.
 */
import fetchMock from 'fetch-mock';
import WS from 'jest-websocket-mock';
import sinon from 'sinon';
import * as uiCore from '@superset-ui/core';
import * as asyncEvent from 'src/middleware/asyncEvent';

describe('asyncEvent middleware', () => {
  const asyncPendingEvent = {
    status: 'pending',
    result_url: null,
    job_id: 'foo123',
    channel_id: '999',
    errors: [],
  };
  const asyncDoneEvent = {
    id: '1518951480106-0',
    status: 'done',
    result_url: '/api/v1/chart/data/cache-key-1',
    job_id: 'foo123',
    channel_id: '999',
    errors: [],
  };
  const asyncErrorEvent = {
    id: '1518951480107-0',
    status: 'error',
    result_url: null,
    job_id: 'foo123',
    channel_id: '999',
    errors: [{ message: "Error: relation 'foo' does not exist" }],
  };
  const chartData = {
    result: [
      {
        cache_key: '199f01f81f99c98693694821e4458111',
        cached_dttm: null,
        cache_timeout: 86400,
        annotation_data: {},
        error: null,
        is_cached: false,
        query:
          'SELECT product_line AS product_line,\n       sum(sales) AS "(Sales)"\nFROM cleaned_sales_data\nGROUP BY product_line\nLIMIT 50000',
        status: 'success',
        stacktrace: null,
        rowcount: 7,
        colnames: ['product_line', '(Sales)'],
        coltypes: [1, 0],
        data: [
          {
            product_line: 'Classic Cars',
            '(Sales)': 3919615.66,
          },
        ],
        applied_filters: [
          {
            column: '__time_range',
          },
        ],
        rejected_filters: [],
      },
    ],
  };

  const EVENTS_ENDPOINT = 'glob:*/api/v1/async_event/*';
  const CACHED_DATA_ENDPOINT = 'glob:*/api/v1/chart/data/*';
  let featureEnabledStub: any;

  beforeEach(async () => {
    featureEnabledStub = sinon.stub(uiCore, 'isFeatureEnabled');
    featureEnabledStub.withArgs('GLOBAL_ASYNC_QUERIES').returns(true);
  });

  afterEach(() => {
    fetchMock.reset();
    featureEnabledStub.restore();
  });

  afterAll(fetchMock.reset);

  describe('polling transport', () => {
    const config = {
      GLOBAL_ASYNC_QUERIES_TRANSPORT: 'polling',
      GLOBAL_ASYNC_QUERIES_POLLING_DELAY: 50,
      GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL: '',
    };

    beforeEach(async () => {
      fetchMock.get(EVENTS_ENDPOINT, {
        status: 200,
        body: { result: [asyncDoneEvent] },
      });
      fetchMock.get(CACHED_DATA_ENDPOINT, {
        status: 200,
        body: { result: chartData },
      });
      asyncEvent.init(config);
    });

    it('resolves with chart data on event done status', async () => {
      await expect(
        asyncEvent.waitForAsyncData(asyncPendingEvent),
      ).resolves.toEqual([chartData]);

      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1);
      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
    });

    it('rejects on event error status', async () => {
      fetchMock.reset();
      fetchMock.get(EVENTS_ENDPOINT, {
        status: 200,
        body: { result: [asyncErrorEvent] },
      });
      const errorResponse = await uiCore.parseErrorJson(asyncErrorEvent);
      await expect(
        asyncEvent.waitForAsyncData(asyncPendingEvent),
      ).rejects.toEqual(errorResponse);

      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1);
      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(0);
    });

    it('rejects on cached data fetch error', async () => {
      fetchMock.reset();
      fetchMock.get(EVENTS_ENDPOINT, {
        status: 200,
        body: { result: [asyncDoneEvent] },
      });
      fetchMock.get(CACHED_DATA_ENDPOINT, {
        status: 400,
      });

      const errorResponse = [{ error: 'Bad Request' }];
      await expect(
        asyncEvent.waitForAsyncData(asyncPendingEvent),
      ).rejects.toEqual(errorResponse);

      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(1);
      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
    });
  });

  describe('ws transport', () => {
    let wsServer: WS;
    const config = {
      GLOBAL_ASYNC_QUERIES_TRANSPORT: 'ws',
      GLOBAL_ASYNC_QUERIES_POLLING_DELAY: 50,
      GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL: 'ws://127.0.0.1:8080/',
    };

    beforeEach(async () => {
      fetchMock.get(EVENTS_ENDPOINT, {
        status: 200,
        body: { result: [asyncDoneEvent] },
      });
      fetchMock.get(CACHED_DATA_ENDPOINT, {
        status: 200,
        body: { result: chartData },
      });

      wsServer = new WS(config.GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL);
      asyncEvent.init(config);
    });

    afterEach(() => {
      WS.clean();
    });

    it('resolves with chart data on event done status', async () => {
      await wsServer.connected;

      const promise = asyncEvent.waitForAsyncData(asyncPendingEvent);

      wsServer.send(JSON.stringify(asyncDoneEvent));

      await expect(promise).resolves.toEqual([chartData]);

      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0);
    });

    it('rejects on event error status', async () => {
      await wsServer.connected;

      const promise = asyncEvent.waitForAsyncData(asyncPendingEvent);

      wsServer.send(JSON.stringify(asyncErrorEvent));

      const errorResponse = await uiCore.parseErrorJson(asyncErrorEvent);

      await expect(promise).rejects.toEqual(errorResponse);

      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(0);
      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0);
    });

    it('rejects on cached data fetch error', async () => {
      fetchMock.reset();
      fetchMock.get(CACHED_DATA_ENDPOINT, {
        status: 400,
      });

      await wsServer.connected;

      const promise = asyncEvent.waitForAsyncData(asyncPendingEvent);

      wsServer.send(JSON.stringify(asyncDoneEvent));

      const errorResponse = [{ error: 'Bad Request' }];

      await expect(promise).rejects.toEqual(errorResponse);

      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0);
    });

    it('resolves when events are received before listener', async () => {
      await wsServer.connected;

      wsServer.send(JSON.stringify(asyncDoneEvent));

      const promise = asyncEvent.waitForAsyncData(asyncPendingEvent);
      await expect(promise).resolves.toEqual([chartData]);

      expect(fetchMock.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
      expect(fetchMock.calls(EVENTS_ENDPOINT)).toHaveLength(0);
    });
  });
});