-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add test harness for migration integration tests
- Loading branch information
Showing
2 changed files
with
249 additions
and
0 deletions.
There are no files selected for viewing
73 changes: 73 additions & 0 deletions
73
src/core/server/ui_settings/integration_tests/so_migrations.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { createTestHarness, SavedObjectTestHarness } from '../../../test_helpers/so_migrations'; | ||
|
||
/** | ||
* These tests are a little unnecessary because these migrations are incredibly simple, however | ||
* this file serves as an example of how to use test_helpers/so_migrations. | ||
*/ | ||
describe('ui settings migrations', () => { | ||
let testHarness: SavedObjectTestHarness; | ||
|
||
beforeAll(async () => { | ||
testHarness = createTestHarness(); | ||
await testHarness.start(); | ||
}); | ||
|
||
afterAll(async () => { | ||
await testHarness.stop(); | ||
}); | ||
|
||
it('migrates siem:* configs', async () => { | ||
const input = [ | ||
{ | ||
type: 'config', | ||
id: '1', | ||
attributes: { | ||
'siem:value-one': 1000, | ||
'siem:value-two': 'hello', | ||
}, | ||
references: [], | ||
}, | ||
]; | ||
expect(await testHarness.migrate(input)).toEqual([ | ||
expect.objectContaining({ | ||
type: 'config', | ||
id: '1', | ||
attributes: { | ||
'securitySolution:value-one': 1000, | ||
'securitySolution:value-two': 'hello', | ||
}, | ||
references: [], | ||
}), | ||
]); | ||
}); | ||
|
||
it('migrates ml:fileDataVisualizerMaxFileSize', async () => { | ||
const input = [ | ||
{ | ||
type: 'config', | ||
id: '1', | ||
attributes: { 'ml:fileDataVisualizerMaxFileSize': '1000' }, | ||
// This field can be added if you only want this object to go through the > 7.12.0 migrations | ||
// If this field is omitted the object will be run through all migrations available. | ||
migrationVersion: { config: '7.12.0' }, | ||
references: [], | ||
}, | ||
]; | ||
expect(await testHarness.migrate(input)).toEqual([ | ||
expect.objectContaining({ | ||
type: 'config', | ||
id: '1', | ||
attributes: { 'fileUpload:maxFileSize': '1000' }, | ||
references: [], | ||
}), | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import * as kbnTestServer from './kbn_server'; | ||
import { SavedObject } from '../types'; | ||
|
||
type ExportOptions = { type: string } | { objects: Array<{ id: string; type: string }> }; | ||
|
||
/** | ||
* Creates a test harness utility running migrations on a fully configured Kibana and Elasticsearch instance with all | ||
* Kibana plugins loaded. Useful for testing more complex migrations that have dependencies on other plugins. Should | ||
* only be used within the jest_integration suite. | ||
* | ||
* @example | ||
* ```ts | ||
* describe('my migrations', () => { | ||
* let testHarness: SavedObjectTestHarness; | ||
* beforeAll(async () => { | ||
* testHarness = createTestHarness(); | ||
* await testHarness.start(); | ||
* }); | ||
* afterAll(() => testHarness.stop()); | ||
* | ||
* | ||
* it('migrates the documents', async () => { | ||
* expect( | ||
* await testHarness.migrate( | ||
* { type: 'my-type', id: 'my-id', attributes: { ... }, references: [] } | ||
* ) | ||
* ).toEqual([ | ||
* expect.objectContaining({ type: 'my-type', id: 'my-id', attributes: { ... }, references: [] }) | ||
* ]); | ||
* }); | ||
* }); | ||
* ``` | ||
*/ | ||
export const createTestHarness = () => { | ||
let started = false; | ||
let stopped = false; | ||
let esServer: kbnTestServer.TestElasticsearchUtils; | ||
const { startES } = kbnTestServer.createTestServers({ adjustTimeout: jest.setTimeout }); | ||
const root = kbnTestServer.createRootWithCorePlugins({}, { oss: false }); | ||
|
||
/** | ||
* Imports an array of objects into Kibana and applies migrations before persisting to Elasticsearch. Will overwrite | ||
* any existing objects with the same id. | ||
* @param objects | ||
*/ | ||
const importObjects = async (objects: SavedObject[]) => { | ||
if (!started) | ||
throw new Error(`SavedObjectTestHarness must be started before objects can be imported`); | ||
if (stopped) throw new Error(`SavedObjectTestHarness cannot import objects after stopped`); | ||
|
||
const response = await kbnTestServer | ||
// Always use overwrite=true flag so we can isolate this harness to migrations | ||
.getSupertest(root, 'post', '/api/saved_objects/_import?overwrite=true') | ||
.set('Content-Type', 'multipart/form-data; boundary=EXAMPLE') | ||
.send( | ||
[ | ||
'--EXAMPLE', | ||
'Content-Disposition: form-data; name="file"; filename="export.ndjson"', | ||
'Content-Type: application/ndjson', | ||
'', | ||
...objects.map((o) => JSON.stringify(o)), | ||
'--EXAMPLE--', | ||
].join('\r\n') | ||
) | ||
.expect(200); | ||
|
||
if (response.body.errors?.length > 0) { | ||
throw new Error( | ||
`Errors importing objects: ${JSON.stringify(response.body.errors, undefined, 2)}` | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* Exports objects from Kibana with all migrations applied. | ||
* @param options | ||
*/ | ||
const exportObjects = async (options: ExportOptions): Promise<SavedObject[]> => { | ||
if (!started) | ||
throw new Error(`SavedObjectTestHarness must be started before objects can be imported`); | ||
if (stopped) throw new Error(`SavedObjectTestHarness cannot import objects after stopped`); | ||
|
||
const response = await kbnTestServer | ||
.getSupertest(root, 'post', '/api/saved_objects/_export') | ||
.send({ | ||
...options, | ||
excludeExportDetails: true, | ||
}) | ||
.expect(200); | ||
|
||
// Parse ndjson response | ||
return response.text.split('\n').map((s: string) => JSON.parse(s)); | ||
}; | ||
|
||
return { | ||
/** | ||
* Start Kibana and Elasticsearch for migration testing. Must be called before `migrate`. | ||
* In most cases, this can be called during your test's `beforeAll` hook and does not need to be called for each | ||
* individual test. | ||
*/ | ||
start: async () => { | ||
if (started) | ||
throw new Error(`SavedObjectTestHarness already started! Cannot call start again`); | ||
if (stopped) | ||
throw new Error(`SavedObjectTestHarness already stopped! Cannot call start again`); | ||
|
||
started = true; | ||
esServer = await startES(); | ||
await root.setup(); | ||
await root.start(); | ||
|
||
console.log(`Waiting for Kibana to be ready...`); | ||
await waitForTrue(async () => { | ||
const statusApi = kbnTestServer.getSupertest(root, 'get', '/api/status'); | ||
const response = await statusApi.send(); | ||
return response.status === 200; | ||
}); | ||
}, | ||
|
||
/** | ||
* Stop Kibana and Elasticsearch for migration testing. Must be called after `start`. | ||
* In most cases, this can be called during your test's `afterAll` hook and does not need to be called for each | ||
* individual test. | ||
*/ | ||
stop: async () => { | ||
if (!started) throw new Error(`SavedObjectTestHarness not started! Cannot call stop`); | ||
if (stopped) | ||
throw new Error(`SavedObjectTestHarness already stopped! Cannot call stop again`); | ||
|
||
stopped = true; | ||
await root.shutdown(); | ||
await esServer.stop(); | ||
}, | ||
|
||
/** | ||
* Migrates an array of SavedObjects and returns the results. Assumes that the objects will retain the same type | ||
* and id after the migration. When testing migrations that may change a document's type or id, use `importObjects` | ||
* and `exportObjects` directly. | ||
* @param objects | ||
*/ | ||
migrate: async (objects: SavedObject[]) => { | ||
await importObjects(objects); | ||
return exportObjects({ | ||
objects: objects.map(({ type, id }) => ({ type, id })), | ||
}); | ||
}, | ||
|
||
importObjects, | ||
exportObjects, | ||
}; | ||
}; | ||
|
||
export type SavedObjectTestHarness = ReturnType<typeof createTestHarness>; | ||
|
||
const waitForTrue = async (predicate: () => Promise<boolean>) => { | ||
let attempt = 0; | ||
do { | ||
attempt++; | ||
const result = await predicate(); | ||
if (result) { | ||
return; | ||
} | ||
|
||
await new Promise((r) => setTimeout(r, attempt * 500)); | ||
} while (attempt <= 10); | ||
|
||
throw new Error(`Predicate never resolved after ${attempt} attempts`); | ||
}; |