Testing x pack jest integration (#26825)

* progress

* progress

* cleanup and elastic configs

* make upgrades to support adding aditional users, with

* use defaultDeep to ensure settings pass correctly

* move needed configs to start servers into kbn_server (except x-pack plugin paths and users)

* move xpack config to an export

* add more time

* diff rollbacks

* roll back prettier diff

* revert setupUsers signature

* remove more bluebird

* update bluebird for fixes with jest compatability

* fix ts errors

* dont allow jest to keep going making errors confising

* Separates configs for jest integration core/x-pack.

* Pass nested kbn config parameters.

* Adds example x-pack integration test using live es.

* Cloud detectors should be configurable for tests.

* Cloud detectors should use native promises only.

* No erroneous comments...

* Util is only for promisify, duh!

* New tests should have docuementation to help those looking to utilize them.

* Doc section headings should be consistent with each other.

* With git there is no need to commit commented code.
This commit is contained in:
nicknak 2019-01-10 14:31:12 -05:00 committed by GitHub
parent 0487c070e9
commit 7cc7147cd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 411 additions and 104 deletions

View file

@ -123,7 +123,7 @@
"babel-loader": "7.1.5",
"babel-polyfill": "6.26.0",
"babel-register": "6.26.0",
"bluebird": "2.9.34",
"bluebird": "3.5.3",
"boom": "^7.2.0",
"brace": "0.11.1",
"cache-loader": "1.2.2",

View file

@ -20,12 +20,12 @@
import { format as formatUrl } from 'url';
import request from 'request';
import { delay, fromNode as fcb } from 'bluebird';
import { delay } from 'bluebird';
export const DEFAULT_SUPERUSER_PASS = 'iamsuperuser';
async function updateCredentials(port, auth, username, password, retries = 10) {
const result = await fcb(cb =>
const result = await new Promise((resolve, reject) =>
request(
{
method: 'PUT',
@ -40,13 +40,15 @@ async function updateCredentials(port, auth, username, password, retries = 10) {
body: { password },
},
(err, httpResponse, body) => {
cb(err, { httpResponse, body });
if (err) return reject(err);
resolve({ httpResponse, body });
}
)
);
const { body, httpResponse } = result;
const { statusCode } = httpResponse;
if (statusCode === 200) {
return;
}
@ -59,21 +61,61 @@ async function updateCredentials(port, auth, username, password, retries = 10) {
throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
}
export async function setupUsers(log, config) {
const esPort = config.get('servers.elasticsearch.port');
export async function setupUsers(log, esPort, updates) {
// track the current credentials for the `elastic` user as
// they will likely change as we apply updates
let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`;
// list of updates we need to apply
const updates = [config.get('servers.elasticsearch'), config.get('servers.kibana')];
for (const { username, password, roles } of updates) {
// If working with a built-in user, just change the password
if (['logstash_system', 'elastic', 'kibana'].includes(username)) {
await updateCredentials(esPort, auth, username, password);
log.info('setting %j user password to %j', username, password);
// If not a builtin user, add them
} else {
await insertUser(esPort, auth, username, password, roles);
log.info('Added %j user with password to %j', username, password);
}
for (const { username, password } of updates) {
log.info('setting %j user password to %j', username, password);
await updateCredentials(esPort, auth, username, password);
if (username === 'elastic') {
auth = `elastic:${password}`;
}
}
}
async function insertUser(port, auth, username, password, roles = [], retries = 10) {
const result = await new Promise((resolve, reject) =>
request(
{
method: 'POST',
uri: formatUrl({
protocol: 'http:',
auth,
hostname: 'localhost',
port,
pathname: `/_xpack/security/user/${username}`,
}),
json: true,
body: { password, roles },
},
(err, httpResponse, body) => {
if (err) return reject(err);
resolve({ httpResponse, body });
}
)
);
const { body, httpResponse } = result;
const { statusCode } = httpResponse;
if (statusCode === 200) {
return;
}
if (retries > 0) {
await delay(2500);
return await insertUser(port, auth, username, password, retries - 1);
}
throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`);
}

View file

@ -44,7 +44,10 @@ export async function runElasticsearch({ config, options }) {
await cluster.start(esArgs);
if (isTrialLicense) {
await setupUsers(log, config);
await setupUsers(log, config.get('servers.elasticsearch.port'), [
config.get('servers.elasticsearch'),
config.get('servers.kibana'),
]);
}
return cluster;

View file

@ -26,3 +26,5 @@ export { OPTIMIZE_BUNDLE_DIR, KIBANA_ROOT } from './functional_tests/lib/paths';
export { esTestConfig, createEsTestCluster } from './es';
export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn';
export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth';

View file

@ -18,9 +18,17 @@
*/
import { ToolingLog } from '@kbn/dev-utils';
// @ts-ignore: implicit any for JS file
import { createEsTestCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test';
import { defaultsDeep } from 'lodash';
import {
createEsTestCluster,
DEFAULT_SUPERUSER_PASS,
esTestConfig,
kbnTestConfig,
kibanaServerTestUser,
kibanaTestUser,
setupUsers,
// @ts-ignore: implicit any for JS file
} from '@kbn/test';
import { defaultsDeep, get } from 'lodash';
import { resolve } from 'path';
import { BehaviorSubject } from 'rxjs';
import supertest from 'supertest';
@ -144,11 +152,40 @@ export async function startTestServers({
settings = {},
}: {
adjustTimeout: (timeout: number) => void;
settings: Record<string, any>;
settings: {
es?: {
license: 'oss' | 'basic' | 'gold' | 'trial';
[key: string]: any;
};
kbn?: {
/**
* An array of directories paths, passed in via absolute path strings
*/
plugins?: {
paths: string[];
[key: string]: any;
};
[key: string]: any;
};
/**
* Users passed in via this prop are created in ES in adition to the standard elastic and kibana users.
* Note, this prop is ignored when using an oss, or basic license
*/
users?: Array<{ username: string; password: string; roles: string[] }>;
};
}) {
if (!adjustTimeout) {
throw new Error('adjustTimeout is required in order to avoid flaky tests');
}
const license = get<'oss' | 'basic' | 'gold' | 'trial'>(settings, 'es.license', 'oss');
const usersToBeAdded = get(settings, 'users', []);
if (usersToBeAdded.length > 0) {
if (license !== 'trial') {
throw new Error(
'Adding users is only supported by startTestServers when using a trial license'
);
}
}
const log = new ToolingLog({
level: 'debug',
@ -159,15 +196,41 @@ export async function startTestServers({
log.info('starting elasticsearch');
log.indent(4);
const es = createEsTestCluster({ log });
const es = createEsTestCluster(
defaultsDeep({}, get(settings, 'es', {}), {
log,
license,
password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined,
})
);
log.indent(-4);
adjustTimeout(es.getStartTimeout());
// Add time for KBN and adding users
adjustTimeout(es.getStartTimeout() + 100000);
await es.start();
const root = createRootWithCorePlugins(settings);
const kbnSettings: any = get(settings, 'kbn', {});
if (['gold', 'trial'].includes(license)) {
await setupUsers(log, esTestConfig.getUrlParts().port, [
...usersToBeAdded,
// user elastic
esTestConfig.getUrlParts(),
// user kibana
kbnTestConfig.getUrlParts(),
]);
// Override provided configs, we know what the elastic user is now
kbnSettings.elasticsearch = {
url: esTestConfig.getUrl(),
username: esTestConfig.getUrlParts().username,
password: esTestConfig.getUrlParts().password,
};
}
const root = createRootWithCorePlugins(kbnSettings);
await root.start();
const kbnServer = getKbnServer(root);

View file

@ -27,12 +27,14 @@ export async function startServers() {
servers = await startTestServers({
adjustTimeout: (t) => this.timeout(t),
settings: {
uiSettings: {
overrides: {
foo: 'bar',
}
kbn: {
uiSettings: {
overrides: {
foo: 'bar',
}
},
},
}
},
});
kbnServer = servers.kbnServer;
}

View file

@ -100,6 +100,16 @@ We also have SAML API integration tests which set up Elasticsearch and Kibana wi
node scripts/functional_tests --config test/saml_api_integration/config
```
#### Running and building Jest integration tests
Jest integration tests can be used to test behavior with Elasticsearch and the Kibana server.
```sh
node scripts/jest_integration
```
An example test exists at [test_utils/jest/integration_tests/example_integration.test.ts](test_utils/jest/integration_tests/example_integration.test.ts)
#### Running Reporting functional tests
See [here](test/reporting/README.md) for more information on running reporting tests.

View file

@ -149,7 +149,7 @@
"babel-register": "^6.26.0",
"babel-runtime": "^6.26.0",
"base64-js": "^1.2.1",
"bluebird": "3.1.1",
"bluebird": "3.5.3",
"boom": "^7.2.0",
"brace": "0.11.1",
"chroma-js": "^1.3.6",

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// file.skip
// @ts-ignore
import { createEsTestCluster } from '@kbn/test';
import { config as beatsPluginConfig } from '../../../../..';
// @ts-ignore
import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server';
import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter';
import { PLUGIN } from './../../../../../common/constants/plugin';
import { CONFIG_PREFIX } from './../../../../../common/constants/plugin';
import { contractTests } from './test_contract';
const kbnServer = kbnTestServer.createRootWithCorePlugins({ server: { maxPayloadBytes: 100 } });
const legacyServer = kbnTestServer.getKbnServer(kbnServer);
contractTests('Kibana Framework Adapter', {
before: async () => {
await kbnServer.start();
const config = legacyServer.server.config();
config.extendSchema(beatsPluginConfig, {}, CONFIG_PREFIX);
config.set('xpack.beats.encryptionKey', 'foo');
},
after: async () => {
await kbnServer.shutdown();
},
adapterSetup: () => {
return new KibanaBackendFrameworkAdapter(PLUGIN.ID, legacyServer.server);
},
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// file.skip
// @ts-ignore
import { esTestConfig, kbnTestConfig, OPTIMIZE_BUNDLE_DIR } from '@kbn/test';
// @ts-ignore
import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server';
// @ts-ignore
import { xpackKbnServerConfig } from '../../../../../../../test_utils/kbn_server_config';
import { PLUGIN } from './../../../../../common/constants/plugin';
import { KibanaBackendFrameworkAdapter } from './../kibana_framework_adapter';
import { contractTests } from './test_contract';
let servers: any;
contractTests('Kibana Framework Adapter', {
async before() {
servers = await kbnTestServer.startTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: xpackKbnServerConfig,
});
},
async after() {
await servers.stop();
},
adapterSetup: () => {
return new KibanaBackendFrameworkAdapter(PLUGIN.ID, servers.kbnServer.server);
},
});

View file

@ -6,29 +6,24 @@
import { BackendFrameworkAdapter } from '../adapter_types';
interface ContractConfig {
before?(): Promise<void>;
after?(): Promise<void>;
before(): Promise<void>;
after(): Promise<void>;
adapterSetup(): BackendFrameworkAdapter;
}
export const contractTests = (testName: string, config: ContractConfig) => {
describe.skip(testName, () => {
// let frameworkAdapter: BackendFrameworkAdapter;
beforeAll(async () => {
jest.setTimeout(100000); // 1 second
if (config.before) {
await config.before();
}
});
afterAll(async () => config.after && (await config.after()));
describe(testName, () => {
let frameworkAdapter: any;
beforeAll(config.before);
afterAll(config.after);
beforeEach(async () => {
// FIXME: one of these always should exist, type ContractConfig as such
// const frameworkAdapter = config.adapterSetup();
frameworkAdapter = config.adapterSetup();
});
it('Should have tests here', () => {
expect(true).toEqual(true);
expect(frameworkAdapter).toHaveProperty('server');
expect(frameworkAdapter.server).toHaveProperty('plugins');
expect(frameworkAdapter.server.plugins).toHaveProperty('security');
});
});
};

View file

@ -82,6 +82,11 @@ export const config = (Joi) => {
alwaysPresentCertificate: Joi.boolean().default(false),
}).default(),
apiVersion: Joi.string().default('master')
}).default()
}).default(),
tests: Joi.object({
cloud_detector: Joi.object({
enabled: Joi.boolean().default(true)
}).default()
}).default(),
}).default();
};

View file

@ -5,7 +5,7 @@
*/
import { get, isString, omit } from 'lodash';
import { fromCallback } from 'bluebird';
import { promisify } from 'util';
import { CloudService } from './cloud_service';
import { CloudServiceResponse } from './cloud_response';
import fs from 'fs';
@ -38,8 +38,8 @@ export class AWSCloudService extends CloudService {
json: true
};
return fromCallback(callback => request(req, callback), { multiArgs: true })
.then(response => this._parseResponse(response[1], (body) => this._parseBody(body)))
return promisify(request)(req)
.then(response => this._parseResponse(response.body, (body) => this._parseBody(body)))
// fall back to file detection
.catch(() => this._tryToDetectUuid());
}
@ -96,7 +96,7 @@ export class AWSCloudService extends CloudService {
_tryToDetectUuid() {
// Windows does not have an easy way to check
if (!this._isWindows) {
return fromCallback(callback => this._fs.readFile('/sys/hypervisor/uuid', 'utf8', callback))
return promisify(this._fs.readFile)('/sys/hypervisor/uuid', 'utf8')
.then(uuid => {
if (isString(uuid)) {
// Some AWS APIs return it lowercase (like the file did in testing), while others return it uppercase

View file

@ -5,7 +5,7 @@
*/
import { get, omit } from 'lodash';
import { fromCallback } from 'bluebird';
import { promisify } from 'util';
import { CloudService } from './cloud_service';
import { CloudServiceResponse } from './cloud_response';
import { CLOUD_METADATA_SERVICES } from '../../common/constants';
@ -30,9 +30,11 @@ class AzureCloudService extends CloudService {
json: true
};
return fromCallback(callback => request(req, callback), { multiArgs: true })
return promisify(request)(req)
// Note: there is no fallback option for Azure
.then(response => this._parseResponse(response[1], (body) => this._parseBody(body)));
.then(response => {
return this._parseResponse(response.body, (body) => this._parseBody(body));
});
}
/**

View file

@ -5,7 +5,7 @@
*/
import { isString } from 'lodash';
import { fromCallback, map } from 'bluebird';
import { promisify } from 'util';
import { CloudService } from './cloud_service';
import { CloudServiceResponse } from './cloud_response';
import { CLOUD_METADATA_SERVICES } from '../../common/constants';
@ -23,13 +23,19 @@ class GCPCloudService extends CloudService {
// we need to call GCP individually for each field
const fields = [ 'id', 'machine-type', 'zone' ];
return map(fields, field => fromCallback(callback => request(this._createRequestForField(field), callback), { multiArgs: true }))
const create = this._createRequestForField;
const allRequests = fields.map(field => promisify(request)(create(field)));
return Promise.all(allRequests)
/*
Note: there is no fallback option for GCP;
responses are arrays containing [fullResponse, body];
because GCP returns plaintext, we have no way of validating without using the response code
*/
.then(responses => responses.map(response => this._extractBody(...response)))
.then(responses => {
return responses.map(response => {
return this._extractBody(response, response.body);
});
})
.then(([id, machineType, zone]) => this._combineResponses(id, machineType, zone));
}

View file

@ -17,7 +17,10 @@ import { CloudDetector } from '../../../cloud';
export function opsBuffer(server) {
// determine the cloud service in the background
const cloudDetector = new CloudDetector();
cloudDetector.detectCloudService();
if(server.config().get('xpack.monitoring.tests.cloud_detector.enabled')) {
cloudDetector.detectCloudService();
}
const eventRoller = new EventRoller();

View file

@ -5,7 +5,6 @@
*/
import Joi from 'joi';
import Promise from 'bluebird';
import { getClusterStatus } from '../../../../lib/logstash/get_cluster_status';
import { getNodes } from '../../../../lib/logstash/get_nodes';
import { handleError } from '../../../../lib/errors';

View file

@ -8,7 +8,7 @@ import * as Rx from 'rxjs';
import { first, tap, mergeMap } from 'rxjs/operators';
import fs from 'fs';
import getPort from 'get-port';
import { promisify } from 'bluebird';
import { promisify } from 'util';
import { LevelLogger } from '../../../server/lib/level_logger';
import { i18n } from '@kbn/i18n';
@ -329,3 +329,4 @@ export function screenshotsObservableFactory(server) {
);
};
}

View file

@ -5,7 +5,7 @@
*/
import getosSync from 'getos';
import { promisify } from 'bluebird';
import { promisify } from 'util';
const getos = promisify(getosSync);

View file

@ -5,7 +5,7 @@
*/
import getosSync from 'getos';
import { promisify } from 'bluebird';
import { promisify } from 'util';
const getos = promisify(getosSync);

View file

@ -10,7 +10,7 @@ import expect from 'expect.js';
import { extract } from '../extract';
import { ExtractError } from '../extract_error';
import { promisify } from 'bluebird';
import { promisify } from 'util';
const FIXTURES_FOLDER = `${__dirname}/__fixtures__`;
const SRC_FILE_UNCOMPRESSED = `${FIXTURES_FOLDER}/extract_test_file.js`;

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// # Run Jest integration tests
//
// All args will be forwarded directly to Jest, e.g. to watch tests run:
//
// node scripts/jest_integration --watch
//
// or to build code coverage:
//
// node scripts/jest_integration --coverage
//
// See all cli options in https://facebook.github.io/jest/docs/cli.html
const resolve = require('path').resolve;
process.argv.push('--config', resolve(__dirname, '../test_utils/jest/config.integration.js'));
require('../../src/setup_node_env');
require('../../src/dev/jest/cli');

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import config from './config';
export default {
...config,
testMatch: [
'**/integration_tests/**/*.test.js',
'**/integration_tests/**/*.test.ts',
],
testPathIgnorePatterns: config.testPathIgnorePatterns.filter(
(pattern) => !pattern.includes('integration_tests')
),
reporters: [
'default',
['<rootDir>/../src/dev/jest/junit_reporter.js', { reportName: 'Jest Integration Tests' }],
],
};

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export default {
rootDir: '../../',
roots: [
'<rootDir>/plugins',
'<rootDir>/server',
'<rootDir>/common',
'<rootDir>/test_utils/jest/integration_tests',
],
collectCoverageFrom: [
'plugins/**/*.js',
'common/**/*.js',
'server/**/*.js',
],
moduleNameMapper: {
'^ui/(.*)': '<rootDir>**/public/$1',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/dev/jest/mocks/file_mock.js',
'\\.(css|less|scss)$': '<rootDir>/../src/dev/jest/mocks/style_mock.js',
},
setupFiles: [
'<rootDir>/../src/dev/jest/setup/babel_polyfill.js',
'<rootDir>/../src/dev/jest/setup/polyfills.js',
'<rootDir>/../src/dev/jest/setup/enzyme.js',
'<rootDir>/../src/dev/jest/setup/throw_on_console_error.js',
],
coverageDirectory: '<rootDir>/../target/jest-coverage',
coverageReporters: [
'html',
],
globals: {
'ts-jest': {
skipBabel: true,
},
},
moduleFileExtensions: [
'js',
'json',
'ts',
'tsx',
],
modulePathIgnorePatterns: [
'__fixtures__/',
'target/',
],
testMatch: [
'**/*.test.{js,ts,tsx}'
],
testPathIgnorePatterns: [
'<rootDir>/packages/kbn-ui-framework/(dist|doc_site|generator-kui)/',
'<rootDir>/packages/kbn-pm/dist/',
'integration_tests/'
],
transform: {
'^.+\\.js$': '<rootDir>/../src/dev/jest/babel_transform.js',
'^.+\\.tsx?$': '<rootDir>/../src/dev/jest/ts_transform.js',
'^.+\\.txt?$': 'jest-raw-loader',
'^.+\\.html?$': 'jest-raw-loader',
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.js$',
'packages/kbn-pm/dist/index.js'
],
snapshotSerializers: [
'<rootDir>/../node_modules/enzyme-to-json/serializer',
],
reporters: [
'default',
'<rootDir>/../src/dev/jest/junit_reporter.js',
],
};

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as kbnTestServer from '../../../../src/test_utils/kbn_server';
import { TestKbnServerConfig } from '../../kbn_server_config';
describe('example integration test with kbn server', async () => {
let servers: any = null;
beforeAll(async () => {
servers = await kbnTestServer.startTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: TestKbnServerConfig,
});
expect(servers).toBeDefined();
});
afterAll(async () => {
await servers.stop();
});
it('should have started new platform server correctly', () => {
expect(servers.kbnServer).toBeDefined();
expect(servers.kbnServer.server).toBeDefined();
expect(servers.kbnServer.server.plugins).toBeDefined();
});
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { resolve } from 'path';
type Licenses = 'oss' | 'basic' | 'gold' | 'trial';
export const TestKbnServerConfig = {
kbn: {
plugins: { paths: [resolve(__dirname, '../../node_modules/x-pack')] },
xpack: {
monitoring: {
tests: {
cloud_detector: {
enabled: false,
},
},
},
},
},
es: {
license: 'trial' as Licenses,
},
users: [
{
username: 'kibana_user',
password: 'x-pack-test-password',
roles: ['kibana_user'],
},
],
};

View file

@ -4176,16 +4176,6 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
bluebird@2.9.34:
version "2.9.34"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.34.tgz#2f7b4ec80216328a9fddebdf69c8d4942feff7d8"
integrity sha1-L3tOyAIWMoqf3evfacjUlC/v99g=
bluebird@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.1.1.tgz#7e2e4318d62ae72a674f6aea6357bb4def1a6e41"
integrity sha1-fi5DGNYq5ypnT2rqY1e7Te8abkE=
bluebird@3.4.6:
version "3.4.6"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f"
@ -4196,6 +4186,11 @@ bluebird@3.5.1, bluebird@^3.3.0, bluebird@^3.3.1:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==
bluebird@3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
bluebird@^2.10.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"