Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import Boom from '@hapi/boom';
import { errors as esErrors } from '@elastic/elasticsearch';

import {
isEsIndexNotFoundError,
createNoMatchingIndicesError,
isNoMatchingIndicesError,
convertEsError,
} from './errors';

// Mirrors the live ES "index_not_found_exception" payload shape so the
// behaviour stays in sync with what `@elastic/elasticsearch` throws.
const buildIndexNotFoundError = () =>
new esErrors.ResponseError({
statusCode: 404,
body: {
error: {
type: 'index_not_found_exception',
reason: 'no such index [SHOULD NOT EXIST]',
index: 'SHOULD NOT EXIST',
'resource.id': 'SHOULD NOT EXIST',
'resource.type': 'index_or_alias',
},
},
} as ConstructorParameters<typeof esErrors.ResponseError>[0]);

const buildDocNotFoundError = () =>
new esErrors.ResponseError({
statusCode: 404,
body: {
_index: 'basic_index',
_id: '1234',
found: false,
},
} as ConstructorParameters<typeof esErrors.ResponseError>[0]);

describe('index_patterns/* error handler', () => {
describe('isEsIndexNotFoundError()', () => {
it('identifies index not found errors', () => {
expect(isEsIndexNotFoundError(buildIndexNotFoundError())).toBe(true);
});

it('rejects doc not found errors', () => {
expect(isEsIndexNotFoundError(buildDocNotFoundError())).toBe(false);
});
});

describe('createNoMatchingIndicesError()', () => {
it('returns a boom error', () => {
const error = createNoMatchingIndicesError('foo*') as Boom.Boom;
expect(error.isBoom).toBe(true);
});

it('sets output code to "no_matching_indices"', () => {
const error = createNoMatchingIndicesError('foo*') as Boom.Boom;
expect(error.output.payload).toHaveProperty('code', 'no_matching_indices');
});
});

describe('isNoMatchingIndicesError()', () => {
it('returns true for errors from createNoMatchingIndicesError()', () => {
expect(isNoMatchingIndicesError(createNoMatchingIndicesError('foo*'))).toBe(true);
});

it('returns false for indexNotFoundError', () => {
expect(isNoMatchingIndicesError(buildIndexNotFoundError())).toBe(false);
});

it('returns false for docNotFoundError', () => {
expect(isNoMatchingIndicesError(buildDocNotFoundError())).toBe(false);
});
});

describe('convertEsError()', () => {
const indices = ['foo', 'bar'];

it('converts indexNotFoundErrors into NoMatchingIndices errors', () => {
const converted = convertEsError(indices, buildIndexNotFoundError());
expect(isNoMatchingIndicesError(converted)).toBe(true);
});

it('wraps other errors in Boom', () => {
const error = new esErrors.ResponseError({
statusCode: 403,
body: {
root_cause: [
{
type: 'security_exception',
reason: 'action [indices:data/read/field_caps] is unauthorized for user [standard]',
},
],
type: 'security_exception',
reason: 'action [indices:data/read/field_caps] is unauthorized for user [standard]',
},
} as ConstructorParameters<typeof esErrors.ResponseError>[0]);

expect(error).not.toHaveProperty('isBoom');
const converted = convertEsError(indices, error) as Boom.Boom;
expect(converted).toHaveProperty('isBoom');
expect(converted.output.statusCode).toBe(403);
});

it('handles errors that are already Boom errors', () => {
const error = new Error() as Error & { statusCode?: number };
error.statusCode = 401;
const boomError = Boom.boomify(error, { statusCode: error.statusCode });

const converted = convertEsError(indices, boomError) as Boom.Boom;

expect(converted.output.statusCode).toBe(401);
});

it('preserves headers from Boom errors', () => {
const error = new Error() as Error & { statusCode?: number };
error.statusCode = 401;
const boomError = Boom.boomify(error, { statusCode: error.statusCode });
const wwwAuthenticate = 'Basic realm="Authorization Required"';
boomError.output.headers['WWW-Authenticate'] = wwwAuthenticate;
const converted = convertEsError(indices, boomError) as Boom.Boom;

expect(converted.output.headers['WWW-Authenticate']).toBe(wwwAuthenticate);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,35 @@ export const COMMON_HEADERS = {
export const ES_ARCHIVE_BASIC_INDEX =
'src/platform/test/api_integration/fixtures/es_archiver/index_patterns/basic_index';

export const ES_ARCHIVE_CONFLICTS =
'src/platform/test/api_integration/fixtures/es_archiver/index_patterns/conflicts';

export const KBN_ARCHIVE_SAVED_OBJECTS_BASIC =
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json';

export const KBN_ARCHIVE_SAVED_OBJECTS_RELATIONSHIPS =
'src/platform/test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json';

export const DATA_VIEW_PATH_LEGACY = 'api/index_patterns/index_pattern';
export const DATA_VIEW_PATH = 'api/data_views/data_view';
export const SERVICE_PATH_LEGACY = 'api/index_patterns';
export const SERVICE_PATH = 'api/data_views';
export const SERVICE_KEY_LEGACY = 'index_pattern';
export const SERVICE_KEY = 'data_view';
export const HAS_USER_DATA_VIEW_PATH = 'api/data_views/has_user_data_view';
export const HAS_USER_INDEX_PATTERN_PATH = 'api/index_patterns/has_user_index_pattern';

export const ID_OVER_MAX_LENGTH = 'x'.repeat(1759);

export const INTERNAL_COMMON_HEADERS = {
'kbn-xsrf': 'some-xsrf-token',
[X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'kibana',
[ELASTIC_HTTP_VERSION_HEADER]: '1',
};

export const EXISTING_INDICES_PATH = 'internal/data_views/_existing_indices';
export const FIELDS_FOR_WILDCARD_PATH = 'internal/data_views/_fields_for_wildcard';
export const FIELDS_ROUTE_PATH = 'internal/data_views/fields';
export const RESOLVE_INDEX_PATH = 'internal/index-pattern-management/resolve_index';
export const SWAP_REFERENCES_PATH = `${SERVICE_PATH}/swap_references`;
export const SWAP_REFERENCES_PREVIEW_PATH = `${SERVICE_PATH}/swap_references/_preview`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { apiTest, tags, type RoleApiCredentials } from '@kbn/scout';
import { expect } from '@kbn/scout/api';
import { COMMON_HEADERS, DATA_VIEW_PATH, SERVICE_KEY } from '../../fixtures/constants';

apiTest.describe(
`POST ${DATA_VIEW_PATH}/{id}/fields - errors (data view api)`,
{ tag: tags.deploymentAgnostic },
() => {
let adminApiCredentials: RoleApiCredentials;
let createdIds: string[] = [];

apiTest.beforeAll(async ({ requestAuth }) => {
adminApiCredentials = await requestAuth.getApiKey('admin');
});

apiTest.afterEach(async ({ apiServices }) => {
for (const id of createdIds) {
await apiServices.dataViews.delete(id);
}
createdIds = [];
});

apiTest('returns 404 error on non-existing data view', async ({ apiClient }) => {
const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`;
const response = await apiClient.post(`${DATA_VIEW_PATH}/${id}/fields`, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
fields: {
foo: {},
},
},
});

expect(response).toHaveStatusCode(404);
});

apiTest('returns error when "fields" payload attribute is invalid', async ({ apiClient }) => {
const title = `foo-${Date.now()}-${Math.random()}*`;

const createResponse = await apiClient.post(DATA_VIEW_PATH, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
[SERVICE_KEY]: { title },
},
});

expect(createResponse).toHaveStatusCode(200);
const id = createResponse.body[SERVICE_KEY].id;
createdIds.push(id);

const response = await apiClient.post(`${DATA_VIEW_PATH}/${id}/fields`, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
fields: 123,
},
});

expect(response).toHaveStatusCode(400);
expect(response.body.statusCode).toBe(400);
expect(response.body.message).toBe(
'[request body.fields]: expected value of type [object] but got [number]'
);
});

apiTest('returns error if no changes are specified', async ({ apiClient }) => {
const title = `foo-${Date.now()}-${Math.random()}*`;

const createResponse = await apiClient.post(DATA_VIEW_PATH, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
[SERVICE_KEY]: { title },
},
});

expect(createResponse).toHaveStatusCode(200);
const id = createResponse.body[SERVICE_KEY].id;
createdIds.push(id);

const response = await apiClient.post(`${DATA_VIEW_PATH}/${id}/fields`, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
fields: {
foo: {},
bar: {},
baz: {},
},
},
});

expect(response).toHaveStatusCode(400);
expect(response.body.statusCode).toBe(400);
expect(response.body.message).toBe('Change set is empty.');
});

apiTest('returns validation error for too long customDescription', async ({ apiClient }) => {
const title = `foo-${Date.now()}-${Math.random()}*`;

const createResponse = await apiClient.post(DATA_VIEW_PATH, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
[SERVICE_KEY]: { title },
},
});

expect(createResponse).toHaveStatusCode(200);
const id = createResponse.body[SERVICE_KEY].id;
createdIds.push(id);

const response = await apiClient.post(`${DATA_VIEW_PATH}/${id}/fields`, {
headers: {
...COMMON_HEADERS,
...adminApiCredentials.apiKeyHeader,
},
responseType: 'json',
body: {
fields: {
foo: {
customDescription: 'too long value'.repeat(50),
},
},
},
});

expect(response).toHaveStatusCode(400);
expect(response.body.statusCode).toBe(400);
expect(response.body.message).toContain('it must have a maximum length');
});
}
);
Loading
Loading