mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Model version testing: add utility to test MV transforms (#167861)
## Summary Follow-up of https://github.com/elastic/kibana/pull/167501 Adds a new utility to `@kbn/core-test-helpers-model-versions` to easily assert model version document transformations in unit tests. ```ts const mySoType = someSoType(); const migrator = createModelVersionTestMigrator({ type: mySoType }); const obj = createSomeSavedObject(); const migrated = migrator.migrate({ document: obj, fromVersion: 1, toVersion: 2, }); expect(migrated.properties).toEqual(myExpectedProperties); ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3ea2f23708
commit
7baab02533
14 changed files with 431 additions and 21 deletions
|
@ -31,11 +31,16 @@ export interface DocumentMigrateOptions {
|
|||
* Defaults to `false`.
|
||||
*/
|
||||
allowDowngrade?: boolean;
|
||||
/**
|
||||
* If specified, will migrate to the given version instead of the latest known version.
|
||||
*/
|
||||
targetTypeVersion?: string;
|
||||
}
|
||||
|
||||
interface TransformOptions {
|
||||
convertNamespaceTypes?: boolean;
|
||||
allowDowngrade?: boolean;
|
||||
targetTypeVersion?: string;
|
||||
}
|
||||
|
||||
interface DocumentMigratorOptions {
|
||||
|
@ -149,10 +154,11 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
*/
|
||||
public migrate(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
{ allowDowngrade = false }: DocumentMigrateOptions = {}
|
||||
{ allowDowngrade = false, targetTypeVersion }: DocumentMigrateOptions = {}
|
||||
): SavedObjectUnsanitizedDoc {
|
||||
const { document } = this.transform(doc, {
|
||||
allowDowngrade,
|
||||
targetTypeVersion,
|
||||
});
|
||||
return document;
|
||||
}
|
||||
|
@ -171,15 +177,20 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
|
||||
private transform(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
{ convertNamespaceTypes = false, allowDowngrade = false }: TransformOptions = {}
|
||||
{
|
||||
convertNamespaceTypes = false,
|
||||
allowDowngrade = false,
|
||||
targetTypeVersion,
|
||||
}: TransformOptions = {}
|
||||
) {
|
||||
if (!this.migrations) {
|
||||
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
|
||||
}
|
||||
const typeMigrations = this.migrations[doc.type];
|
||||
if (downgradeRequired(doc, typeMigrations?.latestVersion ?? {})) {
|
||||
if (downgradeRequired(doc, typeMigrations?.latestVersion ?? {}, targetTypeVersion)) {
|
||||
const currentVersion = doc.typeMigrationVersion ?? doc.migrationVersion?.[doc.type];
|
||||
const latestVersion = this.migrations[doc.type].latestVersion[TransformType.Migrate];
|
||||
const latestVersion =
|
||||
targetTypeVersion ?? this.migrations[doc.type].latestVersion[TransformType.Migrate];
|
||||
if (!allowDowngrade) {
|
||||
throw Boom.badData(
|
||||
`Document "${doc.id}" belongs to a more recent version of Kibana [${currentVersion}] when the last known version is [${latestVersion}].`
|
||||
|
@ -187,13 +198,16 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
}
|
||||
return this.transformDown(doc, { targetTypeVersion: latestVersion! });
|
||||
} else {
|
||||
return this.transformUp(doc, { convertNamespaceTypes });
|
||||
return this.transformUp(doc, { convertNamespaceTypes, targetTypeVersion });
|
||||
}
|
||||
}
|
||||
|
||||
private transformUp(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
{ convertNamespaceTypes }: { convertNamespaceTypes: boolean }
|
||||
{
|
||||
convertNamespaceTypes,
|
||||
targetTypeVersion,
|
||||
}: { convertNamespaceTypes: boolean; targetTypeVersion?: string }
|
||||
) {
|
||||
if (!this.migrations) {
|
||||
throw new Error('Migrations are not ready. Make sure prepareMigrations is called first.');
|
||||
|
@ -201,6 +215,7 @@ export class DocumentMigrator implements VersionedTransformer {
|
|||
|
||||
const pipeline = new DocumentUpgradePipeline({
|
||||
document: doc,
|
||||
targetTypeVersion,
|
||||
migrations: this.migrations,
|
||||
kibanaVersion: this.options.kibanaVersion,
|
||||
convertNamespaceTypes,
|
||||
|
|
|
@ -154,4 +154,40 @@ describe('downgradeRequired', () => {
|
|||
|
||||
expect(downgradeRequired(doc, latestVersions)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when targetTypeVersion is specified and lower than the document version', () => {
|
||||
const doc = createDoc({
|
||||
typeMigrationVersion: '8.0.0',
|
||||
});
|
||||
const latestVersions = {
|
||||
[TransformType.Migrate]: '8.5.0',
|
||||
} as Record<TransformType, string>;
|
||||
const targetTypeVersion = '7.9.0';
|
||||
|
||||
expect(downgradeRequired(doc, latestVersions, targetTypeVersion)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when targetTypeVersion is specified and higher than the document version', () => {
|
||||
const doc = createDoc({
|
||||
typeMigrationVersion: '8.0.0',
|
||||
});
|
||||
const latestVersions = {
|
||||
[TransformType.Migrate]: '7.9.0',
|
||||
} as Record<TransformType, string>;
|
||||
const targetTypeVersion = '8.5.0';
|
||||
|
||||
expect(downgradeRequired(doc, latestVersions, targetTypeVersion)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when targetTypeVersion is specified and the same as the document version', () => {
|
||||
const doc = createDoc({
|
||||
typeMigrationVersion: '8.0.0',
|
||||
});
|
||||
const latestVersions = {
|
||||
[TransformType.Migrate]: '7.9.0',
|
||||
} as Record<TransformType, string>;
|
||||
const targetTypeVersion = '8.0.0';
|
||||
|
||||
expect(downgradeRequired(doc, latestVersions, targetTypeVersion)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -92,13 +92,14 @@ export function transformComparator(a: Transform, b: Transform) {
|
|||
*/
|
||||
export function downgradeRequired(
|
||||
doc: SavedObjectUnsanitizedDoc,
|
||||
latestVersions: Record<TransformType, string>
|
||||
latestVersions: Record<TransformType, string>,
|
||||
targetTypeVersion?: string
|
||||
): boolean {
|
||||
const docTypeVersion = doc.typeMigrationVersion ?? doc.migrationVersion?.[doc.type];
|
||||
const latestMigrationVersion = maxVersion(
|
||||
latestVersions[TransformType.Migrate],
|
||||
latestVersions[TransformType.Convert]
|
||||
);
|
||||
const latestMigrationVersion =
|
||||
targetTypeVersion ??
|
||||
maxVersion(latestVersions[TransformType.Migrate], latestVersions[TransformType.Convert]);
|
||||
|
||||
if (!docTypeVersion || !latestMigrationVersion) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,44 @@
|
|||
# @kbn/core-test-helpers-model-versions
|
||||
|
||||
Package exposing utilities for model version integration testing.
|
||||
Package exposing utilities for model version testing:
|
||||
- unit testing
|
||||
- integration testing
|
||||
|
||||
## Unit testing
|
||||
|
||||
### Model version test migrator
|
||||
|
||||
The `createModelVersionTestMigrator` helper allows to create a test migrator that
|
||||
can be used to test model version changes between versions.
|
||||
|
||||
```ts
|
||||
const mySoType = someSoType();
|
||||
const migrator = createModelVersionTestMigrator({ type: mySoType });
|
||||
|
||||
const obj = createSomeSavedObject();
|
||||
|
||||
const migrated = migrator.migrate({
|
||||
document: obj,
|
||||
fromVersion: 1,
|
||||
toVersion: 2,
|
||||
});
|
||||
|
||||
expect(migrated.properties).toEqual(myExpectedProperties);
|
||||
```
|
||||
|
||||
Please refer to the code documentation for more detailed examples.
|
||||
|
||||
## Integration testing
|
||||
|
||||
### Model version test bed
|
||||
|
||||
This package exposes a `createModelVersionTestBed` utility which allow simulating
|
||||
a testbed environment where we're in the cohabitation period between two versions, to test the interactions
|
||||
between two model versions of a set of SO types.
|
||||
|
||||
### Limitations:
|
||||
Please refer to the code documentation for more detailed examples.
|
||||
|
||||
*Limitations:*
|
||||
|
||||
Because the test bed is only creating the parts of Core required to create the two SO
|
||||
repositories, and because we're not loading all plugins (for proper isolation), the integration
|
||||
|
|
|
@ -6,11 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { createModelVersionTestBed } from './src/test_bed';
|
||||
|
||||
export type {
|
||||
ModelVersionTestBed,
|
||||
ModelVersionTestKit,
|
||||
ModelVersionTestkitOptions,
|
||||
SavedObjectTestkitDefinition,
|
||||
} from './src/types';
|
||||
export {
|
||||
createModelVersionTestBed,
|
||||
type ModelVersionTestBed,
|
||||
type ModelVersionTestKit,
|
||||
type ModelVersionTestkitOptions,
|
||||
type SavedObjectTestkitDefinition,
|
||||
} from './src/test_bed';
|
||||
export {
|
||||
createModelVersionTestMigrator,
|
||||
type ModelVersionTestMigrator,
|
||||
} from './src/model_version_tester';
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import type { SavedObjectsType, SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import { createModelVersionTestMigrator } from './model_version_tester';
|
||||
|
||||
const createObject = (parts: Partial<SavedObject>): SavedObject => {
|
||||
return {
|
||||
type: 'test-type',
|
||||
id: 'test-id',
|
||||
attributes: {},
|
||||
references: [],
|
||||
...parts,
|
||||
};
|
||||
};
|
||||
|
||||
describe('modelVersionTester', () => {
|
||||
const testType: SavedObjectsType = {
|
||||
name: 'test-type',
|
||||
hidden: false,
|
||||
namespaceType: 'single',
|
||||
mappings: { properties: {} },
|
||||
modelVersions: {
|
||||
1: {
|
||||
changes: [],
|
||||
schemas: {
|
||||
forwardCompatibility: schema.object(
|
||||
{
|
||||
fieldV1: schema.string(),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
),
|
||||
},
|
||||
},
|
||||
2: {
|
||||
changes: [
|
||||
{
|
||||
type: 'data_backfill',
|
||||
backfillFn: (document) => {
|
||||
return {
|
||||
attributes: {
|
||||
fieldAddedInV2: '2',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: {
|
||||
forwardCompatibility: schema.object(
|
||||
{
|
||||
fieldV1: schema.string(),
|
||||
fieldAddedInV2: schema.string(),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
),
|
||||
},
|
||||
},
|
||||
3: {
|
||||
changes: [
|
||||
{
|
||||
type: 'data_backfill',
|
||||
backfillFn: (doc) => {
|
||||
return {
|
||||
attributes: {
|
||||
fieldAddedInV3: '3',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: {
|
||||
forwardCompatibility: schema.object(
|
||||
{
|
||||
fieldV1: schema.string(),
|
||||
fieldAddedInV2: schema.string(),
|
||||
fieldAddedInV3: schema.string(),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
),
|
||||
},
|
||||
},
|
||||
4: {
|
||||
changes: [
|
||||
{
|
||||
type: 'unsafe_transform',
|
||||
transformFn: (doc) => {
|
||||
doc.attributes = {
|
||||
...doc.attributes,
|
||||
fieldUnsafelyAddedInV4: '4',
|
||||
};
|
||||
|
||||
return { document: doc };
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: {
|
||||
forwardCompatibility: schema.object(
|
||||
{
|
||||
fieldV1: schema.string(),
|
||||
fieldAddedInV2: schema.string(),
|
||||
fieldAddedInV3: schema.string(),
|
||||
fieldUnsafelyAddedInV4: schema.string(),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('upward migrate one version', () => {
|
||||
const migrator = createModelVersionTestMigrator({ type: testType });
|
||||
|
||||
const obj = createObject({
|
||||
attributes: {
|
||||
fieldV1: 'v1',
|
||||
},
|
||||
});
|
||||
|
||||
const migrated = migrator.migrate({ document: obj, fromVersion: 1, toVersion: 2 });
|
||||
|
||||
expect(migrated.attributes).toEqual({
|
||||
fieldV1: 'v1',
|
||||
fieldAddedInV2: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('upward migrate multiple version', () => {
|
||||
const migrator = createModelVersionTestMigrator({ type: testType });
|
||||
|
||||
const obj = createObject({
|
||||
attributes: {
|
||||
fieldV1: 'v1',
|
||||
},
|
||||
});
|
||||
|
||||
const migrated = migrator.migrate({ document: obj, fromVersion: 1, toVersion: 4 });
|
||||
|
||||
expect(migrated.attributes).toEqual({
|
||||
fieldV1: 'v1',
|
||||
fieldAddedInV2: '2',
|
||||
fieldAddedInV3: '3',
|
||||
fieldUnsafelyAddedInV4: '4',
|
||||
});
|
||||
});
|
||||
|
||||
it('downward migrate one version', () => {
|
||||
const migrator = createModelVersionTestMigrator({ type: testType });
|
||||
|
||||
const obj = createObject({
|
||||
attributes: {
|
||||
fieldV1: 'v1',
|
||||
fieldAddedInV2: '2',
|
||||
fieldAddedInV3: '3',
|
||||
fieldUnsafelyAddedInV4: '4',
|
||||
},
|
||||
});
|
||||
|
||||
const migrated = migrator.migrate({ document: obj, fromVersion: 4, toVersion: 3 });
|
||||
|
||||
expect(migrated.attributes).toEqual({
|
||||
fieldV1: 'v1',
|
||||
fieldAddedInV2: '2',
|
||||
fieldAddedInV3: '3',
|
||||
});
|
||||
});
|
||||
|
||||
it('downward migrate multiple versions', () => {
|
||||
const migrator = createModelVersionTestMigrator({ type: testType });
|
||||
|
||||
const obj = createObject({
|
||||
attributes: {
|
||||
fieldV1: 'v1',
|
||||
fieldAddedInV2: '2',
|
||||
fieldAddedInV3: '3',
|
||||
fieldUnsafelyAddedInV4: '4',
|
||||
},
|
||||
});
|
||||
|
||||
const migrated = migrator.migrate({ document: obj, fromVersion: 4, toVersion: 1 });
|
||||
|
||||
expect(migrated.attributes).toEqual({
|
||||
fieldV1: 'v1',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { loggerMock } from '@kbn/logging-mocks';
|
||||
import { getEnvOptions } from '@kbn/config-mocks';
|
||||
import { Env } from '@kbn/config';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import type { SavedObjectsType, SavedObject } from '@kbn/core-saved-objects-server';
|
||||
import {
|
||||
modelVersionToVirtualVersion,
|
||||
SavedObjectTypeRegistry,
|
||||
globalSwitchToModelVersionAt,
|
||||
} from '@kbn/core-saved-objects-base-server-internal';
|
||||
import { DocumentMigrator } from '@kbn/core-saved-objects-migration-server-internal';
|
||||
|
||||
const env = Env.createDefault(REPO_ROOT, getEnvOptions());
|
||||
const currentVersion = env.packageInfo.version;
|
||||
const lastCoreVersion = '8.8.0';
|
||||
|
||||
/**
|
||||
* Options for {@link ModelVersionTestMigrator.migrate}
|
||||
*/
|
||||
interface ModelVersionTestMigrateOptions<T = unknown> {
|
||||
/**
|
||||
* The document to migrate.
|
||||
*/
|
||||
document: SavedObject<T>;
|
||||
/**
|
||||
* The model version the input document should be considered in.
|
||||
*/
|
||||
fromVersion: number;
|
||||
/**
|
||||
* The model version the document should be migrated to.
|
||||
*/
|
||||
toVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test utility allowing to test model version changes between versions.
|
||||
*/
|
||||
export interface ModelVersionTestMigrator {
|
||||
/**
|
||||
* Migrate the document from the provided source to destination model version.
|
||||
*
|
||||
* @see {@link ModelVersionTestMigrateOptions}
|
||||
*/
|
||||
migrate<In = unknown, Out = unknown>(
|
||||
options: ModelVersionTestMigrateOptions<In>
|
||||
): SavedObject<Out>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link ModelVersionTestMigrator | test migrator} that can be used
|
||||
* to test model version changes between versions.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const mySoType = someSoType();
|
||||
* const migrator = createModelVersionTestMigrator({ type: mySoType });
|
||||
*
|
||||
* const obj = createSomeSavedObject();
|
||||
*
|
||||
* const migrated = migrator.migrate({
|
||||
* document: obj,
|
||||
* fromVersion: 1,
|
||||
* toVersion: 2,
|
||||
* });
|
||||
*
|
||||
* expect(migrated.properties).toEqual(myExpectedProperties);
|
||||
* ```
|
||||
*/
|
||||
export const createModelVersionTestMigrator = ({
|
||||
type,
|
||||
}: {
|
||||
type: SavedObjectsType;
|
||||
}): ModelVersionTestMigrator => {
|
||||
const typeRegistry = new SavedObjectTypeRegistry();
|
||||
typeRegistry.registerType({
|
||||
switchToModelVersionAt: globalSwitchToModelVersionAt,
|
||||
...type,
|
||||
});
|
||||
|
||||
const logger = loggerMock.create();
|
||||
|
||||
const migrator = new DocumentMigrator({
|
||||
typeRegistry,
|
||||
log: logger,
|
||||
kibanaVersion: currentVersion,
|
||||
});
|
||||
migrator.prepareMigrations();
|
||||
|
||||
return {
|
||||
migrate: ({ document, fromVersion, toVersion }) => {
|
||||
const docCopy: SavedObject = {
|
||||
...document,
|
||||
coreMigrationVersion: lastCoreVersion,
|
||||
typeMigrationVersion: modelVersionToVirtualVersion(fromVersion),
|
||||
};
|
||||
|
||||
const migratedDoc = migrator.migrate(docCopy, {
|
||||
allowDowngrade: true,
|
||||
targetTypeVersion: modelVersionToVirtualVersion(toVersion),
|
||||
});
|
||||
|
||||
return migratedDoc as SavedObject<any>;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createModelVersionTestBed } from './test_bed';
|
||||
|
||||
export type {
|
||||
ModelVersionTestBed,
|
||||
ModelVersionTestKit,
|
||||
ModelVersionTestkitOptions,
|
||||
SavedObjectTestkitDefinition,
|
||||
} from './types';
|
|
@ -33,5 +33,7 @@
|
|||
"@kbn/doc-links",
|
||||
"@kbn/core-doc-links-server",
|
||||
"@kbn/core-node-server",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/logging-mocks",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue