Basic server side import API for saved objects (#32158)

* Initial work

* Add overwrite and skip support

* Cleanup and add tests

* Move code into separate files

* Remove reduce

* New API parameters

* Add support to replace references

* Add better error handling

* Add spaces tests

* Fix return type in collectSavedObjects

* Apply PR feedback

* Update jest tests due to jest version upgrade

* Add docs

* WIP

* Split import routes pt1

* Add tests

* Fix broken tests

* Update docs and fix broken test

* Add successCount to _import endpoint

* Make skip by default in resolution API

* Update tests for removal of skips

* Add back support for skips

* Add success count

* Add back resolve import conflicts x-pack tests

* Remove writev from filter stream

* Delete _mock_server.d.ts file

* Rename lib/import_saved_objects to lib/import

* Filter records at stream level for conflict resolution

* Update docs

* Add tests to validate documentation

* Return 200 instead of other code for errors, include errors array

* Change [] to {}

* Apply PR feedback

* Fix import object limit to not return 500

* Change some wording in the docs

* Fix status code

* Apply PR feedback pt2

* Lower maxImportPayloadBytes to 10MB

* Add unknown type tests for import

* Add unknown type tests for resolve_import_conflicts

* Fix tslint issues
This commit is contained in:
Mike Côté 2019-03-08 13:18:36 -05:00 committed by GitHub
parent a1a9511446
commit 7cf91316ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 3646 additions and 6 deletions

View file

@ -19,6 +19,8 @@ NOTE: You cannot access these endpoints via the Console in Kibana.
* <<saved-objects-api-update>>
* <<saved-objects-api-delete>>
* <<saved-objects-api-export>>
* <<saved-objects-api-import>>
* <<saved-objects-api-resolve-import-conflicts>>
include::saved-objects/get.asciidoc[]
include::saved-objects/bulk_get.asciidoc[]
@ -28,3 +30,5 @@ include::saved-objects/bulk_create.asciidoc[]
include::saved-objects/update.asciidoc[]
include::saved-objects/delete.asciidoc[]
include::saved-objects/export.asciidoc[]
include::saved-objects/import.asciidoc[]
include::saved-objects/resolve_import_conflicts.asciidoc[]

View file

@ -0,0 +1,96 @@
[[saved-objects-api-import]]
=== Import Objects
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
The import saved objects API enables you to create a set of Kibana saved objects from a file created by the export API.
Note: You cannot access this endpoint via the Console in Kibana.
==== Request
`POST /api/saved_objects/_import`
==== Query Parameters
`overwrite` (optional)::
(boolean) Overwrite saved objects if they exist already
==== Request body
The request body must be of type multipart/form-data.
`file`::
A file exported using the export API.
==== Response body
The response body will have a top level `success` property that indicates
if the import was successful or not as well as a `successCount` indicating how many records are successfully imported.
In the scenario the import wasn't successful a top level `errors` array will contain the objects that failed to import.
==== Examples
The following example imports an index pattern and dashboard.
[source,js]
--------------------------------------------------
POST api/saved_objects/_import
Content-Type: multipart/form-data; boundary=EXAMPLE
--EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
--EXAMPLE--
--------------------------------------------------
// KIBANA
A successful call returns a response code of `200` and a response body
containing a JSON structure similar to the following example:
[source,js]
--------------------------------------------------
{
"success": true,
"successCount": 2
}
--------------------------------------------------
The following example imports an index pattern and dashboard but has a conflict on the index pattern.
[source,js]
--------------------------------------------------
POST api/saved_objects/_import
Content-Type: multipart/form-data; boundary=EXAMPLE
--EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
--EXAMPLE--
--------------------------------------------------
// KIBANA
The call returns a response code of `200` and a response body
containing a JSON structure similar to the following example:
[source,js]
--------------------------------------------------
{
"success": false,
"successCount": 1,
"errors": [
{
"id": "my-pattern",
"type": "index-pattern",
"error": {
"statusCode": 409,
"message": "version conflict, document already exists",
},
},
],
}
--------------------------------------------------

View file

@ -0,0 +1,104 @@
[[saved-objects-api-resolve-import-conflicts]]
=== Resolve Import Conflicts
experimental[This functionality is *experimental* and may be changed or removed completely in a future release.]
The resolve import conflicts API enables you to resolve conflicts given by the import API by either overwriting specific saved objects or changing references to a newly created object.
Note: You cannot access this endpoint via the Console in Kibana.
==== Request
`POST /api/saved_objects/_resolve_import_conflicts`
==== Request body
The request body must be of type multipart/form-data.
`file`::
(ndjson) The same new line delimited JSON objects given to the import API.
`overwrites` (optional)::
(array) A list of `type` and `id` objects allowed to be overwritten on import.
`replaceReferences` (optional)::
(array) A list of `type`, `from` and `to` used to change imported saved object references to.
`skips` (optional)::
(array) A list of `type` and `id` objects to skip importing.
==== Response body
The response body will have a top level `success` property that indicates
if the import was successful or not as well as a `successCount` indicating how many records are successfully resolved.
In the scenario the import wasn't successful a top level `errors` array will contain the objects that failed to import.
==== Examples
The following example resolves conflicts for an index pattern and dashboard but indicates to skip the index pattern.
This will cause the index pattern to not be in the system and the dashboard to overwrite the existing saved object.
[source,js]
--------------------------------------------------
POST api/saved_objects/_resolve_import_conflicts
Content-Type: multipart/form-data; boundary=EXAMPLE
--EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson
{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}
--EXAMPLE
Content-Disposition: form-data; name="skips"
[{"type":"index-pattern","id":"my-pattern"}]
--EXAMPLE
Content-Disposition: form-data; name="overwrites"
[{"type":"dashboard","id":"my-dashboard"}]
--EXAMPLE--
--------------------------------------------------
// KIBANA
A successful call returns a response code of `200` and a response body
containing a JSON structure similar to the following example:
[source,js]
--------------------------------------------------
{
"success": true,
"successCount": 1
}
--------------------------------------------------
The following example resolves conflicts for a visualization and dashboard but indicates
to replace the dashboard references to another visualization.
[source,js]
--------------------------------------------------
POST api/saved_objects/_resolve_import_conflicts
Content-Type: multipart/form-data; boundary=EXAMPLE
--EXAMPLE
Content-Disposition: form-data; name="file"; filename="export.ndjson"
Content-Type: application/ndjson
{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}}
{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"panel_0","type":"visualization","id":"my-vis"}]}
--EXAMPLE
Content-Disposition: form-data; name="replaceReferences"
[{"type":"visualization","from":"my-vis","to":"my-vis-2"}]
--EXAMPLE--
--------------------------------------------------
// KIBANA
A successful call returns a response code of `200` and a response body
containing a JSON structure similar to the following example:
[source,js]
--------------------------------------------------
{
"success": true,
"successCount": 1
}
--------------------------------------------------

View file

@ -252,6 +252,7 @@ export default () => Joi.object({
}).default(),
savedObjects: Joi.object({
maxImportPayloadBytes: Joi.number().default(10485760),
maxImportExportSize: Joi.number().default(10000),
}).default(),

View file

@ -0,0 +1,815 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 { Readable } from 'stream';
import {
createConcatStream,
createListStream,
createPromiseFromStreams,
} from '../../../utils/streams';
import { SavedObject } from '../service';
import {
collectSavedObjects,
createLimitStream,
createObjectsFilter,
extractErrors,
importSavedObjects,
resolveImportConflicts,
} from './import';
describe('extractErrors()', () => {
test('returns empty array when no errors exist', () => {
const savedObjects: SavedObject[] = [];
const result = extractErrors(savedObjects);
expect(result).toMatchInlineSnapshot(`Array []`);
});
test('extracts errors from saved objects', () => {
const savedObjects: SavedObject[] = [
{
id: '1',
type: 'dashboard',
attributes: {},
references: [],
},
{
id: '2',
type: 'dashboard',
attributes: {},
references: [],
error: {
statusCode: 409,
message: 'Conflict',
},
},
];
const result = extractErrors(savedObjects);
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"error": Object {
"message": "Conflict",
"statusCode": 409,
},
"id": "2",
"type": "dashboard",
},
]
`);
});
});
describe('createLimitStream()', () => {
test('limit of 5 allows 5 items through', async () => {
await createPromiseFromStreams([createListStream([1, 2, 3, 4, 5]), createLimitStream(5)]);
});
test('limit of 5 errors out when 6 items are through', async () => {
await expect(
createPromiseFromStreams([createListStream([1, 2, 3, 4, 5, 6]), createLimitStream(5)])
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't import more than 5 objects"`);
});
test('send the values on the output stream', async () => {
const result = await createPromiseFromStreams([
createListStream([1, 2, 3]),
createLimitStream(3),
createConcatStream([]),
]);
expect(result).toMatchInlineSnapshot(`
Array [
1,
2,
3,
]
`);
});
});
describe('collectSavedObjects()', () => {
test('collects nothing when stream is empty', async () => {
const readStream = new Readable({
read() {
this.push(null);
},
});
const objects = await collectSavedObjects(readStream, 10);
expect(objects).toMatchInlineSnapshot(`Array []`);
});
test('collects objects from stream', async () => {
const readStream = new Readable({
read() {
this.push('{"foo":true}');
this.push(null);
},
});
const objects = await collectSavedObjects(readStream, 1);
expect(objects).toMatchInlineSnapshot(`
Array [
Object {
"foo": true,
},
]
`);
});
test('filters out empty lines', async () => {
const readStream = new Readable({
read() {
this.push('{"foo":true}\n\n');
this.push(null);
},
});
const objects = await collectSavedObjects(readStream, 1);
expect(objects).toMatchInlineSnapshot(`
Array [
Object {
"foo": true,
},
]
`);
});
test('throws error when object limit is reached', async () => {
const readStream = new Readable({
read() {
this.push('{"foo":true}\n');
this.push('{"bar":true}\n');
this.push(null);
},
});
await expect(collectSavedObjects(readStream, 1)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Can't import more than 1 objects"`
);
});
});
describe('createObjectsFilter()', () => {
test('filters should return false when contains empty parameters', () => {
const fn = createObjectsFilter([], [], []);
expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(false);
});
test('filters should exclude skips', () => {
const fn = createObjectsFilter(
[
{
type: 'a',
id: '1',
},
],
[],
[
{
type: 'b',
from: '1',
to: '2',
},
]
);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [{ name: 'ref_0', type: 'b', id: '1' }],
})
).toEqual(false);
expect(
fn({
type: 'a',
id: '2',
attributes: {},
references: [{ name: 'ref_0', type: 'b', id: '1' }],
})
).toEqual(true);
});
test('filter should include references to replace', () => {
const fn = createObjectsFilter(
[],
[],
[
{
type: 'b',
from: '1',
to: '2',
},
]
);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '1',
},
],
})
).toEqual(true);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '2',
},
],
})
).toEqual(false);
});
test('filter should include objects to overwrite', () => {
const fn = createObjectsFilter(
[],
[
{
type: 'a',
id: '1',
},
],
[]
);
expect(fn({ type: 'a', id: '1', attributes: {}, references: [] })).toEqual(true);
expect(fn({ type: 'a', id: '2', attributes: {}, references: [] })).toEqual(false);
});
test('filter should work with skips, overwrites and replaceReferences', () => {
const fn = createObjectsFilter(
[
{
type: 'a',
id: '1',
},
],
[
{
type: 'a',
id: '2',
},
],
[
{
type: 'b',
from: '1',
to: '2',
},
]
);
expect(
fn({
type: 'a',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '1',
},
],
})
).toEqual(false);
expect(
fn({
type: 'a',
id: '2',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '2',
},
],
})
).toEqual(true);
expect(
fn({
type: 'a',
id: '3',
attributes: {},
references: [
{
name: 'ref_0',
type: 'b',
id: '1',
},
],
})
).toEqual(true);
});
});
describe('importSavedObjects()', () => {
const savedObjects: SavedObject[] = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'search',
attributes: {},
references: [],
},
{
id: '3',
type: 'visualization',
attributes: {},
references: [],
},
{
id: '4',
type: 'dashboard',
attributes: {},
references: [],
},
];
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
beforeEach(() => {
savedObjectsClient.bulkCreate.mockReset();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
});
test('calls bulkCreate without overwrite', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await importSavedObjects({
readStream,
objectLimit: 4,
overwrite: false,
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 4,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
Object {
"attributes": Object {},
"id": "2",
"references": Array [],
"type": "search",
},
Object {
"attributes": Object {},
"id": "3",
"references": Array [],
"type": "visualization",
},
Object {
"attributes": Object {},
"id": "4",
"references": Array [],
"type": "dashboard",
},
],
Object {
"overwrite": false,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('calls bulkCreate with overwrite', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await importSavedObjects({
readStream,
objectLimit: 4,
overwrite: true,
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 4,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
Object {
"attributes": Object {},
"id": "2",
"references": Array [],
"type": "search",
},
Object {
"attributes": Object {},
"id": "3",
"references": Array [],
"type": "visualization",
},
Object {
"attributes": Object {},
"id": "4",
"references": Array [],
"type": "dashboard",
},
],
Object {
"overwrite": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('extracts errors', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects.map(savedObject => ({
type: savedObject.type,
id: savedObject.id,
error: {
statusCode: 409,
message: 'conflict',
},
})),
});
const result = await importSavedObjects({
readStream,
objectLimit: 4,
overwrite: false,
savedObjectsClient,
});
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
},
"id": "1",
"type": "index-pattern",
},
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
},
"id": "2",
"type": "search",
},
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
},
"id": "3",
"type": "visualization",
},
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
},
"id": "4",
"type": "dashboard",
},
],
"success": false,
"successCount": 0,
}
`);
});
});
describe('resolveImportConflicts()', () => {
const savedObjects: SavedObject[] = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'search',
attributes: {},
references: [],
},
{
id: '3',
type: 'visualization',
attributes: {},
references: [],
},
{
id: '4',
type: 'dashboard',
attributes: {},
references: [
{
name: 'panel_0',
type: 'visualization',
id: '3',
},
],
},
];
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
beforeEach(() => {
savedObjectsClient.bulkCreate.mockReset();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
});
test('works with empty parameters', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await resolveImportConflicts({
readStream,
objectLimit: 4,
skips: [],
overwrites: [],
savedObjectsClient,
replaceReferences: [],
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 0,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
});
test('works with skips', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await resolveImportConflicts({
readStream,
objectLimit: 4,
skips: [
{
type: 'dashboard',
id: '4',
},
],
overwrites: [],
savedObjectsClient,
replaceReferences: [
{
type: 'visualization',
from: '3',
to: '30',
},
],
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 0,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
});
test('works with overwrites', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await resolveImportConflicts({
readStream,
objectLimit: 4,
skips: [],
overwrites: [
{
type: 'index-pattern',
id: '1',
},
],
savedObjectsClient,
replaceReferences: [],
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 1,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
],
Object {
"overwrite": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('works wtih replaceReferences', async () => {
const readStream = new Readable({
read() {
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
this.push(null);
},
});
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
const result = await resolveImportConflicts({
readStream,
objectLimit: 4,
skips: [],
overwrites: [],
savedObjectsClient,
replaceReferences: [
{
type: 'visualization',
from: '3',
to: '13',
},
],
});
expect(result).toMatchInlineSnapshot(`
Object {
"success": true,
"successCount": 1,
}
`);
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {},
"id": "4",
"references": Array [
Object {
"id": "13",
"name": "panel_0",
"type": "visualization",
},
],
"type": "dashboard",
},
],
Object {
"overwrite": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
});

View file

@ -0,0 +1,218 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 Boom from 'boom';
import { Readable, Transform } from 'stream';
import {
createConcatStream,
createFilterStream,
createMapStream,
createPromiseFromStreams,
createSplitStream,
} from '../../../utils/streams';
import { SavedObject, SavedObjectsClient } from '../service';
interface CustomError {
id: string;
type: string;
error: {
message: string;
statusCode: number;
};
}
interface ImportResponse {
success: boolean;
successCount: number;
errors?: CustomError[];
}
interface ImportSavedObjectsOptions {
readStream: Readable;
objectLimit: number;
overwrite: boolean;
savedObjectsClient: SavedObjectsClient;
}
interface ResolveImportConflictsOptions {
readStream: Readable;
objectLimit: number;
savedObjectsClient: SavedObjectsClient;
overwrites: Array<{
type: string;
id: string;
}>;
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>;
skips: Array<{
type: string;
id: string;
}>;
}
export function extractErrors(savedObjects: SavedObject[]) {
const errors: CustomError[] = [];
for (const savedObject of savedObjects) {
if (savedObject.error) {
errors.push({
id: savedObject.id,
type: savedObject.type,
error: savedObject.error,
});
}
}
return errors;
}
export function createLimitStream(limit: number) {
let counter = 0;
return new Transform({
objectMode: true,
async transform(obj, enc, done) {
if (counter >= limit) {
return done(Boom.badRequest(`Can't import more than ${limit} objects`));
}
counter++;
done(undefined, obj);
},
});
}
export async function collectSavedObjects(
readStream: Readable,
objectLimit: number,
filter?: (obj: SavedObject) => boolean
): Promise<SavedObject[]> {
return (await createPromiseFromStreams([
readStream,
createSplitStream('\n'),
createMapStream((str: string) => {
if (str && str !== '') {
return JSON.parse(str);
}
}),
createFilterStream<SavedObject>(obj => !!obj),
createLimitStream(objectLimit),
createFilterStream<SavedObject>(obj => (filter ? filter(obj) : true)),
createConcatStream([]),
])) as SavedObject[];
}
export function createObjectsFilter(
skips: Array<{
type: string;
id: string;
}>,
overwrites: Array<{
type: string;
id: string;
}>,
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>
) {
const refReplacements = replaceReferences.map(ref => `${ref.type}:${ref.from}`);
return (obj: SavedObject) => {
if (skips.some(skipObj => skipObj.type === obj.type && skipObj.id === obj.id)) {
return false;
}
if (
overwrites.some(overwriteObj => overwriteObj.type === obj.type && overwriteObj.id === obj.id)
) {
return true;
}
for (const reference of obj.references || []) {
if (refReplacements.includes(`${reference.type}:${reference.id}`)) {
return true;
}
}
return false;
};
}
export async function importSavedObjects({
readStream,
objectLimit,
overwrite,
savedObjectsClient,
}: ImportSavedObjectsOptions): Promise<ImportResponse> {
const objectsToImport = await collectSavedObjects(readStream, objectLimit);
if (objectsToImport.length === 0) {
return {
success: true,
successCount: 0,
};
}
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToImport, {
overwrite,
});
const errors = extractErrors(bulkCreateResult.saved_objects);
return {
success: errors.length === 0,
successCount: objectsToImport.length - errors.length,
...(errors.length ? { errors } : {}),
};
}
export async function resolveImportConflicts({
readStream,
objectLimit,
skips,
overwrites,
savedObjectsClient,
replaceReferences,
}: ResolveImportConflictsOptions): Promise<ImportResponse> {
let errors: CustomError[] = [];
const filter = createObjectsFilter(skips, overwrites, replaceReferences);
const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter);
// Replace references
const refReplacementsMap: Record<string, string> = {};
for (const { type, to, from } of replaceReferences) {
refReplacementsMap[`${type}:${from}`] = to;
}
for (const savedObject of objectsToResolve) {
for (const reference of savedObject.references || []) {
if (refReplacementsMap[`${reference.type}:${reference.id}`]) {
reference.id = refReplacementsMap[`${reference.type}:${reference.id}`];
}
}
}
if (objectsToResolve.length) {
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToResolve, {
overwrite: true,
});
errors = extractErrors(bulkCreateResult.saved_objects);
}
return {
success: errors.length === 0,
successCount: objectsToResolve.length - errors.length,
...(errors.length ? { errors } : {}),
};
}

View file

@ -17,6 +17,5 @@
* under the License.
*/
import Hapi from 'hapi';
export function MockServer(config?: { [key: string]: any }): Hapi.Server;
export { importSavedObjects, resolveImportConflicts } from './import';
export { getSortedObjectsForExport } from './export';

View file

@ -23,6 +23,7 @@ import { defaultValidationErrorHandler } from '../../../../core/server/http/http
const defaultConfig = {
'kibana.index': '.kibana',
'savedObjects.maxImportExportSize': 10000,
'savedObjects.maxImportPayloadBytes': 52428800,
};
export function createMockServer(config: { [key: string]: any } = defaultConfig) {

View file

@ -21,7 +21,7 @@ import Hapi from 'hapi';
import Joi from 'joi';
import stringify from 'json-stable-stringify';
import { SavedObjectsClient } from '../';
import { getSortedObjectsForExport } from '../lib/export';
import { getSortedObjectsForExport } from '../lib';
import { Prerequisites } from './types';
const ALLOWED_TYPES = ['index-pattern', 'search', 'visualization', 'dashboard'];

View file

@ -0,0 +1,188 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 Hapi from 'hapi';
import { createMockServer } from './_mock_server';
import { createImportRoute } from './import';
describe('POST /api/saved_objects/_import', () => {
let server: Hapi.Server;
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
beforeEach(() => {
server = createMockServer();
savedObjectsClient.bulkCreate.mockReset();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method() {
return savedObjectsClient;
},
},
};
server.route(createImportRoute(prereqs, server));
});
test('formats successful response', async () => {
const request = {
method: 'POST',
url: '/api/saved_objects/_import',
payload: [
'--BOUNDARY',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'',
'--BOUNDARY--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=BOUNDARY',
},
};
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({
success: true,
successCount: 0,
});
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
});
test('imports an index pattern and dashboard', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
url: '/api/saved_objects/_import',
payload: [
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
'--EXAMPLE--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
type: 'index-pattern',
id: 'my-pattern',
attributes: {
title: 'my-pattern-*',
},
},
{
type: 'dashboard',
id: 'my-dashboard',
attributes: {
title: 'Look at my dashboard',
},
},
],
});
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({
success: true,
successCount: 2,
});
});
test('imports an index pattern and dashboard but has a conflict on the index pattern', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
url: '/api/saved_objects/_import',
payload: [
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
'--EXAMPLE--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
type: 'index-pattern',
id: 'my-pattern',
attributes: {},
references: [],
error: {
statusCode: 409,
message: 'version conflict, document already exists',
},
},
{
type: 'dashboard',
id: 'my-dashboard',
attributes: {
title: 'Look at my dashboard',
},
references: [],
},
],
});
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({
success: false,
successCount: 1,
errors: [
{
id: 'my-pattern',
type: 'index-pattern',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
},
},
],
});
});
});

View file

@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 Boom from 'boom';
import Hapi from 'hapi';
import Joi from 'joi';
import { extname } from 'path';
import { Readable } from 'stream';
import { SavedObjectsClient } from '../';
import { importSavedObjects } from '../lib';
import { Prerequisites, WithoutQueryAndParams } from './types';
interface HapiReadableStream extends Readable {
hapi: {
filename: string;
};
}
interface ImportRequest extends WithoutQueryAndParams<Hapi.Request> {
pre: {
savedObjectsClient: SavedObjectsClient;
};
query: {
overwrite: boolean;
};
payload: {
file: HapiReadableStream;
};
}
export const createImportRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
path: '/api/saved_objects/_import',
method: 'POST',
config: {
pre: [prereqs.getSavedObjectsClient],
payload: {
maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'),
output: 'stream',
allow: 'multipart/form-data',
},
validate: {
query: Joi.object()
.keys({
overwrite: Joi.boolean().default(false),
})
.default(),
payload: Joi.object({
file: Joi.object().required(),
}).default(),
},
},
async handler(request: ImportRequest, h: Hapi.ResponseToolkit) {
const { savedObjectsClient } = request.pre;
const { filename } = request.payload.file.hapi;
const fileExtension = extname(filename).toLowerCase();
if (fileExtension !== '.ndjson') {
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
}
return await importSavedObjects({
savedObjectsClient,
readStream: request.payload.file,
objectLimit: request.server.config().get('savedObjects.maxImportExportSize'),
overwrite: request.query.overwrite,
});
},
});

View file

@ -23,5 +23,7 @@ export { createCreateRoute } from './create';
export { createDeleteRoute } from './delete';
export { createFindRoute } from './find';
export { createGetRoute } from './get';
export { createImportRoute } from './import';
export { createResolveImportConflictsRoute } from './resolve_import_conflicts';
export { createUpdateRoute } from './update';
export { createExportRoute } from './export';

View file

@ -0,0 +1,229 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 Hapi from 'hapi';
import { createMockServer } from './_mock_server';
import { createResolveImportConflictsRoute } from './resolve_import_conflicts';
describe('POST /api/saved_objects/_resolve_import_conflicts', () => {
let server: Hapi.Server;
const savedObjectsClient = {
errors: {} as any,
bulkCreate: jest.fn(),
bulkGet: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
beforeEach(() => {
server = createMockServer();
savedObjectsClient.bulkCreate.mockReset();
savedObjectsClient.bulkGet.mockReset();
savedObjectsClient.create.mockReset();
savedObjectsClient.delete.mockReset();
savedObjectsClient.find.mockReset();
savedObjectsClient.get.mockReset();
savedObjectsClient.update.mockReset();
const prereqs = {
getSavedObjectsClient: {
assign: 'savedObjectsClient',
method() {
return savedObjectsClient;
},
},
};
server.route(createResolveImportConflictsRoute(prereqs, server));
});
test('formats successful response', async () => {
const request = {
method: 'POST',
url: '/api/saved_objects/_resolve_import_conflicts',
payload: [
'--BOUNDARY',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'',
'--BOUNDARY--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=BOUNDARY',
},
};
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({ success: true, successCount: 0 });
expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
});
test('resolves conflicts for an index pattern and dashboard but skips the index pattern', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
url: '/api/saved_objects/_resolve_import_conflicts',
payload: [
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"index-pattern","id":"my-pattern","attributes":{"title":"my-pattern-*"}}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"}}',
'--EXAMPLE',
'Content-Disposition: form-data; name="skips"',
'',
'[{"type":"index-pattern","id":"my-pattern"}]',
'--EXAMPLE',
'Content-Disposition: form-data; name="overwrites"',
'',
'[{"type":"dashboard","id":"my-dashboard"}]',
'--EXAMPLE--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
type: 'dashboard',
id: 'my-dashboard',
attributes: {
title: 'Look at my dashboard',
},
},
],
});
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({ success: true, successCount: 1 });
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {
"title": "Look at my dashboard",
},
"id": "my-dashboard",
"type": "dashboard",
},
],
Object {
"overwrite": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test('resolves conflicts by replacing the visualization references', async () => {
// NOTE: changes to this scenario should be reflected in the docs
const request = {
method: 'POST',
url: '/api/saved_objects/_resolve_import_conflicts',
payload: [
'--EXAMPLE',
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
'Content-Type: application/ndjson',
'',
'{"type":"visualization","id":"my-vis","attributes":{"title":"Look at my visualization"}}',
'{"type":"dashboard","id":"my-dashboard","attributes":{"title":"Look at my dashboard"},"references":[{"name":"panel_0","type":"visualization","id":"my-vis"}]}',
'--EXAMPLE',
'Content-Disposition: form-data; name="replaceReferences"',
'',
'[{"type":"visualization","from":"my-vis","to":"my-vis-2"}]',
'--EXAMPLE--',
].join('\r\n'),
headers: {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
type: 'dashboard',
id: 'my-dashboard',
attributes: {
title: 'Look at my dashboard',
},
references: [
{
name: 'panel_0',
type: 'visualization',
id: 'my-vis-2',
},
],
},
],
});
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
expect(response).toEqual({ success: true, successCount: 1 });
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"attributes": Object {
"title": "Look at my dashboard",
},
"id": "my-dashboard",
"references": Array [
Object {
"id": "my-vis-2",
"name": "panel_0",
"type": "visualization",
},
],
"type": "dashboard",
},
],
Object {
"overwrite": true,
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
});

View file

@ -0,0 +1,114 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 Boom from 'boom';
import Hapi from 'hapi';
import Joi from 'joi';
import { extname } from 'path';
import { Readable } from 'stream';
import { SavedObjectsClient } from '../';
import { resolveImportConflicts } from '../lib';
import { Prerequisites } from './types';
interface HapiReadableStream extends Readable {
hapi: {
filename: string;
};
}
interface ImportRequest extends Hapi.Request {
pre: {
savedObjectsClient: SavedObjectsClient;
};
payload: {
file: HapiReadableStream;
overwrites: Array<{
type: string;
id: string;
}>;
replaceReferences: Array<{
type: string;
from: string;
to: string;
}>;
skips: Array<{
type: string;
id: string;
}>;
};
}
export const createResolveImportConflictsRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
path: '/api/saved_objects/_resolve_import_conflicts',
method: 'POST',
config: {
pre: [prereqs.getSavedObjectsClient],
payload: {
maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'),
output: 'stream',
allow: 'multipart/form-data',
},
validate: {
payload: Joi.object({
file: Joi.object().required(),
overwrites: Joi.array()
.items(
Joi.object({
type: Joi.string().required(),
id: Joi.string().required(),
})
)
.default([]),
replaceReferences: Joi.array()
.items(
Joi.object({
type: Joi.string().required(),
from: Joi.string().required(),
to: Joi.string().required(),
})
)
.default([]),
skips: Joi.array()
.items(
Joi.object({
type: Joi.string().required(),
id: Joi.string().required(),
})
)
.default([]),
}).default(),
},
},
async handler(request: ImportRequest) {
const { savedObjectsClient } = request.pre;
const { filename } = request.payload.file.hapi;
const fileExtension = extname(filename).toLowerCase();
if (fileExtension !== '.ndjson') {
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
}
return await resolveImportConflicts({
savedObjectsClient,
readStream: request.payload.file,
objectLimit: request.server.config().get('savedObjects.maxImportExportSize'),
skips: request.payload.skips,
overwrites: request.payload.overwrites,
replaceReferences: request.payload.replaceReferences,
});
},
});

View file

@ -32,6 +32,8 @@ import {
createGetRoute,
createUpdateRoute,
createExportRoute,
createImportRoute,
createResolveImportConflictsRoute,
} from './routes';
export function savedObjectsMixin(kbnServer, server) {
@ -63,6 +65,8 @@ export function savedObjectsMixin(kbnServer, server) {
server.route(createGetRoute(prereqs));
server.route(createUpdateRoute(prereqs));
server.route(createExportRoute(prereqs, server));
server.route(createImportRoute(prereqs, server));
server.route(createResolveImportConflictsRoute(prereqs, server));
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
const serializer = new SavedObjectsSerializer(schema);

View file

@ -102,9 +102,9 @@ describe('Saved Objects Mixin', () => {
});
describe('Routes', () => {
it('should create 8 routes', () => {
it('should create 10 routes', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledTimes(8);
expect(mockServer.route).toHaveBeenCalledTimes(10);
});
it('should add POST /api/saved_objects/_bulk_create', () => {
savedObjectsMixin(mockKbnServer, mockServer);
@ -138,6 +138,15 @@ describe('Saved Objects Mixin', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_export', method: 'POST' }));
});
it('should add POST /api/saved_objects/_import', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_import', method: 'POST' }));
});
it('should add POST /api/saved_objects/_resolve_import_conflicts', () => {
savedObjectsMixin(mockKbnServer, mockServer);
expect(mockServer.route)
.toHaveBeenCalledWith(expect.objectContaining({ path: '/api/saved_objects/_resolve_import_conflicts', method: 'POST' }));
});
});
describe('Saved object service', () => {

View file

@ -26,6 +26,7 @@ export interface BaseOptions {
export interface CreateOptions extends BaseOptions {
id?: string;
overwrite?: boolean;
migrationVersion?: MigrationVersion;
references?: SavedObjectReference[];
}
@ -89,6 +90,7 @@ export interface SavedObject<T extends SavedObjectAttributes = any> {
updated_at?: string;
error?: {
message: string;
statusCode: number;
};
attributes: T;
references: SavedObjectReference[];

View file

@ -102,7 +102,9 @@ export class SavedObjectsClient {
* @param {object} [options={}]
* @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion=undefined]
* @property {string} [options.namespace]
* @property {array} [options.references] - [{ name, type, id }]
* @returns {promise} - { id, type, version, attributes }
*/
async create(type, attributes = {}, options = {}) {

View file

@ -0,0 +1,77 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 {
createConcatStream,
createFilterStream,
createListStream,
createPromiseFromStreams,
} from './';
describe('createFilterStream()', () => {
test('calls the function with each item in the source stream', async () => {
const filter = jest.fn().mockReturnValue(true);
await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createFilterStream(filter)]);
expect(filter).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
"a",
],
Array [
"b",
],
Array [
"c",
],
],
"results": Array [
Object {
"type": "return",
"value": true,
},
Object {
"type": "return",
"value": true,
},
Object {
"type": "return",
"value": true,
},
],
}
`);
});
test('send the filtered values on the output stream', async () => {
const result = await createPromiseFromStreams([
createListStream([1, 2, 3]),
createFilterStream<number>(n => n % 2 === 0),
createConcatStream([]),
]);
expect(result).toMatchInlineSnapshot(`
Array [
2,
]
`);
});
});

View file

@ -0,0 +1,33 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 { Transform } from 'stream';
export function createFilterStream<T>(fn: (obj: T) => boolean) {
return new Transform({
objectMode: true,
async transform(obj, enc, done) {
const canPushDownStream = fn(obj);
if (canPushDownStream) {
this.push(obj);
}
done();
},
});
}

31
src/legacy/utils/streams/index.d.ts vendored Normal file
View file

@ -0,0 +1,31 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 { Readable, Transform, Writable, TransformOptions } from 'stream';
export function concatStreamProviders(sourceProviders: Readable[], options: TransformOptions): Transform;
export function createIntersperseStream(intersperseChunk: string | Buffer): Transform;
export function createSplitStream<T>(splitChunk: T): Transform;
export function createListStream(items: any[]): Readable;
export function createReduceStream<T>(reducer: (value: any, chunk: T, enc: string) => T): Transform;
export function createPromiseFromStreams<T>([first, ...rest]: [Readable, ...Writable[]]): Promise<T>;
export function createConcatStream(initial: any): Transform;
export function createMapStream<T>(fn: (value: T, i: number) => void): Transform;
export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform;
export function createFilterStream<T>(fn: (obj: T) => boolean): Transform;

View file

@ -26,3 +26,4 @@ export { createPromiseFromStreams } from './promise_from_streams';
export { createConcatStream } from './concat_stream';
export { createMapStream } from './map_stream';
export { createReplaceStream } from './replace_stream';
export { createFilterStream } from './filter_stream';

View file

@ -0,0 +1,135 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 expect from 'expect.js';
import { join } from 'path';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('import', () => {
describe('with kibana index', () => {
describe('with basic data existing', () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));
it('should return 200', async () => {
await supertest
.post('/api/saved_objects/_import')
.query({ overwrite: true })
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 3,
});
});
});
it('should return 415 when no file passed in', async () => {
await supertest
.post('/api/saved_objects/_import')
.expect(415)
.then((resp) => {
expect(resp.body).to.eql({
statusCode: 415,
error: 'Unsupported Media Type',
message: 'Unsupported Media Type',
});
});
});
it('should return 409 when conflicts exist', async () => {
await supertest
.post('/api/saved_objects/_import')
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: false,
successCount: 0,
errors: [
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
}
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
}
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
}
},
],
});
});
});
it('should return 200 when conflicts exist but overwrite is passed in', async () => {
await supertest
.post('/api/saved_objects/_import')
.query({
overwrite: true,
})
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 3,
});
});
});
it('should return 400 when trying to import more than 10,000 objects', async () => {
const fileChunks = [];
for (let i = 0; i < 10001; i++) {
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
}
await supertest
.post('/api/saved_objects/_import')
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
.expect(400)
.then((resp) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'Can\'t import more than 10000 objects',
});
});
});
});
});
});
}

View file

@ -26,6 +26,8 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./export'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./import'));
loadTestFile(require.resolve('./resolve_import_conflicts'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./migrations'));
});

View file

@ -0,0 +1,239 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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 expect from 'expect.js';
import { join } from 'path';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('resolve_import_conflicts', () => {
describe('without kibana index', () => {
// Cleanup data that got created in import
after(() => esArchiver.unload('saved_objects/basic'));
it('should return 200 and import nothing when empty parameters are passed in', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 0,
});
});
});
it('should return 200 and import everything when overwrite parameters contains all objects', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.field('overwrites', JSON.stringify([
{
type: 'index-pattern',
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
},
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
},
{
type: 'dashboard',
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
},
]))
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 3,
});
});
});
it('should return 400 when no file passed in', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.field('skips', '[]')
.expect(400)
.then((resp) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'child "file" fails because ["file" is required]',
validation: { source: 'payload', keys: [ 'file' ] }
});
});
});
it('should return 200 when replacing references', async () => {
const objToInsert = {
id: '1',
type: 'visualization',
attributes: {
title: 'My favorite vis',
},
references: [
{
name: 'ref_0',
type: 'search',
id: '1',
},
]
};
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.field('replaceReferences', JSON.stringify(
[
{
type: 'search',
from: '1',
to: '2',
}
]
))
.attach('file', Buffer.from(JSON.stringify(objToInsert), 'utf8'), 'export.ndjson')
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: true,
successCount: 1,
});
});
await supertest
.get('/api/saved_objects/visualization/1')
.expect(200)
.then((resp) => {
expect(resp.body.references).to.eql([
{
name: 'ref_0',
type: 'search',
id: '2',
},
]);
});
});
it('should return 400 when resolving conflicts with a file containing more than 10,000 objects', async () => {
const fileChunks = [];
for (let i = 0; i < 10001; i++) {
fileChunks.push(`{"type":"visualization","id":"${i}","attributes":{},"references":[]}`);
}
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
.expect(400)
.then((resp) => {
expect(resp.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'Can\'t import more than 10000 objects',
});
});
});
});
describe('with kibana index', () => {
describe('with basic data existing', () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));
it('should return 200 when skipping all the records', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.field('skips', JSON.stringify(
[
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
},
]
))
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({ success: true, successCount: 0 });
});
});
it('should return 200 when manually overwriting each object', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.field('overwrites', JSON.stringify(
[
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
},
]
))
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({ success: true, successCount: 3 });
});
});
it('should return 200 with only one record when overwriting 1 and skipping 1', async () => {
await supertest
.post('/api/saved_objects/_resolve_import_conflicts')
.field('overwrites', JSON.stringify(
[
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
},
]
))
.field('skips', JSON.stringify(
[
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
},
]
))
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({ success: true, successCount: 1 });
});
});
});
});
});
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,152 @@
/*
* 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 expect from 'expect.js';
import { SuperTest } from 'supertest';
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
interface ImportTest {
statusCode: number;
response: (resp: { [key: string]: any }) => void;
}
interface ImportTests {
default: ImportTest;
unknownType: ImportTest;
}
interface ImportTestDefinition {
user?: TestDefinitionAuthentication;
spaceId?: string;
tests: ImportTests;
}
const createImportData = (spaceId: string) => [
{
type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
attributes: {
title: 'A great new dashboard',
},
},
{
type: 'globaltype',
id: '05976c65-1145-4858-bbf0-d225cc78a06e',
attributes: {
name: 'A new globaltype object',
},
},
];
export function importTestSuiteFactory(es: any, esArchiver: any, supertest: SuperTest<any>) {
const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: {
[key: string]: any;
}) => {
expect(resp.body).to.eql({
success: true,
successCount: 2,
});
};
const expectUnknownType = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
success: false,
successCount: 2,
errors: [
{
id: '1',
type: 'wigwags',
error: {
message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400,
error: 'Bad Request',
},
},
],
});
};
const expectRbacForbidden = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_create dashboard,globaltype, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create`,
});
};
const expectRbacForbiddenWithUnknownType = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/wigwags/bulk_create`,
});
};
const expectRbacForbiddenForUnknownType = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/wigwags/bulk_create`,
});
};
const makeImportTest = (describeFn: DescribeFn) => (
description: string,
definition: ImportTestDefinition
) => {
const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition;
describeFn(description, () => {
before(() => esArchiver.load('saved_objects/spaces'));
after(() => esArchiver.unload('saved_objects/spaces'));
it(`should return ${tests.default.statusCode}`, async () => {
const data = createImportData(spaceId);
await supertest
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
.auth(user.username, user.password)
.attach('file', Buffer.from(JSON.stringify(data), 'utf8'), 'export.ndjson')
.expect(tests.default.statusCode)
.then(tests.default.response);
});
describe('unknown type', () => {
it(`should return ${tests.unknownType.statusCode}`, async () => {
const data = createImportData(spaceId);
data.push({
type: 'wigwags',
id: '1',
attributes: {
title: 'Wigwags title',
},
});
await supertest
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_import`)
.query({ overwrite: true })
.auth(user.username, user.password)
.attach('file', Buffer.from(JSON.stringify(data), 'utf8'), 'export.ndjson')
.expect(tests.unknownType.statusCode)
.then(tests.unknownType.response);
});
});
});
};
const importTest = makeImportTest(describe);
// @ts-ignore
importTest.only = makeImportTest(describe.only);
return {
importTest,
createExpectResults,
expectRbacForbidden,
expectUnknownType,
expectRbacForbiddenWithUnknownType,
expectRbacForbiddenForUnknownType,
};
}

View file

@ -0,0 +1,185 @@
/*
* 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 expect from 'expect.js';
import { SuperTest } from 'supertest';
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
import { getIdPrefix, getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
interface ResolveImportConflictsTest {
statusCode: number;
response: (resp: { [key: string]: any }) => void;
}
interface ResolveImportConflictsTests {
default: ResolveImportConflictsTest;
unknownType: ResolveImportConflictsTest;
}
interface ResolveImportConflictsTestDefinition {
user?: TestDefinitionAuthentication;
spaceId?: string;
tests: ResolveImportConflictsTests;
}
const createImportData = (spaceId: string) => [
{
type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
attributes: {
title: 'A great new dashboard',
},
},
{
type: 'globaltype',
id: '05976c65-1145-4858-bbf0-d225cc78a06e',
attributes: {
name: 'A new globaltype object',
},
},
];
export function resolveImportConflictsTestSuiteFactory(
es: any,
esArchiver: any,
supertest: SuperTest<any>
) {
const createExpectResults = (spaceId = DEFAULT_SPACE_ID) => async (resp: {
[key: string]: any;
}) => {
expect(resp.body).to.eql({
success: true,
successCount: 1,
});
};
const expectUnknownType = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
success: false,
successCount: 1,
errors: [
{
id: '1',
type: 'wigwags',
error: {
message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400,
error: 'Bad Request',
},
},
],
});
};
const expectRbacForbidden = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_create dashboard, missing action:saved_objects/dashboard/bulk_create`,
});
};
const expectRbacForbiddenWithUnknownType = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/wigwags/bulk_create`,
});
};
const expectRbacForbiddenForUnknownType = (resp: { [key: string]: any }) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/wigwags/bulk_create`,
});
};
const makeResolveImportConflictsTest = (describeFn: DescribeFn) => (
description: string,
definition: ResolveImportConflictsTestDefinition
) => {
const { user = {}, spaceId = DEFAULT_SPACE_ID, tests } = definition;
describeFn(description, () => {
before(() => esArchiver.load('saved_objects/spaces'));
after(() => esArchiver.unload('saved_objects/spaces'));
it(`should return ${tests.default.statusCode}`, async () => {
const data = createImportData(spaceId);
await supertest
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_conflicts`)
.auth(user.username, user.password)
.field(
'overwrites',
JSON.stringify([
{
type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
},
])
)
.attach(
'file',
Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'),
'export.ndjson'
)
.expect(tests.default.statusCode)
.then(tests.default.response);
});
describe('unknown type', () => {
it(`should return ${tests.unknownType.statusCode}`, async () => {
const data = createImportData(spaceId);
data.push({
type: 'wigwags',
id: '1',
attributes: {
title: 'Wigwags title',
},
});
await supertest
.post(`${getUrlPrefix(spaceId)}/api/saved_objects/_resolve_import_conflicts`)
.auth(user.username, user.password)
.field(
'overwrites',
JSON.stringify([
{
type: 'wigwags',
id: '1',
},
{
type: 'dashboard',
id: `${getIdPrefix(spaceId)}a01b2f57-fcfd-4864-b735-09e28f0d815e`,
},
])
)
.attach(
'file',
Buffer.from(data.map(obj => JSON.stringify(obj)).join('\n'), 'utf8'),
'export.ndjson'
)
.expect(tests.unknownType.statusCode)
.then(tests.unknownType.response);
});
});
});
};
const resolveImportConflictsTest = makeResolveImportConflictsTest(describe);
// @ts-ignore
resolveImportConflictsTest.only = makeResolveImportConflictsTest(describe.only);
return {
resolveImportConflictsTest,
createExpectResults,
expectRbacForbidden,
expectUnknownType,
expectRbacForbiddenWithUnknownType,
expectRbacForbiddenForUnknownType,
};
}

View file

@ -0,0 +1,211 @@
/*
* 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 { AUTHENTICATION } from '../../common/lib/authentication';
import { SPACES } from '../../common/lib/spaces';
import { TestInvoker } from '../../common/lib/types';
import { importTestSuiteFactory } from '../../common/suites/import';
// tslint:disable:no-default-export
export default function({ getService }: TestInvoker) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
const es = getService('es');
const {
importTest,
createExpectResults,
expectRbacForbidden,
expectUnknownType,
expectRbacForbiddenWithUnknownType,
expectRbacForbiddenForUnknownType,
} = importTestSuiteFactory(es, esArchiver, supertest);
describe('_import', () => {
[
{
spaceId: SPACES.DEFAULT.spaceId,
users: {
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
superuser: AUTHENTICATION.SUPERUSER,
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
},
},
{
spaceId: SPACES.SPACE_1.spaceId,
users: {
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
superuser: AUTHENTICATION.SUPERUSER,
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
},
},
].forEach(scenario => {
importTest(`user with no access within the ${scenario.spaceId} space`, {
user: scenario.users.noAccess,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`superuser within the ${scenario.spaceId} space`, {
user: scenario.users.superuser,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
importTest(`legacy user within the ${scenario.spaceId} space`, {
user: scenario.users.legacyAll,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`dual-privileges user within the ${scenario.spaceId} space`, {
user: scenario.users.dualAll,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
importTest(`dual-privileges readonly user within the ${scenario.spaceId} space`, {
user: scenario.users.dualRead,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with all globally within the ${scenario.spaceId} space`, {
user: scenario.users.allGlobally,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
importTest(`rbac user with read globally within the ${scenario.spaceId} space`, {
user: scenario.users.readGlobally,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with all at the space within the ${scenario.spaceId} space`, {
user: scenario.users.allAtSpace,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
importTest(`rbac user with read at the space within the ${scenario.spaceId} space`, {
user: scenario.users.readAtSpace,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with all at other space within the ${scenario.spaceId} space`, {
user: scenario.users.allAtOtherSpace,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
});
});
}

View file

@ -26,6 +26,8 @@ export default function({ getService, loadTestFile }: TestInvoker) {
loadTestFile(require.resolve('./export'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./import'));
loadTestFile(require.resolve('./resolve_import_conflicts'));
loadTestFile(require.resolve('./update'));
});
}

View file

@ -0,0 +1,229 @@
/*
* 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 { AUTHENTICATION } from '../../common/lib/authentication';
import { SPACES } from '../../common/lib/spaces';
import { TestInvoker } from '../../common/lib/types';
import { resolveImportConflictsTestSuiteFactory } from '../../common/suites/resolve_import_conflicts';
// tslint:disable:no-default-export
export default function({ getService }: TestInvoker) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
const es = getService('es');
const {
resolveImportConflictsTest,
createExpectResults,
expectRbacForbidden,
expectUnknownType,
expectRbacForbiddenWithUnknownType,
expectRbacForbiddenForUnknownType,
} = resolveImportConflictsTestSuiteFactory(es, esArchiver, supertest);
describe('_resolve_import_conflicts', () => {
[
{
spaceId: SPACES.DEFAULT.spaceId,
users: {
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
superuser: AUTHENTICATION.SUPERUSER,
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
},
},
{
spaceId: SPACES.SPACE_1.spaceId,
users: {
noAccess: AUTHENTICATION.NOT_A_KIBANA_USER,
superuser: AUTHENTICATION.SUPERUSER,
legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER,
allGlobally: AUTHENTICATION.KIBANA_RBAC_USER,
readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
},
},
].forEach(scenario => {
resolveImportConflictsTest(`user with no access within the ${scenario.spaceId} space`, {
user: scenario.users.noAccess,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`superuser within the ${scenario.spaceId} space`, {
user: scenario.users.superuser,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
resolveImportConflictsTest(`legacy user within the ${scenario.spaceId} space`, {
user: scenario.users.legacyAll,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`dual-privileges user within the ${scenario.spaceId} space`, {
user: scenario.users.dualAll,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
resolveImportConflictsTest(
`dual-privileges readonly user within the ${scenario.spaceId} space`,
{
user: scenario.users.dualRead,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
}
);
resolveImportConflictsTest(
`rbac user with all globally within the ${scenario.spaceId} space`,
{
user: scenario.users.allGlobally,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
}
);
resolveImportConflictsTest(
`rbac user with read globally within the ${scenario.spaceId} space`,
{
user: scenario.users.readGlobally,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
}
);
resolveImportConflictsTest(
`rbac user with all at the space within the ${scenario.spaceId} space`,
{
user: scenario.users.allAtSpace,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 200,
response: createExpectResults(scenario.spaceId),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
}
);
resolveImportConflictsTest(
`rbac user with read at the space within the ${scenario.spaceId} space`,
{
user: scenario.users.readAtSpace,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
}
);
resolveImportConflictsTest(
`rbac user with all at other space within the ${scenario.spaceId} space`,
{
user: scenario.users.allAtOtherSpace,
spaceId: scenario.spaceId,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
}
);
});
});
}

View file

@ -0,0 +1,181 @@
/*
* 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 { AUTHENTICATION } from '../../common/lib/authentication';
import { TestInvoker } from '../../common/lib/types';
import { importTestSuiteFactory } from '../../common/suites/import';
// tslint:disable:no-default-export
export default function({ getService }: TestInvoker) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
const es = getService('es');
const {
importTest,
createExpectResults,
expectRbacForbidden,
expectUnknownType,
expectRbacForbiddenWithUnknownType,
expectRbacForbiddenForUnknownType,
} = importTestSuiteFactory(es, esArchiver, supertest);
describe('_import', () => {
importTest(`user with no access`, {
user: AUTHENTICATION.NOT_A_KIBANA_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`superuser`, {
user: AUTHENTICATION.SUPERUSER,
tests: {
default: {
statusCode: 200,
response: createExpectResults(),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
importTest(`legacy user`, {
user: AUTHENTICATION.KIBANA_LEGACY_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`dual-privileges user`, {
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
tests: {
default: {
statusCode: 200,
response: createExpectResults(),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
importTest(`dual-privileges readonly user`, {
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with all globally`, {
user: AUTHENTICATION.KIBANA_RBAC_USER,
tests: {
default: {
statusCode: 200,
response: createExpectResults(),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
importTest(`rbac readonly user`, {
user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with all at default space`, {
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with read at default space`, {
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with all at space_1`, {
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
importTest(`rbac user with read at space_1`, {
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
});
}

View file

@ -26,6 +26,8 @@ export default function({ getService, loadTestFile }: TestInvoker) {
loadTestFile(require.resolve('./export'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./import'));
loadTestFile(require.resolve('./resolve_import_conflicts'));
loadTestFile(require.resolve('./update'));
});
}

View file

@ -0,0 +1,181 @@
/*
* 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 { AUTHENTICATION } from '../../common/lib/authentication';
import { TestInvoker } from '../../common/lib/types';
import { resolveImportConflictsTestSuiteFactory } from '../../common/suites/resolve_import_conflicts';
// tslint:disable:no-default-export
export default function({ getService }: TestInvoker) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
const es = getService('es');
const {
resolveImportConflictsTest,
createExpectResults,
expectRbacForbidden,
expectUnknownType,
expectRbacForbiddenWithUnknownType,
expectRbacForbiddenForUnknownType,
} = resolveImportConflictsTestSuiteFactory(es, esArchiver, supertest);
describe('_resolve_import_conflicts', () => {
resolveImportConflictsTest(`user with no access`, {
user: AUTHENTICATION.NOT_A_KIBANA_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`superuser`, {
user: AUTHENTICATION.SUPERUSER,
tests: {
default: {
statusCode: 200,
response: createExpectResults(),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
resolveImportConflictsTest(`legacy user`, {
user: AUTHENTICATION.KIBANA_LEGACY_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`dual-privileges user`, {
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER,
tests: {
default: {
statusCode: 200,
response: createExpectResults(),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
resolveImportConflictsTest(`dual-privileges readonly user`, {
user: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`rbac user with all globally`, {
user: AUTHENTICATION.KIBANA_RBAC_USER,
tests: {
default: {
statusCode: 200,
response: createExpectResults(),
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenForUnknownType,
},
},
});
resolveImportConflictsTest(`rbac readonly user`, {
user: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`rbac user with all at default space`, {
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`rbac user with read at default space`, {
user: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`rbac user with all at space_1`, {
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
resolveImportConflictsTest(`rbac user with read at space_1`, {
user: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER,
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
unknownType: {
statusCode: 403,
response: expectRbacForbiddenWithUnknownType,
},
},
});
});
}

View file

@ -0,0 +1,52 @@
/*
* 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 { SPACES } from '../../common/lib/spaces';
import { TestInvoker } from '../../common/lib/types';
import { importTestSuiteFactory } from '../../common/suites/import';
// tslint:disable:no-default-export
export default function({ getService }: TestInvoker) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
const { importTest, createExpectResults, expectUnknownType } = importTestSuiteFactory(
es,
esArchiver,
supertest
);
describe('_import', () => {
importTest('in the current space (space_1)', {
...SPACES.SPACE_1,
tests: {
default: {
statusCode: 200,
response: createExpectResults(SPACES.SPACE_1.spaceId),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
importTest('in the default space', {
...SPACES.DEFAULT,
tests: {
default: {
statusCode: 200,
response: createExpectResults(SPACES.DEFAULT.spaceId),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
});
}

View file

@ -18,6 +18,8 @@ export default function({ loadTestFile }: TestInvoker) {
loadTestFile(require.resolve('./export'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./import'));
loadTestFile(require.resolve('./resolve_import_conflicts'));
loadTestFile(require.resolve('./update'));
});
}

View file

@ -0,0 +1,52 @@
/*
* 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 { SPACES } from '../../common/lib/spaces';
import { TestInvoker } from '../../common/lib/types';
import { resolveImportConflictsTestSuiteFactory } from '../../common/suites/resolve_import_conflicts';
// tslint:disable:no-default-export
export default function({ getService }: TestInvoker) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
const {
resolveImportConflictsTest,
createExpectResults,
expectUnknownType,
} = resolveImportConflictsTestSuiteFactory(es, esArchiver, supertest);
describe('_resolve_import_conflicts', () => {
resolveImportConflictsTest('in the current space (space_1)', {
...SPACES.SPACE_1,
tests: {
default: {
statusCode: 200,
response: createExpectResults(SPACES.SPACE_1.spaceId),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
resolveImportConflictsTest('in the default space', {
...SPACES.DEFAULT,
tests: {
default: {
statusCode: 200,
response: createExpectResults(SPACES.DEFAULT.spaceId),
},
unknownType: {
statusCode: 200,
response: expectUnknownType,
},
},
});
});
}