Return import errors for saved objects referencing a missing index pattern or search (#33005)

* cherry-pick fd2bc9b

* Return errors when objects are missing references

* Fix import tslint

* Fix failing jest tests

* Fix x-pack integration tests

* Rename ensureReferencesExist to validateReferences

* Fix test naming to use validateReferences

* Update resolve_import_errors API to reflect new type attribute

* Validate references for search type as well

* Clarify comment

* Apply PR feedback

* Modify saved object bulkGet to be able to filter fields

* Apply PR feedback
This commit is contained in:
Mike Côté 2019-03-14 19:22:30 -04:00 committed by GitHub
parent d91c909e7d
commit 34c7e52a87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 854 additions and 43 deletions

View file

@ -23,6 +23,9 @@ contains the following properties:
`id` (required)::
(string) ID of object to retrieve
`fields` (optional)::
(array) The fields to return in the object's response
==== Response body
The response body will have a top level `saved_objects` property that contains

View file

@ -45,18 +45,36 @@ describe('extractErrors()', () => {
message: 'Conflict',
},
},
{
id: '3',
type: 'dashboard',
attributes: {},
references: [],
error: {
statusCode: 400,
message: 'Bad Request',
},
},
];
const result = extractErrors(savedObjects);
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"error": Object {
"message": "Conflict",
"statusCode": 409,
"type": "conflict",
},
"id": "2",
"type": "dashboard",
},
Object {
"error": Object {
"message": "Bad Request",
"statusCode": 400,
"type": "unknown",
},
"id": "3",
"type": "dashboard",
},
]
`);
});

View file

@ -18,24 +18,29 @@
*/
import { SavedObject } from '../service';
export interface CustomError {
id: string;
type: string;
error: {
message: string;
statusCode: number;
};
}
import { ImportError } from './types';
export function extractErrors(savedObjects: SavedObject[]) {
const errors: CustomError[] = [];
const errors: ImportError[] = [];
for (const savedObject of savedObjects) {
if (savedObject.error) {
if (savedObject.error.statusCode === 409) {
errors.push({
id: savedObject.id,
type: savedObject.type,
error: {
type: 'conflict',
},
});
continue;
}
errors.push({
id: savedObject.id,
type: savedObject.type,
error: savedObject.error,
error: {
...savedObject.error,
type: 'unknown',
},
});
}
}

View file

@ -76,6 +76,7 @@ describe('importSavedObjects()', () => {
this.push(null);
},
});
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
@ -143,6 +144,7 @@ Object {
this.push(null);
},
});
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects,
});
@ -210,6 +212,7 @@ Object {
this.push(null);
},
});
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: savedObjects.map(savedObject => ({
type: savedObject.type,
@ -231,32 +234,28 @@ Object {
"errors": Array [
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
"type": "conflict",
},
"id": "1",
"type": "index-pattern",
},
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
"type": "conflict",
},
"id": "2",
"type": "search",
},
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
"type": "conflict",
},
"id": "3",
"type": "visualization",
},
Object {
"error": Object {
"message": "conflict",
"statusCode": 409,
"type": "conflict",
},
"id": "4",
"type": "dashboard",

View file

@ -20,7 +20,9 @@
import { Readable } from 'stream';
import { SavedObjectsClient } from '../service';
import { collectSavedObjects } from './collect_saved_objects';
import { CustomError, extractErrors } from './extract_errors';
import { extractErrors } from './extract_errors';
import { ImportError } from './types';
import { validateReferences } from './validate_references';
interface ImportSavedObjectsOptions {
readStream: Readable;
@ -32,7 +34,7 @@ interface ImportSavedObjectsOptions {
interface ImportResponse {
success: boolean;
successCount: number;
errors?: CustomError[];
errors?: ImportError[];
}
export async function importSavedObjects({
@ -41,23 +43,29 @@ export async function importSavedObjects({
overwrite,
savedObjectsClient,
}: ImportSavedObjectsOptions): Promise<ImportResponse> {
const objectsToImport = await collectSavedObjects(readStream, objectLimit);
const objectsFromStream = await collectSavedObjects(readStream, objectLimit);
if (objectsToImport.length === 0) {
const { filteredObjects, errors: validationErrors } = await validateReferences(
objectsFromStream,
savedObjectsClient
);
if (filteredObjects.length === 0) {
return {
success: true,
success: validationErrors.length === 0,
successCount: 0,
...(validationErrors.length ? { errors: validationErrors } : {}),
};
}
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToImport, {
const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, {
overwrite,
});
const errors = extractErrors(bulkCreateResult.saved_objects);
const errors = [...validationErrors, ...extractErrors(bulkCreateResult.saved_objects)];
return {
success: errors.length === 0,
successCount: objectsToImport.length - errors.length,
successCount: objectsFromStream.length - errors.length,
...(errors.length ? { errors } : {}),
};
}

View file

@ -21,7 +21,8 @@ import { Readable } from 'stream';
import { SavedObjectsClient } from '../service';
import { collectSavedObjects } from './collect_saved_objects';
import { createObjectsFilter } from './create_objects_filter';
import { CustomError, extractErrors } from './extract_errors';
import { extractErrors } from './extract_errors';
import { ImportError } from './types';
interface ResolveImportErrorsOptions {
readStream: Readable;
@ -45,7 +46,7 @@ interface ResolveImportErrorsOptions {
interface ImportResponse {
success: boolean;
successCount: number;
errors?: CustomError[];
errors?: ImportError[];
}
export async function resolveImportErrors({
@ -56,7 +57,7 @@ export async function resolveImportErrors({
savedObjectsClient,
replaceReferences,
}: ResolveImportErrorsOptions): Promise<ImportResponse> {
let errors: CustomError[] = [];
let errors: ImportError[] = [];
const filter = createObjectsFilter(skips, overwrites, replaceReferences);
const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter);

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
interface ConflictError {
type: 'conflict';
}
interface UnknownError {
type: 'unknown';
message: string;
statusCode: number;
}
interface MissingReferencesError {
type: 'missing_references';
references: Array<{
type: string;
id: string;
}>;
}
export interface ImportError {
id: string;
type: string;
error: ConflictError | MissingReferencesError | UnknownError;
}

View file

@ -0,0 +1,586 @@
/*
* 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 { getNonExistingReferenceAsKeys, validateReferences } from './validate_references';
describe('getNonExistingReferenceAsKeys()', () => {
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(() => {
jest.resetAllMocks();
});
test('returns empty response when no objects exist', async () => {
const result = await getNonExistingReferenceAsKeys([], savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('removes references that exist within savedObjects', async () => {
const savedObjects = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('removes references that exist within es', async () => {
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
});
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "1",
"type": "index-pattern",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test(`doesn't handle saved object types outside of ENFORCED_TYPES`, async () => {
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'foo',
id: '1',
},
],
},
];
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual([]);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('returns references within ENFORCED_TYPES when they are missing', async () => {
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
{
name: 'ref_1',
type: 'search',
id: '3',
},
{
name: 'ref_2',
type: 'foo',
id: '4',
},
],
},
];
savedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
id: '1',
type: 'index-pattern',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
id: '3',
type: 'search',
error: {
statusCode: 404,
message: 'Not found',
},
},
],
});
const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
expect(result).toEqual(['index-pattern:1', 'search:3']);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "1",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "3",
"type": "search",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
});
describe('validateReferences()', () => {
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(() => {
jest.resetAllMocks();
});
test('returns empty when no objects are passed in', async () => {
const result = await validateReferences([], savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test('returns errors when references are missing', async () => {
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
type: 'index-pattern',
id: '3',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
type: 'index-pattern',
id: '5',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
type: 'index-pattern',
id: '6',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
type: 'search',
id: '7',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
id: '8',
type: 'search',
attributes: {},
references: [],
},
],
});
const savedObjects = [
{
id: '1',
type: 'visualization',
attributes: {},
references: [],
},
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '3',
},
],
},
{
id: '4',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '5',
},
{
name: 'ref_1',
type: 'index-pattern',
id: '6',
},
{
name: 'ref_2',
type: 'search',
id: '7',
},
{
name: 'ref_3',
type: 'search',
id: '8',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"error": Object {
"references": Array [
Object {
"id": "3",
"type": "index-pattern",
},
],
"type": "missing_references",
},
"id": "2",
"type": "visualization",
},
Object {
"error": Object {
"references": Array [
Object {
"id": "5",
"type": "index-pattern",
},
Object {
"id": "6",
"type": "index-pattern",
},
Object {
"id": "7",
"type": "search",
},
],
"type": "missing_references",
},
"id": "4",
"type": "visualization",
},
],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "visualization",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
[MockFunction] {
"calls": Array [
Array [
Array [
Object {
"fields": Array [
"id",
],
"id": "3",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "5",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "6",
"type": "index-pattern",
},
Object {
"fields": Array [
"id",
],
"id": "7",
"type": "search",
},
Object {
"fields": Array [
"id",
],
"id": "8",
"type": "search",
},
],
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
}
`);
});
test(`doesn't return errors when references exist in Elasticsearch`, async () => {
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
],
});
const savedObjects = [
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "2",
"references": Array [
Object {
"id": "1",
"name": "ref_0",
"type": "index-pattern",
},
],
"type": "visualization",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
});
test(`doesn't return errors when references exist within the saved objects`, async () => {
const savedObjects = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'visualization',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [],
"type": "index-pattern",
},
Object {
"attributes": Object {},
"id": "2",
"references": Array [
Object {
"id": "1",
"name": "ref_0",
"type": "index-pattern",
},
],
"type": "visualization",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
test(`doesn't validate references on types not part of ENFORCED_TYPES`, async () => {
const savedObjects = [
{
id: '1',
type: 'dashboard',
attributes: {},
references: [
{
name: 'ref_0',
type: 'visualization',
id: '2',
},
{
name: 'ref_1',
type: 'other-type',
id: '3',
},
],
},
];
const result = await validateReferences(savedObjects, savedObjectsClient);
expect(result).toMatchInlineSnapshot(`
Object {
"errors": Array [],
"filteredObjects": Array [
Object {
"attributes": Object {},
"id": "1",
"references": Array [
Object {
"id": "2",
"name": "ref_0",
"type": "visualization",
},
Object {
"id": "3",
"name": "ref_1",
"type": "other-type",
},
],
"type": "dashboard",
},
],
}
`);
expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
});
});

View file

@ -0,0 +1,93 @@
/*
* 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 { SavedObject, SavedObjectsClient } from '../service';
import { ImportError } from './types';
const ENFORCED_TYPES = ['index-pattern', 'search'];
export async function getNonExistingReferenceAsKeys(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
) {
const collector = new Map();
for (const savedObject of savedObjects) {
for (const { type, id } of savedObject.references || []) {
if (!ENFORCED_TYPES.includes(type)) {
continue;
}
collector.set(`${type}:${id}`, { type, id });
}
}
for (const savedObject of savedObjects) {
collector.delete(`${savedObject.type}:${savedObject.id}`);
}
if (collector.size) {
const bulkGetOpts = Array.from(collector.values()).map(obj => ({ ...obj, fields: ['id'] }));
const bulkGetResponse = await savedObjectsClient.bulkGet(bulkGetOpts);
for (const savedObject of bulkGetResponse.saved_objects) {
if (savedObject.error) {
continue;
}
collector.delete(`${savedObject.type}:${savedObject.id}`);
}
}
return [...collector.keys()];
}
export async function validateReferences(
savedObjects: SavedObject[],
savedObjectsClient: SavedObjectsClient
) {
const errors: ImportError[] = [];
const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(
savedObjects,
savedObjectsClient
);
// Filter out objects with missing references, add to error object
const filteredObjects = savedObjects.filter(savedObject => {
const missingReferences = [];
for (const { type: refType, id: refId } of savedObject.references || []) {
if (!ENFORCED_TYPES.includes(refType)) {
continue;
}
if (nonExistingReferenceKeys.includes(`${refType}:${refId}`)) {
missingReferences.push({ type: refType, id: refId });
}
}
if (missingReferences.length) {
errors.push({
id: savedObject.id,
type: savedObject.type,
error: {
type: 'missing_references',
references: missingReferences,
},
});
}
return missingReferences.length === 0;
});
return {
errors,
filteredObjects,
};
}

View file

@ -29,6 +29,7 @@ interface BulkGetRequest extends Hapi.Request {
payload: Array<{
type: string;
id: string;
fields?: string[];
}>;
}
@ -42,6 +43,7 @@ export const createBulkGetRoute = (prereqs: Prerequisites) => ({
Joi.object({
type: Joi.string().required(),
id: Joi.string().required(),
fields: Joi.array().items(Joi.string()),
}).required()
),
},

View file

@ -72,6 +72,7 @@ describe('POST /api/saved_objects/_import', () => {
'content-Type': 'multipart/form-data; boundary=BOUNDARY',
},
};
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
const { payload, statusCode } = await server.inject(request);
const response = JSON.parse(payload);
expect(statusCode).toBe(200);
@ -100,6 +101,7 @@ describe('POST /api/saved_objects/_import', () => {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
@ -145,6 +147,7 @@ describe('POST /api/saved_objects/_import', () => {
'content-Type': 'multipart/form-data; boundary=EXAMPLE',
},
};
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
savedObjectsClient.bulkCreate.mockResolvedValueOnce({
saved_objects: [
{
@ -178,8 +181,7 @@ describe('POST /api/saved_objects/_import', () => {
id: 'my-pattern',
type: 'index-pattern',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
type: 'conflict',
},
},
],

View file

@ -414,7 +414,7 @@ export class SavedObjectsRepository {
/**
* Returns an array of objects by id
*
* @param {array} objects - an array ids, or an array of objects containing id and optionally type
* @param {array} objects - an array of objects containing id, type and optionally fields
* @param {object} [options={}]
* @property {string} [options.namespace]
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }] }
@ -436,10 +436,11 @@ export class SavedObjectsRepository {
const response = await this._callCluster('mget', {
index: this._index,
body: {
docs: objects.reduce((acc, { type, id }) => {
docs: objects.reduce((acc, { type, id, fields }) => {
if (this._isTypeAllowed(type)) {
acc.push({
_id: this._serializer.generateRawId(namespace, type, id),
_source: includedFields(type, fields),
});
} else {
unsupportedTypes.push({

View file

@ -68,6 +68,7 @@ export interface UpdateOptions extends BaseOptions {
export interface BulkGetObject {
id: string;
type: string;
fields?: string[];
}
export type BulkGetObjects = BulkGetObject[];

View file

@ -57,7 +57,7 @@ export default function ({ getService }) {
});
});
it('should return 409 when conflicts exist', async () => {
it('should return errors when conflicts exist', async () => {
await supertest
.post('/api/saved_objects/_import')
.attach('file', join(__dirname, '../../fixtures/import.ndjson'))
@ -71,24 +71,21 @@ export default function ({ getService }) {
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
type: 'conflict',
}
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
type: 'conflict',
}
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
error: {
statusCode: 409,
message: 'version conflict, document already exists',
type: 'conflict',
}
},
],
@ -129,6 +126,57 @@ export default function ({ getService }) {
});
});
});
it('should return errors when index patterns or search are missing', async () => {
const objectsToImport = [
JSON.stringify({
type: 'visualization',
id: '1',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: 'non-existing',
},
{
name: 'ref_1',
type: 'search',
id: 'non-existing-search',
},
],
}),
];
await supertest
.post('/api/saved_objects/_import')
.attach('file', Buffer.from(objectsToImport.join('\n'), 'utf8'), 'export.ndjson')
.expect(200)
.then((resp) => {
expect(resp.body).to.eql({
success: false,
successCount: 0,
errors: [
{
type: 'visualization',
id: '1',
error: {
type: 'missing_references',
references: [
{
type: 'index-pattern',
id: 'non-existing',
},
{
type: 'search',
id: 'non-existing-search',
},
],
},
},
],
});
});
});
});
});
});

View file

@ -65,6 +65,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400,
error: 'Bad Request',
type: 'unknown',
},
},
],

View file

@ -69,6 +69,7 @@ export function resolveImportErrorsTestSuiteFactory(
message: `Unsupported saved object type: 'wigwags': Bad Request`,
statusCode: 400,
error: 'Bad Request',
type: 'unknown',
},
},
],