Skip to content

Commit 79040fa

Browse files
committed
Add test harness for migration integration tests
1 parent daa81c9 commit 79040fa

File tree

2 files changed

+249
-0
lines changed

2 files changed

+249
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { createTestHarness, SavedObjectTestHarness } from '../../../test_helpers/so_migrations';
10+
11+
/**
12+
* These tests are a little unnecessary because these migrations are incredibly simple, however
13+
* this file serves as an example of how to use test_helpers/so_migrations.
14+
*/
15+
describe('ui settings migrations', () => {
16+
let testHarness: SavedObjectTestHarness;
17+
18+
beforeAll(async () => {
19+
testHarness = createTestHarness();
20+
await testHarness.start();
21+
});
22+
23+
afterAll(async () => {
24+
await testHarness.stop();
25+
});
26+
27+
it('migrates siem:* configs', async () => {
28+
const input = [
29+
{
30+
type: 'config',
31+
id: '1',
32+
attributes: {
33+
'siem:value-one': 1000,
34+
'siem:value-two': 'hello',
35+
},
36+
references: [],
37+
},
38+
];
39+
expect(await testHarness.migrate(input)).toEqual([
40+
expect.objectContaining({
41+
type: 'config',
42+
id: '1',
43+
attributes: {
44+
'securitySolution:value-one': 1000,
45+
'securitySolution:value-two': 'hello',
46+
},
47+
references: [],
48+
}),
49+
]);
50+
});
51+
52+
it('migrates ml:fileDataVisualizerMaxFileSize', async () => {
53+
const input = [
54+
{
55+
type: 'config',
56+
id: '1',
57+
attributes: { 'ml:fileDataVisualizerMaxFileSize': '1000' },
58+
// This field can be added if you only want this object to go through the > 7.12.0 migrations
59+
// If this field is omitted the object will be run through all migrations available.
60+
migrationVersion: { config: '7.12.0' },
61+
references: [],
62+
},
63+
];
64+
expect(await testHarness.migrate(input)).toEqual([
65+
expect.objectContaining({
66+
type: 'config',
67+
id: '1',
68+
attributes: { 'fileUpload:maxFileSize': '1000' },
69+
references: [],
70+
}),
71+
]);
72+
});
73+
});
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import * as kbnTestServer from './kbn_server';
10+
import { SavedObject } from '../types';
11+
12+
type ExportOptions = { type: string } | { objects: Array<{ id: string; type: string }> };
13+
14+
/**
15+
* Creates a test harness utility running migrations on a fully configured Kibana and Elasticsearch instance with all
16+
* Kibana plugins loaded. Useful for testing more complex migrations that have dependencies on other plugins. Should
17+
* only be used within the jest_integration suite.
18+
*
19+
* @example
20+
* ```ts
21+
* describe('my migrations', () => {
22+
* let testHarness: SavedObjectTestHarness;
23+
* beforeAll(async () => {
24+
* testHarness = createTestHarness();
25+
* await testHarness.start();
26+
* });
27+
* afterAll(() => testHarness.stop());
28+
*
29+
*
30+
* it('migrates the documents', async () => {
31+
* expect(
32+
* await testHarness.migrate(
33+
* { type: 'my-type', id: 'my-id', attributes: { ... }, references: [] }
34+
* )
35+
* ).toEqual([
36+
* expect.objectContaining({ type: 'my-type', id: 'my-id', attributes: { ... }, references: [] })
37+
* ]);
38+
* });
39+
* });
40+
* ```
41+
*/
42+
export const createTestHarness = () => {
43+
let started = false;
44+
let stopped = false;
45+
let esServer: kbnTestServer.TestElasticsearchUtils;
46+
const { startES } = kbnTestServer.createTestServers({ adjustTimeout: jest.setTimeout });
47+
const root = kbnTestServer.createRootWithCorePlugins({}, { oss: false });
48+
49+
/**
50+
* Imports an array of objects into Kibana and applies migrations before persisting to Elasticsearch. Will overwrite
51+
* any existing objects with the same id.
52+
* @param objects
53+
*/
54+
const importObjects = async (objects: SavedObject[]) => {
55+
if (!started)
56+
throw new Error(`SavedObjectTestHarness must be started before objects can be imported`);
57+
if (stopped) throw new Error(`SavedObjectTestHarness cannot import objects after stopped`);
58+
59+
const response = await kbnTestServer
60+
// Always use overwrite=true flag so we can isolate this harness to migrations
61+
.getSupertest(root, 'post', '/api/saved_objects/_import?overwrite=true')
62+
.set('Content-Type', 'multipart/form-data; boundary=EXAMPLE')
63+
.send(
64+
[
65+
'--EXAMPLE',
66+
'Content-Disposition: form-data; name="file"; filename="export.ndjson"',
67+
'Content-Type: application/ndjson',
68+
'',
69+
...objects.map((o) => JSON.stringify(o)),
70+
'--EXAMPLE--',
71+
].join('\r\n')
72+
)
73+
.expect(200);
74+
75+
if (response.body.errors?.length > 0) {
76+
throw new Error(
77+
`Errors importing objects: ${JSON.stringify(response.body.errors, undefined, 2)}`
78+
);
79+
}
80+
};
81+
82+
/**
83+
* Exports objects from Kibana with all migrations applied.
84+
* @param options
85+
*/
86+
const exportObjects = async (options: ExportOptions): Promise<SavedObject[]> => {
87+
if (!started)
88+
throw new Error(`SavedObjectTestHarness must be started before objects can be imported`);
89+
if (stopped) throw new Error(`SavedObjectTestHarness cannot import objects after stopped`);
90+
91+
const response = await kbnTestServer
92+
.getSupertest(root, 'post', '/api/saved_objects/_export')
93+
.send({
94+
...options,
95+
excludeExportDetails: true,
96+
})
97+
.expect(200);
98+
99+
// Parse ndjson response
100+
return response.text.split('\n').map((s: string) => JSON.parse(s));
101+
};
102+
103+
return {
104+
/**
105+
* Start Kibana and Elasticsearch for migration testing. Must be called before `migrate`.
106+
* In most cases, this can be called during your test's `beforeAll` hook and does not need to be called for each
107+
* individual test.
108+
*/
109+
start: async () => {
110+
if (started)
111+
throw new Error(`SavedObjectTestHarness already started! Cannot call start again`);
112+
if (stopped)
113+
throw new Error(`SavedObjectTestHarness already stopped! Cannot call start again`);
114+
115+
started = true;
116+
esServer = await startES();
117+
await root.setup();
118+
await root.start();
119+
120+
console.log(`Waiting for Kibana to be ready...`);
121+
await waitForTrue(async () => {
122+
const statusApi = kbnTestServer.getSupertest(root, 'get', '/api/status');
123+
const response = await statusApi.send();
124+
return response.status === 200;
125+
});
126+
},
127+
128+
/**
129+
* Stop Kibana and Elasticsearch for migration testing. Must be called after `start`.
130+
* In most cases, this can be called during your test's `afterAll` hook and does not need to be called for each
131+
* individual test.
132+
*/
133+
stop: async () => {
134+
if (!started) throw new Error(`SavedObjectTestHarness not started! Cannot call stop`);
135+
if (stopped)
136+
throw new Error(`SavedObjectTestHarness already stopped! Cannot call stop again`);
137+
138+
stopped = true;
139+
await root.shutdown();
140+
await esServer.stop();
141+
},
142+
143+
/**
144+
* Migrates an array of SavedObjects and returns the results. Assumes that the objects will retain the same type
145+
* and id after the migration. When testing migrations that may change a document's type or id, use `importObjects`
146+
* and `exportObjects` directly.
147+
* @param objects
148+
*/
149+
migrate: async (objects: SavedObject[]) => {
150+
await importObjects(objects);
151+
return exportObjects({
152+
objects: objects.map(({ type, id }) => ({ type, id })),
153+
});
154+
},
155+
156+
importObjects,
157+
exportObjects,
158+
};
159+
};
160+
161+
export type SavedObjectTestHarness = ReturnType<typeof createTestHarness>;
162+
163+
const waitForTrue = async (predicate: () => Promise<boolean>) => {
164+
let attempt = 0;
165+
do {
166+
attempt++;
167+
const result = await predicate();
168+
if (result) {
169+
return;
170+
}
171+
172+
await new Promise((r) => setTimeout(r, attempt * 500));
173+
} while (attempt <= 10);
174+
175+
throw new Error(`Predicate never resolved after ${attempt} attempts`);
176+
};

0 commit comments

Comments
 (0)