@eclipse-emfcloud/modelserver-client
Version:
Typescript rest client to interact with an EMF.cloud modelserver
430 lines (340 loc) • 18.6 kB
text/typescript
/********************************************************************************
* Copyright (c) 2022 STMicroelectronics and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0, or the MIT License which is
* available at https://opensource.org/licenses/MIT.
*
* SPDX-License-Identifier: EPL-2.0 OR MIT
*******************************************************************************/
import { expect } from 'chai';
import jsonpatch, { deepClone, Operation } from 'fast-json-patch';
import URI from 'urijs';
import {
add,
create,
IncrementalUpdateNotificationV2,
ModelServerClientV2,
ModelServerNotification,
ModelServerNotificationListenerV2,
ModelServerObjectV2,
NotificationSubscriptionListenerV2,
Operations,
removeObject,
removeValueAt,
replace,
SetCommand
} from '.';
import { ModelServerClientApiV2 } from './model-server-client-api-v2';
describe('Integration tests for ModelServerClientV2', () => {
let client: ModelServerClientV2;
const baseUrl = new URI({
protocol: 'http',
hostname: 'localhost',
port: '8081',
path: ModelServerClientApiV2.API_ENDPOINT
});
const testUndoRedo: (modeluri: URI, originalModel: any, patchedModel: any) => Promise<void> = async (
modeluri,
originalModel,
patchedModel
) => {
// Expected: originalModel === undoModel === patchedUndoModel
// Expected: patchedModel === redoModel === patchedRedoModel
const undoPatch = await client.undo(modeluri);
const undoModel = await client.get(modeluri, ModelServerObjectV2.is);
expect(undoModel).to.deep.equal(originalModel);
const patchedUndoModel = undoPatch.patchModel!(patchedModel, true);
expect(patchedUndoModel).to.deep.equal(originalModel);
const redoPatch = await client.redo(modeluri);
const redoModel = await client.get(modeluri, ModelServerObjectV2.is);
expect(redoModel).to.deep.equal(patchedModel);
const patchedRedoModel = redoPatch.patchModel!(patchedUndoModel, true);
expect(patchedRedoModel).to.deep.equal(patchedModel);
// Restore initial state
await client.undo(modeluri);
};
beforeEach(() => {
client = new ModelServerClientV2();
client.initialize(baseUrl, 'json-v2');
});
describe('test requests', () => {
it('edit with patch', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const patch = replace(modeluri, machine, 'name', newName);
await client.edit(modeluri, patch);
const model = await client.get(modeluri);
expect(model.name).to.be.equal(newName);
await testUndoRedo(modeluri, machine, model);
});
it('create with patch', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const originalModel = await client.get(modeluri, ModelServerObjectV2.is);
const newWorkflowName = 'New Test Workflow';
const initialWorkflowsCount: number = (originalModel as any).workflows.length;
const patch = create(
modeluri,
originalModel,
'workflows',
'http://www.eclipsesource.com/modelserver/example/coffeemodel#//Workflow',
{ name: newWorkflowName }
);
await client.edit(modeluri, patch);
const patchedModel = await client.get(modeluri);
const workflows = patchedModel.workflows as any[];
expect(workflows.length).to.be.equal(initialWorkflowsCount + 1);
const newWorkflow = (patchedModel as any).workflows[initialWorkflowsCount];
expect(newWorkflow.name).to.be.equal(newWorkflowName);
expect(newWorkflow).to.have.property('$id');
expect(newWorkflow.$id).to.be.a('string');
await testUndoRedo(modeluri, originalModel, patchedModel);
});
it('add with patch', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const initialModel = await client.get(modeluri, ModelServerObjectV2.is);
// Add a second workflow to the model; we'll use it to move a Task from a workflow to the other
const createWorkflow = create(
modeluri,
initialModel,
'workflows',
'http://www.eclipsesource.com/modelserver/example/coffeemodel#//Workflow',
{ name: 'New Workflow' }
);
await client.edit(modeluri, createWorkflow);
const originalModel = await client.get(modeluri, ModelServerObjectV2.is);
const sourceWF = (originalModel as any).workflows[0];
const targetWF = (originalModel as any).workflows[1];
const patch = add(modeluri, targetWF, 'nodes', sourceWF.nodes[0]);
await client.edit(modeluri, patch);
const patchedModel = await client.get(modeluri);
const patchedSourceWF = (patchedModel as any).workflows[0];
const patchedTargetWF = (patchedModel as any).workflows[1];
expect(patchedSourceWF.nodes).to.be.undefined;
expect(patchedTargetWF.nodes).to.be.an('array').of.length(1);
expect(patchedTargetWF.nodes[0].name).to.be.equal(sourceWF.nodes[0].name); // Node was moved
await testUndoRedo(modeluri, originalModel, patchedModel);
});
it('delete with patch - index based', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const originalModel = await client.get(modeluri, ModelServerObjectV2.is);
const parentWorkflow = (originalModel as any).workflows[0];
expect(parentWorkflow.nodes).to.be.an('array').that.is.not.empty;
const patch = removeValueAt(modeluri, parentWorkflow, 'nodes', 0);
await client.edit(modeluri, patch);
const patchedModel = await client.get(modeluri);
const patchedParentWorkflow = (patchedModel as any).workflows[0];
expect(patchedParentWorkflow.nodes).to.be.undefined;
await testUndoRedo(modeluri, originalModel, patchedModel);
});
it('delete with patch - object', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const originalModel = await client.get(modeluri, ModelServerObjectV2.is);
const parentWorkflow = (originalModel as any).workflows[0];
expect(parentWorkflow.nodes).to.be.an('array').that.is.not.empty;
const valueToRemove = parentWorkflow.nodes[0];
const patch = removeObject(modeluri, valueToRemove);
await client.edit(modeluri, patch);
const patchedModel = await client.get(modeluri);
const patchedParentWorkflow = (patchedModel as any).workflows[0];
expect(patchedParentWorkflow.nodes).to.be.undefined;
await testUndoRedo(modeluri, originalModel, patchedModel);
});
it('edit with command', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const originalModel = await client.get(modeluri, ModelServerObjectV2.is);
const owner = {
eClass: originalModel.$type,
$ref: URI.build({ path: 'SuperBrewer3000.coffee', fragment: originalModel.$id })
};
const command = new SetCommand(owner, 'name', [newName]);
await client.edit(modeluri, command);
const model = await client.get(modeluri);
expect(model.name).to.be.equal(newName);
await testUndoRedo(modeluri, originalModel, model);
});
it('incremental patch update', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const patch = replace(modeluri, machine, 'name', newName);
const updateResult = await client.edit(modeluri, patch);
expect(updateResult.success).to.be.true;
expect(updateResult.patchModel).to.not.be.undefined;
expect(updateResult.patch).to.not.be.undefined;
// Patch a copy of the model (machine), to make sure the original model is
// unchanged. We'll need it later to check undo/redo behavior.
const patchedMachine = updateResult.patchModel!(machine, true);
expect((patchedMachine as any).name).to.be.equal(newName);
// Check that the incremental update is consistent with the server version of the model
const newMachine = await client.get(modeluri, ModelServerObjectV2.is);
expect(newMachine).to.deep.equal(patchedMachine);
await testUndoRedo(modeluri, machine, patchedMachine);
});
it('subscribe to changes', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const owner = {
eClass: machine.$type,
$ref: URI.build({ path: 'SuperBrewer3000.coffee', fragment: machine.$id })
};
const command = new SetCommand(owner, 'name', [newName]);
const listener: ModelServerNotificationListenerV2 = {};
const patchNotification: Promise<IncrementalUpdateNotificationV2> = new Promise((resolve, _rej) => {
listener.onIncrementalUpdateV2 = resolve;
});
const subscription: Promise<ModelServerNotification> = new Promise((resolve, _rej) => {
listener.onSuccess = resolve;
});
client.subscribe(modeluri, new NotificationSubscriptionListenerV2(listener));
// Make sure the subscription is initialized before editing the model,
// so that we don't miss the notification
await subscription;
await client.edit(modeluri, command);
const notification = await patchNotification;
const patch = notification.patch;
expect(notification.modeluri.toString()).to.be.equal(modeluri.toString());
expect(patch.length).to.be.equal(1);
const operation = patch[0];
expect(Operations.isReplace(operation, 'string')).to.be.equal(true);
expect(operation.path).to.be.equal('/name');
if (Operations.isReplace(operation, 'string')) {
expect(operation.value).to.be.equal(newName);
}
await client.undo(modeluri);
client.unsubscribe(modeluri);
});
it('subscribe to incremental updates', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const owner = {
eClass: machine.$type,
$ref: URI.build({ path: 'SuperBrewer3000.coffee', fragment: machine.$id })
};
const command = new SetCommand(owner, 'name', [newName]);
const listener: ModelServerNotificationListenerV2 = {};
const patchNotification: Promise<IncrementalUpdateNotificationV2> = new Promise((resolve, _rej) => {
listener.onIncrementalUpdateV2 = resolve;
});
const subscription: Promise<ModelServerNotification> = new Promise((resolve, _rej) => {
listener.onSuccess = resolve;
});
client.subscribe(modeluri, new NotificationSubscriptionListenerV2(listener));
// Make sure the subscription is initialized before editing the model,
// so that we don't miss the notification
await subscription;
await client.edit(modeluri, command);
const notification = await patchNotification;
// Apply the incremental patch on the original model
const incrementalPatchedModel = notification.patchModel(machine, true);
// Retrieve the current model from the model server
const patchedModel = await client.get(modeluri, ModelServerObjectV2.is);
// Check that the incrementally-patched model to be identical to the version
// from the model server.
expect(incrementalPatchedModel).to.deep.equal(patchedModel);
await client.undo(modeluri);
client.unsubscribe(modeluri);
});
it('pure Json Patch changes', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const patchedMachine = deepClone(machine);
// Directly change the model
patchedMachine.name = newName;
patchedMachine.children[1].processor.clockSpeed = 6;
// Generate patch by diffing the original model and the patched one
const patch = jsonpatch.compare(machine, patchedMachine);
await client.edit(modeluri, patch);
const model = await client.get(modeluri);
expect(model.name).to.be.equal(newName);
expect((model as any).children[1].processor.clockSpeed).to.be.equal(6);
await testUndoRedo(modeluri, machine, model);
});
it('clear list with "remove" patch operation', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const initialWorkflowsSize = (machine as any).workflows.length;
expect(initialWorkflowsSize).to.not.be.equal(0);
const patch: Operation[] = [
{
op: 'remove',
path: '/workflows'
}
];
await client.edit(modeluri, patch);
const model = await client.get(modeluri);
const newWorkflows = (model as any).workflows;
expect(newWorkflows).to.be.undefined;
await testUndoRedo(modeluri, machine, model);
});
it('unset value with "remove" patch operation', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const initialValue = (machine as any).children[1].processor.thermalDesignPower;
expect(initialValue).to.not.be.oneOf([undefined, 0]);
const patch: Operation[] = [
{
op: 'remove',
path: '/children/1/processor/thermalDesignPower'
}
];
await client.edit(modeluri, patch);
const model = await client.get(modeluri);
const newValue = (model as any).children[1].processor.thermalDesignPower;
expect(newValue).to.be.oneOf([undefined, 0]); // Should be === 0, but default values are not converted to Json at the moment; so we also expect 'undefined'
await testUndoRedo(modeluri, machine, model);
});
it('check all patch replies', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const patchedMachine = deepClone(machine);
// Directly change the model
patchedMachine.name = newName;
patchedMachine.children[1].processor.clockSpeed = 6;
// Generate patch by diffing the original model and the patched one
const patch = jsonpatch.compare(machine, patchedMachine);
const result = await client.edit(modeluri, patch);
expect(result.success).to.be.true;
expect(result.patch).to.not.be.undefined;
expect(result.allPatches).to.not.be.undefined;
expect(result.allPatches).to.be.an('array').of.length(1);
// Patch the main resource
const updatedMachineMainPatch = result.patchModel!(machine, true);
expect(patchedMachine).to.deep.equal(updatedMachineMainPatch);
// Patch the first resource
const updatedMachineFirstPatch = result.patchModel!(machine, true, new URI(result.allPatches![0].modelUri));
expect(patchedMachine).to.deep.equal(updatedMachineFirstPatch);
await testUndoRedo(modeluri, machine, updatedMachineFirstPatch);
});
it('test model patches', async () => {
const modeluri = new URI('SuperBrewer3000.coffee');
const newName = 'Super Brewer 6000';
const machine = await client.get(modeluri, ModelServerObjectV2.is);
const patchedMachine = deepClone(machine);
// Directly change the model
patchedMachine.name = newName;
patchedMachine.children[1].processor.clockSpeed = 6;
// Generate patches by diffing the original model and the patched one
const patch = jsonpatch.compare(machine, patchedMachine);
const result = await client.edit(modeluri, patch);
expect(result.success).to.be.true;
expect(result.patch).to.not.be.undefined;
expect(result.allPatches).to.not.be.undefined;
expect(result.allPatches).to.be.an('array').of.length(1);
// Patch the main resource
const updatedMachineMainPatch = result.patchModel!(machine, true);
expect(patchedMachine).to.deep.equal(updatedMachineMainPatch);
// Patch the first resource
const updatedMachineFirstPatch = result.patchModel!(machine, true, new URI(result.allPatches![0].modelUri));
expect(patchedMachine).to.deep.equal(updatedMachineFirstPatch);
await testUndoRedo(modeluri, machine, updatedMachineFirstPatch);
});
});
});