mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Playground][Backend] Saving Playground CRUD (#217761)
## Summary This PR creates CRUD endpoints for search playgrounds. This will enable us to make new pages for saved playgrounds that are shared in a space. ## Notes Usages of `ALL_SAVED_OBJECT_INDICES` had to be updated to include `ignore_unavailable` since a new index was added for search solution saved objects, but these are not always registered when search plugins are disabled. Because of this refresh and other calls using `ALL_SAVED_OBJECT_INDICES` were failing when the new `.kibana_search_solution` index did not exist. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Gerard Soldevila <gerard.soldevila@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
bea20769c9
commit
f7b28d7b5f
45 changed files with 2725 additions and 38 deletions
|
@ -168,6 +168,7 @@ enabled:
|
|||
- x-pack/test/api_integration/apis/monitoring_collection/config.ts
|
||||
- x-pack/test/api_integration/apis/search/config.ts
|
||||
- x-pack/test/api_integration/apis/searchprofiler/config.ts
|
||||
- x-pack/test/api_integration/apis/search_playground/config.ts
|
||||
- x-pack/test/api_integration/apis/security/config.ts
|
||||
- x-pack/test/api_integration/apis/spaces/config.ts
|
||||
- x-pack/test/api_integration/apis/stats/config.ts
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -2103,6 +2103,7 @@ x-pack/test/api_integration/apis/management/index_management/inference_endpoints
|
|||
/x-pack/test_serverless/functional/page_objects/svl_api_keys.ts @elastic/search-kibana
|
||||
/x-pack/test_serverless/functional/page_objects/svl_search_* @elastic/search-kibana
|
||||
/x-pack/test/functional_search/ @elastic/search-kibana
|
||||
/x-pack/test/api_integration/apis/search_playground/ @elastic/search-kibana
|
||||
|
||||
# workchat
|
||||
/x-pack/test_serverless/api_integration/test_suites/chat @elastic/search-kibana @elastic/workchat-eng
|
||||
|
|
|
@ -927,6 +927,9 @@
|
|||
"username"
|
||||
],
|
||||
"search-telemetry": [],
|
||||
"search_playground": [
|
||||
"name"
|
||||
],
|
||||
"security-ai-prompt": [
|
||||
"description",
|
||||
"model",
|
||||
|
|
|
@ -3069,6 +3069,19 @@
|
|||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"search_playground": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security-ai-prompt": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
|
|
|
@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration';
|
|||
|
||||
// set minimum number of registered saved objects to ensure no object types are removed after 8.8
|
||||
// declared in internal implementation exclicilty to prevent unintended changes.
|
||||
export const SAVED_OBJECT_TYPES_COUNT = 129 as const;
|
||||
export const SAVED_OBJECT_TYPES_COUNT = 130 as const;
|
||||
|
|
|
@ -64,6 +64,7 @@ export {
|
|||
ANALYTICS_SAVED_OBJECT_INDEX,
|
||||
USAGE_COUNTERS_SAVED_OBJECT_INDEX,
|
||||
ALL_SAVED_OBJECT_INDICES,
|
||||
SEARCH_SOLUTION_SAVED_OBJECT_INDEX,
|
||||
} from './src/saved_objects_index_pattern';
|
||||
export type {
|
||||
SavedObjectsType,
|
||||
|
|
|
@ -21,6 +21,7 @@ export const ALERTING_CASES_SAVED_OBJECT_INDEX = `${MAIN_SAVED_OBJECT_INDEX}_ale
|
|||
export const SECURITY_SOLUTION_SAVED_OBJECT_INDEX = `${MAIN_SAVED_OBJECT_INDEX}_security_solution`;
|
||||
export const ANALYTICS_SAVED_OBJECT_INDEX = `${MAIN_SAVED_OBJECT_INDEX}_analytics`;
|
||||
export const USAGE_COUNTERS_SAVED_OBJECT_INDEX = `${MAIN_SAVED_OBJECT_INDEX}_usage_counters`;
|
||||
export const SEARCH_SOLUTION_SAVED_OBJECT_INDEX = `${MAIN_SAVED_OBJECT_INDEX}_search_solution`;
|
||||
|
||||
export const ALL_SAVED_OBJECT_INDICES = [
|
||||
MAIN_SAVED_OBJECT_INDEX,
|
||||
|
@ -30,4 +31,5 @@ export const ALL_SAVED_OBJECT_INDICES = [
|
|||
SECURITY_SOLUTION_SAVED_OBJECT_INDEX,
|
||||
ANALYTICS_SAVED_OBJECT_INDEX,
|
||||
USAGE_COUNTERS_SAVED_OBJECT_INDEX,
|
||||
SEARCH_SOLUTION_SAVED_OBJECT_INDEX,
|
||||
];
|
||||
|
|
|
@ -158,6 +158,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"search": "0aa6eefb37edd3145be340a8b67779c2ca578b22",
|
||||
"search-session": "b2fcd840e12a45039ada50b1355faeafa39876d1",
|
||||
"search-telemetry": "b568601618744720b5662946d3103e3fb75fe8ee",
|
||||
"search_playground": "9e06ddbaad7c9eeb24b24c871b6b3df484d6c1ed",
|
||||
"security-ai-prompt": "cc8ee5aaa9d001e89c131bbd5af6bc80bc271046",
|
||||
"security-rule": "07abb4d7e707d91675ec0495c73816394c7b521f",
|
||||
"security-solution-signals-migration": "9d99715fe5246f19de2273ba77debd2446c36bb1",
|
||||
|
|
|
@ -339,9 +339,13 @@ export const deleteSavedObjectIndices = async (
|
|||
client: ElasticsearchClient,
|
||||
index: string[] = ALL_SAVED_OBJECT_INDICES
|
||||
) => {
|
||||
const indices = await client.indices.get({ index, allow_no_indices: true }, { ignore: [404] });
|
||||
const res = await client.indices.get({ index, ignore_unavailable: true }, { ignore: [404] });
|
||||
const indices = Object.keys(res);
|
||||
if (!indices.length) {
|
||||
return [];
|
||||
}
|
||||
return await client.indices.delete(
|
||||
{ index: Object.keys(indices), allow_no_indices: true },
|
||||
{ index: indices, ignore_unavailable: true },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
};
|
||||
|
|
|
@ -125,6 +125,7 @@ const previouslyRegisteredTypes = [
|
|||
'search',
|
||||
'search-session',
|
||||
'search-telemetry',
|
||||
'search_playground',
|
||||
'security-ai-prompt',
|
||||
'security-rule',
|
||||
'security-solution-signals-migration',
|
||||
|
|
|
@ -17,7 +17,13 @@ export async function emptyKibanaIndexAction({ client, log }: { client: Client;
|
|||
const stats = createStats('emptyKibanaIndex', log);
|
||||
|
||||
await cleanSavedObjectIndices({ client, stats, log });
|
||||
await client.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES });
|
||||
// Refresh indices to prevent a race condition between a write and subsequent read operation. To
|
||||
// fix it deterministically we have to refresh saved object indices and wait until it's done.
|
||||
await client.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
|
||||
// Additionally, we need to clear the cache to ensure that the next read operation will
|
||||
// not return stale data.
|
||||
await client.indices.clearCache({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
|
||||
return stats.toJSON();
|
||||
}
|
||||
|
|
|
@ -122,6 +122,7 @@ export async function cleanSavedObjectIndices({
|
|||
const resp = await client.deleteByQuery(
|
||||
{
|
||||
index,
|
||||
ignore_unavailable: true,
|
||||
refresh: true,
|
||||
query: {
|
||||
bool: {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MockedVersionedRouter } from '@kbn/core-http-router-server-mocks';
|
||||
import {
|
||||
IRouter,
|
||||
KibanaRequest,
|
||||
|
@ -23,6 +24,7 @@ type PayloadType = 'params' | 'query' | 'body';
|
|||
interface IMockRouter {
|
||||
method: MethodType;
|
||||
path: string;
|
||||
version?: string;
|
||||
context?: jest.Mocked<RequestHandlerContext>;
|
||||
}
|
||||
interface IMockRouterRequest {
|
||||
|
@ -36,14 +38,21 @@ export class MockRouter {
|
|||
public router!: jest.Mocked<IRouter>;
|
||||
public method: MethodType;
|
||||
public path: string;
|
||||
public version?: string;
|
||||
public context: jest.Mocked<RequestHandlerContext>;
|
||||
public payload?: PayloadType;
|
||||
public response = httpServerMock.createResponseFactory();
|
||||
|
||||
constructor({ method, path, context = {} as jest.Mocked<RequestHandlerContext> }: IMockRouter) {
|
||||
constructor({
|
||||
method,
|
||||
path,
|
||||
version,
|
||||
context = {} as jest.Mocked<RequestHandlerContext>,
|
||||
}: IMockRouter) {
|
||||
this.createRouter();
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
this.version = version;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
@ -84,6 +93,20 @@ export class MockRouter {
|
|||
};
|
||||
|
||||
private findRouteRegistration = () => {
|
||||
if (this.version) {
|
||||
const mockedRoute = (this.router.versioned as MockedVersionedRouter).getRoute(
|
||||
this.method,
|
||||
this.path
|
||||
);
|
||||
if (mockedRoute.versions[this.version]) {
|
||||
return [
|
||||
mockedRoute.versions[this.version].config,
|
||||
mockedRoute.versions[this.version].handler,
|
||||
];
|
||||
}
|
||||
throw new Error('No matching version routes registered.');
|
||||
}
|
||||
|
||||
const routerCalls = this.router[this.method].mock.calls as any[];
|
||||
if (!routerCalls.length) throw new Error('No routes registered.');
|
||||
|
||||
|
|
|
@ -21,3 +21,9 @@ export const DEFAULT_PAGINATION: Pagination = {
|
|||
size: 10,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
export enum ROUTE_VERSIONS {
|
||||
v1 = '1',
|
||||
}
|
||||
|
||||
export const PLAYGROUND_SAVED_OBJECT_TYPE = 'search_playground';
|
||||
|
|
|
@ -46,14 +46,22 @@ export interface QuerySourceFields {
|
|||
skipped_fields: number;
|
||||
}
|
||||
|
||||
const BASE_API_PATH = '/internal/search_playground';
|
||||
|
||||
export enum APIRoutes {
|
||||
POST_API_KEY = '/internal/search_playground/api_key',
|
||||
POST_CHAT_MESSAGE = '/internal/search_playground/chat',
|
||||
POST_QUERY_SOURCE_FIELDS = '/internal/search_playground/query_source_fields',
|
||||
GET_INDICES = '/internal/search_playground/indices',
|
||||
POST_SEARCH_QUERY = '/internal/search_playground/search',
|
||||
GET_INDEX_MAPPINGS = '/internal/search_playground/mappings',
|
||||
POST_QUERY_TEST = '/internal/search_playground/query_test',
|
||||
BASE_API = BASE_API_PATH,
|
||||
POST_API_KEY = `${BASE_API_PATH}/api_key`,
|
||||
POST_CHAT_MESSAGE = `${BASE_API_PATH}/chat`,
|
||||
POST_QUERY_SOURCE_FIELDS = `${BASE_API_PATH}/query_source_fields`,
|
||||
GET_INDICES = `${BASE_API_PATH}/indices`,
|
||||
POST_SEARCH_QUERY = `${BASE_API_PATH}/search`,
|
||||
GET_INDEX_MAPPINGS = `${BASE_API_PATH}/mappings`,
|
||||
POST_QUERY_TEST = `${BASE_API_PATH}/query_test`,
|
||||
PUT_PLAYGROUND_CREATE = `${BASE_API_PATH}/playgrounds`,
|
||||
PUT_PLAYGROUND_UPDATE = `${BASE_API_PATH}/playgrounds/{id}`,
|
||||
GET_PLAYGROUND = `${BASE_API_PATH}/playgrounds/{id}`,
|
||||
GET_PLAYGROUNDS = `${BASE_API_PATH}/playgrounds`,
|
||||
DELETE_PLAYGROUND = `${BASE_API_PATH}/playgrounds/{id}`,
|
||||
}
|
||||
|
||||
export enum LLMs {
|
||||
|
@ -99,3 +107,49 @@ export interface QueryTestResponse {
|
|||
documents?: Document[];
|
||||
searchResponse: SearchResponse;
|
||||
}
|
||||
export interface PlaygroundMetadata {
|
||||
id: string;
|
||||
createdAt?: string;
|
||||
createdBy?: string;
|
||||
updatedAt?: string;
|
||||
updatedBy?: string;
|
||||
}
|
||||
export interface PlaygroundSavedObject {
|
||||
name: string;
|
||||
indices: string[];
|
||||
queryFields: Record<string, string[] | undefined>;
|
||||
elasticsearchQueryJSON: string;
|
||||
userElasticsearchQueryJSON?: string;
|
||||
prompt?: string;
|
||||
citations?: boolean;
|
||||
context?: {
|
||||
sourceFields: Record<string, string[] | undefined>;
|
||||
docSize: number;
|
||||
};
|
||||
summarizationModel?: {
|
||||
connectorId: string;
|
||||
modelId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlaygroundResponse {
|
||||
_meta: PlaygroundMetadata;
|
||||
data: PlaygroundSavedObject;
|
||||
}
|
||||
|
||||
export interface PlaygroundListObject {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt?: string;
|
||||
createdBy?: string;
|
||||
updatedAt?: string;
|
||||
updatedBy?: string;
|
||||
}
|
||||
export interface PlaygroundListResponse {
|
||||
_meta: {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
};
|
||||
items: PlaygroundListObject[];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsType } from '@kbn/core/server';
|
||||
import { SEARCH_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
|
||||
import { PLAYGROUND_SAVED_OBJECT_TYPE } from '../../common';
|
||||
import { playgroundAttributesSchema } from './schema/v1/v1';
|
||||
|
||||
export const createPlaygroundSavedObjectType = (): SavedObjectsType => ({
|
||||
name: PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
indexPattern: SEARCH_SOLUTION_SAVED_OBJECT_INDEX,
|
||||
hidden: false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
keyword: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
modelVersions: {
|
||||
1: {
|
||||
changes: [],
|
||||
schemas: {
|
||||
forwardCompatibility: playgroundAttributesSchema.extends({}, { unknowns: 'ignore' }),
|
||||
create: playgroundAttributesSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const playgroundAttributesSchema = schema.object({
|
||||
name: schema.string(),
|
||||
// Common fields
|
||||
indices: schema.arrayOf(schema.string(), { minSize: 1 }),
|
||||
queryFields: schema.recordOf(schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })),
|
||||
elasticsearchQueryJSON: schema.string(),
|
||||
userElasticsearchQueryJSON: schema.maybe(schema.string()),
|
||||
// Chat fields
|
||||
prompt: schema.maybe(schema.string()),
|
||||
citations: schema.maybe(schema.boolean()),
|
||||
context: schema.maybe(
|
||||
schema.object({
|
||||
sourceFields: schema.recordOf(
|
||||
schema.string(),
|
||||
schema.arrayOf(schema.string(), { minSize: 1 })
|
||||
),
|
||||
docSize: schema.number({ defaultValue: 3, min: 1 }),
|
||||
})
|
||||
),
|
||||
summarizationModel: schema.maybe(
|
||||
schema.object({
|
||||
connectorId: schema.string(),
|
||||
modelId: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
});
|
|
@ -23,7 +23,8 @@ import {
|
|||
SearchPlaygroundPluginStartDependencies,
|
||||
} from './types';
|
||||
import { defineRoutes } from './routes';
|
||||
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
|
||||
import { PLUGIN_ID, PLUGIN_NAME, PLAYGROUND_SAVED_OBJECT_TYPE } from '../common';
|
||||
import { createPlaygroundSavedObjectType } from './playground_saved_object/playground_saved_object';
|
||||
|
||||
export class SearchPlaygroundPlugin
|
||||
implements
|
||||
|
@ -45,6 +46,9 @@ export class SearchPlaygroundPlugin
|
|||
{ features }: SearchPlaygroundPluginSetupDependencies
|
||||
) {
|
||||
this.logger.debug('searchPlayground: Setup');
|
||||
|
||||
core.savedObjects.registerType(createPlaygroundSavedObjectType());
|
||||
|
||||
const router = core.http.createRouter();
|
||||
|
||||
defineRoutes({ router, logger: this.logger, getStartServices: core.getStartServices });
|
||||
|
@ -66,16 +70,17 @@ export class SearchPlaygroundPlugin
|
|||
api: [PLUGIN_ID],
|
||||
catalogue: [PLUGIN_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
all: [PLAYGROUND_SAVED_OBJECT_TYPE],
|
||||
read: [PLAYGROUND_SAVED_OBJECT_TYPE],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
disabled: true,
|
||||
api: [PLUGIN_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
read: [PLAYGROUND_SAVED_OBJECT_TYPE],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { IRouter, StartServicesAccessor } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PLUGIN_ID } from '../common';
|
||||
import { sendMessageEvent, SendMessageEventData } from './analytics/events';
|
||||
|
@ -19,10 +17,9 @@ import { errorHandler } from './utils/error_handler';
|
|||
import { handleStreamResponse } from './utils/handle_stream_response';
|
||||
import {
|
||||
APIRoutes,
|
||||
DefineRoutesOptions,
|
||||
ElasticsearchRetrieverContentField,
|
||||
QueryTestResponse,
|
||||
SearchPlaygroundPluginStart,
|
||||
SearchPlaygroundPluginStartDependencies,
|
||||
} from './types';
|
||||
import { getChatParams } from './lib/get_chat_params';
|
||||
import { fetchIndices } from './lib/fetch_indices';
|
||||
|
@ -32,6 +29,7 @@ import { ContextLimitError } from './lib/errors';
|
|||
import { contextDocumentHitMapper } from './utils/context_document_mapper';
|
||||
import { parseSourceFields } from './utils/parse_source_fields';
|
||||
import { getErrorMessage } from '../common/errors';
|
||||
import { defineSavedPlaygroundRoutes } from './routes/saved_playgrounds';
|
||||
|
||||
const EMPTY_INDICES_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.searchPlayground.serverErrors.emptyIndices',
|
||||
|
@ -60,18 +58,9 @@ export function parseElasticsearchQuery(esQuery: string) {
|
|||
};
|
||||
}
|
||||
|
||||
export function defineRoutes({
|
||||
logger,
|
||||
router,
|
||||
getStartServices,
|
||||
}: {
|
||||
logger: Logger;
|
||||
router: IRouter;
|
||||
getStartServices: StartServicesAccessor<
|
||||
SearchPlaygroundPluginStartDependencies,
|
||||
SearchPlaygroundPluginStart
|
||||
>;
|
||||
}) {
|
||||
export function defineRoutes(routeOptions: DefineRoutesOptions) {
|
||||
const { logger, router, getStartServices } = routeOptions;
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: APIRoutes.POST_QUERY_SOURCE_FIELDS,
|
||||
|
@ -496,4 +485,6 @@ export function defineRoutes({
|
|||
}
|
||||
})
|
||||
);
|
||||
|
||||
defineSavedPlaygroundRoutes(routeOptions);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,680 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import {
|
||||
RequestHandlerContext,
|
||||
SavedObjectsErrorHelpers,
|
||||
StartServicesAccessor,
|
||||
} from '@kbn/core/server';
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { MockRouter } from '../../__mocks__/router.mock';
|
||||
import {
|
||||
APIRoutes,
|
||||
SearchPlaygroundPluginStart,
|
||||
SearchPlaygroundPluginStartDependencies,
|
||||
} from '../types';
|
||||
|
||||
import { ROUTE_VERSIONS } from '../../common';
|
||||
import { defineSavedPlaygroundRoutes } from './saved_playgrounds';
|
||||
|
||||
describe('Search Playground - Playgrounds API', () => {
|
||||
const mockLogger = loggingSystemMock.createLogger().get();
|
||||
let mockRouter: MockRouter;
|
||||
const mockSOClient = {
|
||||
create: jest.fn(),
|
||||
find: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
update: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
const mockCore = {
|
||||
savedObjects: { client: mockSOClient },
|
||||
};
|
||||
|
||||
let context: jest.Mocked<RequestHandlerContext>;
|
||||
let mockGetStartServices: jest.Mocked<
|
||||
StartServicesAccessor<SearchPlaygroundPluginStartDependencies, SearchPlaygroundPluginStart>
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
mockGetStartServices = jest.fn().mockResolvedValue([coreStart, {}, {}]);
|
||||
|
||||
context = {
|
||||
core: Promise.resolve(mockCore),
|
||||
} as unknown as jest.Mocked<RequestHandlerContext>;
|
||||
});
|
||||
|
||||
describe('GET /internal/search_playground/playgrounds', () => {
|
||||
describe('v1', () => {
|
||||
beforeEach(() => {
|
||||
mockRouter = new MockRouter({
|
||||
context,
|
||||
method: 'get',
|
||||
path: APIRoutes.GET_PLAYGROUNDS,
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
});
|
||||
defineSavedPlaygroundRoutes({
|
||||
logger: mockLogger,
|
||||
router: mockRouter.router,
|
||||
getStartServices: mockGetStartServices,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the find method of the saved objects client', async () => {
|
||||
mockSOClient.find.mockResolvedValue({
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'search_playground',
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
updated_at: '2023-10-01T00:00:00Z',
|
||||
attributes: {
|
||||
name: 'Playground 1',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
query: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockSOClient.find).toHaveBeenCalledWith({
|
||||
type: 'search_playground',
|
||||
perPage: 10,
|
||||
page: 1,
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
_meta: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 1,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Playground 1',
|
||||
createdAt: '2023-10-01T00:00:00Z',
|
||||
createdBy: undefined,
|
||||
updatedAt: '2023-10-01T00:00:00Z',
|
||||
updatedBy: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
});
|
||||
it('uses query parameters to call the saved objects search', async () => {
|
||||
mockSOClient.find.mockResolvedValue({
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'search_playground',
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
updated_at: '2023-10-01T00:00:00Z',
|
||||
attributes: {
|
||||
name: 'Playground 1',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
query: {
|
||||
page: 2,
|
||||
size: 15,
|
||||
sortField: 'updated_at',
|
||||
sortOrder: 'asc',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockSOClient.find).toHaveBeenCalledWith({
|
||||
type: 'search_playground',
|
||||
perPage: 15,
|
||||
page: 2,
|
||||
sortField: 'updated_at',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
});
|
||||
it('handles saved object client errors', async () => {
|
||||
mockSOClient.find.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.decorateForbiddenError(new Error('Forbidden'))
|
||||
);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
query: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 403,
|
||||
body: {
|
||||
message: 'Forbidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('re-raises errors from the saved objects client', async () => {
|
||||
const error = new Error('Saved object error');
|
||||
mockSOClient.find.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
query: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('GET /internal/search_playground/playgrounds/{id}', () => {
|
||||
describe('v1', () => {
|
||||
beforeEach(() => {
|
||||
mockRouter = new MockRouter({
|
||||
context,
|
||||
method: 'get',
|
||||
path: APIRoutes.GET_PLAYGROUND,
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
});
|
||||
defineSavedPlaygroundRoutes({
|
||||
logger: mockLogger,
|
||||
router: mockRouter.router,
|
||||
getStartServices: mockGetStartServices,
|
||||
});
|
||||
});
|
||||
it('should parse and return SO response', async () => {
|
||||
mockSOClient.get.mockResolvedValue({
|
||||
id: '1',
|
||||
type: 'search_playground',
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
updated_at: '2023-10-01T00:00:00Z',
|
||||
attributes: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
references: [],
|
||||
version: '1',
|
||||
namespaces: ['default'],
|
||||
migrationVersion: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockSOClient.get).toHaveBeenCalledWith('search_playground', '1');
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
_meta: {
|
||||
id: '1',
|
||||
createdAt: '2023-10-01T00:00:00Z',
|
||||
updatedAt: '2023-10-01T00:00:00Z',
|
||||
},
|
||||
data: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
});
|
||||
it('should handle 404s from so client', async () => {
|
||||
mockSOClient.get.mockResolvedValue({
|
||||
error: {
|
||||
statusCode: 404,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.notFound).toHaveBeenCalledWith({
|
||||
body: {
|
||||
message: '1 playground not found',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should reformat other errors from so client', async () => {
|
||||
mockSOClient.get.mockResolvedValue({
|
||||
error: {
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
error: 'some error message',
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 401,
|
||||
body: {
|
||||
message: 'Unauthorized',
|
||||
attributes: {
|
||||
error: 'some error message',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should handle thrown errors from so client', async () => {
|
||||
mockSOClient.get.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.decorateForbiddenError(new Error('Unauthorized'))
|
||||
);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 403,
|
||||
body: {
|
||||
message: 'Unauthorized',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should re-throw exceptions from so client', async () => {
|
||||
const error = new Error('Saved object error');
|
||||
mockSOClient.get.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('PUT /internal/search_playground/playgrounds', () => {
|
||||
describe('v1', () => {
|
||||
beforeEach(() => {
|
||||
mockRouter = new MockRouter({
|
||||
context,
|
||||
method: 'put',
|
||||
path: APIRoutes.PUT_PLAYGROUND_CREATE,
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
});
|
||||
defineSavedPlaygroundRoutes({
|
||||
logger: mockLogger,
|
||||
router: mockRouter.router,
|
||||
getStartServices: mockGetStartServices,
|
||||
});
|
||||
});
|
||||
it('returns full response with ID for success', async () => {
|
||||
mockSOClient.create.mockResolvedValue({
|
||||
id: '1',
|
||||
type: 'search_playground',
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
updated_at: '2023-10-01T00:00:00Z',
|
||||
attributes: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
references: [],
|
||||
version: '1',
|
||||
namespaces: ['default'],
|
||||
migrationVersion: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
body: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockSOClient.create).toHaveBeenCalledWith('search_playground', {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
});
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
_meta: {
|
||||
id: '1',
|
||||
createdAt: '2023-10-01T00:00:00Z',
|
||||
updatedAt: '2023-10-01T00:00:00Z',
|
||||
},
|
||||
data: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
});
|
||||
it('handles errors from the saved objects client', async () => {
|
||||
mockSOClient.create.mockResolvedValue({
|
||||
error: {
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
error: 'some error message',
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
body: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 401,
|
||||
body: {
|
||||
message: 'Unauthorized',
|
||||
attributes: {
|
||||
error: 'some error message',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('handles thrown errors from the saved objects client', async () => {
|
||||
mockSOClient.create.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.decorateForbiddenError(new Error('Forbidden'))
|
||||
);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
body: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 403,
|
||||
body: {
|
||||
message: 'Forbidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('re-throws exceptions from the saved objects client', async () => {
|
||||
const error = new Error('Saved object error');
|
||||
mockSOClient.create.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
body: {
|
||||
name: 'Playground 1',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('PUT /internal/search_playground/playgrounds/{id}', () => {
|
||||
describe('v1', () => {
|
||||
beforeEach(() => {
|
||||
mockRouter = new MockRouter({
|
||||
context,
|
||||
method: 'put',
|
||||
path: APIRoutes.PUT_PLAYGROUND_UPDATE,
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
});
|
||||
defineSavedPlaygroundRoutes({
|
||||
logger: mockLogger,
|
||||
router: mockRouter.router,
|
||||
getStartServices: mockGetStartServices,
|
||||
});
|
||||
});
|
||||
it('returns empty response on success', async () => {
|
||||
mockSOClient.update.mockResolvedValue({});
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Playground',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockSOClient.update).toHaveBeenCalledWith('search_playground', '1', {
|
||||
name: 'Updated Playground',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
});
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith();
|
||||
});
|
||||
it('handles errors from the saved objects client', async () => {
|
||||
mockSOClient.update.mockResolvedValue({
|
||||
error: {
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
error: 'some error message',
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Playground',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 401,
|
||||
body: {
|
||||
message: 'Unauthorized',
|
||||
attributes: {
|
||||
error: 'some error message',
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('handles saved object client errors', async () => {
|
||||
mockSOClient.update.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError('1', 'search_playground')
|
||||
);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Playground',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 404,
|
||||
body: {
|
||||
message: 'Saved object [1/search_playground] not found',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('re-throws exceptions from the saved objects client', async () => {
|
||||
const error = new Error('Saved object error');
|
||||
mockSOClient.update.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
body: {
|
||||
name: 'Updated Playground',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`,
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('DELETE /internal/search_playground/playgrounds/{id}', () => {
|
||||
describe('v1', () => {
|
||||
beforeEach(() => {
|
||||
mockRouter = new MockRouter({
|
||||
context,
|
||||
method: 'delete',
|
||||
path: APIRoutes.DELETE_PLAYGROUND,
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
});
|
||||
defineSavedPlaygroundRoutes({
|
||||
logger: mockLogger,
|
||||
router: mockRouter.router,
|
||||
getStartServices: mockGetStartServices,
|
||||
});
|
||||
});
|
||||
it('returns empty response on success', async () => {
|
||||
mockSOClient.delete.mockResolvedValue({});
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockSOClient.delete).toHaveBeenCalledWith('search_playground', '1');
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith();
|
||||
});
|
||||
it('handles 404s from the saved objects client', async () => {
|
||||
mockSOClient.delete.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError('1', 'search_playground')
|
||||
);
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 404,
|
||||
body: {
|
||||
message: 'Saved object [1/search_playground] not found',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('handles errors from the saved objects client', async () => {
|
||||
mockSOClient.delete.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.decorateForbiddenError(new Error('Forbidden'))
|
||||
);
|
||||
|
||||
await expect(
|
||||
mockRouter.callRoute({
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual(undefined);
|
||||
|
||||
expect(mockRouter.response.customError).toHaveBeenCalledWith({
|
||||
statusCode: 403,
|
||||
body: {
|
||||
message: 'Forbidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PLUGIN_ID, ROUTE_VERSIONS, PLAYGROUND_SAVED_OBJECT_TYPE } from '../../common';
|
||||
|
||||
import {
|
||||
APIRoutes,
|
||||
DefineRoutesOptions,
|
||||
PlaygroundListResponse,
|
||||
PlaygroundResponse,
|
||||
PlaygroundSavedObject,
|
||||
} from '../types';
|
||||
import { errorHandler } from '../utils/error_handler';
|
||||
import { parsePlaygroundSO, parsePlaygroundSOList, validatePlayground } from '../utils/playgrounds';
|
||||
import { playgroundAttributesSchema } from '../playground_saved_object/schema/v1/v1';
|
||||
|
||||
export const defineSavedPlaygroundRoutes = ({ logger, router }: DefineRoutesOptions) => {
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: APIRoutes.GET_PLAYGROUNDS,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 1, min: 1 }),
|
||||
size: schema.number({ defaultValue: 10, min: 1, max: 1000 }),
|
||||
sortField: schema.string({
|
||||
defaultValue: 'created_at',
|
||||
}),
|
||||
sortOrder: schema.oneOf([schema.literal('desc'), schema.literal('asc')], {
|
||||
defaultValue: 'desc',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const soPlaygrounds = await soClient.find<PlaygroundSavedObject>({
|
||||
type: PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
perPage: request.query.size,
|
||||
page: request.query.page,
|
||||
sortField: request.query.sortField,
|
||||
sortOrder: request.query.sortOrder,
|
||||
});
|
||||
const body: PlaygroundListResponse = parsePlaygroundSOList(soPlaygrounds);
|
||||
return response.ok({
|
||||
body,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
})
|
||||
);
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: APIRoutes.GET_PLAYGROUND,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const soPlayground = await soClient.get<PlaygroundSavedObject>(
|
||||
PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
request.params.id
|
||||
);
|
||||
if (soPlayground.error) {
|
||||
if (soPlayground.error.statusCode === 404) {
|
||||
return response.notFound({
|
||||
body: {
|
||||
message: i18n.translate('xpack.searchPlayground.savedPlaygrounds.notFoundError', {
|
||||
defaultMessage: '{id} playground not found',
|
||||
values: { id: request.params.id },
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
logger.error(
|
||||
i18n.translate('xpack.searchPlayground.savedPlaygrounds.getSOError', {
|
||||
defaultMessage: 'SavedObject error getting search playground {id}',
|
||||
values: { id: request.params.id },
|
||||
})
|
||||
);
|
||||
return response.customError({
|
||||
statusCode: soPlayground.error.statusCode,
|
||||
body: {
|
||||
message: soPlayground.error.message,
|
||||
attributes: {
|
||||
error: soPlayground.error.error,
|
||||
...(soPlayground.error.metadata ?? {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const responseBody: PlaygroundResponse = parsePlaygroundSO(soPlayground);
|
||||
return response.ok({
|
||||
body: responseBody,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
})
|
||||
);
|
||||
// Create
|
||||
router.versioned
|
||||
.put({
|
||||
access: 'internal',
|
||||
path: APIRoutes.PUT_PLAYGROUND_CREATE,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
validate: {
|
||||
request: {
|
||||
body: playgroundAttributesSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
// Validate playground request
|
||||
const playground = request.body;
|
||||
const validationErrors = validatePlayground(playground);
|
||||
if (validationErrors && validationErrors.length > 0) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: i18n.translate('xpack.searchPlayground.savedPlaygrounds.validationError', {
|
||||
defaultMessage: 'Invalid playground request',
|
||||
}),
|
||||
attributes: {
|
||||
errors: validationErrors,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const soPlayground = await soClient.create<PlaygroundSavedObject>(
|
||||
PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
playground
|
||||
);
|
||||
if (soPlayground.error) {
|
||||
return response.customError({
|
||||
statusCode: soPlayground.error.statusCode,
|
||||
body: {
|
||||
message: soPlayground.error.message,
|
||||
attributes: {
|
||||
error: soPlayground.error.error,
|
||||
...(soPlayground.error.metadata ?? {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const responseBody: PlaygroundResponse = parsePlaygroundSO(soPlayground);
|
||||
|
||||
return response.ok({
|
||||
body: responseBody,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Update
|
||||
router.versioned
|
||||
.put({
|
||||
access: 'internal',
|
||||
path: APIRoutes.PUT_PLAYGROUND_UPDATE,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
body: playgroundAttributesSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
const playground = request.body;
|
||||
const validationErrors = validatePlayground(playground);
|
||||
if (validationErrors && validationErrors.length > 0) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: i18n.translate('xpack.searchPlayground.savedPlaygrounds.validationError', {
|
||||
defaultMessage: 'Invalid playground request',
|
||||
}),
|
||||
attributes: {
|
||||
errors: validationErrors,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const soPlayground = await soClient.update<PlaygroundSavedObject>(
|
||||
PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
request.params.id,
|
||||
playground
|
||||
);
|
||||
if (soPlayground.error) {
|
||||
return response.customError({
|
||||
statusCode: soPlayground.error.statusCode,
|
||||
body: {
|
||||
message: soPlayground.error.message,
|
||||
attributes: {
|
||||
error: soPlayground.error.error,
|
||||
...(soPlayground.error.metadata ?? {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return response.ok();
|
||||
})
|
||||
);
|
||||
|
||||
// Delete
|
||||
router.versioned
|
||||
.delete({
|
||||
access: 'internal',
|
||||
path: APIRoutes.DELETE_PLAYGROUND,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [PLUGIN_ID],
|
||||
},
|
||||
},
|
||||
version: ROUTE_VERSIONS.v1,
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
errorHandler(logger)(async (context, request, response) => {
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
await soClient.delete(PLAYGROUND_SAVED_OBJECT_TYPE, request.params.id);
|
||||
return response.ok();
|
||||
})
|
||||
);
|
||||
};
|
|
@ -5,12 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
|
||||
import type { FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import type { InferenceServerStart } from '@kbn/inference-plugin/server';
|
||||
import type { Document } from '@langchain/core/documents';
|
||||
import type { SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { IRouter, StartServicesAccessor } from '@kbn/core/server';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SearchPlaygroundPluginSetup {}
|
||||
|
@ -34,3 +36,12 @@ export * from '../common/types';
|
|||
export type HitDocMapper = (hit: SearchHit) => Document;
|
||||
|
||||
export type ElasticsearchRetrieverContentField = string | Record<string, string | string[]>;
|
||||
|
||||
export interface DefineRoutesOptions {
|
||||
logger: Logger;
|
||||
router: IRouter;
|
||||
getStartServices: StartServicesAccessor<
|
||||
SearchPlaygroundPluginStartDependencies,
|
||||
SearchPlaygroundPluginStart
|
||||
>;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { RequestHandlerWrapper } from '@kbn/core-http-server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { KibanaServerError } from '@kbn/kibana-utils-plugin/common';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
|
@ -22,6 +23,14 @@ export const errorHandler: (logger: Logger) => RequestHandlerWrapper = (logger)
|
|||
if (isKibanaServerError(e)) {
|
||||
return response.customError({ statusCode: e.statusCode, body: e.message });
|
||||
}
|
||||
if (SavedObjectsErrorHelpers.isSavedObjectsClientError(e)) {
|
||||
return response.customError({
|
||||
statusCode: e.output.statusCode,
|
||||
body: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import { PLAYGROUND_SAVED_OBJECT_TYPE } from '../../common';
|
||||
import { type PlaygroundSavedObject } from '../types';
|
||||
import { validatePlayground, parsePlaygroundSO, parsePlaygroundSOList } from './playgrounds';
|
||||
|
||||
const defaultElasticsearchQueryJSON = `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}}}`;
|
||||
const validSearchPlayground: PlaygroundSavedObject = {
|
||||
name: 'Test Playground',
|
||||
indices: ['index1'],
|
||||
queryFields: { index1: ['field1'] },
|
||||
elasticsearchQueryJSON: defaultElasticsearchQueryJSON,
|
||||
};
|
||||
const validChatPlayground: PlaygroundSavedObject = {
|
||||
...validSearchPlayground,
|
||||
prompt: 'Test prompt',
|
||||
citations: true,
|
||||
context: {
|
||||
sourceFields: { index1: ['field1'] },
|
||||
docSize: 3,
|
||||
},
|
||||
summarizationModel: {
|
||||
connectorId: 'connectorId',
|
||||
modelId: 'model',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Playground utils', () => {
|
||||
describe('validatePlayground', () => {
|
||||
it('should return an empty array when search playground is valid', () => {
|
||||
const errors = validatePlayground(validSearchPlayground);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
it('should return an empty array when chat playground is valid', () => {
|
||||
const errors = validatePlayground(validChatPlayground);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
it('should return an empty array when playground user elasticsearch query is valid', () => {
|
||||
const playground: PlaygroundSavedObject = {
|
||||
...validSearchPlayground,
|
||||
userElasticsearchQueryJSON:
|
||||
'{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}',
|
||||
};
|
||||
const errors = validatePlayground(playground);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an error when playground name is empty', () => {
|
||||
const playground: PlaygroundSavedObject = {
|
||||
...validSearchPlayground,
|
||||
name: '',
|
||||
};
|
||||
expect(validatePlayground(playground)).toContain('Playground name cannot be empty');
|
||||
playground.name = ' ';
|
||||
expect(validatePlayground(playground)).toContain('Playground name cannot be empty');
|
||||
});
|
||||
it('should return an error when elasticsearchQuery is invalid JSON', () => {
|
||||
const playground: PlaygroundSavedObject = {
|
||||
...validSearchPlayground,
|
||||
elasticsearchQueryJSON: '{invalidJson}',
|
||||
};
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
"Elasticsearch query JSON is invalid\nExpected property name or '}' in JSON at position 1"
|
||||
);
|
||||
playground.elasticsearchQueryJSON = 'invalidJson';
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
`Elasticsearch query JSON is invalid\nUnexpected token 'i', "invalidJson" is not valid JSON`
|
||||
);
|
||||
});
|
||||
it('should validate queryFields', () => {
|
||||
const playground: PlaygroundSavedObject = {
|
||||
...validSearchPlayground,
|
||||
queryFields: { index1: ['field1', ''] },
|
||||
};
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
'Query field cannot be empty, index1 item 1 is empty'
|
||||
);
|
||||
playground.queryFields = { index1: [] };
|
||||
expect(validatePlayground(playground)).toContain('Query fields cannot be empty');
|
||||
playground.queryFields = { index2: ['field1'] };
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
'Query fields index index2 does not match selected indices'
|
||||
);
|
||||
playground.queryFields = { index1: ['field1'] };
|
||||
expect(validatePlayground(playground)).toEqual([]);
|
||||
playground.queryFields = { index1: [''] };
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
'Query field cannot be empty, index1 item 0 is empty'
|
||||
);
|
||||
});
|
||||
it('should validate context sourceFields', () => {
|
||||
const playground: PlaygroundSavedObject = {
|
||||
...validChatPlayground,
|
||||
context: {
|
||||
sourceFields: { index1: ['field1', ''] },
|
||||
docSize: 3,
|
||||
},
|
||||
};
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
'Source field cannot be empty, index1 item 1 is empty'
|
||||
);
|
||||
playground.context!.sourceFields = { index1: [] };
|
||||
expect(validatePlayground(playground)).toContain('Source fields cannot be empty');
|
||||
playground.context!.sourceFields = { index2: ['field1'] };
|
||||
expect(validatePlayground(playground)).toContain(
|
||||
'Source fields index index2 does not match selected indices'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('parsePlaygroundSO', () => {
|
||||
it('should parse saved object to api response', () => {
|
||||
const savedObject: SavedObject<PlaygroundSavedObject> = {
|
||||
id: 'my-fake-id',
|
||||
type: PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
updated_at: '2023-10-01T00:00:00Z',
|
||||
attributes: validChatPlayground,
|
||||
references: [],
|
||||
version: '1',
|
||||
namespaces: ['default'],
|
||||
migrationVersion: {},
|
||||
coreMigrationVersion: '9.0.0',
|
||||
};
|
||||
expect(parsePlaygroundSO(savedObject)).toEqual({
|
||||
_meta: {
|
||||
id: 'my-fake-id',
|
||||
createdAt: '2023-10-01T00:00:00Z',
|
||||
updatedAt: '2023-10-01T00:00:00Z',
|
||||
},
|
||||
data: {
|
||||
...validChatPlayground,
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should include all available metadata in api response', () => {
|
||||
const savedObject: SavedObject<PlaygroundSavedObject> = {
|
||||
id: 'my-fake-id',
|
||||
type: PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
created_by: 'user1',
|
||||
updated_at: '2023-10-02T00:00:00Z',
|
||||
updated_by: 'user2',
|
||||
attributes: validChatPlayground,
|
||||
references: [],
|
||||
version: '1',
|
||||
namespaces: ['default'],
|
||||
migrationVersion: {},
|
||||
coreMigrationVersion: '9.0.0',
|
||||
};
|
||||
expect(parsePlaygroundSO(savedObject)).toEqual({
|
||||
_meta: {
|
||||
id: 'my-fake-id',
|
||||
createdAt: '2023-10-01T00:00:00Z',
|
||||
createdBy: 'user1',
|
||||
updatedAt: '2023-10-02T00:00:00Z',
|
||||
updatedBy: 'user2',
|
||||
},
|
||||
data: {
|
||||
...validChatPlayground,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('parsePlaygroundSOList', () => {
|
||||
it('should parse saved object list to api response', () => {
|
||||
const savedObjects: Array<SavedObjectsFindResult<PlaygroundSavedObject>> = [
|
||||
{
|
||||
id: 'my-fake-id',
|
||||
type: PLAYGROUND_SAVED_OBJECT_TYPE,
|
||||
created_at: '2023-10-01T00:00:00Z',
|
||||
updated_at: '2023-10-01T00:00:00Z',
|
||||
attributes: validChatPlayground,
|
||||
references: [],
|
||||
version: '1',
|
||||
namespaces: ['default'],
|
||||
migrationVersion: {},
|
||||
coreMigrationVersion: '9.0.0',
|
||||
score: 1,
|
||||
sort: [0],
|
||||
},
|
||||
];
|
||||
const response = parsePlaygroundSOList({
|
||||
total: 1,
|
||||
// @ts-ignore-next-line
|
||||
saved_objects: savedObjects,
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
});
|
||||
expect(response).toEqual({
|
||||
_meta: {
|
||||
total: 1,
|
||||
page: 1,
|
||||
size: 1,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: 'my-fake-id',
|
||||
name: validChatPlayground.name,
|
||||
createdAt: '2023-10-01T00:00:00Z',
|
||||
updatedAt: '2023-10-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsFindResponse, type SavedObject } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type {
|
||||
PlaygroundSavedObject,
|
||||
PlaygroundResponse,
|
||||
PlaygroundListResponse,
|
||||
PlaygroundListObject,
|
||||
} from '../types';
|
||||
|
||||
export function validatePlayground(playground: PlaygroundSavedObject): string[] {
|
||||
const errors: string[] = [];
|
||||
if (playground.name.trim().length === 0) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.playgroundNameError', {
|
||||
defaultMessage: 'Playground name cannot be empty',
|
||||
})
|
||||
);
|
||||
}
|
||||
try {
|
||||
JSON.parse(playground.elasticsearchQueryJSON);
|
||||
} catch (e) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.esQueryJSONError', {
|
||||
defaultMessage: 'Elasticsearch query JSON is invalid\n{jsonParseError}',
|
||||
values: { jsonParseError: e.message },
|
||||
})
|
||||
);
|
||||
}
|
||||
if (playground.userElasticsearchQueryJSON) {
|
||||
try {
|
||||
JSON.parse(playground.userElasticsearchQueryJSON);
|
||||
} catch (e) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.userESQueryJSONError', {
|
||||
defaultMessage: 'User Elasticsearch query JSON is invalid\n{jsonParseError}',
|
||||
values: { jsonParseError: e.message },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
// validate query fields greater than 0 and match selected indices
|
||||
let totalFieldsCount = 0;
|
||||
Object.entries(playground.queryFields).forEach(([index, fields]) => {
|
||||
if (!playground.indices.includes(index)) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.queryFieldsIndexError', {
|
||||
defaultMessage: 'Query fields index {index} does not match selected indices',
|
||||
values: { index },
|
||||
})
|
||||
);
|
||||
}
|
||||
fields?.forEach((field, i) => {
|
||||
if (field.trim().length === 0) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.queryFieldsError', {
|
||||
defaultMessage: 'Query field cannot be empty, {index} item {i} is empty',
|
||||
values: { index, i },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
totalFieldsCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (totalFieldsCount === 0) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.queryFieldsEmptyError', {
|
||||
defaultMessage: 'Query fields cannot be empty',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (playground.context) {
|
||||
// validate source fields greater than 0 and match a selected index
|
||||
let totalSourceFieldsCount = 0;
|
||||
Object.entries(playground.context.sourceFields).forEach(([index, fields]) => {
|
||||
if (!playground.indices.includes(index)) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.sourceFieldsIndexError', {
|
||||
defaultMessage: 'Source fields index {index} does not match selected indices',
|
||||
values: { index },
|
||||
})
|
||||
);
|
||||
}
|
||||
fields?.forEach((field, i) => {
|
||||
if (field.trim().length === 0) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.sourceFieldsError', {
|
||||
defaultMessage: 'Source field cannot be empty, {index} item {i} is empty',
|
||||
values: { index, i },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
totalSourceFieldsCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (totalSourceFieldsCount === 0) {
|
||||
errors.push(
|
||||
i18n.translate('xpack.searchPlayground.sourceFieldsEmptyError', {
|
||||
defaultMessage: 'Source fields cannot be empty',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function parsePlaygroundSO(
|
||||
soPlayground: SavedObject<PlaygroundSavedObject>
|
||||
): PlaygroundResponse {
|
||||
const {
|
||||
id,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
updated_at: updatedAt,
|
||||
updated_by: updatedBy,
|
||||
attributes,
|
||||
} = soPlayground;
|
||||
|
||||
return {
|
||||
_meta: {
|
||||
id,
|
||||
createdAt,
|
||||
createdBy,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
},
|
||||
data: attributes,
|
||||
};
|
||||
}
|
||||
|
||||
export function parsePlaygroundSOList(
|
||||
playgroundsResponse: SavedObjectsFindResponse<PlaygroundSavedObject, unknown>
|
||||
): PlaygroundListResponse {
|
||||
const items: PlaygroundListObject[] = playgroundsResponse.saved_objects.map((soPlayground) => {
|
||||
const {
|
||||
id,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
updated_at: updatedAt,
|
||||
updated_by: updatedBy,
|
||||
attributes: { name },
|
||||
} = soPlayground;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
createdBy,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
};
|
||||
});
|
||||
return {
|
||||
_meta: {
|
||||
total: playgroundsResponse.total,
|
||||
page: playgroundsResponse.page,
|
||||
size: playgroundsResponse.per_page,
|
||||
},
|
||||
items,
|
||||
};
|
||||
}
|
|
@ -57,6 +57,8 @@
|
|||
"@kbn/code-editor",
|
||||
"@kbn/monaco",
|
||||
"@kbn/react-hooks",
|
||||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-saved-objects-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -28,9 +28,9 @@ import { ALL_SAVED_OBJECT_INDICES } from '@kbn/core-saved-objects-server';
|
|||
export const refreshSavedObjectIndices = async (es: Client) => {
|
||||
// Refresh indices to prevent a race condition between a write and subsequent read operation. To
|
||||
// fix it deterministically we have to refresh saved object indices and wait until it's done.
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES });
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
|
||||
// Additionally, we need to clear the cache to ensure that the next read operation will
|
||||
// not return stale data.
|
||||
await es.indices.clearCache({ index: ALL_SAVED_OBJECT_INDICES });
|
||||
await es.indices.clearCache({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
};
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { User, Role, UserInfo } from './types';
|
||||
import type { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export const getUserInfo = (user: User): UserInfo => ({
|
||||
username: user.username,
|
||||
full_name: user.username.replace('_', ' '),
|
||||
email: `${user.username}@elastic.co`,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces
|
||||
* scenarios but can be passed specific ones as well.
|
||||
*/
|
||||
export const createUsersAndRoles = async (
|
||||
getService: FtrProviderContext['getService'],
|
||||
usersToCreate: User[],
|
||||
rolesToCreate: Role[]
|
||||
) => {
|
||||
const security = getService('security');
|
||||
|
||||
const createRole = async ({ name, privileges }: Role) => {
|
||||
return await security.role.create(name, privileges);
|
||||
};
|
||||
|
||||
const createUser = async (user: User) => {
|
||||
const userInfo = getUserInfo(user);
|
||||
|
||||
return await security.user.create(user.username, {
|
||||
password: user.password,
|
||||
roles: user.roles,
|
||||
full_name: userInfo.full_name,
|
||||
email: userInfo.email,
|
||||
});
|
||||
};
|
||||
|
||||
await Promise.all(rolesToCreate.map((role) => createRole(role)));
|
||||
await Promise.all(usersToCreate.map((user) => createUser(user)));
|
||||
};
|
||||
|
||||
export const deleteUsersAndRoles = async (
|
||||
getService: FtrProviderContext['getService'],
|
||||
usersToDelete: User[],
|
||||
rolesToDelete: Role[]
|
||||
) => {
|
||||
const security = getService('security');
|
||||
|
||||
try {
|
||||
await Promise.allSettled(usersToDelete.map((user) => security.user.delete(user.username)));
|
||||
} catch (error) {
|
||||
// ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.allSettled(rolesToDelete.map((role) => security.role.delete(role.name)));
|
||||
} catch (error) {
|
||||
// ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users
|
||||
}
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Role } from './types';
|
||||
|
||||
export const playgroundAllRole: Role = {
|
||||
name: 'playground_test_all',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [{ names: ['*'], privileges: ['all'] }],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const playgroundReadRole: Role = {
|
||||
name: 'playground_test_read',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [{ names: ['*'], privileges: ['read'] }],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['read'],
|
||||
feature: {},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const playgroundNoAccessRole: Role = {
|
||||
name: 'playground_test_no_access',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [{ names: ['*'], privileges: ['all'] }],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
discover: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ROLES = {
|
||||
ALL: playgroundAllRole,
|
||||
READ: playgroundReadRole,
|
||||
NO_ACCESS: playgroundNoAccessRole,
|
||||
};
|
||||
export const ALL_ROLES = Object.values(ROLES);
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
description?: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
interface FeaturesPrivileges {
|
||||
[featureId: string]: string[];
|
||||
}
|
||||
|
||||
interface ElasticsearchIndices {
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
}
|
||||
|
||||
export interface ElasticSearchPrivilege {
|
||||
cluster?: string[];
|
||||
indices?: ElasticsearchIndices[];
|
||||
}
|
||||
|
||||
export interface KibanaPrivilege {
|
||||
spaces: string[];
|
||||
base?: string[];
|
||||
feature?: FeaturesPrivileges;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
name: string;
|
||||
privileges: {
|
||||
elasticsearch?: ElasticSearchPrivilege;
|
||||
kibana?: KibanaPrivilege[];
|
||||
};
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { User } from './types';
|
||||
import { ROLES } from './roles';
|
||||
|
||||
export const playgroundAllUser: User = {
|
||||
username: 'playground_test_all',
|
||||
password: 'password',
|
||||
roles: [ROLES.ALL.name],
|
||||
};
|
||||
|
||||
export const playgroundReadUser: User = {
|
||||
username: 'playground_test_read',
|
||||
password: 'password',
|
||||
roles: [ROLES.READ.name],
|
||||
};
|
||||
|
||||
export const nonPlaygroundUser: User = {
|
||||
username: 'playground_test_no_access',
|
||||
password: 'password',
|
||||
roles: [ROLES.NO_ACCESS.name],
|
||||
};
|
||||
|
||||
export const USERS = {
|
||||
ALL: playgroundAllUser,
|
||||
READ: playgroundReadUser,
|
||||
NO_ACCESS: nonPlaygroundUser,
|
||||
};
|
||||
|
||||
export const ALL_USERS = Object.values(USERS);
|
17
x-pack/test/api_integration/apis/search_playground/config.ts
Normal file
17
x-pack/test/api_integration/apis/search_playground/config.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts'));
|
||||
|
||||
return {
|
||||
...baseIntegrationTestsConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
};
|
||||
}
|
14
x-pack/test/api_integration/apis/search_playground/index.ts
Normal file
14
x-pack/test/api_integration/apis/search_playground/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('search_playground apis', () => {
|
||||
loadTestFile(require.resolve('./playgrounds'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,467 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { ALL_USERS, USERS } from './common/users';
|
||||
import { ALL_ROLES } from './common/roles';
|
||||
import { createUsersAndRoles, deleteUsersAndRoles } from './common/helpers';
|
||||
|
||||
const INTERNAL_API_BASE_PATH = '/internal/search_playground/playgrounds';
|
||||
const INITIAL_REST_VERSION = '1' as const;
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('playgrounds - /internal/search_playground/playgrounds', function () {
|
||||
const testPlaygroundIds: Set<string> = new Set<string>();
|
||||
let testPlaygroundId: string | undefined;
|
||||
|
||||
before(async () => {
|
||||
await createUsersAndRoles(getService, ALL_USERS, ALL_ROLES);
|
||||
});
|
||||
after(async () => {
|
||||
if (testPlaygroundIds.size > 0) {
|
||||
for (const id of testPlaygroundIds) {
|
||||
try {
|
||||
await supertestWithoutAuth
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/${id}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
} catch (err) {
|
||||
log.warning('[Cleanup error] Error deleting playground', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUsersAndRoles(getService, ALL_USERS, ALL_ROLES);
|
||||
});
|
||||
describe('developer', function () {
|
||||
describe('PUT playgrounds', function () {
|
||||
it('should allow creating a new playground', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: 'Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should allow creating chat playground', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: 'Test Chat Playground',
|
||||
indices: ['test-index', 'my-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
prompt: 'Test prompt',
|
||||
citations: false,
|
||||
context: {
|
||||
sourceFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
docSize: 3,
|
||||
},
|
||||
summarizationModel: {
|
||||
connectorId: 'connectorId',
|
||||
},
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body?._meta?.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should allow creating search playground with custom query', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: 'My awesome Playground',
|
||||
indices: ['test-index', 'my-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
userElasticsearchQueryJSON: `{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}`,
|
||||
prompt: 'Test prompt',
|
||||
citations: false,
|
||||
context: {
|
||||
sourceFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
docSize: 3,
|
||||
},
|
||||
summarizationModel: {
|
||||
connectorId: 'connectorId',
|
||||
modelId: 'modelId',
|
||||
},
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body?._meta?.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should allow creating chat playground with custom query', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: 'Another Chat Playground',
|
||||
indices: ['test-index', 'my-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
userElasticsearchQueryJSON: `{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body?._meta?.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should validate playground create request', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: '',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': [''] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(400);
|
||||
});
|
||||
it('should validate playground create request with custom query', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: '',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': [''] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
userElasticsearchQueryJSON: `{"query":{"multi_match":{"query":"{query}","fields":["field1`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
describe('GET playgrounds/{id}', function () {
|
||||
before(() => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
testPlaygroundId = Array.from(testPlaygroundIds)[0];
|
||||
});
|
||||
after(() => {
|
||||
testPlaygroundId = undefined;
|
||||
});
|
||||
it('should return existing playground', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toBeDefined();
|
||||
expect(body._meta.id).toEqual(testPlaygroundId);
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.name).toBeDefined();
|
||||
});
|
||||
it('should return 404 for unknown playground', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
describe('GET playgrounds', function () {
|
||||
it('should return playgrounds', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.get(INTERNAL_API_BASE_PATH)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.total).toBeDefined();
|
||||
expect(body.items).toBeDefined();
|
||||
expect(body._meta.total).toBeGreaterThan(0);
|
||||
expect(body.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
it('should return playgrounds with pagination & sorting', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}?page=1&size=1&sortOrder=asc`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.total).toBeDefined();
|
||||
expect(body.items).toBeDefined();
|
||||
expect(body._meta.total).toBeGreaterThan(0);
|
||||
expect(body.items.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
describe('PUT playgrounds/{id}', function () {
|
||||
before(() => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
testPlaygroundId = Array.from(testPlaygroundIds)[0];
|
||||
});
|
||||
after(() => {
|
||||
testPlaygroundId = undefined;
|
||||
});
|
||||
it('should update existing playground', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.send({
|
||||
name: 'Updated Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toEqual(testPlaygroundId);
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.name).toEqual('Updated Test Playground');
|
||||
});
|
||||
it('should return 404 for unknown playground', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.send({
|
||||
name: 'Updated Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(404);
|
||||
});
|
||||
it('should validate playground update request', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.send({
|
||||
name: '',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': [''] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
describe('DELETE playgrounds/{id}', function () {
|
||||
it('should allow you to delete an existing playground', async () => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
const playgroundId = Array.from(testPlaygroundIds)[0];
|
||||
await supertestWithoutAuth
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/${playgroundId}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(200);
|
||||
testPlaygroundIds.delete(playgroundId);
|
||||
|
||||
await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${playgroundId}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(404);
|
||||
});
|
||||
it('should return 404 for unknown playground', async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.ALL.username, USERS.ALL.password)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('viewer', function () {
|
||||
before(async () => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
testPlaygroundId = Array.from(testPlaygroundIds)[0];
|
||||
});
|
||||
|
||||
describe('GET playgrounds', function () {
|
||||
it('should have playgrounds to test with', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.get(INTERNAL_API_BASE_PATH)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.READ.username, USERS.READ.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.total).toBeDefined();
|
||||
expect(body.items).toBeDefined();
|
||||
expect(body._meta.total).toBeGreaterThan(0);
|
||||
expect(body.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
describe('GET playgrounds/{id}', function () {
|
||||
it('should return existing playground', async () => {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.READ.username, USERS.READ.password)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toBeDefined();
|
||||
expect(body._meta.id).toEqual(testPlaygroundId);
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.name).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe('PUT playgrounds', function () {
|
||||
it('should fail', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: 'Viewer Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.READ.username, USERS.READ.password)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
describe('PUT playgrounds/{id}', function () {
|
||||
it('should fail', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.send({
|
||||
name: 'Updated Test Playground viewer',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.READ.username, USERS.READ.password)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
describe('DELETE playgrounds/{id}', function () {
|
||||
it('should fail', async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.READ.username, USERS.READ.password)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('non-playground users', function () {
|
||||
describe('Playground routes should be unavailable', () => {
|
||||
it('GET playgrounds', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(INTERNAL_API_BASE_PATH)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.NO_ACCESS.username, USERS.NO_ACCESS.password)
|
||||
.expect(403);
|
||||
});
|
||||
it('GET playgrounds/{id}', async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.NO_ACCESS.username, USERS.NO_ACCESS.password)
|
||||
.expect(403);
|
||||
});
|
||||
it('PUT playgrounds', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.send({
|
||||
name: 'Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.NO_ACCESS.username, USERS.NO_ACCESS.password)
|
||||
.expect(403);
|
||||
});
|
||||
it('PUT playgrounds/{id}', async () => {
|
||||
await supertestWithoutAuth
|
||||
.put(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.send({
|
||||
name: 'Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.NO_ACCESS.username, USERS.NO_ACCESS.password)
|
||||
.expect(403);
|
||||
});
|
||||
it('DELETE playgrounds/{id}', async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.auth(USERS.NO_ACCESS.username, USERS.NO_ACCESS.password)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -178,6 +178,7 @@ export function getTestDataLoader({ getService }: Pick<FtrProviderContext, 'getS
|
|||
deleteAllSavedObjectsFromKibanaIndex: async () => {
|
||||
await es.deleteByQuery({
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
wait_for_completion: true,
|
||||
conflicts: 'proceed',
|
||||
query: {
|
||||
|
|
|
@ -71,7 +71,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
// Refresh ES indices to avoid race conditions between write and reading of indeces
|
||||
// See implementation utility function at x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/install_prebuilt_rules_fleet_package.ts
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES });
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
|
||||
// Verify that status is updated after package installation
|
||||
const statusAfterPackageInstallation = await getPrebuiltRulesStatus(es, supertest);
|
||||
|
|
|
@ -37,9 +37,9 @@ export const refreshIndex = async (es: Client, index?: string) => {
|
|||
export const refreshSavedObjectIndices = async (es: Client) => {
|
||||
// Refresh indices to prevent a race condition between a write and subsequent read operation. To
|
||||
// fix it deterministically we have to refresh saved object indices and wait until it's done.
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES });
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
|
||||
// Additionally, we need to clear the cache to ensure that the next read operation will
|
||||
// not return stale data.
|
||||
await es.indices.clearCache({ index: ALL_SAVED_OBJECT_INDICES });
|
||||
await es.indices.clearCache({ index: ALL_SAVED_OBJECT_INDICES, ignore_unavailable: true });
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ export async function getSavedObjectFromES<T>(
|
|||
return await es.search<T>(
|
||||
{
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
|
|
@ -42,6 +42,7 @@ export function getTestScenariosForSpace(spaceId: string) {
|
|||
export function getAggregatedSpaceData(es: Client, objectTypes: string[]) {
|
||||
return es.search({
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
request_cache: false,
|
||||
size: 0,
|
||||
runtime_mappings: {
|
||||
|
|
|
@ -199,6 +199,7 @@ export function getTestDataLoader({ getService }: Pick<FtrProviderContext, 'getS
|
|||
deleteAllSavedObjectsFromKibanaIndex: async () => {
|
||||
await es.deleteByQuery({
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
wait_for_completion: true,
|
||||
conflicts: 'proceed',
|
||||
query: {
|
||||
|
|
|
@ -178,6 +178,7 @@ export function deleteTestSuiteFactory({ getService }: DeploymentAgnosticFtrProv
|
|||
// are updated to remove it, and of those, any that don't exist in any space are deleted.
|
||||
const multiNamespaceResponse = await es.search<Record<string, any>>({
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
size: 100,
|
||||
query: { terms: { type: ['index-pattern'] } },
|
||||
});
|
||||
|
|
|
@ -99,11 +99,15 @@ export function updateObjectsSpacesTestSuiteFactory(
|
|||
if (expectAliasDifference !== undefined) {
|
||||
// if we deleted an object that had an alias pointing to it, the alias should have been deleted as well
|
||||
if (!hasRefreshed) {
|
||||
await es.indices.refresh({ index: ALL_SAVED_OBJECT_INDICES }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching
|
||||
await es.indices.refresh({
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
}); // alias deletion uses refresh: false, so we need to manually refresh the index before searching
|
||||
hasRefreshed = true;
|
||||
}
|
||||
const searchResponse = await es.search({
|
||||
index: ALL_SAVED_OBJECT_INDICES,
|
||||
ignore_unavailable: true,
|
||||
size: 0,
|
||||
query: { terms: { type: ['legacy-url-alias'] } },
|
||||
track_total_hits: true,
|
||||
|
|
|
@ -15,5 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./cases/post_case'));
|
||||
loadTestFile(require.resolve('./serverless_search'));
|
||||
loadTestFile(require.resolve('./platform_security'));
|
||||
loadTestFile(require.resolve('./search_playground'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('search playground APIs', function () {
|
||||
loadTestFile(require.resolve('./playgrounds'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,378 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||
import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integration/deployment_agnostic/services';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const INTERNAL_API_BASE_PATH = '/internal/search_playground/playgrounds';
|
||||
const INITIAL_REST_VERSION = '1' as const;
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const roleScopedSupertest = getService('roleScopedSupertest');
|
||||
|
||||
let supertestDeveloperWithCookieCredentials: SupertestWithRoleScopeType;
|
||||
|
||||
describe('playgrounds routes', function () {
|
||||
const testPlaygroundIds: Set<string> = new Set<string>();
|
||||
let testPlaygroundId: string | undefined;
|
||||
|
||||
before(async () => {
|
||||
supertestDeveloperWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
|
||||
'developer',
|
||||
{
|
||||
useCookieHeader: true,
|
||||
withInternalHeaders: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
after(async () => {
|
||||
if (testPlaygroundIds.size > 0) {
|
||||
for (const id of testPlaygroundIds) {
|
||||
try {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/${id}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
} catch (err) {
|
||||
log.warning('[Cleanup error] Error deleting playground', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
describe('developer', function () {
|
||||
describe('PUT playgrounds', function () {
|
||||
it('should allow creating a new playground', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should allow creating chat playground', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Test Chat Playground',
|
||||
indices: ['test-index', 'my-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
prompt: 'Test prompt',
|
||||
citations: false,
|
||||
context: {
|
||||
sourceFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
docSize: 3,
|
||||
},
|
||||
summarizationModel: {
|
||||
connectorId: 'connectorId',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body?._meta?.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should allow creating search playground with custom query', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'My awesome Playground',
|
||||
indices: ['test-index', 'my-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
userElasticsearchQueryJSON: `{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}`,
|
||||
prompt: 'Test prompt',
|
||||
citations: false,
|
||||
context: {
|
||||
sourceFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
docSize: 3,
|
||||
},
|
||||
summarizationModel: {
|
||||
connectorId: 'connectorId',
|
||||
modelId: 'modelId',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body?._meta?.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should allow creating chat playground with custom query', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Another Chat Playground',
|
||||
indices: ['test-index', 'my-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'], 'my-index': ['field3'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
userElasticsearchQueryJSON: `{"query":{"multi_match":{"query":"{query}","fields":["field1"]}}}`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body?._meta?.id).toBeDefined();
|
||||
testPlaygroundIds.add(body._meta.id);
|
||||
});
|
||||
it('should validate playground create request', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: '',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': [''] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}`,
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
it('should validate playground create request with custom query', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: '',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': [''] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
userElasticsearchQueryJSON: `{"query":{"multi_match":{"query":"{query}","fields":["field1`,
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
describe('GET playgrounds/{id}', function () {
|
||||
before(() => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
testPlaygroundId = Array.from(testPlaygroundIds)[0];
|
||||
});
|
||||
after(() => {
|
||||
testPlaygroundId = undefined;
|
||||
});
|
||||
it('should return existing playground', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toBeDefined();
|
||||
expect(body._meta.id).toEqual(testPlaygroundId);
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.name).toBeDefined();
|
||||
});
|
||||
it('should return 404 for unknown playground', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.get(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
describe('GET playgrounds', function () {
|
||||
it('should return playgrounds', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.get(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.total).toBeDefined();
|
||||
expect(body.items).toBeDefined();
|
||||
expect(body._meta.total).toBeGreaterThan(0);
|
||||
expect(body.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
it('should return playgrounds with pagination & sorting', async () => {
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.get(`${INTERNAL_API_BASE_PATH}?page=1&size=1&sortOrder=asc`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.total).toBeDefined();
|
||||
expect(body.items).toBeDefined();
|
||||
expect(body._meta.total).toBeGreaterThan(0);
|
||||
expect(body.items.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
describe('PUT playgrounds/{id}', function () {
|
||||
before(() => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
testPlaygroundId = Array.from(testPlaygroundIds)[0];
|
||||
});
|
||||
after(() => {
|
||||
testPlaygroundId = undefined;
|
||||
});
|
||||
it('should update existing playground', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.put(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Updated Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertestDeveloperWithCookieCredentials
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toEqual(testPlaygroundId);
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.name).toEqual('Updated Test Playground');
|
||||
});
|
||||
it('should return 404 for unknown playground', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.put(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Updated Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
it('should validate playground update request', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.put(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: '',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': [''] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}`,
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
describe('DELETE playgrounds/{id}', function () {
|
||||
it('should allow you to delete an existing playground', async () => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
const playgroundId = Array.from(testPlaygroundIds)[0];
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/${playgroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
testPlaygroundIds.delete(playgroundId);
|
||||
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${playgroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(404);
|
||||
});
|
||||
it('should return 404 for unknown playground', async () => {
|
||||
await supertestDeveloperWithCookieCredentials
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/some-fake-id`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('viewer', function () {
|
||||
let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType;
|
||||
before(async () => {
|
||||
expect(testPlaygroundIds.size).toBeGreaterThan(0);
|
||||
testPlaygroundId = Array.from(testPlaygroundIds)[0];
|
||||
|
||||
supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope(
|
||||
'viewer',
|
||||
{
|
||||
useCookieHeader: true,
|
||||
withInternalHeaders: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('GET playgrounds', function () {
|
||||
it('should have playgrounds to test with', async () => {
|
||||
const { body } = await supertestViewerWithCookieCredentials
|
||||
.get(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.total).toBeDefined();
|
||||
expect(body.items).toBeDefined();
|
||||
expect(body._meta.total).toBeGreaterThan(0);
|
||||
expect(body.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
describe('GET playgrounds/{id}', function () {
|
||||
it('should return existing playground', async () => {
|
||||
const { body } = await supertestViewerWithCookieCredentials
|
||||
.get(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(200);
|
||||
|
||||
expect(body).toBeDefined();
|
||||
expect(body._meta).toBeDefined();
|
||||
expect(body._meta.id).toBeDefined();
|
||||
expect(body._meta.id).toEqual(testPlaygroundId);
|
||||
expect(body.data).toBeDefined();
|
||||
expect(body.data.name).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe('PUT playgrounds', function () {
|
||||
it('should fail', async () => {
|
||||
await supertestViewerWithCookieCredentials
|
||||
.put(INTERNAL_API_BASE_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Viewer Test Playground',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
describe('PUT playgrounds/{id}', function () {
|
||||
it('should fail', async () => {
|
||||
await supertestViewerWithCookieCredentials
|
||||
.put(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
name: 'Updated Test Playground viewer',
|
||||
indices: ['test-index'],
|
||||
queryFields: { 'test-index': ['field1', 'field2'] },
|
||||
elasticsearchQueryJSON: `{"retriever":{"standard":{"query":{"multi_match":{"query":"{query}","fields":["field1","field2"]}}}}}`,
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
describe('DELETE playgrounds/{id}', function () {
|
||||
it('should fail', async () => {
|
||||
await supertestViewerWithCookieCredentials
|
||||
.delete(`${INTERNAL_API_BASE_PATH}/${testPlaygroundId}`)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue