diff --git a/.i18nrc.json b/.i18nrc.json index 04606cb6d5519..8cb0025ad05e0 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -12,6 +12,7 @@ "embeddableExamples": "examples/embeddable_examples", "share": "src/plugins/share", "home": "src/plugins/home", + "charts": "src/plugins/charts", "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", diff --git a/docs/developer/add-data-guide.asciidoc b/docs/developer/add-data-guide.asciidoc index aec44a9537ee8..e00e46868bb2d 100644 --- a/docs/developer/add-data-guide.asciidoc +++ b/docs/developer/add-data-guide.asciidoc @@ -11,23 +11,24 @@ Each tutorial contains three sets of instructions: [float] === Creating a new tutorial -// TODO: update path to where the directory must be created on the new platform -1. Create a new directory in the link:https://github.com/elastic/kibana/tree/master/src/legacy/core_plugins/kibana/server/tutorials[tutorials directory]. -2. In the new directory, create a file called `index.js` that exports a function. -The function must return a JavaScript object that conforms to the link:https://github.com/elastic/kibana/blob/master/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts[tutorial schema]. -// TODO: update path to where the tutorial must be registered on the new platform -3. Register the tutorial in link:https://github.com/elastic/kibana/blob/master/src/legacy/core_plugins/kibana/server/tutorials/register.js[register.js] by calling `server.newPlatform.setup.plugins.home.tutorials.registerTutorial(myFuncImportedFromIndexJs)`. -// TODO: update path to where the image assets must be added on the new platform +1. Create a new directory in the link:https://github.com/elastic/kibana/tree/master/src/plugins/home/server/tutorials[tutorials directory]. +2. In the new directory, create a file called `index.ts` that exports a function. +The function must return a function object that conforms to the `TutorialSchema` interface link:https://github.com/elastic/kibana/blob/master/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts[tutorial schema]. +3. Register the tutorial in link:https://github.com/elastic/kibana/blob/master/src/plugins/home/server/tutorials/register.ts[register.ts] by adding it to the `builtInTutorials`. +// TODO update path once assets are migrated 4. Add image assets to the link:https://github.com/elastic/kibana/tree/master/src/legacy/core_plugins/kibana/public/home/tutorial_resources[tutorial_resources directory]. 5. Run Kibana locally to preview the tutorial. 6. Create a PR and go through the review process to get the changes approved. +If you are creating a new plugin and the tutorial is only related to that plugin, you can also place the `TutorialSchema` object into your plugin folder. Add `home` to the `requiredPlugins` list in your `kibana.json` file. +Then register the tutorial object by calling `home.tutorials.registerTutorial(tutorialObject)` in the `setup` lifecycle of your server plugin. + [float] ==== Variables String values can contain variables that are substituted when rendered. Variables are specified by `{}`. For example: `{config.docs.version}` is rendered as `6.2` when running the tutorial in Kibana 6.2. -link:https://github.com/elastic/kibana/blob/master/src/legacy/core_plugins/kibana/public/home/components/tutorial/replace_template_strings.js#L23[Provided variables] +link:https://github.com/elastic/kibana/blob/master/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js#L23[Provided variables] [float] ==== Markdown diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 467110b3874b8..23c0879e4411e 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -41,7 +41,7 @@ If the unit under test expects a particular response from a Core API, the test w #### Example -```ts +```typescript import { elasticsearchServiceMock } from 'src/core/server/mocks'; test('my test', async () => { @@ -59,11 +59,319 @@ test('my test', async () => { }); ``` -### Strategies for specific Core APIs +## Strategies for specific Core APIs -#### HTTP Routes +### HTTP Routes +The HTTP API interface is another public contract of Kibana, although not every Kibana endpoint is for external use. When evaluating the required level of test coverage for an HTTP resource, make your judgment based on whether an endpoint is considered to be public or private. Public API is expected to have a higher level of test coverage. +Public API tests should cover the **observable behavior** of the system, therefore they should be close to the real user interactions as much as possible, ideally by using HTTP requests to communicate with the Kibana server as a real user would do. -_How to test route handlers_ +##### Preconditions +We are going to add tests for `myPlugin` plugin that allows to format user-provided text, store and retrieve it later. +The plugin has *thin* route controllers isolating all the network layer dependencies and delegating all the logic to the plugin model. + +```typescript +class TextFormatter { + public static async format(text: string, sanitizer: Deps['sanitizer']) { + // sanitizer.sanitize throws MisformedTextError when passed text contains HTML markup + const sanitizedText = await sanitizer.sanitize(text); + return sanitizedText; + } + + public static async save(text: string, savedObjectsClient: SavedObjectsClient) { + const { id } = await savedObjectsClient.update('myPlugin-type', 'myPlugin', { + userText: text + }); + return { id }; + } + + public static async getById(id: string, savedObjectsClient: SavedObjectsClient) { + const { attributes } = await savedObjectsClient.get('myPlugin-type', id); + return { text: attributes.userText }; + } +} +router.get( + { + path: '/myPlugin/formatter', + validate: { + query: schema.object({ + text: schema.string({ maxLength: 100 }), + }), + }, + }, + async (context, request, response) => { + try { + const formattedText = await TextFormatter.format(request.query.text, deps.sanitizer); + return response.ok({ body: formattedText }); + } catch(error) { + if (error instanceof MisformedTextError) { + return response.badRequest({ body: error.message }) + } + + throw e; + } + } +); +router.post( + { + path: '/myPlugin/formatter/text', + validate: { + body: schema.object({ + text: schema.string({ maxLength: 100 }), + }), + }, + }, + async (context, request, response) => { + try { + const { id } = await TextFormatter.save(request.query.text, context.core.savedObjects.client); + return response.ok({ body: { id } }); + } catch(error) { + if (SavedObjectsErrorHelpers.isConflictError(error)) { + return response.conflict({ body: error.message }) + } + throw e; + } + } +); + +router.get( + { + path: '/myPlugin/formatter/text/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + try { + const { text } = await TextFormatter.getById(request.params.id, context.core.savedObjects.client); + return response.ok({ + body: text + }); + } catch(error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + return response.notFound() + } + throw e; + } + } +); +``` + +#### Unit testing +Unit tests provide the simplest and fastest way to test the logic in your route controllers and plugin models. +Use them whenever adding an integration test is hard and slow due to complex setup or the number of logic permutations. +Since all external core and plugin dependencies are mocked, you don't have the guarantee that the whole system works as +expected. +Pros: +- fast +- easier to debug + +Cons: +- doesn't test against real dependencies +- doesn't cover integration with other plugins + +###### Example +You can leverage existing unit-test infrastructure for this. You should add `*.test.ts` file and use dependencies mocks to cover the functionality with a broader test suit that covers: +- input permutations +- input edge cases +- expected exception +- interaction with dependencies +```typescript +// src/plugins/my_plugin/server/formatter.test.ts +describe('TextFormatter', () => { + describe('format()', () => { + const sanitizer = sanitizerMock.createSetup(); + sanitizer.sanitize.mockImplementation((input: string) => `sanitizer result:${input}`); + + it('formats text to a ... format', async () => { + expect(await TextFormatter.format('aaa', sanitizer)).toBe('...'); + }); + + it('calls Sanitizer.sanitize with correct arguments', async () => { + await TextFormatter.format('aaa', sanitizer); + expect(sanitizer.sanitize).toHaveBeenCalledTimes(1); + expect(sanitizer.sanitize).toHaveBeenCalledWith('aaa'); + }); + + it('throws MisformedTextError if passed string contains banned symbols', async () => { + sanitizer.sanitize.mockRejectedValueOnce(new MisformedTextError()); + await expect(TextFormatter.format('any', sanitizer)).rejects.toThrow(MisformedTextError); + }); + // ... other tests + }); +}); +``` + +#### Integration tests +Depending on the number of external dependencies, you can consider implementing several high-level integration tests. +They would work as a set of [smoke tests](https://en.wikipedia.org/wiki/Smoke_testing_(software)) for the most important functionality. +Main subjects for tests should be: +- authenticated / unauthenticated access to an endpoint. +- endpoint validation (params, query, body). +- main business logic. +- dependencies on other plugins. + +##### Functional Test Runner +If your plugin relies on the elasticsearch server to store data and supports additional configuration, you can leverage the Functional Test Runner(FTR) to implement integration tests. +FTR bootstraps an elasticsearch and a Kibana instance and runs the test suite against it. +Pros: +- runs the whole Elastic stack +- tests cross-plugin integration +- emulates a real user interaction with the stack +- allows adjusting config values + +Cons: +- slow start +- hard to debug +- brittle tests + +###### Example +You can reuse existing [api_integration](/test/api_integration/config.js) setup by registering a test file within a [test loader](/test/api_integration/apis/index.js). More about the existing FTR setup in the [contribution guide](/CONTRIBUTING.md#running-specific-kibana-tests) + +The tests cover: +- authenticated / non-authenticated user access (when applicable) +```typescript +// TODO after https://github.com/elastic/kibana/pull/53208/ +``` +- request validation +```typescript +// test/api_integration/apis/my_plugin/something.ts +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('myPlugin', () => { + it('validate params before to store text', async () => { + const response = await supertest + .post('/myPlugin/formatter/text') + .set('content-type', 'application/json') + .send({ text: 'aaa'.repeat(100) }) + .expect(400); + + expect(response.body).to.have.property('message'); + expect(response.body.message).to.contain('must have a maximum length of [100]'); + }); + }); +``` +- the main logic of the plugin +```typescript +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('myPlugin', () => { + it('stores text', async () => { + const response = await supertest + .post('/myPlugin/formatter/text') + .set('content-type', 'application/json') + .send({ text: 'aaa' }) + .expect(200); + + expect(response.body).to.have.property('id'); + expect(response.body.id).to.be.a('string'); + }); + + it('retrieves text', async () => { + const { body } = await supertest + .post('/myPlugin/formatter/text') + .set('content-type', 'application/json') + .send({ text: 'bbb' }) + .expect(200); + + const response = await supertest.get(`/myPlugin/formatter/text/${body.id}`).expect(200); + expect(response.text).be('bbb'); + }); + + it('returns NotFound error when cannot find a text', async () => { + await supertest + .get('/myPlugin/something/missing') + .expect(404, 'Saved object [myPlugin-type/missing] not found'); + }); + }); +``` + +##### TestUtils +It can be utilized if your plugin doesn't interact with the elasticsearch server or mocks the own methods doing so. +Runs tests against real Kibana server instance. +Pros: +- runs the real Kibana instance +- tests cross-plugin integration +- emulates a real user interaction with the HTTP resources + +Cons: +- faster than FTR because it doesn't run elasticsearch instance, but still slow +- hard to debug +- doesn't cover Kibana CLI logic + +###### Example +To have access to Kibana TestUtils, you should create `integration_tests` folder and import `test_utils` within a test file: +```typescript +// src/plugins/my_plugin/server/integration_tests/formatter.test.ts +import * as kbnTestServer from 'src/test_utils/kbn_server'; + +describe('myPlugin', () => { + describe('GET /myPlugin/formatter', () => { + let root: ReturnType; + beforeAll(async () => { + root = kbnTestServer.createRoot(); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + it('validates given text', async () => { + const response = await kbnTestServer.request + .get(root, '/myPlugin/formatter') + .query({ text: 'input string'.repeat(100) }) + .expect(400); + + expect(response.body).toHaveProperty('message'); + }); + + it('formats given text', async () => { + const response = await kbnTestServer.request + .get(root, '/myPlugin/formatter') + .query({ text: 'input string' }) + .expect(200); + + expect(response.text).toBe('...'); + }); + + it('returns BadRequest if passed string contains banned symbols', async () => { + await kbnTestServer.request + .get(root, '/myPlugin/formatter') + .query({ text: '