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:
Pierre Gayvallet 2023-10-04 10:43:33 +02:00 committed by GitHub
parent 3ea2f23708
commit 7baab02533
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 431 additions and 21 deletions

View file

@ -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,

View file

@ -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);
});
});

View file

@ -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;
}

View file

@ -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

View file

@ -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';

View file

@ -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',
});
});
});

View file

@ -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>;
},
};
};

View file

@ -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';

View file

@ -33,5 +33,7 @@
"@kbn/doc-links",
"@kbn/core-doc-links-server",
"@kbn/core-node-server",
"@kbn/config-schema",
"@kbn/logging-mocks",
]
}