[Spaces] Space solution property (#183986)

## Summary

Added solution property for the space.

- Forbidden in serverless.
- To facilitate iterative development made the property as optional in
stateful offering until all of the workstreams are complete.

### How to test API changes
```
# Should create space
POST kbn:/api/spaces/space 
{
  "name": "space without solution",
  "id": "my-space-solution-1",
  "description": "a description",
  "color": "#5c5959",
  "disabledFeatures": []
}

# Should fail with 400
POST kbn:/api/spaces/space 
{
  "name": "space with solution",
  "id": "my-space-solution-2",
  "description": "a description",
  "color": "#5c5959",
  "solution": "some_solution",
  "disabledFeatures": []
}

# Should fail with 400
POST kbn:/api/spaces/space 
{
  "name": "space with solution",
  "id": "my-space-solution-2",
  "description": "a description",
  "color": "#5c5959",
  "solution": null,
  "disabledFeatures": []
}

# Should create space
POST kbn:/api/spaces/space 
{
  "name": "space with solution",
  "id": "my-space-solution-2",
  "description": "a description",
  "color": "#5c5959",
  "solution": "search",
  "disabledFeatures": []
}

# Should get 'my-space-solution-1' space without solution field
GET kbn:/api/spaces/space/my-space-solution-1

# Should get 'my-space-solution-2' space with solution field
GET kbn:/api/spaces/space/my-space-solution-2 

# Should fail to update with 400
PUT kbn:/api/spaces/space/my-space-solution-1
{
  "id": "my-space-solution-1",
  "name": "my-space-solution-1 name",
  "solution": "some_solution"
}

# Should fail to update with 400
PUT kbn:/api/spaces/space/my-space-solution-1
{
  "id": "my-space-solution-1",
  "name": "my-space-solution-1 name",
  "solution": null
}

# Should update 'my-space-solution-1'
PUT kbn:/api/spaces/space/my-space-solution-1
{
  "id": "my-space-solution-1",
  "name": "my-space-solution-1 name",
  "solution": "security"
}

# Should get 'my-space-solution-1' space wit solution field set to 'security'
GET kbn:/api/spaces/space/my-space-solution-1

# Should return list where 
# 1. 'my-space-solution-1' has solution 'security'
# 2. 'my-space-solution-2' has solution 'search'
# 3. Other spaces don't have solution field present
GET kbn:/api/spaces/space
```


### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed ([Security and Spaces
config](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6076),
[Spaces only
config](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6075))

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

__Fixes: https://github.com/elastic/kibana/issues/183559__

## Release note
Added optional solution property for Space in a stateful offering.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
elena-shostak 2024-05-27 12:25:44 +02:00 committed by GitHub
parent 982dfa3551
commit afb3d37469
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 605 additions and 89 deletions

View file

@ -31,6 +31,7 @@ The API returns the following:
"color": "#aabbcc",
"initials": "MK",
"disabledFeatures": [],
"imageUrl": ""
"imageUrl": "",
"solution": "search"
}
--------------------------------------------------

View file

@ -71,7 +71,8 @@ The API returns the following:
"name": "Sales",
"initials": "MK",
"disabledFeatures": ["discover"],
"imageUrl": ""
"imageUrl": "",
"solution": "observability"
}
]
--------------------------------------------------

View file

@ -36,6 +36,9 @@ experimental[] Create a {kib} space.
(Optional, string) The data-URL encoded image to display in the space avatar. If specified, `initials` will not be displayed, and the `color` will be visible as the background color for transparent images.
For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.
`solution`::
(Optional, string) The solution defined for the space. Can be one of `security`, `observability`, `search`, `classic`
[[spaces-api-post-response-codes]]
==== Response codes

View file

@ -36,6 +36,9 @@ experimental[] Update an existing {kib} space.
(Optional, string) Specifies the data-url encoded image to display in the space avatar. If specified, `initials` will not be displayed, and the `color` will be visible as the background color for transparent images.
For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.
`solution`::
(Optional, string) The solution defined for the space. Can be one of `security`, `observability`, `search`, `classic`.
[[spaces-api-put-response-codes]]
==== Response codes

View file

@ -931,7 +931,8 @@
],
"slo-settings": [],
"space": [
"name"
"name",
"solution"
],
"spaces-usage-stats": [],
"synthetics-monitor": [

View file

@ -3062,6 +3062,9 @@
}
},
"type": "text"
},
"solution": {
"type": "keyword"
}
}
},

View file

@ -149,7 +149,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"siem-ui-timeline-pinned-event": "082daa3ce647b33873f6abccf340bdfa32057c8d",
"slo": "9a9995e4572de1839651c43b5fc4dc8276bb5815",
"slo-settings": "f6b5ed339470a6a2cda272bde1750adcf504a11b",
"space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e",
"space": "d38fa4bc669b9b1d6ec86aac2983d4c6675723ed",
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
"synthetics-monitor": "5ceb25b6249bd26902c9b34273c71c3dce06dbea",
"synthetics-param": "3ebb744e5571de678b1312d5c418c8188002cf5e",

View file

@ -15,6 +15,7 @@ import { Env } from '@kbn/config';
import { getEnvOptions } from '@kbn/config-mocks';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { SavedObjectsRawDoc } from '@kbn/core-saved-objects-server';
import { modelVersionToVirtualVersion } from '@kbn/core-saved-objects-base-server-internal';
import {
createTestServers,
createRootWithCorePlugins,
@ -125,7 +126,7 @@ describe('migrating from 7.3.0-xpack which used v1 migrations', () => {
.getTypeRegistry()
.getAllTypes()
.reduce((versionMap, type) => {
const { name, migrations, convertToMultiNamespaceTypeVersion } = type;
const { name, migrations, convertToMultiNamespaceTypeVersion, modelVersions } = type;
if (migrations || convertToMultiNamespaceTypeVersion) {
const migrationsMap = typeof migrations === 'function' ? migrations() : migrations;
const migrationsKeys = migrationsMap ? Object.keys(migrationsMap) : [];
@ -133,6 +134,16 @@ describe('migrating from 7.3.0-xpack which used v1 migrations', () => {
// Setting this option registers a conversion migration that is reflected in the object's `typeMigrationVersions` field
migrationsKeys.push(convertToMultiNamespaceTypeVersion);
}
const modelVersionCreateSchemas =
typeof modelVersions === 'function' ? modelVersions() : modelVersions ?? {};
Object.entries(modelVersionCreateSchemas).forEach(([key, modelVersion]) => {
if (modelVersion.schemas?.create) {
migrationsKeys.push(modelVersionToVirtualVersion(key));
}
});
const highestVersion = migrationsKeys.sort(Semver.compare).reverse()[0];
return {
...versionMap,

View file

@ -58,6 +58,11 @@ export interface Space {
* @private
*/
_reserved?: boolean;
/**
* Solution selected for this space.
*/
solution?: 'security' | 'observability' | 'search' | 'classic';
}
/**

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { spaceSchema } from './space_schema';
import { getSpaceSchema } from './space_schema';
// non-serverless space schema
const spaceBaseSchema = getSpaceSchema(false);
const spaceServerlessSchema = getSpaceSchema(true);
const defaultProperties = {
id: 'foo',
@ -15,7 +19,7 @@ const defaultProperties = {
describe('#id', () => {
test('is required', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
id: undefined,
})
@ -26,7 +30,7 @@ describe('#id', () => {
test('allows lowercase a-z, 0-9, "_" and "-"', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
id: 'abcdefghijklmnopqrstuvwxyz0123456789_-',
})
@ -35,7 +39,7 @@ describe('#id', () => {
test(`doesn't allow uppercase`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
id: 'Foo',
})
@ -46,7 +50,7 @@ describe('#id', () => {
test(`doesn't allow an empty string`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
id: '',
})
@ -59,7 +63,7 @@ describe('#id', () => {
(invalidCharacter) => {
test(`doesn't allow ${invalidCharacter}`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
id: `foo-${invalidCharacter}`,
})
@ -72,7 +76,7 @@ describe('#id', () => {
describe('#disabledFeatures', () => {
test('is optional', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
disabledFeatures: undefined,
})
@ -80,7 +84,7 @@ describe('#disabledFeatures', () => {
});
test('defaults to an empty array', () => {
const result = spaceSchema.validate({
const result = spaceBaseSchema.validate({
...defaultProperties,
disabledFeatures: undefined,
});
@ -89,7 +93,7 @@ describe('#disabledFeatures', () => {
test('must be an array if provided', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
disabledFeatures: 'foo',
})
@ -100,7 +104,7 @@ describe('#disabledFeatures', () => {
test('allows an array of strings', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
disabledFeatures: ['foo', 'bar'],
})
@ -109,7 +113,7 @@ describe('#disabledFeatures', () => {
test('does not allow an array containing non-string elements', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
disabledFeatures: ['foo', true],
})
@ -122,7 +126,7 @@ describe('#disabledFeatures', () => {
describe('#color', () => {
test('is optional', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: undefined,
})
@ -131,7 +135,7 @@ describe('#color', () => {
test(`doesn't allow an empty string`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '',
})
@ -142,7 +146,7 @@ describe('#color', () => {
test(`allows lower case hex color code`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '#aabbcc',
})
@ -151,7 +155,7 @@ describe('#color', () => {
test(`allows upper case hex color code`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '#AABBCC',
})
@ -160,7 +164,7 @@ describe('#color', () => {
test(`allows numeric hex color code`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '#123456',
})
@ -169,7 +173,7 @@ describe('#color', () => {
test(`must start with a hash`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '123456',
})
@ -180,7 +184,7 @@ describe('#color', () => {
test(`cannot exceed 6 digits following the hash`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '1234567',
})
@ -191,7 +195,7 @@ describe('#color', () => {
test(`cannot be fewer than 6 digits following the hash`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
color: '12345',
})
@ -204,7 +208,7 @@ describe('#color', () => {
describe('#imageUrl', () => {
test('is optional', () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
imageUrl: undefined,
})
@ -213,7 +217,7 @@ describe('#imageUrl', () => {
test(`must start with data:image`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
imageUrl: 'notValid',
})
@ -222,7 +226,7 @@ describe('#imageUrl', () => {
test(`checking that a valid image is accepted as imageUrl`, () => {
expect(() =>
spaceSchema.validate({
spaceBaseSchema.validate({
...defaultProperties,
imageUrl:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAAB3klEQVRYR+2WzUrDQBCARzwqehE8ir1WPfgqRRA1bePBXgpe/MGCB9/Aiw+j+ASCB6kotklaEwW1F0WwNSaps9lV69awGzBpDzt8pJP9mXxsmk3ABH2oUEIilJAIJSRCCYlQQiKUkIh4QgY5agZodVjBowFrBktWQzDBU2ykiYaDuQpCYgnl3QunGzM6Z6YF+b5SkcgK1UH/aLbYReQiYL9d9/o+XFop5IU0Vl4uapAzoXC3eEBPw9vH1/wT6Vs2otPSkoH/IZzlzO/TU2vgQm8nl69Hp0H7nZ4OXogLJSSKBIUC3w88n+Ueyfv56fVZnqCQNVnCHbLrkV0Gd2d+GNkglsk438dhaTxloZDutV4wb06Vf40JcWZ2sMttPpE8NaHGeBnzIAhwPXqHseVB11EyLD0hxLUeaYud2a3B0g3k7GyFtrhX7F2RqhC+yV3jgTb2Rqdqf7/kUxYiWBOlTtXxfPJEtc8b5thGb+8AhL4ohnCNqQjZ2T2+K5rnw2M6KwEhKNDSGM3pTdxjhDgLbHkw/v/zw4AiPuSsfMzAiTidKxiF/ArpFqyzK8SMOlkwvloUMYRCtNvZLWeuIomd2Za/WZS4QomjhEQoIRFKSIQSEqGERAyfEH4YDBFQ/ARU6BiBxCAIQQAAAABJRU5ErkJggg==',
@ -230,3 +234,32 @@ describe('#imageUrl', () => {
).not.toThrowError();
});
});
describe('#solution', () => {
it('should throw error if solution is defined in serverless offering', () => {
expect(() =>
spaceServerlessSchema.validate({ ...defaultProperties, solution: 'search' })
).toThrow();
});
it('should not throw error if solution is undefined in classic offering', () => {
expect(() =>
spaceBaseSchema.validate({ ...defaultProperties, solution: undefined }, {})
).not.toThrow();
});
it('should throw error if solution is invalid in classic offering', () => {
expect(() => spaceBaseSchema.validate({ ...defaultProperties, solution: 'some_value' }, {}))
.toThrowErrorMatchingInlineSnapshot(`
"[solution]: types that failed validation:
- [solution.0]: expected value to equal [security]
- [solution.1]: expected value to equal [observability]
- [solution.2]: expected value to equal [search]
- [solution.3]: expected value to equal [classic]"
`);
expect(() =>
spaceBaseSchema.validate({ ...defaultProperties, solution: ' search ' }, {})
).toThrow();
});
});

View file

@ -11,7 +11,7 @@ import { MAX_SPACE_INITIALS } from '../../common';
export const SPACE_ID_REGEX = /^[a-z0-9_\-]+$/;
export const spaceSchema = schema.object({
const spaceSchema = schema.object({
id: schema.string({
validate: (value) => {
if (!SPACE_ID_REGEX.test(value)) {
@ -43,3 +43,18 @@ export const spaceSchema = schema.object({
})
),
});
const solutionSchema = schema.oneOf([
schema.literal('security'),
schema.literal('observability'),
schema.literal('search'),
schema.literal('classic'),
]);
export const getSpaceSchema = (isServerless: boolean) => {
if (isServerless) {
return spaceSchema;
}
return spaceSchema.extends({ solution: schema.maybe(solutionSchema) });
};

View file

@ -125,7 +125,10 @@ export class SpacesPlugin
this.hasOnlyDefaultSpace$ = this.config$.pipe(map(({ maxSpaces }) => maxSpaces === 1));
this.log = initializerContext.logger.get();
this.spacesService = new SpacesService();
this.spacesClientService = new SpacesClientService((message) => this.log.debug(message));
this.spacesClientService = new SpacesClientService(
(message) => this.log.debug(message),
initializerContext.env.packageInfo.buildFlavor
);
}
public setup(core: CoreSetup<PluginsStart>, plugins: PluginsSetup): SpacesPluginSetup {
@ -168,16 +171,14 @@ export class SpacesPlugin
const router = core.http.createRouter<SpacesRequestHandlerContext>();
initExternalSpacesApi(
{
router,
log: this.log,
getStartServices: core.getStartServices,
getSpacesService,
usageStatsServicePromise,
},
this.initializerContext.env.packageInfo.buildFlavor
);
initExternalSpacesApi({
router,
log: this.log,
getStartServices: core.getStartServices,
getSpacesService,
usageStatsServicePromise,
isServerless: this.initializerContext.env.packageInfo.buildFlavor === 'serverless',
});
initInternalSpacesApi({
router,

View file

@ -57,7 +57,7 @@ describe('copy to space', () => {
createResolveSavedObjectsImportErrorsMock()
);
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -85,6 +85,7 @@ describe('copy to space', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [[ctsRouteDefinition, ctsRouteHandler], [resolveRouteDefinition, resolveRouteHandler]] =

View file

@ -42,7 +42,7 @@ describe('Spaces Public API', () => {
const coreStart = coreMock.createStart();
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -67,6 +67,7 @@ describe('Spaces Public API', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [routeDefinition, routeHandler] = router.delete.mock.calls[0];

View file

@ -42,7 +42,7 @@ describe('_disable_legacy_url_aliases', () => {
const log = loggingSystemMock.create().get('spaces');
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -70,6 +70,7 @@ describe('_disable_legacy_url_aliases', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [routeDefinition, routeHandler] = router.post.mock.calls[0];

View file

@ -41,7 +41,7 @@ describe('GET space', () => {
const log = loggingSystemMock.create().get('spaces');
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -66,6 +66,7 @@ describe('GET space', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
return {

View file

@ -30,7 +30,9 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) {
try {
const space = await spacesClient.get(spaceId);
return response.ok({ body: space });
return response.ok({
body: space,
});
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound();

View file

@ -43,7 +43,7 @@ describe('GET /spaces/space', () => {
const log = loggingSystemMock.create().get('spaces');
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -68,6 +68,7 @@ describe('GET /spaces/space', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
return {

View file

@ -42,7 +42,7 @@ describe('get shareable references', () => {
const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces);
coreStart.savedObjects = savedObjects;
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -66,6 +66,7 @@ describe('get shareable references', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [[getShareableReferences, getShareableReferencesRouteHandler]] = router.post.mock.calls;

View file

@ -4,8 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { BuildFlavor } from '@kbn/config/src/types';
import type { CoreSetup, Logger } from '@kbn/core/server';
import { initCopyToSpacesApi } from './copy_to_space';
@ -27,9 +25,10 @@ export interface ExternalRouteDeps {
getSpacesService: () => SpacesServiceStart;
usageStatsServicePromise: Promise<UsageStatsServiceSetup>;
log: Logger;
isServerless: boolean;
}
export function initExternalSpacesApi(deps: ExternalRouteDeps, buildFlavor: BuildFlavor) {
export function initExternalSpacesApi(deps: ExternalRouteDeps) {
// These two routes are always registered, internal in serverless by default
initGetSpaceApi(deps);
initGetAllSpacesApi(deps);
@ -37,7 +36,7 @@ export function initExternalSpacesApi(deps: ExternalRouteDeps, buildFlavor: Buil
// In the serverless environment, Spaces are enabled but are effectively hidden from the user. We
// do not support more than 1 space: the default space. These HTTP APIs for creating, deleting,
// updating, and manipulating saved objects across multiple spaces are not needed.
if (buildFlavor !== 'serverless') {
if (!deps.isServerless) {
initPutSpacesApi(deps);
initDeleteSpacesApi(deps);
initPostSpacesApi(deps);

View file

@ -42,7 +42,7 @@ describe('Spaces Public API', () => {
const log = loggingSystemMock.create().get('spaces');
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -67,6 +67,7 @@ describe('Spaces Public API', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [routeDefinition, routeHandler] = router.post.mock.calls[0];

View file

@ -11,17 +11,17 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { ExternalRouteDeps } from '.';
import { wrapError } from '../../../lib/errors';
import { spaceSchema } from '../../../lib/space_schema';
import { getSpaceSchema } from '../../../lib/space_schema';
import { createLicensedRouteHandler } from '../../lib';
export function initPostSpacesApi(deps: ExternalRouteDeps) {
const { router, log, getSpacesService } = deps;
const { router, log, getSpacesService, isServerless } = deps;
router.post(
{
path: '/api/spaces/space',
validate: {
body: spaceSchema,
body: getSpaceSchema(isServerless),
},
},
createLicensedRouteHandler(async (context, request, response) => {

View file

@ -42,7 +42,7 @@ describe('PUT /api/spaces/space', () => {
const log = loggingSystemMock.create().get('spaces');
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -67,6 +67,7 @@ describe('PUT /api/spaces/space', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [routeDefinition, routeHandler] = router.put.mock.calls[0];

View file

@ -11,11 +11,11 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { ExternalRouteDeps } from '.';
import type { Space } from '../../../../common';
import { wrapError } from '../../../lib/errors';
import { spaceSchema } from '../../../lib/space_schema';
import { getSpaceSchema } from '../../../lib/space_schema';
import { createLicensedRouteHandler } from '../../lib';
export function initPutSpacesApi(deps: ExternalRouteDeps) {
const { router, getSpacesService } = deps;
const { router, getSpacesService, isServerless } = deps;
router.put(
{
@ -24,7 +24,7 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) {
params: schema.object({
id: schema.string(),
}),
body: spaceSchema,
body: getSpaceSchema(isServerless),
},
},
createLicensedRouteHandler(async (context, request, response) => {

View file

@ -43,7 +43,7 @@ describe('update_objects_spaces', () => {
const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces);
coreStart.savedObjects = savedObjects;
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);
@ -67,6 +67,7 @@ describe('update_objects_spaces', () => {
log,
getSpacesService: () => spacesServiceStart,
usageStatsServicePromise,
isServerless: false,
});
const [[updateObjectsSpaces, updateObjectsSpacesRouteHandler]] = router.post.mock.calls;

View file

@ -45,7 +45,7 @@ describe('GET /internal/spaces/{spaceId}/content_summary', () => {
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects);
const clientService = new SpacesClientService(jest.fn());
const clientService = new SpacesClientService(jest.fn(), 'traditional');
clientService
.setup({ config$: Rx.of(spacesConfig) })
.setClientRepositoryFactory(() => savedObjectsRepositoryMock);

View file

@ -19,6 +19,9 @@ export const SpacesSavedObjectMappings = deepFreeze({
},
},
},
solution: {
type: 'keyword',
},
},
} as const);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import type { CoreSetup } from '@kbn/core/server';
import { SpacesSavedObjectMappings, UsageStatsMappings } from './mappings';
@ -30,6 +31,30 @@ export class SpacesSavedObjectsService {
migrations: {
'6.6.0': spaceMigrations.migrateTo660,
},
modelVersions: {
1: {
changes: [
{
type: 'mappings_addition',
addedMappings: {
solution: { type: 'keyword' },
},
},
],
schemas: {
create: SpacesSavedObjectSchemas['8.8.0'].extends({
solution: schema.maybe(
schema.oneOf([
schema.literal('security'),
schema.literal('observability'),
schema.literal('search'),
schema.literal('classic'),
])
),
}),
},
},
},
});
core.savedObjects.registerType({

View file

@ -42,6 +42,7 @@ describe('#getAll', () => {
imageUrl: 'go-bots/predates/transformers',
disabledFeatures: [],
_reserved: true,
solution: 'search',
bar: 'foo-bar', // an extra attribute that will be ignored during conversion
},
},
@ -80,6 +81,7 @@ describe('#getAll', () => {
initials: 'FB',
imageUrl: 'go-bots/predates/transformers',
disabledFeatures: [],
solution: 'search',
_reserved: true,
},
{
@ -105,7 +107,13 @@ describe('#getAll', () => {
} as any);
const mockConfig = createMockConfig();
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const actualSpaces = await client.getAll();
expect(actualSpaces).toEqual(expectedSpaces);
@ -117,11 +125,46 @@ describe('#getAll', () => {
});
});
test('strips solution property in serverless build', async () => {
const mockDebugLogger = createMockDebugLogger();
const [SOWithSolution] = savedObjects;
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.find.mockResolvedValue({
saved_objects: [SOWithSolution],
} as any);
const mockConfig = createMockConfig();
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'serverless'
);
const [actualSpace] = await client.getAll();
const [{ solution, ...expectedSpace }] = expectedSpaces;
expect(actualSpace.solution).toBeUndefined();
expect(actualSpace).toEqual(expectedSpace);
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
type: 'space',
page: 1,
perPage: mockConfig.maxSpaces,
sortField: 'name.keyword',
});
});
test(`throws Boom.badRequest when an invalid purpose is provided'`, async () => {
const mockDebugLogger = createMockDebugLogger();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
const mockConfig = createMockConfig();
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
await expect(
client.getAll({ purpose: 'invalid_purpose' as GetAllSpacesPurpose })
).rejects.toThrowErrorMatchingInlineSnapshot(`"unsupported space purpose: invalid_purpose"`);
@ -162,13 +205,64 @@ describe('#get', () => {
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const id = savedObject.id;
const actualSpace = await client.get(id);
expect(actualSpace).toEqual(expectedSpace);
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
});
test('strips solution property in serverless build', async () => {
const mockDebugLogger = createMockDebugLogger();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue({
...savedObject,
attributes: { ...(savedObject.attributes as Record<string, unknown>), solution: 'search' },
});
const mockConfig = createMockConfig();
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'serverless'
);
const id = savedObject.id;
const actualSpace = await client.get(id);
expect(actualSpace.solution).toBeUndefined();
expect(actualSpace).toEqual(expectedSpace);
});
test(`doesn't strip solution property in traditional build`, async () => {
const mockDebugLogger = createMockDebugLogger();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue({
...savedObject,
attributes: { ...(savedObject.attributes as Record<string, unknown>), solution: 'search' },
});
const mockConfig = createMockConfig();
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const id = savedObject.id;
const actualSpace = await client.get(id);
expect(actualSpace).toEqual({ ...expectedSpace, solution: 'search' });
});
});
describe('#create', () => {
@ -219,7 +313,13 @@ describe('#create', () => {
allowFeatureVisibility: true,
});
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const actualSpace = await client.create(spaceToCreate);
@ -249,7 +349,13 @@ describe('#create', () => {
allowFeatureVisibility: true,
});
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
await expect(client.create(spaceToCreate)).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting"`
@ -263,6 +369,93 @@ describe('#create', () => {
expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled();
});
test('throws bad request when solution property is provided in serverless build', async () => {
const maxSpaces = 5;
const mockDebugLogger = createMockDebugLogger();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.create.mockResolvedValue(savedObject);
mockCallWithRequestRepository.find.mockResolvedValue({
total: maxSpaces - 1,
} as any);
const mockConfig = createMockConfig({
enabled: true,
maxSpaces,
allowFeatureVisibility: true,
});
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'serverless'
);
await expect(
client.create({ ...spaceToCreate, solution: undefined })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to create Space, solution property is forbidden in serverless"`
);
await expect(
client.create({ ...spaceToCreate, solution: 'search' })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to create Space, solution property is forbidden in serverless"`
);
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
type: 'space',
page: 1,
perPage: 0,
});
expect(mockCallWithRequestRepository.create).not.toHaveBeenCalled();
});
test('creates space when solution property is provided in traditional build', async () => {
const maxSpaces = 5;
const mockDebugLogger = createMockDebugLogger();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.create.mockResolvedValue({
...savedObject,
attributes: { ...(savedObject.attributes as Record<string, unknown>), solution: 'search' },
});
mockCallWithRequestRepository.find.mockResolvedValue({
total: maxSpaces - 1,
} as any);
const mockConfig = createMockConfig({
enabled: true,
maxSpaces,
allowFeatureVisibility: true,
});
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const actualSpace = await client.create({ ...spaceToCreate, solution: 'search' });
expect(actualSpace).toEqual({ ...expectedReturnedSpace, solution: 'search' });
expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({
type: 'space',
page: 1,
perPage: 0,
});
expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith(
'space',
{ ...attributes, solution: 'search' },
{
id,
}
);
});
describe('when config.allowFeatureVisibility is disabled', () => {
test(`creates space without disabledFeatures`, async () => {
const maxSpaces = 5;
@ -283,7 +476,8 @@ describe('#create', () => {
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[]
[],
'traditional'
);
const actualSpace = await client.create(spaceToCreate);
@ -318,7 +512,8 @@ describe('#create', () => {
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[]
[],
'traditional'
);
await expect(
@ -377,7 +572,13 @@ describe('#update', () => {
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const id = savedObject.id;
const actualSpace = await client.update(id, spaceToUpdate);
@ -386,6 +587,87 @@ describe('#update', () => {
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
});
test('throws bad request when solution property is provided in serverless build', async () => {
const mockDebugLogger = createMockDebugLogger();
const mockConfig = createMockConfig();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'serverless'
);
const id = savedObject.id;
await expect(
client.update(id, { ...spaceToUpdate, solution: undefined })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to update Space, solution property is forbidden in serverless"`
);
await expect(
client.update(id, { ...spaceToUpdate, solution: 'search' })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to update Space, solution property is forbidden in serverless"`
);
expect(mockCallWithRequestRepository.update).not.toHaveBeenCalled();
expect(mockCallWithRequestRepository.get).not.toHaveBeenCalled();
});
test('throws bad request when solution property is undefined in traditional build', async () => {
const mockDebugLogger = createMockDebugLogger();
const mockConfig = createMockConfig();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const id = savedObject.id;
await expect(
client.update(id, { ...spaceToUpdate, solution: undefined })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to update Space, solution property cannot be empty"`
);
expect(mockCallWithRequestRepository.update).not.toHaveBeenCalled();
expect(mockCallWithRequestRepository.get).not.toHaveBeenCalled();
});
test('updates space with solution property using callWithRequestRepository in traditional build', async () => {
const mockDebugLogger = createMockDebugLogger();
const mockConfig = createMockConfig();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(savedObject);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const id = savedObject.id;
await client.update(id, { ...spaceToUpdate, solution: 'search' });
expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, {
...attributes,
solution: 'search',
});
expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id);
});
describe('when config.allowFeatureVisibility is disabled', () => {
test(`updates space without disabledFeatures`, async () => {
const mockDebugLogger = createMockDebugLogger();
@ -401,7 +683,8 @@ describe('#update', () => {
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[]
[],
'traditional'
);
const id = savedObject.id;
const actualSpace = await client.update(id, spaceToUpdate);
@ -425,7 +708,8 @@ describe('#update', () => {
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[]
[],
'traditional'
);
const id = savedObject.id;
@ -473,7 +757,13 @@ describe('#delete', () => {
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(reservedSavedObject);
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
await expect(client.delete(id)).rejects.toThrowErrorMatchingInlineSnapshot(
`"The foo space cannot be deleted because it is reserved."`
@ -488,7 +778,13 @@ describe('#delete', () => {
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
mockCallWithRequestRepository.get.mockResolvedValue(notReservedSavedObject);
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
await client.delete(id);
@ -504,7 +800,13 @@ describe('#disableLegacyUrlAliases', () => {
const mockConfig = createMockConfig();
const mockCallWithRequestRepository = savedObjectsRepositoryMock.create();
const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []);
const client = new SpacesClient(
mockDebugLogger,
mockConfig,
mockCallWithRequestRepository,
[],
'traditional'
);
const aliases = [
{ targetSpace: 'space1', targetType: 'foo', sourceId: '123' },
{ targetSpace: 'space2', targetType: 'bar', sourceId: '456' },

View file

@ -7,6 +7,7 @@
import Boom from '@hapi/boom';
import type { BuildFlavor } from '@kbn/config/src/types';
import type {
ISavedObjectsPointInTimeFinder,
ISavedObjectsRepository,
@ -80,12 +81,17 @@ export interface ISpacesClient {
* Client for interacting with spaces.
*/
export class SpacesClient implements ISpacesClient {
private isServerless = false;
constructor(
private readonly debugLogger: (message: string) => void,
private readonly config: ConfigType,
private readonly repository: ISavedObjectsRepository,
private readonly nonGlobalTypeNames: string[]
) {}
private readonly nonGlobalTypeNames: string[],
private readonly buildFlavour: BuildFlavor
) {
this.isServerless = this.buildFlavour === 'serverless';
}
public async getAll(options: v1.GetAllSpacesOptions = {}): Promise<v1.GetSpaceResult[]> {
const { purpose = DEFAULT_PURPOSE } = options;
@ -130,10 +136,19 @@ export class SpacesClient implements ISpacesClient {
);
}
if (this.isServerless && space.hasOwnProperty('solution')) {
throw Boom.badRequest('Unable to create Space, solution property is forbidden in serverless');
}
if (space.hasOwnProperty('solution') && !space.solution) {
throw Boom.badRequest('Unable to create Space, solution property cannot be empty');
}
this.debugLogger(`SpacesClient.create(), using RBAC. Attempting to create space`);
const id = space.id;
const attributes = this.generateSpaceAttributes(space);
const createdSavedObject = await this.repository.create('space', attributes, { id });
this.debugLogger(`SpacesClient.create(), created space object`);
@ -148,6 +163,14 @@ export class SpacesClient implements ISpacesClient {
);
}
if (this.isServerless && space.hasOwnProperty('solution')) {
throw Boom.badRequest('Unable to update Space, solution property is forbidden in serverless');
}
if (space.hasOwnProperty('solution') && !space.solution) {
throw Boom.badRequest('Unable to update Space, solution property cannot be empty');
}
const attributes = this.generateSpaceAttributes(space);
await this.repository.update('space', id, attributes);
const updatedSavedObject = await this.repository.get('space', id);
@ -181,7 +204,7 @@ export class SpacesClient implements ISpacesClient {
await this.repository.bulkUpdate(objectsToUpdate);
}
private transformSavedObjectToSpace(savedObject: SavedObject<any>): v1.Space {
private transformSavedObjectToSpace = (savedObject: SavedObject<any>): v1.Space => {
return {
id: savedObject.id,
name: savedObject.attributes.name ?? '',
@ -191,10 +214,11 @@ export class SpacesClient implements ISpacesClient {
imageUrl: savedObject.attributes.imageUrl,
disabledFeatures: savedObject.attributes.disabledFeatures ?? [],
_reserved: savedObject.attributes._reserved,
...(!this.isServerless ? { solution: savedObject.attributes.solution } : {}),
} as v1.Space;
}
};
private generateSpaceAttributes(space: v1.Space) {
private generateSpaceAttributes = (space: v1.Space) => {
return {
name: space.name,
description: space.description,
@ -202,6 +226,7 @@ export class SpacesClient implements ISpacesClient {
initials: space.initials,
imageUrl: space.imageUrl,
disabledFeatures: space.disabledFeatures,
...(!this.isServerless && space.solution ? { solution: space.solution } : {}),
};
}
};
}

View file

@ -20,7 +20,7 @@ const debugLogger = jest.fn();
describe('SpacesClientService', () => {
describe('#setup', () => {
it('allows a single repository factory to be set', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
const setup = service.setup({ config$: Rx.of(spacesConfig) });
const repositoryFactory = jest.fn();
@ -32,7 +32,7 @@ describe('SpacesClientService', () => {
});
it('allows a single client wrapper to be set', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
const setup = service.setup({ config$: Rx.of(spacesConfig) });
const clientWrapper = jest.fn();
@ -46,7 +46,7 @@ describe('SpacesClientService', () => {
describe('#start', () => {
it('throws if config is not available', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
service.setup({ config$: new Rx.Observable<ConfigType>() });
const coreStart = coreMock.createStart();
const start = service.start(coreStart);
@ -60,7 +60,7 @@ describe('SpacesClientService', () => {
describe('without a custom repository factory or wrapper', () => {
it('returns an instance of the spaces client using the scoped repository', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
service.setup({ config$: Rx.of(spacesConfig) });
const coreStart = coreMock.createStart();
@ -78,7 +78,7 @@ describe('SpacesClientService', () => {
});
it('uses the custom repository factory when set', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
const setup = service.setup({ config$: Rx.of(spacesConfig) });
const customRepositoryFactory = jest.fn();
@ -98,7 +98,7 @@ describe('SpacesClientService', () => {
});
it('wraps the client in the wrapper when registered', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
const setup = service.setup({ config$: Rx.of(spacesConfig) });
const wrapper = Symbol() as unknown as ISpacesClient;
@ -123,7 +123,7 @@ describe('SpacesClientService', () => {
});
it('wraps the client in the wrapper when registered, using the custom repository factory when configured', () => {
const service = new SpacesClientService(debugLogger);
const service = new SpacesClientService(debugLogger, 'traditional');
const setup = service.setup({ config$: Rx.of(spacesConfig) });
const customRepositoryFactory = jest.fn();

View file

@ -7,6 +7,7 @@
import type { Observable } from 'rxjs';
import type { BuildFlavor } from '@kbn/config/src/types';
import type {
CoreStart,
ISavedObjectsRepository,
@ -72,7 +73,10 @@ export class SpacesClientService {
private clientWrapper?: SpacesClientWrapper;
constructor(private readonly debugLogger: (message: string) => void) {}
constructor(
private readonly debugLogger: (message: string) => void,
private readonly buildFlavour: BuildFlavor
) {}
public setup({ config$ }: SetupDeps): SpacesClientServiceSetup {
config$.subscribe((nextConfig) => {
@ -117,7 +121,8 @@ export class SpacesClientService {
this.debugLogger,
this.config,
this.repositoryFactory!(request, coreStart.savedObjects),
nonGlobalTypeNames
nonGlobalTypeNames,
this.buildFlavour
);
if (this.clientWrapper) {
return this.clientWrapper(request, baseClient);

View file

@ -69,7 +69,7 @@ const createService = (serverBasePath: string = '') => {
basePath: httpSetup.basePath,
});
const spacesClientService = new SpacesClientService(jest.fn());
const spacesClientService = new SpacesClientService(jest.fn(), 'traditional');
spacesClientService.setup({
config$: Rx.of(spacesConfig),
});

View file

@ -19,6 +19,7 @@ interface CreateTests {
newSpace: CreateTest;
alreadyExists: CreateTest;
reservedSpecified: CreateTest;
solutionSpecified: CreateTest;
}
interface CreateTestDefinition {
@ -63,6 +64,17 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
});
};
const expectSolutionSpecifiedResult = (resp: Record<string, any>) => {
expect(resp.body).to.eql({
id: 'solution',
name: 'space with solution',
description: 'a description',
color: '#5c5959',
disabledFeatures: [],
solution: 'search',
});
};
const makeCreateTest =
(describeFn: DescribeFn) =>
(description: string, { user = {}, spaceId, tests }: CreateTestDefinition) => {
@ -128,6 +140,24 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
.then(tests.reservedSpecified.response);
});
});
describe('when solution is specified', () => {
it(`should return ${tests.solutionSpecified.statusCode}`, async () => {
return supertest
.post(`${urlPrefix}/api/spaces/space`)
.auth(user.username, user.password)
.send({
name: 'space with solution',
id: 'solution',
description: 'a description',
color: '#5c5959',
solution: 'search',
disabledFeatures: [],
})
.expect(tests.solutionSpecified.statusCode)
.then(tests.solutionSpecified.response);
});
});
});
});
};
@ -142,5 +172,6 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
expectNewSpaceResult,
expectRbacForbiddenResponse,
expectReservedSpecifiedResult,
expectSolutionSpecifiedResult,
};
}

View file

@ -21,6 +21,7 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
expectReservedSpecifiedResult,
expectConflictResponse,
expectRbacForbiddenResponse,
expectSolutionSpecifiedResult,
} = createTestSuiteFactory(esArchiver, supertestWithoutAuth);
describe('create', () => {
@ -68,6 +69,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 403,
response: expectRbacForbiddenResponse,
},
solutionSpecified: {
statusCode: 403,
response: expectRbacForbiddenResponse,
},
},
});
@ -87,6 +92,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 200,
response: expectReservedSpecifiedResult,
},
solutionSpecified: {
statusCode: 200,
response: expectSolutionSpecifiedResult,
},
},
});
@ -106,6 +115,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 200,
response: expectReservedSpecifiedResult,
},
solutionSpecified: {
statusCode: 200,
response: expectSolutionSpecifiedResult,
},
},
});
@ -125,6 +138,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 200,
response: expectReservedSpecifiedResult,
},
solutionSpecified: {
statusCode: 200,
response: expectSolutionSpecifiedResult,
},
},
});
@ -144,6 +161,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 403,
response: expectRbacForbiddenResponse,
},
solutionSpecified: {
statusCode: 403,
response: expectRbacForbiddenResponse,
},
},
});
@ -163,6 +184,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 403,
response: expectRbacForbiddenResponse,
},
solutionSpecified: {
statusCode: 403,
response: expectRbacForbiddenResponse,
},
},
});
@ -182,6 +207,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 403,
response: expectRbacForbiddenResponse,
},
solutionSpecified: {
statusCode: 403,
response: expectRbacForbiddenResponse,
},
},
});
@ -201,6 +230,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 403,
response: expectRbacForbiddenResponse,
},
solutionSpecified: {
statusCode: 403,
response: expectRbacForbiddenResponse,
},
},
});
});

View file

@ -19,6 +19,7 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
expectNewSpaceResult,
expectConflictResponse,
expectReservedSpecifiedResult,
expectSolutionSpecifiedResult,
} = createTestSuiteFactory(esArchiver, supertestWithoutAuth);
describe('create', () => {
@ -45,6 +46,10 @@ export default function createSpacesOnlySuite({ getService }: FtrProviderContext
statusCode: 200,
response: expectReservedSpecifiedResult,
},
solutionSpecified: {
statusCode: 200,
response: expectSolutionSpecifiedResult,
},
},
});
});