152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
|
import { VersionResolver, SmartMigrationError } from '../ts/index.js';
|
||
|
|
import type { IMigrationStepDefinition } from '../ts/index.js';
|
||
|
|
|
||
|
|
const noopHandler = async () => {};
|
||
|
|
|
||
|
|
function makeStep(id: string, from: string, to: string): IMigrationStepDefinition {
|
||
|
|
return { id, fromVersion: from, toVersion: to, isResumable: false, handler: noopHandler };
|
||
|
|
}
|
||
|
|
|
||
|
|
tap.test('equals / lessThan / greaterThan', async () => {
|
||
|
|
expect(VersionResolver.equals('1.0.0', '1.0.0')).toBeTrue();
|
||
|
|
expect(VersionResolver.equals('1.0.0', '1.0.1')).toBeFalse();
|
||
|
|
expect(VersionResolver.lessThan('1.0.0', '1.0.1')).toBeTrue();
|
||
|
|
expect(VersionResolver.greaterThan('2.0.0', '1.99.99')).toBeTrue();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('assertValid: throws on garbage', async () => {
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.assertValid('not-a-version', 'test');
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught).toBeInstanceOf(SmartMigrationError);
|
||
|
|
expect(caught!.code).toEqual('INVALID_VERSION');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('validateChain: empty chain is valid', async () => {
|
||
|
|
VersionResolver.validateChain([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('validateChain: catches duplicate ids', async () => {
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.validateChain([
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('a', '1.1.0', '1.2.0'),
|
||
|
|
]);
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught!.code).toEqual('DUPLICATE_STEP_ID');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('validateChain: catches non-increasing step', async () => {
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.validateChain([makeStep('a', '1.5.0', '1.5.0')]);
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught!.code).toEqual('NON_INCREASING_STEP');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('validateChain: catches gap in chain', async () => {
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.validateChain([
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.2.0', '1.3.0'), // gap! a.to=1.1.0, b.from=1.2.0
|
||
|
|
]);
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught!.code).toEqual('CHAIN_GAP');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('validateChain: contiguous chain passes', async () => {
|
||
|
|
VersionResolver.validateChain([
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.1.0', '1.5.0'),
|
||
|
|
makeStep('c', '1.5.0', '2.0.0'),
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: returns empty when current === target', async () => {
|
||
|
|
const steps = [makeStep('a', '1.0.0', '1.1.0')];
|
||
|
|
expect(VersionResolver.computePlan(steps, '1.0.0', '1.0.0')).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: returns full chain when starting from beginning', async () => {
|
||
|
|
const steps = [
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.1.0', '1.5.0'),
|
||
|
|
makeStep('c', '1.5.0', '2.0.0'),
|
||
|
|
];
|
||
|
|
const plan = VersionResolver.computePlan(steps, '1.0.0', '2.0.0');
|
||
|
|
expect(plan.map((s) => s.id)).toEqual(['a', 'b', 'c']);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: returns partial chain when starting in the middle', async () => {
|
||
|
|
const steps = [
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.1.0', '1.5.0'),
|
||
|
|
makeStep('c', '1.5.0', '2.0.0'),
|
||
|
|
];
|
||
|
|
const plan = VersionResolver.computePlan(steps, '1.1.0', '2.0.0');
|
||
|
|
expect(plan.map((s) => s.id)).toEqual(['b', 'c']);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: returns sub-slice when target is mid-chain', async () => {
|
||
|
|
const steps = [
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.1.0', '1.5.0'),
|
||
|
|
makeStep('c', '1.5.0', '2.0.0'),
|
||
|
|
];
|
||
|
|
const plan = VersionResolver.computePlan(steps, '1.0.0', '1.5.0');
|
||
|
|
expect(plan.map((s) => s.id)).toEqual(['a', 'b']);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: throws when current does not match any step from', async () => {
|
||
|
|
const steps = [
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.1.0', '2.0.0'),
|
||
|
|
];
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.computePlan(steps, '1.0.5', '2.0.0');
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught!.code).toEqual('CHAIN_NOT_AT_CURRENT');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: throws when target overshoots', async () => {
|
||
|
|
const steps = [
|
||
|
|
makeStep('a', '1.0.0', '1.1.0'),
|
||
|
|
makeStep('b', '1.1.0', '2.0.0'),
|
||
|
|
];
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.computePlan(steps, '1.0.0', '1.5.0');
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught!.code).toEqual('TARGET_NOT_REACHABLE');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('computePlan: rejects downgrade', async () => {
|
||
|
|
const steps = [makeStep('a', '1.0.0', '2.0.0')];
|
||
|
|
let caught: SmartMigrationError | undefined;
|
||
|
|
try {
|
||
|
|
VersionResolver.computePlan(steps, '2.0.0', '1.0.0');
|
||
|
|
} catch (err) {
|
||
|
|
caught = err as SmartMigrationError;
|
||
|
|
}
|
||
|
|
expect(caught!.code).toEqual('DOWNGRADE_NOT_SUPPORTED');
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|