* Revert "[FTR] Use importExport for saved_object/basic archive (#100244) (#102016)" This reverts commit9851b7bcfa
. * Revert "[7.x] [FTR][CI] Use default distribution for all tests (#94968) (#101118)" This reverts commit73225daa87
.
596 lines
23 KiB
TypeScript
596 lines
23 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
* Side Public License, v 1.
|
|
*/
|
|
|
|
import expect from '@kbn/expect';
|
|
import type { FtrProviderContext } from '../../ftr_provider_context';
|
|
import { getKibanaVersion } from './lib/saved_objects_test_utils';
|
|
|
|
function ndjsonToObject(input: string) {
|
|
return input.split('\n').map((str) => JSON.parse(str));
|
|
}
|
|
export default function ({ getService }: FtrProviderContext) {
|
|
const supertest = getService('supertest');
|
|
const esArchiver = getService('esArchiver');
|
|
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
|
|
|
describe('export', () => {
|
|
let KIBANA_VERSION: string;
|
|
|
|
before(async () => {
|
|
KIBANA_VERSION = await getKibanaVersion(getService);
|
|
});
|
|
|
|
describe('with kibana index', () => {
|
|
describe('basic amount of saved objects', () => {
|
|
before(() =>
|
|
esArchiver.load('test/api_integration/fixtures/es_archiver/saved_objects/basic')
|
|
);
|
|
after(() =>
|
|
esArchiver.unload('test/api_integration/fixtures/es_archiver/saved_objects/basic')
|
|
);
|
|
|
|
it('should return objects in dependency order', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
const objects = ndjsonToObject(resp.text);
|
|
expect(objects).to.have.length(4);
|
|
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[0]).to.have.property('type', 'index-pattern');
|
|
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[1]).to.have.property('type', 'visualization');
|
|
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
|
expect(objects[2]).to.have.property('type', 'dashboard');
|
|
expect(objects[3]).to.have.property('exportedCount', 3);
|
|
expect(objects[3]).to.have.property('missingRefCount', 0);
|
|
expect(objects[3].missingReferences).to.have.length(0);
|
|
});
|
|
});
|
|
|
|
it('should exclude the export details if asked', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
const objects = ndjsonToObject(resp.text);
|
|
expect(objects).to.have.length(3);
|
|
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[0]).to.have.property('type', 'index-pattern');
|
|
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[1]).to.have.property('type', 'visualization');
|
|
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
|
expect(objects[2]).to.have.property('type', 'dashboard');
|
|
});
|
|
});
|
|
|
|
it('should support including dependencies when exporting selected objects', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
includeReferencesDeep: true,
|
|
objects: [
|
|
{
|
|
type: 'dashboard',
|
|
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
},
|
|
],
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
const objects = ndjsonToObject(resp.text);
|
|
expect(objects).to.have.length(4);
|
|
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[0]).to.have.property('type', 'index-pattern');
|
|
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[1]).to.have.property('type', 'visualization');
|
|
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
|
expect(objects[2]).to.have.property('type', 'dashboard');
|
|
expect(objects[3]).to.have.property('exportedCount', 3);
|
|
expect(objects[3]).to.have.property('missingRefCount', 0);
|
|
expect(objects[3].missingReferences).to.have.length(0);
|
|
});
|
|
});
|
|
|
|
it('should support including dependencies when exporting by type', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
includeReferencesDeep: true,
|
|
type: ['dashboard'],
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
const objects = resp.text.split('\n').map((str) => JSON.parse(str));
|
|
expect(objects).to.have.length(4);
|
|
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[0]).to.have.property('type', 'index-pattern');
|
|
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[1]).to.have.property('type', 'visualization');
|
|
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
|
expect(objects[2]).to.have.property('type', 'dashboard');
|
|
expect(objects[3]).to.have.property('exportedCount', 3);
|
|
expect(objects[3]).to.have.property('missingRefCount', 0);
|
|
expect(objects[3].missingReferences).to.have.length(0);
|
|
});
|
|
});
|
|
|
|
it('should support including dependencies when exporting by type and search', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
includeReferencesDeep: true,
|
|
type: ['dashboard'],
|
|
search: 'Requests*',
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
const objects = ndjsonToObject(resp.text);
|
|
expect(objects).to.have.length(4);
|
|
expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[0]).to.have.property('type', 'index-pattern');
|
|
expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
|
expect(objects[1]).to.have.property('type', 'visualization');
|
|
expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab');
|
|
expect(objects[2]).to.have.property('type', 'dashboard');
|
|
expect(objects[3]).to.have.property('exportedCount', 3);
|
|
expect(objects[3]).to.have.property('missingRefCount', 0);
|
|
expect(objects[3].missingReferences).to.have.length(0);
|
|
});
|
|
});
|
|
|
|
it(`should throw error when object doesn't exist`, async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
objects: [
|
|
{
|
|
type: 'dashboard',
|
|
id: '1',
|
|
},
|
|
],
|
|
})
|
|
.expect(400)
|
|
.then((resp) => {
|
|
expect(resp.body).to.eql({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Error fetching objects to export',
|
|
attributes: {
|
|
objects: [
|
|
{
|
|
id: '1',
|
|
type: 'dashboard',
|
|
error: {
|
|
error: 'Not Found',
|
|
message: 'Saved object [dashboard/1] not found',
|
|
statusCode: 404,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
it(`should return 400 when exporting unsupported type`, async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['wigwags'],
|
|
})
|
|
.expect(400)
|
|
.then((resp) => {
|
|
expect(resp.body).to.eql({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Trying to export non-exportable type(s): wigwags',
|
|
});
|
|
});
|
|
});
|
|
|
|
it(`should return 400 when exporting objects with unsupported type`, async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
objects: [
|
|
{
|
|
type: 'wigwags',
|
|
id: '1',
|
|
},
|
|
],
|
|
})
|
|
.expect(400)
|
|
.then((resp) => {
|
|
expect(resp.body).to.eql({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: 'Trying to export object(s) with non-exportable types: wigwags:1',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should export object with circular refs', async () => {
|
|
const soWithCycliRefs = [
|
|
{
|
|
type: 'dashboard',
|
|
id: 'dashboard-a',
|
|
attributes: {
|
|
title: 'dashboard-a',
|
|
},
|
|
references: [
|
|
{
|
|
name: 'circular-dashboard-ref',
|
|
id: 'dashboard-b',
|
|
type: 'dashboard',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'dashboard',
|
|
id: 'dashboard-b',
|
|
attributes: {
|
|
title: 'dashboard-b',
|
|
},
|
|
references: [
|
|
{
|
|
name: 'circular-dashboard-ref',
|
|
id: 'dashboard-a',
|
|
type: 'dashboard',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
await supertest.post('/api/saved_objects/_bulk_create').send(soWithCycliRefs).expect(200);
|
|
const resp = await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
includeReferencesDeep: true,
|
|
type: ['dashboard'],
|
|
})
|
|
.expect(200);
|
|
|
|
const objects = ndjsonToObject(resp.text);
|
|
expect(objects.find((o) => o.id === 'dashboard-a')).to.be.ok();
|
|
expect(objects.find((o) => o.id === 'dashboard-b')).to.be.ok();
|
|
});
|
|
});
|
|
|
|
describe('10,000 objects', () => {
|
|
before(() =>
|
|
esArchiver.load('test/api_integration/fixtures/es_archiver/saved_objects/10k')
|
|
);
|
|
after(() =>
|
|
esArchiver.unload('test/api_integration/fixtures/es_archiver/saved_objects/10k')
|
|
);
|
|
|
|
it('should return 400 when exporting without type or objects passed in', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.expect(400)
|
|
.then((resp) => {
|
|
expect(resp.body).to.eql({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: '[request body]: expected a plain object value, but found [null] instead.',
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should return 200 when exporting by single type', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: 'dashboard',
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
expect(resp.header['content-disposition']).to.eql(
|
|
'attachment; filename="export.ndjson"'
|
|
);
|
|
expect(resp.header['content-type']).to.eql('application/ndjson');
|
|
const objects = ndjsonToObject(resp.text);
|
|
|
|
// Sort values aren't deterministic so we need to exclude them
|
|
const { sort, ...obj } = objects[0];
|
|
expect(obj).to.eql({
|
|
attributes: {
|
|
description: '',
|
|
hits: 0,
|
|
kibanaSavedObjectMeta: {
|
|
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
|
},
|
|
optionsJSON: objects[0].attributes.optionsJSON,
|
|
panelsJSON: objects[0].attributes.panelsJSON,
|
|
refreshInterval: {
|
|
display: 'Off',
|
|
pause: false,
|
|
value: 0,
|
|
},
|
|
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
|
timeRestore: true,
|
|
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
|
title: 'Requests',
|
|
version: 1,
|
|
},
|
|
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
migrationVersion: objects[0].migrationVersion,
|
|
coreMigrationVersion: KIBANA_VERSION,
|
|
references: [
|
|
{
|
|
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
|
name: '1:panel_1',
|
|
type: 'visualization',
|
|
},
|
|
],
|
|
type: 'dashboard',
|
|
updated_at: '2017-09-21T18:57:40.826Z',
|
|
version: objects[0].version,
|
|
});
|
|
expect(objects[0].migrationVersion).to.be.ok();
|
|
expect(() =>
|
|
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
|
|
).not.to.throwError();
|
|
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
|
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
|
});
|
|
});
|
|
|
|
it('should return 200 when exporting by array type', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['dashboard'],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
expect(resp.header['content-disposition']).to.eql(
|
|
'attachment; filename="export.ndjson"'
|
|
);
|
|
expect(resp.header['content-type']).to.eql('application/ndjson');
|
|
const objects = ndjsonToObject(resp.text);
|
|
|
|
// Sort values aren't deterministic so we need to exclude them
|
|
const { sort, ...obj } = objects[0];
|
|
expect(obj).to.eql({
|
|
attributes: {
|
|
description: '',
|
|
hits: 0,
|
|
kibanaSavedObjectMeta: {
|
|
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
|
},
|
|
optionsJSON: objects[0].attributes.optionsJSON,
|
|
panelsJSON: objects[0].attributes.panelsJSON,
|
|
refreshInterval: {
|
|
display: 'Off',
|
|
pause: false,
|
|
value: 0,
|
|
},
|
|
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
|
timeRestore: true,
|
|
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
|
title: 'Requests',
|
|
version: 1,
|
|
},
|
|
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
migrationVersion: objects[0].migrationVersion,
|
|
coreMigrationVersion: KIBANA_VERSION,
|
|
references: [
|
|
{
|
|
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
|
name: '1:panel_1',
|
|
type: 'visualization',
|
|
},
|
|
],
|
|
type: 'dashboard',
|
|
updated_at: '2017-09-21T18:57:40.826Z',
|
|
version: objects[0].version,
|
|
});
|
|
expect(objects[0].migrationVersion).to.be.ok();
|
|
expect(() =>
|
|
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
|
|
).not.to.throwError();
|
|
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
|
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
|
});
|
|
});
|
|
|
|
it('should return 200 when exporting by objects', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
objects: [
|
|
{
|
|
type: 'dashboard',
|
|
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
},
|
|
],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
expect(resp.header['content-disposition']).to.eql(
|
|
'attachment; filename="export.ndjson"'
|
|
);
|
|
expect(resp.header['content-type']).to.eql('application/ndjson');
|
|
const objects = ndjsonToObject(resp.text);
|
|
|
|
// Sort values aren't deterministic so we need to exclude them
|
|
const { sort, ...obj } = objects[0];
|
|
expect(obj).to.eql({
|
|
attributes: {
|
|
description: '',
|
|
hits: 0,
|
|
kibanaSavedObjectMeta: {
|
|
searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON,
|
|
},
|
|
optionsJSON: objects[0].attributes.optionsJSON,
|
|
panelsJSON: objects[0].attributes.panelsJSON,
|
|
refreshInterval: {
|
|
display: 'Off',
|
|
pause: false,
|
|
value: 0,
|
|
},
|
|
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
|
timeRestore: true,
|
|
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
|
title: 'Requests',
|
|
version: 1,
|
|
},
|
|
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
migrationVersion: objects[0].migrationVersion,
|
|
coreMigrationVersion: KIBANA_VERSION,
|
|
references: [
|
|
{
|
|
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
|
name: '1:panel_1',
|
|
type: 'visualization',
|
|
},
|
|
],
|
|
type: 'dashboard',
|
|
updated_at: '2017-09-21T18:57:40.826Z',
|
|
version: objects[0].version,
|
|
});
|
|
expect(objects[0].migrationVersion).to.be.ok();
|
|
expect(() =>
|
|
JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)
|
|
).not.to.throwError();
|
|
expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError();
|
|
expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError();
|
|
});
|
|
});
|
|
|
|
it('should return 400 when exporting by type and objects', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: 'dashboard',
|
|
objects: [
|
|
{
|
|
type: 'dashboard',
|
|
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
|
},
|
|
],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(400)
|
|
.then((resp) => {
|
|
expect(resp.body).to.eql({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: `Can't specify both "types" and "objects" properties when exporting`,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('10,001 objects', () => {
|
|
let customVisId: string;
|
|
before(async () => {
|
|
await esArchiver.load('test/api_integration/fixtures/es_archiver/saved_objects/10k');
|
|
await supertest
|
|
.post('/api/saved_objects/visualization')
|
|
.send({
|
|
attributes: {
|
|
title: 'My favorite vis',
|
|
},
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
customVisId = resp.body.id;
|
|
});
|
|
});
|
|
after(async () => {
|
|
await supertest.delete(`/api/saved_objects/visualization/${customVisId}`).expect(200);
|
|
await esArchiver.unload('test/api_integration/fixtures/es_archiver/saved_objects/10k');
|
|
});
|
|
|
|
it('should allow exporting more than 10,000 objects if permitted by maxImportExportSize', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
expect(resp.header['content-disposition']).to.eql(
|
|
'attachment; filename="export.ndjson"'
|
|
);
|
|
expect(resp.header['content-type']).to.eql('application/ndjson');
|
|
const objects = ndjsonToObject(resp.text);
|
|
expect(objects.length).to.eql(10001);
|
|
});
|
|
});
|
|
|
|
it('should return 400 when exporting more than allowed by maxImportExportSize', async () => {
|
|
let anotherCustomVisId: string;
|
|
await supertest
|
|
.post('/api/saved_objects/visualization')
|
|
.send({
|
|
attributes: {
|
|
title: 'My other favorite vis',
|
|
},
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
anotherCustomVisId = resp.body.id;
|
|
});
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(400)
|
|
.then((resp) => {
|
|
expect(resp.body).to.eql({
|
|
statusCode: 400,
|
|
error: 'Bad Request',
|
|
message: `Can't export more than 10001 objects. If your server has enough memory, this limit can be increased by adjusting the \"savedObjects.maxImportExportSize\" setting.`,
|
|
});
|
|
});
|
|
await supertest
|
|
// @ts-expect-error TS complains about using `anotherCustomVisId` before it is assigned
|
|
.delete(`/api/saved_objects/visualization/${anotherCustomVisId}`)
|
|
.expect(200);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('without kibana index', () => {
|
|
before(
|
|
async () =>
|
|
// just in case the kibana server has recreated it
|
|
await esDeleteAllIndices('.kibana*')
|
|
);
|
|
|
|
it('should return empty response', async () => {
|
|
await supertest
|
|
.post('/api/saved_objects/_export')
|
|
.send({
|
|
type: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
|
excludeExportDetails: true,
|
|
})
|
|
.expect(200)
|
|
.then((resp) => {
|
|
expect(resp.text).to.eql('');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|