Skip to content

Commit

Permalink
Add test harness for migration integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover committed Jul 19, 2021
1 parent daa81c9 commit 79040fa
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 0 deletions.
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: [],
}),
]);
});
});
176 changes: 176 additions & 0 deletions src/core/test_helpers/so_migrations.ts
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`);
};

0 comments on commit 79040fa

Please sign in to comment.