61 lines
6.0 KiB
JavaScript
61 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const { createInstallRecipeService } = require('../../src/services/install-recipe-service');
|
|
|
|
const component = (componentId, type, options = {}) => ({ componentId, type, required: options.required ?? true, version: options.version || '1.00', contentId: options.contentId || `UP0001-CUSA12345_00-${componentId.toUpperCase().padEnd(16, 'X')}`, region: options.region || 'US', metadata: { platform: 'ps4', titleId: { value: 'CUSA12345' } } });
|
|
function packageSet() {
|
|
return { id: 7, platform: 'ps4', region: 'US', components: [component('base', 'base'), component('update', 'update', { version: '1.02' }), component('dlc', 'dlc', { required: false })], dependencies: [{ componentId: 'update', dependsOnComponentId: 'base', kind: 'requires' }] };
|
|
}
|
|
const step = (id, componentId, action = 'install-package', options = {}) => ({ id, componentId, action, required: options.required ?? true, dependsOn: options.dependsOn || [], destinationCapabilities: options.destinationCapabilities || [], orderHint: options.orderHint });
|
|
const recipe = steps => ({ schemaVersion: 1, recipeVersion: 3, packageSetId: 7, notes: 'Read-only operator guidance.', steps });
|
|
const capabilities = { canInstall: true, canCopy: true, canMount: true, canRead: true, stagingPatch: true };
|
|
|
|
test('DAG validation produces deterministic base-before-update order and frozen job version', () => {
|
|
const service = createInstallRecipeService({ now: () => 1000 });
|
|
const input = recipe([step('update-step', 'update', 'install-package', { orderHint: 1 }), step('base-step', 'base', 'install-package', { orderHint: 2 })]);
|
|
const first = service.validate(input, packageSet(), { destinationCapabilities: capabilities });
|
|
const second = service.validate(input, packageSet(), { destinationCapabilities: capabilities });
|
|
assert.deepEqual(first.orderedSteps.map(item => item.id), ['base-step', 'update-step']);
|
|
assert.deepEqual(first.orderedSteps, second.orderedSteps);
|
|
const frozen = service.freezeForJob('job-1', input, packageSet(), { destinationCapabilities: capabilities, expectedRecipeVersion: 3 });
|
|
assert.equal(frozen.recipeVersion, 3);
|
|
assert.match(frozen.recipeSha256, /^[0-9a-f]{64}$/);
|
|
assert.equal(Object.isFrozen(frozen.recipe.orderedSteps), true);
|
|
assert.throws(() => service.freezeForJob('job-1', input, packageSet(), { destinationCapabilities: capabilities, expectedRecipeVersion: 2 }), error => error.code === 'RECIPE_VERSION_CHANGED');
|
|
});
|
|
|
|
test('cycles and missing components/dependencies are rejected', () => {
|
|
const service = createInstallRecipeService();
|
|
const cyclic = recipe([step('base-step', 'base', 'install-package', { dependsOn: ['update-step'] }), step('update-step', 'update', 'install-package', { dependsOn: ['base-step'] })]);
|
|
assert.throws(() => service.validate(cyclic, packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_CYCLE');
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'missing')]), packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_COMPONENT_MISSING');
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'base'), step('update-step', 'update', 'install-package', { dependsOn: ['gone'] })]), packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_DEPENDENCY_MISSING');
|
|
});
|
|
|
|
test('base/update/DLC content, region, platform and destination mismatches fail closed', () => {
|
|
const service = createInstallRecipeService();
|
|
const mismatched = packageSet(); mismatched.components[1].contentId = 'UP0001-CUSA99999_00-UPDATEPACKAGE001';
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'base'), step('update-step', 'update')]), mismatched, { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_CONTENT_MISMATCH');
|
|
const wrongRegion = packageSet(); wrongRegion.components[1].region = 'EU';
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'base'), step('update-step', 'update')]), wrongRegion, { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_REGION_MISMATCH');
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'base'), step('update-step', 'update')]), packageSet(), { destinationCapabilities: { ...capabilities, canInstall: false } }), error => error.code === 'RECIPE_DESTINATION_CAPABILITY');
|
|
});
|
|
|
|
test('required components cannot be omitted while optional DLC may be absent', () => {
|
|
const service = createInstallRecipeService();
|
|
const valid = service.validate(recipe([step('base-step', 'base'), step('update-step', 'update')]), packageSet(), { destinationCapabilities: capabilities });
|
|
assert.equal(valid.orderedSteps.length, 2);
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'base')]), packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_REQUIRED_COMPONENT');
|
|
assert.throws(() => service.validate(recipe([step('base-step', 'base', 'install-package', { required: false }), step('update-step', 'update')]), packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_REQUIRED_COMPONENT');
|
|
});
|
|
|
|
test('hostile schema fields, shell actions and raw destinations are never accepted', () => {
|
|
const service = createInstallRecipeService();
|
|
const base = recipe([step('base-step', 'base'), step('update-step', 'update')]);
|
|
assert.throws(() => service.validate({ ...base, command: 'rm -rf /' }, packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_SCHEMA');
|
|
assert.throws(() => service.validate({ ...base, steps: [{ ...base.steps[0], shell: 'echo owned' }, base.steps[1]] }, packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_SCHEMA');
|
|
assert.throws(() => service.validate({ ...base, steps: [{ ...base.steps[0], action: 'shell', destination: '/mnt/raw' }, base.steps[1]] }, packageSet(), { destinationCapabilities: capabilities }), error => error.code === 'RECIPE_SCHEMA');
|
|
});
|