Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d2f9b98
Migrate Spaces API tests to Scout
csr Nov 26, 2025
7d015f0
fix tests
csr Nov 27, 2025
76c22e2
run tests across multiple spaces, created saved object API helper
csr Nov 27, 2025
824e6ae
remove comments
csr Nov 27, 2025
32302dd
move interfaces/types to `types.ts`
csr Nov 27, 2025
923aad5
replace attribute key name
csr Nov 27, 2025
4c97f59
removed unused files
csr Nov 27, 2025
fb69a3d
replace create endpoint with import API
csr Nov 27, 2025
38ba838
move constants to constants.ts, fix tests
csr Nov 27, 2025
2c7f4f8
add note to saved objects API helper
csr Nov 27, 2025
532b942
Saved Object API helper: move types to dedicated `types.ts` file
csr Nov 27, 2025
5be8bc8
refactor tests to use `apiClient` for endpoint validation
csr Nov 28, 2025
e266c0d
improve saved object cleanup
csr Nov 28, 2025
d3749ee
Merge branch 'main' into migrate-spaces-api-api-tests-to-scout
csr Nov 28, 2025
7f18f65
define `savedObjectsManagementCredentials`
csr Nov 28, 2025
6f68506
fix existing test
csr Nov 28, 2025
5b30f3d
remove test
csr Nov 28, 2025
cc28da6
rename files, move `prepareImportFormData` to helpers file
csr Nov 28, 2025
c596a8e
export `RequestAuthFixture`
csr Nov 28, 2025
54e9184
refine tests
csr Nov 28, 2025
167aa48
fix test
csr Nov 28, 2025
61a9a5c
add comments
csr Nov 28, 2025
ffb6bab
add more top-level comments
csr Nov 28, 2025
6516478
Merge branch 'main' into migrate-spaces-api-api-tests-to-scout
csr Dec 1, 2025
3b489ca
Merge branch 'main' into migrate-spaces-api-api-tests-to-scout
csr Dec 1, 2025
035eb11
include Scout configs in `tsconfig.json`
csr Dec 1, 2025
c30b329
enable Spaces Scout tests in the CI
csr Dec 1, 2025
9604981
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Dec 1, 2025
857acd1
Merge branch 'main' into migrate-spaces-api-api-tests-to-scout
csr Dec 2, 2025
497d0c7
use `kbn.savedObjects` to delete created saved objects
csr Dec 2, 2025
2d47a96
delete saved objects API service
csr Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/scout_ci_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ plugins:
- security_solution
- streams_app
- slo
- spaces
disabled:

packages:
Expand Down
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-scout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type {
export type {
ApiServicesFixture,
BrowserAuthFixture,
RequestAuthFixture,
SamlAuth,
SynthtraceFixture,
} from './src/playwright';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ import type { FleetApiService } from './fleet';
import { getFleetApiHelper } from './fleet';
import type { StreamsApiService } from './streams';
import { getStreamsApiService } from './streams';
import type { SavedObjectsApiService } from './saved_objects';
import { getSavedObjectsApiHelper } from './saved_objects';

export interface ApiServicesFixture {
alerting: AlertingApiService;
cases: CasesApiService;
fleet: FleetApiService;
streams: StreamsApiService;
core: CoreApiService;
savedObjects: SavedObjectsApiService;
Copy link
Contributor Author

@csr csr Nov 28, 2025

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 savedObjects API 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 apiClient fixture to send API requests scoped to the role/privileges of our choice (in this case, the savedObjectsManagement privilege).

// add more services here
}

Expand All @@ -43,6 +46,7 @@ export const apiServicesFixture = coreWorkerFixtures.extend<
fleet: getFleetApiHelper(log, kbnClient),
streams: getStreamsApiService({ kbnClient, log }),
core: getCoreApiHelper(log, kbnClient),
savedObjects: getSavedObjectsApiHelper(log, kbnClient),
};

log.serviceLoaded('apiServices');
Expand Down
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elastic/kibana-security we would appreciate if you can verify helpers implementation to make sure we didn't do anything wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up here to say I was able to leverage the kbnClient.savedObects helper just fine, and I have removed the Saved Objects API service that the PR introduced.

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) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware that many of these Saved Object operations are deprecated, except for import/export APIs (and a few others). I have however decided to add most operations to the interface for convenience, as I don't believe there are any plans to remove these anytime soon.

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 };
});
},
};
};
Loading