-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[Spaces] Add Scout tests around using Saved Objects API in different spaces #244323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 28 commits
d2f9b98
7d015f0
76c22e2
824e6ae
32302dd
923aad5
4c97f59
fb69a3d
38ba838
2c7f4f8
532b942
5be8bc8
e266c0d
d3749ee
7f18f65
6f68506
5b30f3d
cc28da6
c596a8e
54e9184
167aa48
61a9a5c
ffb6bab
6516478
3b489ca
035eb11
c30b329
9604981
857acd1
497d0c7
2d47a96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ plugins: | |
| - security_solution | ||
| - streams_app | ||
| - slo | ||
| - spaces | ||
| disabled: | ||
|
|
||
| packages: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,315 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the "Elastic License | ||
| * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| import FormData from 'form-data'; | ||
| import type { KbnClient, ScoutLogger } from '../../../../../../common'; | ||
| import { measurePerformanceAsync } from '../../../../../../common'; | ||
| import type { | ||
| ApiResponse, | ||
| ImportSavedObjectsParams, | ||
| ImportSavedObjectsResponse, | ||
| ExportSavedObjectsParams, | ||
| ExportSavedObjectsResponse, | ||
| ExportedSavedObject, | ||
| UpdateSavedObjectParams, | ||
| } from './types'; | ||
|
|
||
| export interface SavedObjectsApiService { | ||
| // Note: the create and bulk create operations are deprecated in favor of the import API so they weren't added to the API helper | ||
|
||
| get: (type: string, id: string, spaceId?: string) => Promise<ApiResponse>; | ||
| update: (params: UpdateSavedObjectParams, spaceId?: string) => Promise<ApiResponse>; | ||
| delete: (type: string, id: string, spaceId?: string, force?: boolean) => Promise<ApiResponse>; | ||
| bulkGet: ( | ||
| objects: Array<{ type: string; id: string; namespaces?: string[] }>, | ||
| spaceId?: string | ||
| ) => Promise<ApiResponse>; | ||
| bulkUpdate: ( | ||
| objects: Array<{ | ||
| type: string; | ||
| id: string; | ||
| attributes: Record<string, any>; | ||
| namespace?: string; | ||
| }>, | ||
| spaceId?: string | ||
| ) => Promise<ApiResponse>; | ||
| bulkDelete: ( | ||
| objects: Array<{ type: string; id: string; force?: boolean }>, | ||
| spaceId?: string | ||
| ) => Promise<ApiResponse>; | ||
| find: ( | ||
| options: { | ||
| type?: string | string[]; | ||
| search?: string; | ||
| page?: number; | ||
| perPage?: number; | ||
| fields?: string[]; | ||
| namespaces?: string[]; | ||
| }, | ||
| spaceId?: string | ||
| ) => Promise<ApiResponse>; | ||
| import: ( | ||
| params: ImportSavedObjectsParams, | ||
| spaceId?: string | ||
| ) => Promise<ApiResponse<ImportSavedObjectsResponse>>; | ||
| export: ( | ||
| params: ExportSavedObjectsParams, | ||
| spaceId?: string | ||
| ) => Promise<ApiResponse<ExportSavedObjectsResponse>>; | ||
| } | ||
|
|
||
| export const getSavedObjectsApiHelper = ( | ||
| log: ScoutLogger, | ||
| kbnClient: KbnClient | ||
| ): SavedObjectsApiService => { | ||
| const buildSpacePath = (spaceId?: string, path: string = '') => { | ||
| return spaceId && spaceId !== 'default' ? `/s/${spaceId}${path}` : path; | ||
| }; | ||
|
|
||
| // Note: many operations are deprecated, see API specification for guidance | ||
| // Use the non-deprecated methods in your tests where possible | ||
| return { | ||
| import: async (params, spaceId) => { | ||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.import [${params.objects.length} objects]`, | ||
| async () => { | ||
| log.debug( | ||
| `Importing ${params.objects.length} saved objects into space '${spaceId || 'default'}'${ | ||
| params.overwrite ? ' with overwrite' : '' | ||
| }${params.createNewCopies ? ' with createNewCopies' : ''}` | ||
| ); | ||
|
|
||
| // Build the NDJSON file content: each object on its own line | ||
| const ndjsonContent = params.objects.map((obj) => JSON.stringify(obj)).join('\n'); | ||
|
|
||
| // Create FormData for file upload | ||
| const formData = new FormData(); | ||
| formData.append('file', ndjsonContent, 'import.ndjson'); | ||
|
|
||
| // Build query parameters | ||
| const query: Record<string, boolean> = {}; | ||
| if (params.overwrite) { | ||
| query.overwrite = true; | ||
| } | ||
| if (params.createNewCopies) { | ||
| query.createNewCopies = true; | ||
| } | ||
|
|
||
| const response = await kbnClient.request({ | ||
| method: 'POST', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/_import`, | ||
| retries: 3, | ||
| query, | ||
| body: formData, | ||
| headers: formData.getHeaders(), | ||
| ignoreErrors: [400, 409], | ||
| }); | ||
|
|
||
| const importResponse = response.data as any; | ||
|
|
||
| if (response.status === 200) { | ||
| log.debug( | ||
| `Import completed: success=${importResponse.success}, successCount=${importResponse.successCount}` | ||
| ); | ||
| if (importResponse.errors && importResponse.errors.length > 0) { | ||
| log.debug( | ||
| `Import had ${importResponse.errors.length} errors: ${JSON.stringify( | ||
| importResponse.errors.map((e: any) => ({ | ||
| type: e.type, | ||
| id: e.id, | ||
| error: e.error.type, | ||
| })) | ||
| )}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| data: importResponse as ImportSavedObjectsResponse, | ||
| status: response.status, | ||
| }; | ||
| } | ||
| ); | ||
| }, | ||
| export: async (params, spaceId) => { | ||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.export [${params.objects?.length || 'by type'}]`, | ||
| async () => { | ||
| const exportType = params.objects ? 'specific objects' : `type: ${params.type}`; | ||
| log.debug( | ||
| `Exporting ${exportType} from space '${spaceId || 'default'}'${ | ||
| params.includeReferencesDeep ? ' with deep references' : '' | ||
| }` | ||
| ); | ||
|
|
||
| const response = await kbnClient.request({ | ||
| method: 'POST', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/_export`, | ||
| retries: 3, | ||
| body: { | ||
| ...(params.objects && { objects: params.objects }), | ||
| ...(params.type && { type: params.type }), | ||
| ...(params.excludeExportDetails !== undefined && { | ||
| excludeExportDetails: params.excludeExportDetails, | ||
| }), | ||
| ...(params.includeReferencesDeep !== undefined && { | ||
| includeReferencesDeep: params.includeReferencesDeep, | ||
| }), | ||
| }, | ||
| ignoreErrors: [400], | ||
| responseType: 'text', | ||
| }); | ||
|
|
||
| // Export returns NDJSON format - parse it | ||
| const ndjsonText = | ||
| typeof response.data === 'string' ? response.data : JSON.stringify(response.data); | ||
| const lines = ndjsonText.split('\n').filter((line) => line.trim()); | ||
|
|
||
| // Last line is export details (if not excluded) | ||
| const exportedObjects: ExportedSavedObject[] = []; | ||
| let exportDetails; | ||
|
|
||
| for (let i = 0; i < lines.length; i++) { | ||
| const parsed = JSON.parse(lines[i]); | ||
| // Check if this is the export details object | ||
| if (parsed.exportedCount !== undefined) { | ||
| exportDetails = parsed; | ||
| } else { | ||
| exportedObjects.push(parsed as ExportedSavedObject); | ||
| } | ||
| } | ||
|
|
||
| log.debug( | ||
| `Export completed: ${exportedObjects.length} objects${ | ||
| exportDetails ? `, ${exportDetails.exportedCount} total` : '' | ||
| }` | ||
| ); | ||
|
|
||
| return { | ||
| data: { | ||
| exportedObjects, | ||
| ...(exportDetails && { exportDetails }), | ||
| } as ExportSavedObjectsResponse, | ||
| status: response.status, | ||
| }; | ||
| } | ||
| ); | ||
| }, | ||
| delete: async (type, id, spaceId, force) => { | ||
|
||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.delete [${type}/${id}]`, | ||
| async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'DELETE', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/${type}/${id}`, | ||
| retries: 0, | ||
| query: force ? { force: 'true' } : undefined, | ||
| ignoreErrors: [204, 404], | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| } | ||
| ); | ||
| }, | ||
| get: async (type, id, spaceId) => { | ||
| return await measurePerformanceAsync(log, `savedObjectsApi.get [${type}/${id}]`, async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'GET', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/${type}/${id}`, | ||
| retries: 3, | ||
| ignoreErrors: [404], | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| }); | ||
| }, | ||
| update: async (params, spaceId) => { | ||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.update [${params.type}/${params.id}]`, | ||
| async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'PUT', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/${params.type}/${params.id}`, | ||
| retries: 3, | ||
| query: params.upsert ? { upsert: 'true' } : undefined, | ||
| body: { | ||
| attributes: params.attributes, | ||
| }, | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| } | ||
| ); | ||
| }, | ||
| bulkGet: async (objects, spaceId) => { | ||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.bulkGet [${objects.length} objects]`, | ||
| async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'POST', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/_bulk_get`, | ||
| retries: 3, | ||
| body: objects, | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| } | ||
| ); | ||
| }, | ||
| bulkUpdate: async (objects, spaceId) => { | ||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.bulkUpdate [${objects.length} objects]`, | ||
| async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'PUT', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/_bulk_update`, | ||
| retries: 3, | ||
| body: objects, | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| } | ||
| ); | ||
| }, | ||
| bulkDelete: async (objects, spaceId) => { | ||
| return await measurePerformanceAsync( | ||
| log, | ||
| `savedObjectsApi.bulkDelete [${objects.length} objects]`, | ||
| async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'POST', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/_bulk_delete`, | ||
| retries: 0, | ||
| body: objects, | ||
| ignoreErrors: [404], | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| } | ||
| ); | ||
| }, | ||
| find: async (options, spaceId) => { | ||
| return await measurePerformanceAsync(log, 'savedObjectsApi.find', async () => { | ||
| const response = await kbnClient.request({ | ||
| method: 'GET', | ||
| path: `${buildSpacePath(spaceId)}/api/saved_objects/_find`, | ||
| retries: 3, | ||
| query: { | ||
| ...(options.type && { type: options.type }), | ||
| ...(options.search && { search: options.search }), | ||
| ...(options.page && { page: options.page }), | ||
| ...(options.perPage && { per_page: options.perPage }), | ||
| ...(options.fields && { fields: options.fields }), | ||
| ...(options.namespaces && { namespaces: options.namespaces }), | ||
| }, | ||
| }); | ||
| return { data: response.data, status: response.status }; | ||
| }); | ||
| }, | ||
| }; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR introduces a new
savedObjectsAPI helper that can be used within Scout tests to perform common Saved Object operations.While API helpers are fine for setup and teardown operations, in the test body we should use the
apiClientfixture to send API requests scoped to the role/privileges of our choice (in this case, thesavedObjectsManagementprivilege).