Spaces - Migrate to NP Saved Objects Service (#58716)

* use NP saved objects service for type and wrapper registration

* simplifying

* additional testing

* revert snapshot changes

* removing dependency on legacy saved objects service

* consolidate mocks

* fixing imports

* addrress PR feedback

* remove unused docs

* adjust tests for updated corestart contract

* address test flakiness

* address flakiness, part 2

* address test flakiness

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2020-04-03 09:50:06 -04:00 committed by GitHub
parent 678d2206c6
commit 37c826229b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 953 additions and 678 deletions

View file

@ -12,9 +12,7 @@ import { SpacesServiceSetup } from '../../../plugins/spaces/server';
import { SpacesPluginSetup } from '../../../plugins/spaces/server';
// @ts-ignore
import { AuditLogger } from '../../server/lib/audit_logger';
import mappings from './mappings.json';
import { wrapError } from './server/lib/errors';
import { migrateToKibana660 } from './server/lib/migrations';
// @ts-ignore
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
import { initEnterSpaceView } from './server/routes/views';
@ -39,18 +37,6 @@ export const spaces = (kibana: Record<string, any>) =>
managementSections: [],
apps: [],
hacks: ['plugins/spaces/legacy'],
mappings,
migrations: {
space: {
'6.6.0': migrateToKibana660,
},
},
savedObjectSchemas: {
space: {
isNamespaceAgnostic: true,
hidden: true,
},
},
home: [],
injectDefaultVars(server: Server) {
return {
@ -100,7 +86,6 @@ export const spaces = (kibana: Record<string, any>) =>
const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat;
registerLegacyAPI({
savedObjects: server.savedObjects,
auditLogger: {
create: (pluginId: string) =>
new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info),

View file

@ -1,34 +0,0 @@
{
"space": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
},
"description": {
"type": "text"
},
"initials": {
"type": "keyword"
},
"color": {
"type": "keyword"
},
"disabledFeatures": {
"type": "keyword"
},
"imageUrl": {
"type": "text",
"index": false
},
"_reserved": {
"type": "boolean"
}
}
}
}

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { migrateToKibana660 } from './migrate_6x';
describe('migrateTo660', () => {
it('adds a "disabledFeatures" attribute initialized as an empty array', () => {
expect(
migrateToKibana660({
id: 'space:foo',
attributes: {},
})
).toEqual({
id: 'space:foo',
attributes: {
disabledFeatures: [],
},
});
});
it('does not initialize "disabledFeatures" if the property already exists', () => {
// This scenario shouldn't happen organically. Protecting against defects in the migration.
expect(
migrateToKibana660({
id: 'space:foo',
attributes: {
disabledFeatures: ['foo', 'bar', 'baz'],
},
})
).toEqual({
id: 'space:foo',
attributes: {
disabledFeatures: ['foo', 'bar', 'baz'],
},
});
});
});

View file

@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`it throws all other errors from the saved objects client when checking for the default space 1`] = `"unit test: unexpected exception condition"`;
exports[`it throws other errors if there is an error creating the default space 1`] = `"unit test: some other unexpected error"`;

View file

@ -4,20 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
SavedObjectsSchema,
SavedObjectsLegacyService,
SavedObjectsClientContract,
SavedObjectsImportResponse,
SavedObjectsImportOptions,
SavedObjectsExportOptions,
} from 'src/core/server';
import { copySavedObjectsToSpacesFactory } from './copy_to_spaces';
import { Readable } from 'stream';
import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks';
jest.mock('../../../../../../src/core/server', () => {
return {
exportSavedObjectsToStream: jest.fn(),
importSavedObjectsFromStream: jest.fn(),
};
});
import {
exportSavedObjectsToStream,
importSavedObjectsFromStream,
} from '../../../../../../src/core/server';
interface SetupOpts {
objects: Array<{ type: string; id: string; attributes: Record<string, any> }>;
getSortedObjectsForExportImpl?: (opts: SavedObjectsExportOptions) => Promise<Readable>;
importSavedObjectsImpl?: (opts: SavedObjectsImportOptions) => Promise<SavedObjectsImportResponse>;
exportSavedObjectsToStreamImpl?: (opts: SavedObjectsExportOptions) => Promise<Readable>;
importSavedObjectsFromStreamImpl?: (
opts: SavedObjectsImportOptions
) => Promise<SavedObjectsImportResponse>;
}
const expectStreamToContainObjects = async (
@ -40,49 +51,75 @@ const expectStreamToContainObjects = async (
describe('copySavedObjectsToSpaces', () => {
const setup = (setupOpts: SetupOpts) => {
const savedObjectsClient = (null as unknown) as SavedObjectsClientContract;
const coreStart = coreMock.createStart();
const savedObjectsService: SavedObjectsLegacyService = ({
importExport: {
objectLimit: 1000,
getSortedObjectsForExport:
setupOpts.getSortedObjectsForExportImpl ||
jest.fn().mockResolvedValue(
new Readable({
objectMode: true,
read() {
setupOpts.objects.forEach(o => this.push(o));
this.push(null);
},
})
),
importSavedObjects:
setupOpts.importSavedObjectsImpl ||
jest.fn().mockImplementation(async (importOpts: SavedObjectsImportOptions) => {
await expectStreamToContainObjects(importOpts.readStream, setupOpts.objects);
const response: SavedObjectsImportResponse = {
success: true,
successCount: setupOpts.objects.length,
};
return Promise.resolve(response);
}),
const typeRegistry = savedObjectsTypeRegistryMock.create();
typeRegistry.getAllTypes.mockReturnValue([
{
name: 'dashboard',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
types: ['dashboard', 'visualization', 'globalType'],
schema: new SavedObjectsSchema({
globalType: { isNamespaceAgnostic: true },
}),
} as unknown) as SavedObjectsLegacyService;
{
name: 'visualization',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
{
name: 'globaltype',
namespaceAgnostic: true,
hidden: false,
mappings: { properties: {} },
},
]);
typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) =>
typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic)
);
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
(exportSavedObjectsToStream as jest.Mock).mockImplementation(
async (opts: SavedObjectsExportOptions) => {
return (
setupOpts.exportSavedObjectsToStreamImpl?.(opts) ??
new Readable({
objectMode: true,
read() {
setupOpts.objects.forEach(o => this.push(o));
this.push(null);
},
})
);
}
);
(importSavedObjectsFromStream as jest.Mock).mockImplementation(
async (opts: SavedObjectsImportOptions) => {
const defaultImpl = async () => {
await expectStreamToContainObjects(opts.readStream, setupOpts.objects);
const response: SavedObjectsImportResponse = {
success: true,
successCount: setupOpts.objects.length,
};
return Promise.resolve(response);
};
return setupOpts.importSavedObjectsFromStreamImpl?.(opts) ?? defaultImpl();
}
);
return {
savedObjectsClient,
savedObjectsService,
savedObjects: coreStart.savedObjects,
};
};
it('uses the Saved Objects Service to perform an export followed by a series of imports', async () => {
const { savedObjectsClient, savedObjectsService } = setup({
const { savedObjects } = setup({
objects: [
{
type: 'dashboard',
@ -102,9 +139,12 @@ describe('copySavedObjectsToSpaces', () => {
],
});
const request = httpServerMock.createKibanaRequest();
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
savedObjectsClient,
savedObjectsService
savedObjects,
() => 1000,
request
);
const result = await copySavedObjectsToSpaces('sourceSpace', ['destination1', 'destination2'], {
@ -133,8 +173,7 @@ describe('copySavedObjectsToSpaces', () => {
}
`);
expect((savedObjectsService.importExport.getSortedObjectsForExport as jest.Mock).mock.calls)
.toMatchInlineSnapshot(`
expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
@ -148,14 +187,23 @@ describe('copySavedObjectsToSpaces', () => {
"type": "dashboard",
},
],
"savedObjectsClient": null,
"savedObjectsClient": Object {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
"update": [MockFunction],
},
},
],
]
`);
expect((savedObjectsService.importExport.importSavedObjects as jest.Mock).mock.calls)
.toMatchInlineSnapshot(`
expect((importSavedObjectsFromStream as jest.Mock).mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
@ -203,7 +251,17 @@ describe('copySavedObjectsToSpaces', () => {
},
"readable": false,
},
"savedObjectsClient": null,
"savedObjectsClient": Object {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
"update": [MockFunction],
},
"supportedTypes": Array [
"dashboard",
"visualization",
@ -256,7 +314,17 @@ describe('copySavedObjectsToSpaces', () => {
},
"readable": false,
},
"savedObjectsClient": null,
"savedObjectsClient": Object {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
"update": [MockFunction],
},
"supportedTypes": Array [
"dashboard",
"visualization",
@ -285,9 +353,10 @@ describe('copySavedObjectsToSpaces', () => {
attributes: {},
},
];
const { savedObjectsClient, savedObjectsService } = setup({
const { savedObjects } = setup({
objects,
importSavedObjectsImpl: async opts => {
importSavedObjectsFromStreamImpl: async opts => {
if (opts.namespace === 'failure-space') {
throw new Error(`Some error occurred!`);
}
@ -299,9 +368,12 @@ describe('copySavedObjectsToSpaces', () => {
},
});
const request = httpServerMock.createKibanaRequest();
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
savedObjectsClient,
savedObjectsService
savedObjects,
() => 1000,
request
);
const result = await copySavedObjectsToSpaces(
@ -343,7 +415,7 @@ describe('copySavedObjectsToSpaces', () => {
});
it(`handles stream read errors`, async () => {
const { savedObjectsClient, savedObjectsService } = setup({
const { savedObjects } = setup({
objects: [
{
type: 'dashboard',
@ -361,7 +433,7 @@ describe('copySavedObjectsToSpaces', () => {
attributes: {},
},
],
getSortedObjectsForExportImpl: opts => {
exportSavedObjectsToStreamImpl: opts => {
return Promise.resolve(
new Readable({
objectMode: true,
@ -373,9 +445,12 @@ describe('copySavedObjectsToSpaces', () => {
},
});
const request = httpServerMock.createKibanaRequest();
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
savedObjectsClient,
savedObjectsService
savedObjects,
() => 1000,
request
);
await expect(

View file

@ -4,42 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
SavedObjectsClientContract,
SavedObjectsLegacyService,
SavedObject,
} from 'src/core/server';
import { SavedObject, KibanaRequest, CoreStart } from 'src/core/server';
import { Readable } from 'stream';
import { SavedObjectsClientProviderOptions } from 'src/core/server';
import {
exportSavedObjectsToStream,
importSavedObjectsFromStream,
} from '../../../../../../src/core/server';
import { spaceIdToNamespace } from '../utils/namespace';
import { CopyOptions, CopyResponse } from './types';
import { getEligibleTypes } from './lib/get_eligible_types';
import { createReadableStreamFromArray } from './lib/readable_stream_from_array';
import { createEmptyFailureResponse } from './lib/create_empty_failure_response';
import { readStreamToCompletion } from './lib/read_stream_to_completion';
export const COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS: SavedObjectsClientProviderOptions = {
excludedWrappers: ['spaces'],
};
import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts';
export function copySavedObjectsToSpacesFactory(
savedObjectsClient: SavedObjectsClientContract,
savedObjectsService: SavedObjectsLegacyService
savedObjects: CoreStart['savedObjects'],
getImportExportObjectLimit: () => number,
request: KibanaRequest
) {
const { importExport, types, schema } = savedObjectsService;
const eligibleTypes = getEligibleTypes({ types, schema });
const { getTypeRegistry, getScopedClient } = savedObjects;
const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS);
const eligibleTypes = getEligibleTypes(getTypeRegistry());
const exportRequestedObjects = async (
sourceSpaceId: string,
options: Pick<CopyOptions, 'includeReferences' | 'objects'>
) => {
const objectStream = await importExport.getSortedObjectsForExport({
const objectStream = await exportSavedObjectsToStream({
namespace: spaceIdToNamespace(sourceSpaceId),
includeReferencesDeep: options.includeReferences,
excludeExportDetails: true,
objects: options.objects,
savedObjectsClient,
exportSizeLimit: importExport.objectLimit,
exportSizeLimit: getImportExportObjectLimit(),
});
return readStreamToCompletion<SavedObject>(objectStream);
@ -51,9 +51,9 @@ export function copySavedObjectsToSpacesFactory(
options: CopyOptions
) => {
try {
const importResponse = await importExport.importSavedObjects({
const importResponse = await importSavedObjectsFromStream({
namespace: spaceIdToNamespace(spaceId),
objectLimit: importExport.objectLimit,
objectLimit: getImportExportObjectLimit(),
overwrite: options.overwrite,
savedObjectsClient,
supportedTypes: eligibleTypes,

View file

@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsLegacyService } from 'src/core/server';
import { SavedObjectTypeRegistry } from 'src/core/server';
export function getEligibleTypes({
types,
schema,
}: Pick<SavedObjectsLegacyService, 'schema' | 'types'>) {
return types.filter(type => !schema.isNamespaceAgnostic(type));
export function getEligibleTypes(
typeRegistry: Pick<SavedObjectTypeRegistry, 'getAllTypes' | 'isNamespaceAgnostic'>
) {
return typeRegistry
.getAllTypes()
.filter(type => !typeRegistry.isNamespaceAgnostic(type.name))
.map(type => type.name);
}

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function migrateToKibana660(doc: Record<string, any>) {
if (!doc.attributes.hasOwnProperty('disabledFeatures')) {
doc.attributes.disabledFeatures = [];
}
return doc;
}
import { SavedObjectsClientProviderOptions } from 'src/core/server';
export const COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS: SavedObjectsClientProviderOptions = {
excludedWrappers: ['spaces'],
};

View file

@ -4,20 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
SavedObjectsSchema,
SavedObjectsLegacyService,
SavedObjectsClientContract,
SavedObjectsImportResponse,
SavedObjectsResolveImportErrorsOptions,
SavedObjectsExportOptions,
} from 'src/core/server';
import { coreMock, savedObjectsTypeRegistryMock, httpServerMock } from 'src/core/server/mocks';
import { Readable } from 'stream';
import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts';
jest.mock('../../../../../../src/core/server', () => {
return {
exportSavedObjectsToStream: jest.fn(),
resolveSavedObjectsImportErrors: jest.fn(),
};
});
import {
exportSavedObjectsToStream,
resolveSavedObjectsImportErrors,
} from '../../../../../../src/core/server';
interface SetupOpts {
objects: Array<{ type: string; id: string; attributes: Record<string, any> }>;
getSortedObjectsForExportImpl?: (opts: SavedObjectsExportOptions) => Promise<Readable>;
resolveImportErrorsImpl?: (
exportSavedObjectsToStreamImpl?: (opts: SavedObjectsExportOptions) => Promise<Readable>;
resolveSavedObjectsImportErrorsImpl?: (
opts: SavedObjectsResolveImportErrorsOptions
) => Promise<SavedObjectsImportResponse>;
}
@ -42,52 +51,76 @@ const expectStreamToContainObjects = async (
describe('resolveCopySavedObjectsToSpacesConflicts', () => {
const setup = (setupOpts: SetupOpts) => {
const savedObjectsService: SavedObjectsLegacyService = ({
importExport: {
objectLimit: 1000,
getSortedObjectsForExport:
setupOpts.getSortedObjectsForExportImpl ||
jest.fn().mockResolvedValue(
new Readable({
objectMode: true,
read() {
setupOpts.objects.forEach(o => this.push(o));
const coreStart = coreMock.createStart();
this.push(null);
},
})
),
resolveImportErrors:
setupOpts.resolveImportErrorsImpl ||
jest
.fn()
.mockImplementation(async (resolveOpts: SavedObjectsResolveImportErrorsOptions) => {
await expectStreamToContainObjects(resolveOpts.readStream, setupOpts.objects);
const response: SavedObjectsImportResponse = {
success: true,
successCount: setupOpts.objects.length,
};
return response;
}),
const typeRegistry = savedObjectsTypeRegistryMock.create();
typeRegistry.getAllTypes.mockReturnValue([
{
name: 'dashboard',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
types: ['dashboard', 'visualization', 'globalType'],
schema: new SavedObjectsSchema({
globalType: { isNamespaceAgnostic: true },
}),
} as unknown) as SavedObjectsLegacyService;
{
name: 'visualization',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
{
name: 'globaltype',
namespaceAgnostic: true,
hidden: false,
mappings: { properties: {} },
},
]);
const savedObjectsClient = (null as unknown) as SavedObjectsClientContract;
typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) =>
typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic)
);
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
(exportSavedObjectsToStream as jest.Mock).mockImplementation(
async (opts: SavedObjectsExportOptions) => {
return (
setupOpts.exportSavedObjectsToStreamImpl?.(opts) ??
new Readable({
objectMode: true,
read() {
setupOpts.objects.forEach(o => this.push(o));
this.push(null);
},
})
);
}
);
(resolveSavedObjectsImportErrors as jest.Mock).mockImplementation(
async (opts: SavedObjectsResolveImportErrorsOptions) => {
const defaultImpl = async () => {
await expectStreamToContainObjects(opts.readStream, setupOpts.objects);
const response: SavedObjectsImportResponse = {
success: true,
successCount: setupOpts.objects.length,
};
return response;
};
return setupOpts.resolveSavedObjectsImportErrorsImpl?.(opts) ?? defaultImpl();
}
);
return {
savedObjectsClient,
savedObjectsService,
savedObjects: coreStart.savedObjects,
};
};
it('uses the Saved Objects Service to perform an export followed by a series of conflict resolution calls', async () => {
const { savedObjectsClient, savedObjectsService } = setup({
const { savedObjects } = setup({
objects: [
{
type: 'dashboard',
@ -107,9 +140,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
],
});
const request = httpServerMock.createKibanaRequest();
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
savedObjectsClient,
savedObjectsService
savedObjects,
() => 1000,
request
);
const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', {
@ -153,8 +189,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
}
`);
expect((savedObjectsService.importExport.getSortedObjectsForExport as jest.Mock).mock.calls)
.toMatchInlineSnapshot(`
expect((exportSavedObjectsToStream as jest.Mock).mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
@ -168,14 +203,23 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"type": "dashboard",
},
],
"savedObjectsClient": null,
"savedObjectsClient": Object {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
"update": [MockFunction],
},
},
],
]
`);
expect((savedObjectsService.importExport.resolveImportErrors as jest.Mock).mock.calls)
.toMatchInlineSnapshot(`
expect((resolveSavedObjectsImportErrors as jest.Mock).mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
@ -230,7 +274,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"type": "visualization",
},
],
"savedObjectsClient": null,
"savedObjectsClient": Object {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
"update": [MockFunction],
},
"supportedTypes": Array [
"dashboard",
"visualization",
@ -290,7 +344,17 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
"type": "visualization",
},
],
"savedObjectsClient": null,
"savedObjectsClient": Object {
"bulkCreate": [MockFunction],
"bulkGet": [MockFunction],
"bulkUpdate": [MockFunction],
"create": [MockFunction],
"delete": [MockFunction],
"errors": [Function],
"find": [MockFunction],
"get": [MockFunction],
"update": [MockFunction],
},
"supportedTypes": Array [
"dashboard",
"visualization",
@ -320,9 +384,9 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
];
const { savedObjectsClient, savedObjectsService } = setup({
const { savedObjects } = setup({
objects,
resolveImportErrorsImpl: async opts => {
resolveSavedObjectsImportErrorsImpl: async opts => {
if (opts.namespace === 'failure-space') {
throw new Error(`Some error occurred!`);
}
@ -334,9 +398,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
});
const request = httpServerMock.createKibanaRequest();
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
savedObjectsClient,
savedObjectsService
savedObjects,
() => 1000,
request
);
const result = await resolveCopySavedObjectsToSpacesConflicts('sourceSpace', {
@ -396,9 +463,9 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
});
it(`handles stream read errors`, async () => {
const { savedObjectsClient, savedObjectsService } = setup({
const { savedObjects } = setup({
objects: [],
getSortedObjectsForExportImpl: opts => {
exportSavedObjectsToStreamImpl: opts => {
return Promise.resolve(
new Readable({
objectMode: true,
@ -410,9 +477,12 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => {
},
});
const request = httpServerMock.createKibanaRequest();
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
savedObjectsClient,
savedObjectsService
savedObjects,
() => 1000,
request
);
await expect(

View file

@ -4,37 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
SavedObjectsClientContract,
SavedObjectsLegacyService,
SavedObject,
} from 'src/core/server';
import { Readable } from 'stream';
import { SavedObject, CoreStart, KibanaRequest } from 'src/core/server';
import {
exportSavedObjectsToStream,
resolveSavedObjectsImportErrors,
} from '../../../../../../src/core/server';
import { spaceIdToNamespace } from '../utils/namespace';
import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types';
import { getEligibleTypes } from './lib/get_eligible_types';
import { createEmptyFailureResponse } from './lib/create_empty_failure_response';
import { readStreamToCompletion } from './lib/read_stream_to_completion';
import { createReadableStreamFromArray } from './lib/readable_stream_from_array';
import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from './lib/saved_objects_client_opts';
export function resolveCopySavedObjectsToSpacesConflictsFactory(
savedObjectsClient: SavedObjectsClientContract,
savedObjectsService: SavedObjectsLegacyService
savedObjects: CoreStart['savedObjects'],
getImportExportObjectLimit: () => number,
request: KibanaRequest
) {
const { importExport, types, schema } = savedObjectsService;
const eligibleTypes = getEligibleTypes({ types, schema });
const { getTypeRegistry, getScopedClient } = savedObjects;
const savedObjectsClient = getScopedClient(request, COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS);
const eligibleTypes = getEligibleTypes(getTypeRegistry());
const exportRequestedObjects = async (
sourceSpaceId: string,
options: Pick<CopyOptions, 'includeReferences' | 'objects'>
) => {
const objectStream = await importExport.getSortedObjectsForExport({
const objectStream = await exportSavedObjectsToStream({
namespace: spaceIdToNamespace(sourceSpaceId),
includeReferencesDeep: options.includeReferences,
excludeExportDetails: true,
objects: options.objects,
savedObjectsClient,
exportSizeLimit: importExport.objectLimit,
exportSizeLimit: getImportExportObjectLimit(),
});
return readStreamToCompletion<SavedObject>(objectStream);
};
@ -50,9 +55,9 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory(
}>
) => {
try {
const importResponse = await importExport.resolveImportErrors({
const importResponse = await resolveSavedObjectsImportErrors({
namespace: spaceIdToNamespace(spaceId),
objectLimit: importExport.objectLimit,
objectLimit: getImportExportObjectLimit(),
savedObjectsClient,
supportedTypes: eligibleTypes,
readStream: objectsStream,

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { createDefaultSpace } from './create_default_space';
import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server';
import { SavedObjectsErrorHelpers } from 'src/core/server';
interface MockServerSettings {
defaultExists?: boolean;
@ -23,7 +22,7 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
simulateCreateErrorCondition = false,
} = settings;
const mockGet = jest.fn().mockImplementation(() => {
const mockGet = jest.fn().mockImplementation((type, id) => {
if (simulateGetErrorCondition) {
throw new Error('unit test: unexpected exception condition');
}
@ -31,12 +30,14 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
if (defaultExists) {
return;
}
throw Boom.notFound('unit test: default space not found');
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
});
const mockCreate = jest.fn().mockImplementation(() => {
if (simulateConflict) {
throw new Error('unit test: default space already exists');
throw SavedObjectsErrorHelpers.decorateConflictError(
new Error('unit test: default space already exists')
);
}
if (simulateCreateErrorCondition) {
throw new Error('unit test: some other unexpected error');
@ -45,18 +46,9 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
return null;
});
const mockServer = {
config: jest.fn().mockReturnValue({
get: jest.fn(),
}),
return {
savedObjects: {
SavedObjectsClient: {
errors: {
isNotFoundError: (e: Error) => e.message === 'unit test: default space not found',
isConflictError: (e: Error) => e.message === 'unit test: default space already exists',
},
},
getSavedObjectsRepository: jest.fn().mockImplementation(() => {
createInternalRepository: jest.fn().mockImplementation(() => {
return {
get: mockGet,
create: mockCreate,
@ -64,18 +56,6 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
}),
},
};
mockServer.config().get.mockImplementation((key: string) => {
return settings[key];
});
return {
config: mockServer.config(),
savedObjects: (mockServer.savedObjects as unknown) as SavedObjectsLegacyService,
esClient: ({
callAsInternalUser: jest.fn(),
} as unknown) as jest.Mocked<IClusterClient>,
};
};
test(`it creates the default space when one does not exist`, async () => {
@ -85,7 +65,7 @@ test(`it creates the default space when one does not exist`, async () => {
await createDefaultSpace(deps);
const repository = deps.savedObjects.getSavedObjectsRepository();
const repository = deps.savedObjects.createInternalRepository();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(1);
@ -109,7 +89,7 @@ test(`it does not attempt to recreate the default space if it already exists`, a
await createDefaultSpace(deps);
const repository = deps.savedObjects.getSavedObjectsRepository();
const repository = deps.savedObjects.createInternalRepository();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(0);
@ -121,7 +101,9 @@ test(`it throws all other errors from the saved objects client when checking for
simulateGetErrorCondition: true,
});
expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingSnapshot();
expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"unit test: unexpected exception condition"`
);
});
test(`it ignores conflict errors if the default space already exists`, async () => {
@ -132,7 +114,7 @@ test(`it ignores conflict errors if the default space already exists`, async ()
await createDefaultSpace(deps);
const repository = deps.savedObjects.getSavedObjectsRepository();
const repository = deps.savedObjects.createInternalRepository();
expect(repository.get).toHaveBeenCalledTimes(1);
expect(repository.create).toHaveBeenCalledTimes(1);
@ -144,5 +126,7 @@ test(`it throws other errors if there is an error creating the default space`, a
simulateCreateErrorCondition: true,
});
expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingSnapshot();
expect(createDefaultSpace(deps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"unit test: some other unexpected error"`
);
});

View file

@ -5,23 +5,20 @@
*/
import { i18n } from '@kbn/i18n';
import { SavedObjectsLegacyService, IClusterClient } from 'src/core/server';
import { SavedObjectsServiceStart, SavedObjectsRepository } from 'src/core/server';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import { DEFAULT_SPACE_ID } from '../../common/constants';
interface Deps {
esClient: IClusterClient;
savedObjects: SavedObjectsLegacyService;
savedObjects: Pick<SavedObjectsServiceStart, 'createInternalRepository'>;
}
export async function createDefaultSpace({ esClient, savedObjects }: Deps) {
const { getSavedObjectsRepository, SavedObjectsClient } = savedObjects;
export async function createDefaultSpace({ savedObjects }: Deps) {
const { createInternalRepository } = savedObjects;
const savedObjectsRepository = getSavedObjectsRepository(esClient.callAsInternalUser, ['space']);
const savedObjectsRepository = createInternalRepository(['space']);
const defaultSpaceExists = await doesDefaultSpaceExist(
SavedObjectsClient,
savedObjectsRepository
);
const defaultSpaceExists = await doesDefaultSpaceExist(savedObjectsRepository);
if (defaultSpaceExists) {
return;
@ -51,19 +48,19 @@ export async function createDefaultSpace({ esClient, savedObjects }: Deps) {
// Ignore conflict errors.
// It is possible that another Kibana instance, or another invocation of this function
// created the default space in the time it took this to complete.
if (SavedObjectsClient.errors.isConflictError(error)) {
if (SavedObjectsErrorHelpers.isConflictError(error)) {
return;
}
throw error;
}
}
async function doesDefaultSpaceExist(SavedObjectsClient: any, savedObjectsRepository: any) {
async function doesDefaultSpaceExist(savedObjectsRepository: Pick<SavedObjectsRepository, 'get'>) {
try {
await savedObjectsRepository.get('space', DEFAULT_SPACE_ID);
return true;
} catch (e) {
if (SavedObjectsClient.errors.isNotFoundError(e)) {
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
return false;
}
throw e;

View file

@ -11,7 +11,6 @@ import { kibanaTestUser } from '@kbn/test';
import { initSpacesOnRequestInterceptor } from './on_request_interceptor';
import {
CoreSetup,
SavedObjectsLegacyService,
SavedObjectsErrorHelpers,
IBasePath,
IRouter,
@ -19,9 +18,10 @@ import {
import {
elasticsearchServiceMock,
loggingServiceMock,
coreMock,
} from '../../../../../../src/core/server/mocks';
import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server';
import { LegacyAPI, PluginsSetup } from '../../plugin';
import { PluginsSetup } from '../../plugin';
import { SpacesService } from '../../spaces_service';
import { SpacesAuditLogger } from '../audit_logger';
import { convertSavedObjectToSpace } from '../../routes/lib';
@ -152,35 +152,30 @@ describe.skip('onPostAuthInterceptor', () => {
] as Feature[],
} as PluginsSetup['features'];
const savedObjectsService = {
SavedObjectsClient: {
errors: SavedObjectsErrorHelpers,
},
getSavedObjectsRepository: jest.fn().mockImplementation(() => {
return {
get: (type: string, id: string) => {
if (type === 'space') {
const space = availableSpaces.find(s => s.id === id);
if (space) {
return space;
}
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
const mockRepository = jest.fn().mockImplementation(() => {
return {
get: (type: string, id: string) => {
if (type === 'space') {
const space = availableSpaces.find(s => s.id === id);
if (space) {
return space;
}
},
create: () => null,
};
}),
};
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
},
create: () => null,
};
});
const legacyAPI = {
savedObjects: (savedObjectsService as unknown) as SavedObjectsLegacyService,
} as LegacyAPI;
const coreStart = coreMock.createStart();
coreStart.savedObjects.createInternalRepository.mockImplementation(mockRepository);
coreStart.savedObjects.createScopedRepository.mockImplementation(mockRepository);
const service = new SpacesService(loggingMock, () => legacyAPI);
const service = new SpacesService(loggingMock);
const spacesService = await service.setup({
http: (http as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),

View file

@ -8,25 +8,15 @@ import * as Rx from 'rxjs';
import { DEFAULT_SPACE_ID } from '../../common/constants';
import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory';
import { SpacesService } from '../spaces_service';
import { SavedObjectsLegacyService } from 'src/core/server';
import { SpacesAuditLogger } from './audit_logger';
import {
elasticsearchServiceMock,
coreMock,
loggingServiceMock,
} from '../../../../../src/core/server/mocks';
import { coreMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
import { LegacyAPI } from '../plugin';
import { spacesConfig } from './__fixtures__';
import { securityMock } from '../../../security/server/mocks';
const log = loggingServiceMock.createLogger();
const legacyAPI: LegacyAPI = {
savedObjects: {} as SavedObjectsLegacyService,
} as LegacyAPI;
const service = new SpacesService(log, () => legacyAPI);
const service = new SpacesService(log);
describe('createSpacesTutorialContextFactory', () => {
it('should create a valid context factory', async () => {
@ -49,7 +39,7 @@ describe('createSpacesTutorialContextFactory', () => {
it('should create context with the current space id for the default space', async () => {
const spacesService = await service.setup({
http: coreMock.createSetup().http,
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreMock.createStart(), {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),

View file

@ -68,5 +68,30 @@ describe('Spaces Plugin', () => {
expect(usageCollection.getCollectorByType('spaces')).toBeDefined();
});
it('registers the "space" saved object type and client wrapper', async () => {
const initializerContext = coreMock.createPluginInitializerContext({});
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
const features = featuresPluginMock.createSetup();
const licensing = licensingMock.createSetup();
const plugin = new Plugin(initializerContext);
await plugin.setup(core, { features, licensing });
expect(core.savedObjects.registerType).toHaveBeenCalledWith({
name: 'space',
namespaceAgnostic: true,
hidden: true,
mappings: expect.any(Object),
migrations: expect.any(Object),
});
expect(core.savedObjects.addClientWrapper).toHaveBeenCalledWith(
Number.MIN_SAFE_INTEGER,
'spaces',
expect.any(Function)
);
});
});
});

View file

@ -7,12 +7,7 @@
import { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { HomeServerPluginSetup } from 'src/plugins/home/server';
import {
SavedObjectsLegacyService,
CoreSetup,
Logger,
PluginInitializerContext,
} from '../../../../src/core/server';
import { CoreSetup, Logger, PluginInitializerContext } from '../../../../src/core/server';
import {
PluginSetupContract as FeaturesPluginSetup,
PluginStartContract as FeaturesPluginStart,
@ -22,7 +17,6 @@ import { LicensingPluginSetup } from '../../licensing/server';
import { createDefaultSpace } from './lib/create_default_space';
// @ts-ignore
import { AuditLogger } from '../../../../server/lib/audit_logger';
import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory';
import { SpacesAuditLogger } from './lib/audit_logger';
import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory';
import { registerSpacesUsageCollector } from './usage_collection';
@ -34,13 +28,13 @@ import { initExternalSpacesApi } from './routes/api/external';
import { initInternalSpacesApi } from './routes/api/internal';
import { initSpacesViewsRoutes } from './routes/views';
import { setupCapabilities } from './capabilities';
import { SpacesSavedObjectsService } from './saved_objects';
/**
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
* to function properly.
*/
export interface LegacyAPI {
savedObjects: SavedObjectsLegacyService;
auditLogger: {
create: (pluginId: string) => AuditLogger;
};
@ -108,16 +102,19 @@ export class Plugin {
core: CoreSetup<PluginsStart>,
plugins: PluginsSetup
): Promise<SpacesPluginSetup> {
const service = new SpacesService(this.log, this.getLegacyAPI);
const service = new SpacesService(this.log);
const spacesService = await service.setup({
http: core.http,
elasticsearch: core.elasticsearch,
getStartServices: core.getStartServices,
authorization: plugins.security ? plugins.security.authz : null,
getSpacesAuditLogger: this.getSpacesAuditLogger,
config$: this.config$,
});
const savedObjectsService = new SpacesSavedObjectsService();
savedObjectsService.setup({ core, spacesService });
const viewRouter = core.http.createRouter();
initSpacesViewsRoutes({
viewRouter,
@ -128,7 +125,8 @@ export class Plugin {
initExternalSpacesApi({
externalRouter,
log: this.log,
getSavedObjects: () => this.getLegacyAPI().savedObjects,
getStartServices: core.getStartServices,
getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit,
spacesService,
});
@ -170,12 +168,11 @@ export class Plugin {
__legacyCompat: {
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
this.legacyAPI = legacyAPI;
this.setupLegacyComponents(spacesService);
},
createDefaultSpace: async () => {
const [coreStart] = await core.getStartServices();
return await createDefaultSpace({
esClient: core.elasticsearch.adminClient,
savedObjects: this.getLegacyAPI().savedObjects,
savedObjects: coreStart.savedObjects,
});
},
},
@ -183,14 +180,4 @@ export class Plugin {
}
public stop() {}
private setupLegacyComponents(spacesService: SpacesServiceSetup) {
const legacyAPI = this.getLegacyAPI();
const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects;
addScopedSavedObjectsClientWrapperFactory(
Number.MIN_SAFE_INTEGER,
'spaces',
spacesSavedObjectsClientWrapperFactory(spacesService, types)
);
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Readable } from 'stream';
import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils';
async function readStreamToCompletion(stream: Readable) {
return (await (createPromiseFromStreams([stream, createConcatStream([])]) as unknown)) as any[];
}
export const createExportSavedObjectsToStreamMock = () => {
return jest.fn().mockResolvedValue(
new Readable({
objectMode: true,
read() {
this.push(null);
},
})
);
};
export const createImportSavedObjectsFromStreamMock = () => {
return jest.fn().mockImplementation(async (opts: Record<string, any>) => {
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
return {
success: true,
successCount: objectsToImport.length,
};
});
};
export const createResolveSavedObjectsImportErrorsMock = () => {
return jest.fn().mockImplementation(async (opts: Record<string, any>) => {
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
return {
success: true,
successCount: objectsToImport.length,
};
});
};

View file

@ -1,108 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Readable } from 'stream';
import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams';
import { SavedObjectsSchema, SavedObjectsLegacyService } from 'src/core/server';
import { LegacyAPI } from '../../../plugin';
import { Space } from '../../../../common/model/space';
import { createSpaces } from '.';
async function readStreamToCompletion(stream: Readable) {
return (await (createPromiseFromStreams([stream, createConcatStream([])]) as unknown)) as any[];
}
interface LegacyAPIOpts {
spaces?: Space[];
}
export const createLegacyAPI = ({
spaces = createSpaces().map(s => ({ id: s.id, ...s.attributes })),
}: LegacyAPIOpts = {}) => {
const mockSavedObjectsClientContract = {
get: jest.fn((type, id) => {
const result = spaces.filter(s => s.id === id);
if (!result.length) {
throw new Error(`not found: [${type}:${id}]`);
}
return result[0];
}),
find: jest.fn(() => {
return {
total: spaces.length,
saved_objects: spaces,
};
}),
create: jest.fn((type, attributes, { id }) => {
if (spaces.find(s => s.id === id)) {
throw new Error('conflict');
}
return {};
}),
update: jest.fn((type, id) => {
if (!spaces.find(s => s.id === id)) {
throw new Error('not found: during update');
}
return {};
}),
delete: jest.fn((type: string, id: string) => {
return {};
}),
deleteByNamespace: jest.fn(),
};
const savedObjectsService = ({
types: ['visualization', 'dashboard', 'index-pattern', 'globalType'],
schema: new SavedObjectsSchema({
space: {
isNamespaceAgnostic: true,
hidden: true,
},
globalType: {
isNamespaceAgnostic: true,
},
}),
getScopedSavedObjectsClient: jest.fn().mockResolvedValue(mockSavedObjectsClientContract),
importExport: {
objectLimit: 10000,
getSortedObjectsForExport: jest.fn().mockResolvedValue(
new Readable({
objectMode: true,
read() {
this.push(null);
},
})
),
importSavedObjects: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
return {
success: true,
successCount: objectsToImport.length,
};
}),
resolveImportErrors: jest.fn().mockImplementation(async (opts: Record<string, any>) => {
const objectsToImport: any[] = await readStreamToCompletion(opts.readStream);
return {
success: true,
successCount: objectsToImport.length,
};
}),
},
SavedObjectsClient: {
errors: {
isNotFoundError: jest.fn((e: any) => e.message.startsWith('not found:')),
isConflictError: jest.fn((e: any) => e.message.startsWith('conflict')),
},
},
} as unknown) as jest.Mocked<SavedObjectsLegacyService>;
const legacyAPI: jest.Mocked<LegacyAPI> = {
auditLogger: {} as any,
savedObjects: savedObjectsService,
};
return legacyAPI;
};

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server';
import { coreMock, savedObjectsTypeRegistryMock } from '../../../../../../../src/core/server/mocks';
export const createMockSavedObjectsService = (spaces: any[] = []) => {
const mockSavedObjectsClientContract = ({
get: jest.fn((type, id) => {
const result = spaces.filter(s => s.id === id);
if (!result.length) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return result[0];
}),
find: jest.fn(() => {
return {
total: spaces.length,
saved_objects: spaces,
};
}),
create: jest.fn((type, attributes, { id }) => {
if (spaces.find(s => s.id === id)) {
throw SavedObjectsErrorHelpers.decorateConflictError(new Error(), 'space conflict');
}
return {};
}),
update: jest.fn((type, id) => {
if (!spaces.find(s => s.id === id)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return {};
}),
delete: jest.fn((type: string, id: string) => {
return {};
}),
deleteByNamespace: jest.fn(),
} as unknown) as jest.Mocked<SavedObjectsClientContract>;
const { savedObjects } = coreMock.createStart();
const typeRegistry = savedObjectsTypeRegistryMock.create();
typeRegistry.getAllTypes.mockReturnValue([
{
name: 'visualization',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
{
name: 'dashboard',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
{
name: 'index-pattern',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
},
{
name: 'globalType',
namespaceAgnostic: true,
hidden: false,
mappings: { properties: {} },
},
{
name: 'space',
namespaceAgnostic: true,
hidden: true,
mappings: { properties: {} },
},
]);
typeRegistry.isNamespaceAgnostic.mockImplementation((type: string) =>
typeRegistry.getAllTypes().some(t => t.name === type && t.namespaceAgnostic)
);
savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
savedObjects.getScopedClient.mockReturnValue(mockSavedObjectsClientContract);
return savedObjects;
};

View file

@ -5,6 +5,11 @@
*/
export { createSpaces } from './create_spaces';
export { createLegacyAPI } from './create_legacy_api';
export { createMockSavedObjectsRepository } from './create_mock_so_repository';
export { createMockSavedObjectsService } from './create_mock_so_service';
export { mockRouteContext, mockRouteContextWithInvalidLicense } from './route_contexts';
export {
createExportSavedObjectsToStreamMock,
createImportSavedObjectsFromStreamMock,
createResolveSavedObjectsImportErrorsMock,
} from './create_copy_to_space_mocks';

View file

@ -6,17 +6,20 @@
import * as Rx from 'rxjs';
import {
createSpaces,
createLegacyAPI,
createMockSavedObjectsRepository,
mockRouteContext,
mockRouteContextWithInvalidLicense,
createExportSavedObjectsToStreamMock,
createImportSavedObjectsFromStreamMock,
createResolveSavedObjectsImportErrorsMock,
createMockSavedObjectsService,
} from '../__fixtures__';
import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
import {
loggingServiceMock,
elasticsearchServiceMock,
httpServiceMock,
httpServerMock,
coreMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
@ -25,25 +28,55 @@ import { initCopyToSpacesApi } from './copy_to_space';
import { spacesConfig } from '../../../lib/__fixtures__';
import { securityMock } from '../../../../../security/server/mocks';
import { ObjectType } from '@kbn/config-schema';
jest.mock('../../../../../../../src/core/server', () => {
return {
exportSavedObjectsToStream: jest.fn(),
importSavedObjectsFromStream: jest.fn(),
resolveSavedObjectsImportErrors: jest.fn(),
kibanaResponseFactory: jest.requireActual('src/core/server').kibanaResponseFactory,
};
});
import {
exportSavedObjectsToStream,
importSavedObjectsFromStream,
resolveSavedObjectsImportErrors,
} from '../../../../../../../src/core/server';
describe('copy to space', () => {
const spacesSavedObjects = createSpaces();
const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes }));
beforeEach(() => {
(exportSavedObjectsToStream as jest.Mock).mockReset();
(importSavedObjectsFromStream as jest.Mock).mockReset();
(resolveSavedObjectsImportErrors as jest.Mock).mockReset();
});
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const legacyAPI = createLegacyAPI({ spaces });
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
(exportSavedObjectsToStream as jest.Mock).mockImplementation(
createExportSavedObjectsToStreamMock()
);
(importSavedObjectsFromStream as jest.Mock).mockImplementation(
createImportSavedObjectsFromStreamMock()
);
(resolveSavedObjectsImportErrors as jest.Mock).mockImplementation(
createResolveSavedObjectsImportErrorsMock()
);
const log = loggingServiceMock.create().get('spaces');
const service = new SpacesService(log, () => legacyAPI);
const coreStart = coreMock.createStart();
coreStart.savedObjects = createMockSavedObjectsService(spaces);
const service = new SpacesService(log);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
@ -65,7 +98,8 @@ describe('copy to space', () => {
initCopyToSpacesApi({
externalRouter: router,
getSavedObjects: () => legacyAPI.savedObjects,
getStartServices: async () => [coreStart, {}, {}],
getImportExportObjectLimit: () => 1000,
log,
spacesService,
});
@ -76,6 +110,7 @@ describe('copy to space', () => {
] = router.post.mock.calls;
return {
coreStart,
copyToSpace: {
routeValidation: ctsRouteDefinition.validate as RouteValidatorConfig<{}, {}, {}>,
routeHandler: ctsRouteHandler,
@ -85,7 +120,6 @@ describe('copy to space', () => {
routeHandler: resolveRouteHandler,
},
savedObjectsRepositoryMock,
legacyAPI,
};
};
@ -115,7 +149,7 @@ describe('copy to space', () => {
objects: [],
};
const { copyToSpace, legacyAPI } = await setup();
const { copyToSpace, coreStart } = await setup();
const request = httpServerMock.createKibanaRequest({
body: payload,
@ -124,12 +158,9 @@ describe('copy to space', () => {
await copyToSpace.routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(legacyAPI.savedObjects.getScopedSavedObjectsClient).toHaveBeenCalledWith(
expect.any(Object),
{
excludedWrappers: ['spaces'],
}
);
expect(coreStart.savedObjects.getScopedClient).toHaveBeenCalledWith(request, {
excludedWrappers: ['spaces'],
});
});
it(`requires space IDs to be unique`, async () => {
@ -185,7 +216,7 @@ describe('copy to space', () => {
],
};
const { copyToSpace, legacyAPI } = await setup();
const { copyToSpace } = await setup();
const request = httpServerMock.createKibanaRequest({
body: payload,
@ -201,9 +232,8 @@ describe('copy to space', () => {
const { status } = response;
expect(status).toEqual(200);
expect(legacyAPI.savedObjects.importExport.importSavedObjects).toHaveBeenCalledTimes(1);
const [importCallOptions] = (legacyAPI.savedObjects.importExport
.importSavedObjects as any).mock.calls[0];
expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(1);
const [importCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0];
expect(importCallOptions).toMatchObject({
namespace: 'a-space',
@ -217,7 +247,7 @@ describe('copy to space', () => {
objects: [{ type: 'visualization', id: 'bar' }],
};
const { copyToSpace, legacyAPI } = await setup();
const { copyToSpace } = await setup();
const request = httpServerMock.createKibanaRequest({
body: payload,
@ -233,16 +263,14 @@ describe('copy to space', () => {
const { status } = response;
expect(status).toEqual(200);
expect(legacyAPI.savedObjects.importExport.importSavedObjects).toHaveBeenCalledTimes(2);
const [firstImportCallOptions] = (legacyAPI.savedObjects.importExport
.importSavedObjects as any).mock.calls[0];
expect(importSavedObjectsFromStream).toHaveBeenCalledTimes(2);
const [firstImportCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[0];
expect(firstImportCallOptions).toMatchObject({
namespace: 'a-space',
});
const [secondImportCallOptions] = (legacyAPI.savedObjects.importExport
.importSavedObjects as any).mock.calls[1];
const [secondImportCallOptions] = (importSavedObjectsFromStream as jest.Mock).mock.calls[1];
expect(secondImportCallOptions).toMatchObject({
namespace: 'b-space',
@ -284,7 +312,7 @@ describe('copy to space', () => {
objects: [{ type: 'visualization', id: 'bar' }],
};
const { resolveConflicts, legacyAPI } = await setup();
const { resolveConflicts, coreStart } = await setup();
const request = httpServerMock.createKibanaRequest({
body: payload,
@ -293,12 +321,9 @@ describe('copy to space', () => {
await resolveConflicts.routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(legacyAPI.savedObjects.getScopedSavedObjectsClient).toHaveBeenCalledWith(
expect.any(Object),
{
excludedWrappers: ['spaces'],
}
);
expect(coreStart.savedObjects.getScopedClient).toHaveBeenCalledWith(request, {
excludedWrappers: ['spaces'],
});
});
it(`requires objects to be unique`, async () => {
@ -365,7 +390,7 @@ describe('copy to space', () => {
],
};
const { resolveConflicts, legacyAPI } = await setup();
const { resolveConflicts } = await setup();
const request = httpServerMock.createKibanaRequest({
body: payload,
@ -381,9 +406,10 @@ describe('copy to space', () => {
const { status } = response;
expect(status).toEqual(200);
expect(legacyAPI.savedObjects.importExport.resolveImportErrors).toHaveBeenCalledTimes(1);
const [resolveImportErrorsCallOptions] = (legacyAPI.savedObjects.importExport
.resolveImportErrors as any).mock.calls[0];
expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(1);
const [
resolveImportErrorsCallOptions,
] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0];
expect(resolveImportErrorsCallOptions).toMatchObject({
namespace: 'a-space',
@ -412,7 +438,7 @@ describe('copy to space', () => {
},
};
const { resolveConflicts, legacyAPI } = await setup();
const { resolveConflicts } = await setup();
const request = httpServerMock.createKibanaRequest({
body: payload,
@ -428,17 +454,19 @@ describe('copy to space', () => {
const { status } = response;
expect(status).toEqual(200);
expect(legacyAPI.savedObjects.importExport.resolveImportErrors).toHaveBeenCalledTimes(2);
const [resolveImportErrorsFirstCallOptions] = (legacyAPI.savedObjects.importExport
.resolveImportErrors as any).mock.calls[0];
expect(resolveSavedObjectsImportErrors).toHaveBeenCalledTimes(2);
const [
resolveImportErrorsFirstCallOptions,
] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[0];
expect(resolveImportErrorsFirstCallOptions).toMatchObject({
namespace: 'a-space',
supportedTypes: ['visualization', 'dashboard', 'index-pattern'],
});
const [resolveImportErrorsSecondCallOptions] = (legacyAPI.savedObjects.importExport
.resolveImportErrors as any).mock.calls[1];
const [
resolveImportErrorsSecondCallOptions,
] = (resolveSavedObjectsImportErrors as jest.Mock).mock.calls[1];
expect(resolveImportErrorsSecondCallOptions).toMatchObject({
namespace: 'b-space',

View file

@ -12,7 +12,6 @@ import {
resolveCopySavedObjectsToSpacesConflictsFactory,
} from '../../../lib/copy_to_spaces';
import { ExternalRouteDeps } from '.';
import { COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS } from '../../../lib/copy_to_spaces/copy_to_spaces';
import { SPACE_ID_REGEX } from '../../../lib/space_schema';
import { createLicensedRouteHandler } from '../../lib';
@ -22,7 +21,7 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) =>
_.uniq(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length;
export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
const { externalRouter, spacesService, getSavedObjects } = deps;
const { externalRouter, spacesService, getImportExportObjectLimit, getStartServices } = deps;
externalRouter.post(
{
@ -67,13 +66,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
},
},
createLicensedRouteHandler(async (context, request, response) => {
const savedObjectsClient = getSavedObjects().getScopedSavedObjectsClient(
request,
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
);
const [startServices] = await getStartServices();
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
savedObjectsClient,
getSavedObjects()
startServices.savedObjects,
getImportExportObjectLimit,
request
);
const { spaces: destinationSpaceIds, objects, includeReferences, overwrite } = request.body;
const sourceSpaceId = spacesService.getSpaceId(request);
@ -128,13 +126,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
},
},
createLicensedRouteHandler(async (context, request, response) => {
const savedObjectsClient = getSavedObjects().getScopedSavedObjectsClient(
request,
COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS
);
const [startServices] = await getStartServices();
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
savedObjectsClient,
getSavedObjects()
startServices.savedObjects,
getImportExportObjectLimit,
request
);
const { objects, includeReferences, retries } = request.body;
const sourceSpaceId = spacesService.getSpaceId(request);

View file

@ -7,7 +7,6 @@
import * as Rx from 'rxjs';
import {
createSpaces,
createLegacyAPI,
createMockSavedObjectsRepository,
mockRouteContext,
mockRouteContextWithInvalidLicense,
@ -15,9 +14,9 @@ import {
import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
import {
loggingServiceMock,
elasticsearchServiceMock,
httpServiceMock,
httpServerMock,
coreMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
@ -29,22 +28,21 @@ import { ObjectType } from '@kbn/config-schema';
describe('Spaces Public API', () => {
const spacesSavedObjects = createSpaces();
const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes }));
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const legacyAPI = createLegacyAPI({ spaces });
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const log = loggingServiceMock.create().get('spaces');
const service = new SpacesService(log, () => legacyAPI);
const coreStart = coreMock.createStart();
const service = new SpacesService(log);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
@ -66,7 +64,8 @@ describe('Spaces Public API', () => {
initDeleteSpacesApi({
externalRouter: router,
getSavedObjects: () => legacyAPI.savedObjects,
getStartServices: async () => [coreStart, {}, {}],
getImportExportObjectLimit: () => 1000,
log,
spacesService,
});

View file

@ -5,13 +5,14 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server';
import { wrapError } from '../../../lib/errors';
import { SpacesClient } from '../../../lib/spaces_client';
import { ExternalRouteDeps } from '.';
import { createLicensedRouteHandler } from '../../lib';
export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
const { externalRouter, getSavedObjects, spacesService } = deps;
const { externalRouter, spacesService } = deps;
externalRouter.delete(
{
@ -23,7 +24,6 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
},
},
createLicensedRouteHandler(async (context, request, response) => {
const { SavedObjectsClient } = getSavedObjects();
const spacesClient: SpacesClient = await spacesService.scopedClient(request);
const id = request.params.id;
@ -31,7 +31,7 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) {
try {
await spacesClient.delete(id);
} catch (error) {
if (SavedObjectsClient.errors.isNotFoundError(error)) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));

View file

@ -6,7 +6,6 @@
import * as Rx from 'rxjs';
import {
createSpaces,
createLegacyAPI,
createMockSavedObjectsRepository,
mockRouteContextWithInvalidLicense,
mockRouteContext,
@ -15,9 +14,9 @@ import { initGetSpaceApi } from './get';
import { CoreSetup, IRouter, kibanaResponseFactory } from 'src/core/server';
import {
loggingServiceMock,
elasticsearchServiceMock,
httpServiceMock,
httpServerMock,
coreMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
@ -33,16 +32,16 @@ describe('GET space', () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const legacyAPI = createLegacyAPI({ spaces });
const coreStart = coreMock.createStart();
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const log = loggingServiceMock.create().get('spaces');
const service = new SpacesService(log, () => legacyAPI);
const service = new SpacesService(log);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
@ -64,7 +63,8 @@ describe('GET space', () => {
initGetSpaceApi({
externalRouter: router,
getSavedObjects: () => legacyAPI.savedObjects,
getStartServices: async () => [coreStart, {}, {}],
getImportExportObjectLimit: () => 1000,
log,
spacesService,
});

View file

@ -5,12 +5,13 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server';
import { wrapError } from '../../../lib/errors';
import { ExternalRouteDeps } from '.';
import { createLicensedRouteHandler } from '../../lib';
export function initGetSpaceApi(deps: ExternalRouteDeps) {
const { externalRouter, spacesService, getSavedObjects } = deps;
const { externalRouter, spacesService } = deps;
externalRouter.get(
{
@ -23,15 +24,13 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) {
},
createLicensedRouteHandler(async (context, request, response) => {
const spaceId = request.params.id;
const { SavedObjectsClient } = getSavedObjects();
const spacesClient = await spacesService.scopedClient(request);
try {
const space = await spacesClient.get(spaceId);
return response.ok({ body: space });
} catch (error) {
if (SavedObjectsClient.errors.isNotFoundError(error)) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));

View file

@ -6,7 +6,6 @@
import * as Rx from 'rxjs';
import {
createSpaces,
createLegacyAPI,
createMockSavedObjectsRepository,
mockRouteContext,
mockRouteContextWithInvalidLicense,
@ -14,9 +13,9 @@ import {
import { CoreSetup, kibanaResponseFactory, IRouter } from 'src/core/server';
import {
loggingServiceMock,
elasticsearchServiceMock,
httpServiceMock,
httpServerMock,
coreMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
@ -33,16 +32,16 @@ describe('GET /spaces/space', () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const legacyAPI = createLegacyAPI({ spaces });
const coreStart = coreMock.createStart();
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const log = loggingServiceMock.create().get('spaces');
const service = new SpacesService(log, () => legacyAPI);
const service = new SpacesService(log);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
@ -64,7 +63,8 @@ describe('GET /spaces/space', () => {
initGetAllSpacesApi({
externalRouter: router,
getSavedObjects: () => legacyAPI.savedObjects,
getStartServices: async () => [coreStart, {}, {}],
getImportExportObjectLimit: () => 1000,
log,
spacesService,
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger, SavedObjectsLegacyService, IRouter } from 'src/core/server';
import { Logger, IRouter, CoreSetup } from 'src/core/server';
import { initDeleteSpacesApi } from './delete';
import { initGetSpaceApi } from './get';
import { initGetAllSpacesApi } from './get_all';
@ -15,7 +15,8 @@ import { initCopyToSpacesApi } from './copy_to_space';
export interface ExternalRouteDeps {
externalRouter: IRouter;
getSavedObjects: () => SavedObjectsLegacyService;
getStartServices: CoreSetup['getStartServices'];
getImportExportObjectLimit: () => number;
spacesService: SpacesServiceSetup;
log: Logger;
}

View file

@ -6,7 +6,6 @@
import * as Rx from 'rxjs';
import {
createSpaces,
createLegacyAPI,
createMockSavedObjectsRepository,
mockRouteContext,
mockRouteContextWithInvalidLicense,
@ -14,9 +13,9 @@ import {
import { CoreSetup, kibanaResponseFactory, IRouter, RouteValidatorConfig } from 'src/core/server';
import {
loggingServiceMock,
elasticsearchServiceMock,
httpServerMock,
httpServiceMock,
coreMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
@ -28,22 +27,21 @@ import { ObjectType } from '@kbn/config-schema';
describe('Spaces Public API', () => {
const spacesSavedObjects = createSpaces();
const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes }));
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const legacyAPI = createLegacyAPI({ spaces });
const coreStart = coreMock.createStart();
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const log = loggingServiceMock.create().get('spaces');
const service = new SpacesService(log, () => legacyAPI);
const service = new SpacesService(log);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
@ -65,7 +63,8 @@ describe('Spaces Public API', () => {
initPostSpacesApi({
externalRouter: router,
getSavedObjects: () => legacyAPI.savedObjects,
getStartServices: async () => [coreStart, {}, {}],
getImportExportObjectLimit: () => 1000,
log,
spacesService,
});
@ -145,7 +144,7 @@ describe('Spaces Public API', () => {
const { status, payload: responsePayload } = response;
expect(status).toEqual(409);
expect(responsePayload.message).toEqual('space conflict');
expect(responsePayload.message).toEqual('A space with the identifier a-space already exists.');
});
it('should not require disabledFeatures to be specified', async () => {

View file

@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server';
import { wrapError } from '../../../lib/errors';
import { spaceSchema } from '../../../lib/space_schema';
import { ExternalRouteDeps } from '.';
import { createLicensedRouteHandler } from '../../lib';
export function initPostSpacesApi(deps: ExternalRouteDeps) {
const { externalRouter, log, spacesService, getSavedObjects } = deps;
const { externalRouter, log, spacesService } = deps;
externalRouter.post(
{
@ -21,7 +22,6 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) {
},
createLicensedRouteHandler(async (context, request, response) => {
log.debug(`Inside POST /api/spaces/space`);
const { SavedObjectsClient } = getSavedObjects();
const spacesClient = await spacesService.scopedClient(request);
const space = request.body;
@ -31,7 +31,7 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) {
const createdSpace = await spacesClient.create(space);
return response.ok({ body: createdSpace });
} catch (error) {
if (SavedObjectsClient.errors.isConflictError(error)) {
if (SavedObjectsErrorHelpers.isConflictError(error)) {
const { body } = wrapError(
Boom.conflict(`A space with the identifier ${space.id} already exists.`)
);

View file

@ -7,7 +7,6 @@
import * as Rx from 'rxjs';
import {
createSpaces,
createLegacyAPI,
createMockSavedObjectsRepository,
mockRouteContext,
mockRouteContextWithInvalidLicense,
@ -15,9 +14,9 @@ import {
import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server';
import {
loggingServiceMock,
elasticsearchServiceMock,
httpServiceMock,
httpServerMock,
coreMock,
} from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
@ -29,22 +28,21 @@ import { ObjectType } from '@kbn/config-schema';
describe('PUT /api/spaces/space', () => {
const spacesSavedObjects = createSpaces();
const spaces = spacesSavedObjects.map(s => ({ id: s.id, ...s.attributes }));
const setup = async () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const legacyAPI = createLegacyAPI({ spaces });
const coreStart = coreMock.createStart();
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const log = loggingServiceMock.create().get('spaces');
const service = new SpacesService(log, () => legacyAPI);
const service = new SpacesService(log);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),
@ -66,7 +64,8 @@ describe('PUT /api/spaces/space', () => {
initPutSpacesApi({
externalRouter: router,
getSavedObjects: () => legacyAPI.savedObjects,
getStartServices: async () => [coreStart, {}, {}],
getImportExportObjectLimit: () => 1000,
log,
spacesService,
});

View file

@ -5,6 +5,7 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server';
import { Space } from '../../../../common/model/space';
import { wrapError } from '../../../lib/errors';
import { spaceSchema } from '../../../lib/space_schema';
@ -12,7 +13,7 @@ import { ExternalRouteDeps } from '.';
import { createLicensedRouteHandler } from '../../lib';
export function initPutSpacesApi(deps: ExternalRouteDeps) {
const { externalRouter, spacesService, getSavedObjects } = deps;
const { externalRouter, spacesService } = deps;
externalRouter.put(
{
@ -25,7 +26,6 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) {
},
},
createLicensedRouteHandler(async (context, request, response) => {
const { SavedObjectsClient } = getSavedObjects();
const spacesClient = await spacesService.scopedClient(request);
const space = request.body;
@ -35,7 +35,7 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) {
try {
result = await spacesClient.update(id, { ...space });
} catch (error) {
if (SavedObjectsClient.errors.isNotFoundError(error)) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();
}
return response.customError(wrapError(error));

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as Rx from 'rxjs';
import { createLegacyAPI, mockRouteContextWithInvalidLicense } from '../__fixtures__';
import { mockRouteContextWithInvalidLicense } from '../__fixtures__';
import { CoreSetup, kibanaResponseFactory } from 'src/core/server';
import { httpServiceMock, httpServerMock, elasticsearchServiceMock } from 'src/core/server/mocks';
import { httpServiceMock, httpServerMock, coreMock } from 'src/core/server/mocks';
import { SpacesService } from '../../../spaces_service';
import { SpacesAuditLogger } from '../../../lib/audit_logger';
import { spacesConfig } from '../../../lib/__fixtures__';
@ -17,12 +17,12 @@ describe('GET /internal/spaces/_active_space', () => {
const httpService = httpServiceMock.createSetupContract();
const router = httpServiceMock.createRouter();
const legacyAPI = createLegacyAPI();
const coreStart = coreMock.createStart();
const service = new SpacesService(null as any, () => legacyAPI);
const service = new SpacesService(null as any);
const spacesService = await service.setup({
http: (httpService as unknown) as CoreSetup['http'],
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
authorization: null,
getSpacesAuditLogger: () => ({} as SpacesAuditLogger),
config$: Rx.of(spacesConfig),

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { migrateToKibana660 } from './migrate_6x';
export { SpacesSavedObjectsService } from './saved_objects_service';

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { deepFreeze } from '../../../../../src/core/utils';
export const SpacesSavedObjectMappings = deepFreeze({
properties: {
name: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 2048,
},
},
},
description: {
type: 'text',
},
initials: {
type: 'keyword',
},
color: {
type: 'keyword',
},
disabledFeatures: {
type: 'keyword',
},
imageUrl: {
type: 'text',
index: false,
},
_reserved: {
type: 'boolean',
},
},
});

View file

@ -5,16 +5,24 @@
*/
import { migrateToKibana660 } from './migrate_6x';
import { SavedObjectMigrationContext } from 'src/core/server';
const mockContext = {} as SavedObjectMigrationContext;
describe('migrateTo660', () => {
it('adds a "disabledFeatures" attribute initialized as an empty array', () => {
expect(
migrateToKibana660({
id: 'space:foo',
attributes: {},
})
migrateToKibana660(
{
id: 'space:foo',
type: 'space',
attributes: {},
},
mockContext
)
).toEqual({
id: 'space:foo',
type: 'space',
attributes: {
disabledFeatures: [],
},
@ -24,14 +32,19 @@ describe('migrateTo660', () => {
it('does not initialize "disabledFeatures" if the property already exists', () => {
// This scenario shouldn't happen organically. Protecting against defects in the migration.
expect(
migrateToKibana660({
id: 'space:foo',
attributes: {
disabledFeatures: ['foo', 'bar', 'baz'],
migrateToKibana660(
{
id: 'space:foo',
type: 'space',
attributes: {
disabledFeatures: ['foo', 'bar', 'baz'],
},
},
})
mockContext
)
).toEqual({
id: 'space:foo',
type: 'space',
attributes: {
disabledFeatures: ['foo', 'bar', 'baz'],
},

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function migrateToKibana660(doc: Record<string, any>) {
import { SavedObjectMigrationFn } from 'src/core/server';
export const migrateToKibana660: SavedObjectMigrationFn = doc => {
if (!doc.attributes.hasOwnProperty('disabledFeatures')) {
doc.attributes.disabledFeatures = [];
}
return doc;
}
};

View file

@ -4,19 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsClientWrapperFactory } from 'src/core/server';
import {
SavedObjectsClientWrapperFactory,
SavedObjectsClientWrapperOptions,
} from 'src/core/server';
import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
import { SpacesServiceSetup } from '../spaces_service/spaces_service';
export function spacesSavedObjectsClientWrapperFactory(
spacesService: SpacesServiceSetup,
types: string[]
spacesService: SpacesServiceSetup
): SavedObjectsClientWrapperFactory {
return ({ client, request }) =>
return (options: SavedObjectsClientWrapperOptions) =>
new SpacesSavedObjectsClient({
baseClient: client,
request,
baseClient: options.client,
request: options.request,
spacesService,
types,
typeRegistry: options.typeRegistry,
});
}

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { coreMock } from 'src/core/server/mocks';
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
import { SpacesSavedObjectsService } from './saved_objects_service';
describe('SpacesSavedObjectsService', () => {
describe('#setup', () => {
it('registers the "space" saved object type with appropriate mappings and migrations', () => {
const core = coreMock.createSetup();
const spacesService = spacesServiceMock.createSetupContract();
const service = new SpacesSavedObjectsService();
service.setup({ core, spacesService });
expect(core.savedObjects.registerType).toHaveBeenCalledTimes(1);
expect(core.savedObjects.registerType.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"hidden": true,
"mappings": Object {
"properties": Object {
"_reserved": Object {
"type": "boolean",
},
"color": Object {
"type": "keyword",
},
"description": Object {
"type": "text",
},
"disabledFeatures": Object {
"type": "keyword",
},
"imageUrl": Object {
"index": false,
"type": "text",
},
"initials": Object {
"type": "keyword",
},
"name": Object {
"fields": Object {
"keyword": Object {
"ignore_above": 2048,
"type": "keyword",
},
},
"type": "text",
},
},
},
"migrations": Object {
"6.6.0": [Function],
},
"name": "space",
"namespaceAgnostic": true,
},
]
`);
});
it('registers the client wrapper', () => {
const core = coreMock.createSetup();
const spacesService = spacesServiceMock.createSetupContract();
const service = new SpacesSavedObjectsService();
service.setup({ core, spacesService });
expect(core.savedObjects.addClientWrapper).toHaveBeenCalledTimes(1);
expect(core.savedObjects.addClientWrapper).toHaveBeenCalledWith(
Number.MIN_SAFE_INTEGER,
'spaces',
expect.any(Function)
);
});
});
});

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup } from 'src/core/server';
import { SpacesSavedObjectMappings } from './mappings';
import { migrateToKibana660 } from './migrations';
import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory';
import { SpacesServiceSetup } from '../spaces_service';
interface SetupDeps {
core: Pick<CoreSetup, 'savedObjects' | 'getStartServices'>;
spacesService: SpacesServiceSetup;
}
export class SpacesSavedObjectsService {
public setup({ core, spacesService }: SetupDeps) {
core.savedObjects.registerType({
name: 'space',
hidden: true,
namespaceAgnostic: true,
mappings: SpacesSavedObjectMappings,
migrations: {
'6.6.0': migrateToKibana660,
},
});
core.savedObjects.addClientWrapper(
Number.MIN_SAFE_INTEGER,
'spaces',
spacesSavedObjectsClientWrapperFactory(spacesService)
);
}
}

View file

@ -4,12 +4,33 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DEFAULT_SPACE_ID } from '../../../common/constants';
import { DEFAULT_SPACE_ID } from '../../common/constants';
import { SpacesSavedObjectsClient } from './spaces_saved_objects_client';
import { spacesServiceMock } from '../../spaces_service/spaces_service.mock';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { SavedObjectTypeRegistry } from 'src/core/server';
const types = ['foo', 'bar', 'space'];
const typeRegistry = new SavedObjectTypeRegistry();
typeRegistry.registerType({
name: 'foo',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
});
typeRegistry.registerType({
name: 'bar',
namespaceAgnostic: false,
hidden: false,
mappings: { properties: {} },
});
typeRegistry.registerType({
name: 'space',
namespaceAgnostic: true,
hidden: true,
mappings: { properties: {} },
});
const createMockRequest = () => ({});
@ -44,7 +65,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -63,7 +84,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const type = Symbol();
const id = Symbol();
@ -89,7 +110,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -110,7 +131,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const objects = [{ type: 'foo' }];
@ -136,7 +157,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -160,7 +181,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const options = Object.freeze({ type: 'foo' });
@ -189,7 +210,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const options = Object.freeze({ type: ['foo', 'bar'] });
@ -213,7 +234,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -232,7 +253,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const type = Symbol();
@ -259,7 +280,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -280,7 +301,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const objects = [{ type: 'foo' }];
@ -306,7 +327,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -326,7 +347,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const type = Symbol();
@ -358,7 +379,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const actualReturnValue = await client.bulkUpdate([
@ -390,7 +411,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
await expect(
@ -410,7 +431,7 @@ const createMockResponse = () => ({
request,
baseClient,
spacesService,
types,
typeRegistry,
});
const type = Symbol();

View file

@ -13,15 +13,16 @@ import {
SavedObjectsCreateOptions,
SavedObjectsFindOptions,
SavedObjectsUpdateOptions,
ISavedObjectTypeRegistry,
} from 'src/core/server';
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
import { spaceIdToNamespace } from '../utils/namespace';
import { SpacesServiceSetup } from '../spaces_service/spaces_service';
import { spaceIdToNamespace } from '../lib/utils/namespace';
interface SpacesSavedObjectsClientOptions {
baseClient: SavedObjectsClientContract;
request: any;
spacesService: SpacesServiceSetup;
types: string[];
typeRegistry: ISavedObjectTypeRegistry;
}
const coerceToArray = (param: string | string[]) => {
@ -45,11 +46,11 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract {
public readonly errors: SavedObjectsClientContract['errors'];
constructor(options: SpacesSavedObjectsClientOptions) {
const { baseClient, request, spacesService, types } = options;
const { baseClient, request, spacesService, typeRegistry } = options;
this.client = baseClient;
this.spaceId = spacesService.getSpaceId(request);
this.types = types;
this.types = typeRegistry.getAllTypes().map(t => t.name);
this.errors = baseClient.errors;
}

View file

@ -5,58 +5,53 @@
*/
import * as Rx from 'rxjs';
import { SpacesService } from './spaces_service';
import {
coreMock,
elasticsearchServiceMock,
httpServerMock,
loggingServiceMock,
} from 'src/core/server/mocks';
import { coreMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks';
import { SpacesAuditLogger } from '../lib/audit_logger';
import {
KibanaRequest,
SavedObjectsLegacyService,
SavedObjectsErrorHelpers,
HttpServiceSetup,
SavedObjectsRepository,
} from 'src/core/server';
import { DEFAULT_SPACE_ID } from '../../common/constants';
import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser';
import { LegacyAPI } from '../plugin';
import { spacesConfig } from '../lib/__fixtures__';
import { securityMock } from '../../../security/server/mocks';
const mockLogger = loggingServiceMock.createLogger();
const createService = async (serverBasePath: string = '') => {
const legacyAPI = {
savedObjects: ({
getSavedObjectsRepository: jest.fn().mockReturnValue({
get: jest.fn().mockImplementation((type, id) => {
if (type === 'space' && id === 'foo') {
return Promise.resolve({
id: 'space:foo',
attributes: {
name: 'Foo Space',
disabledFeatures: [],
},
});
}
if (type === 'space' && id === 'default') {
return Promise.resolve({
id: 'space:default',
attributes: {
name: 'Default Space',
disabledFeatures: [],
_reserved: true,
},
});
}
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}),
}),
} as unknown) as SavedObjectsLegacyService,
} as LegacyAPI;
const spacesService = new SpacesService(mockLogger);
const spacesService = new SpacesService(mockLogger, () => legacyAPI);
const coreStart = coreMock.createStart();
const respositoryMock = ({
get: jest.fn().mockImplementation((type, id) => {
if (type === 'space' && id === 'foo') {
return Promise.resolve({
id: 'space:foo',
attributes: {
name: 'Foo Space',
disabledFeatures: [],
},
});
}
if (type === 'space' && id === 'default') {
return Promise.resolve({
id: 'space:default',
attributes: {
name: 'Default Space',
disabledFeatures: [],
_reserved: true,
},
});
}
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}),
} as unknown) as SavedObjectsRepository;
coreStart.savedObjects.createInternalRepository.mockReturnValue(respositoryMock);
coreStart.savedObjects.createScopedRepository.mockReturnValue(respositoryMock);
const httpSetup = coreMock.createSetup().http;
httpSetup.basePath = {
@ -73,7 +68,7 @@ const createService = async (serverBasePath: string = '') => {
const spacesServiceSetup = await spacesService.setup({
http: httpSetup,
elasticsearch: elasticsearchServiceMock.createSetup(),
getStartServices: async () => [coreStart, {}, {}],
config$: Rx.of(spacesConfig),
authorization: securityMock.createSetup().authz,
getSpacesAuditLogger: () => new SpacesAuditLogger({}),

View file

@ -9,7 +9,6 @@ import { Observable, Subscription } from 'rxjs';
import { Legacy } from 'kibana';
import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server';
import { SecurityPluginSetup } from '../../../security/server';
import { LegacyAPI } from '../plugin';
import { SpacesClient } from '../lib/spaces_client';
import { ConfigType } from '../config';
import { getSpaceIdFromPath, addSpaceIdToPath } from '../../common/lib/spaces_url_parser';
@ -37,7 +36,7 @@ export interface SpacesServiceSetup {
interface SpacesServiceDeps {
http: CoreSetup['http'];
elasticsearch: CoreSetup['elasticsearch'];
getStartServices: CoreSetup['getStartServices'];
authorization: SecurityPluginSetup['authz'] | null;
config$: Observable<ConfigType>;
getSpacesAuditLogger(): any;
@ -46,11 +45,11 @@ interface SpacesServiceDeps {
export class SpacesService {
private configSubscription$?: Subscription;
constructor(private readonly log: Logger, private readonly getLegacyAPI: () => LegacyAPI) {}
constructor(private readonly log: Logger) {}
public async setup({
http,
elasticsearch,
getStartServices,
authorization,
config$,
getSpacesAuditLogger,
@ -69,18 +68,15 @@ export class SpacesService {
};
const getScopedClient = async (request: KibanaRequest) => {
const [coreStart] = await getStartServices();
return config$
.pipe(
map(config => {
const internalRepository = this.getLegacyAPI().savedObjects.getSavedObjectsRepository(
elasticsearch.adminClient.callAsInternalUser,
['space']
);
const internalRepository = coreStart.savedObjects.createInternalRepository(['space']);
const callCluster = elasticsearch.adminClient.asScoped(request).callAsCurrentUser;
const callWithRequestRepository = this.getLegacyAPI().savedObjects.getSavedObjectsRepository(
callCluster,
const callWithRequestRepository = coreStart.savedObjects.createScopedRepository(
request,
['space']
);

View file

@ -8,7 +8,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'endpoint']);
const pageObjects = getPageObjects(['common', 'endpoint', 'header']);
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
@ -18,6 +18,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
before(async () => {
await esArchiver.load('endpoint/metadata/api_feature');
await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts');
await pageObjects.header.waitUntilLoadingHasFinished();
});
it('finds title', async () => {
@ -114,6 +115,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// clear out the data and reload the page
await esArchiver.unload('endpoint/metadata/api_feature');
await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts');
await pageObjects.header.waitUntilLoadingHasFinished();
});
after(async () => {
// reload the data so the other tests continue to pass
@ -135,6 +137,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
'/hosts',
'selected_host=fc0ff548-feba-41b6-8367-65e8790d0eaf'
);
await pageObjects.header.waitUntilLoadingHasFinished();
});
it('shows a flyout', async () => {

View file

@ -9,11 +9,12 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
const PageObjects = getPageObjects(['common', 'header']);
const goToUptimeRoot = async () => {
await retry.tryForTime(30 * 1000, async () => {
await PageObjects.common.navigateToApp('uptime');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('uptimeOverviewPage', { timeout: 2000 });
});
};