@eclipse-emfcloud/model-validation
Version:
Generic model validation framework.
265 lines (258 loc) • 11.1 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2023-2024 STMicroelectronics.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: MIT License which is
// available at https://opensource.org/licenses/MIT.
//
// SPDX-License-Identifier: EPL-2.0 OR MIT
// *****************************************************************************
import Chai, { expect } from 'chai';
import Sinon from 'sinon';
import SinonChai from 'sinon-chai';
import { ModelValidationServiceImpl } from '../model-validation-service';
import { Diagnostic, ok } from '../diagnostic';
import { Validator } from '../validator';
Chai.use(SinonChai);
describe('ModelValidationServiceImpl', () => {
let modelValidationService: ModelValidationServiceImpl<string>;
let validatorOK: Validator<string, object>;
let validatorError: Validator<string, object>;
let validatorException: Validator<string, object>;
let validatorReject: Validator<string, object>;
const customOKInstance: Diagnostic = {
severity: 'ok',
source: '@example/test',
code: '0',
path: '',
message: 'Hello.',
};
const errorInstance: Diagnostic = {
severity: 'error',
source: '@example/test',
code: '4',
path: 'foo/errorProp',
message: 'Error, error!',
};
let consoleWarnStub: Sinon.SinonStub<Parameters<typeof console.warn>>;
beforeEach(() => {
modelValidationService = new ModelValidationServiceImpl();
validatorOK = {
validate: (_modelId: string, _model: object) => {
return Promise.resolve(customOKInstance);
},
};
validatorError = {
validate: (_modelId: string, _model: object) => {
return Promise.resolve(errorInstance);
},
};
validatorException = {
validate: (_modelId: string, _model: object) => {
throw new Error('This is an error');
},
};
validatorReject = {
validate: (_modelId: string, _model: object) => {
return Promise.reject('rejected Promise');
},
};
consoleWarnStub = Sinon.stub(console, 'warn');
});
afterEach(() => {
consoleWarnStub.restore();
});
describe('validate', () => {
it('validate w/o validator', async () => {
const result = await modelValidationService.validate('key1', {});
expect(result).to.be.deep.equal(ok());
});
it('getValidationState for ok Diagnostic validate', async () => {
modelValidationService.addValidator(validatorOK);
await modelValidationService.validate('key1', {});
expect(
modelValidationService.getValidationState('key1')
).to.be.deep.equal(ok());
});
it('getValidationState for multi validate', async () => {
modelValidationService.addValidator(validatorOK);
modelValidationService.addValidator(validatorError);
await modelValidationService.validate('key1', {});
expect(
modelValidationService.getValidationState('key1')
).to.be.deep.equal(errorInstance);
});
it('throws exception from validator do not crash the whole process', async () => {
modelValidationService.addValidator(validatorOK);
modelValidationService.addValidator(validatorException);
await modelValidationService.validate('key1', {});
expect(
modelValidationService.getValidationState('key1')
).to.be.deep.equal(ok());
const msg = `An error occurred within a validator during the validation of 'key1'. Error: This is an error. Validation continues ignoring the failed validator`;
expect(
consoleWarnStub.calledWith(
'model-validation/model-validation-service:',
msg
)
).to.be.true;
});
it('rejects the promises from validator do not crash the whole process', async () => {
modelValidationService.addValidator(validatorOK);
modelValidationService.addValidator(validatorReject);
await modelValidationService.validate('key1', {});
expect(
modelValidationService.getValidationState('key1')
).to.be.deep.equal(ok());
const msg = `An error occurred within a validator during the validation of 'key1' (cause: rejected Promise). Validation continues ignoring the failed validator`;
expect(
consoleWarnStub.calledWith(
'model-validation/model-validation-service:',
msg
)
).to.be.true;
});
});
describe('subscribe', () => {
it('subscribe to the updates to the validation state of a single model', async () => {
const subscription = modelValidationService.subscribe('key1');
subscription.onValidationChanged = Sinon.spy();
await modelValidationService.validate('key1', {});
await modelValidationService.validate('key1', {});
expect(subscription.onValidationChanged).to.be.calledOnce;
await modelValidationService.validate('key2', {});
expect(subscription.onValidationChanged).to.be.calledOnce;
subscription.close();
await modelValidationService.validate('key1', {});
expect(subscription.onValidationChanged).to.be.calledOnce;
});
it('subscribe to the updates to the validation state w/o onValidationChanges', async () => {
const subscriptionA = modelValidationService.subscribe('key1');
const subscriptionB = modelValidationService.subscribe('key1');
subscriptionA.onValidationChanged = Sinon.spy();
await modelValidationService.validate('key1', {});
expect(subscriptionA.onValidationChanged).to.be.calledOnce;
subscriptionB.close();
subscriptionB.close();
});
it('subscribe to the updates to the validation state of several models', async () => {
const subscription = modelValidationService.subscribe(
'key1',
'key2',
'key3'
);
subscription.onValidationChanged = Sinon.spy();
await modelValidationService.validate('key1', {});
await modelValidationService.validate('key2', {});
expect(subscription.onValidationChanged).to.be.calledTwice;
subscription.close();
await modelValidationService.validate('key1', {});
await modelValidationService.validate('key2', {});
await modelValidationService.validate('key3', {});
expect(subscription.onValidationChanged).to.be.calledTwice;
});
it('subscribe to the updates to the validation state of all models', async () => {
const subscription = modelValidationService.subscribe();
subscription.onValidationChanged = Sinon.spy();
await modelValidationService.validate('key1', {});
await modelValidationService.validate('key2', {});
expect(subscription.onValidationChanged).to.be.calledTwice;
await modelValidationService.validate('key2', {});
expect(subscription.onValidationChanged).to.be.calledTwice;
subscription.close();
await modelValidationService.validate('key3', {});
expect(subscription.onValidationChanged).to.be.calledTwice;
});
it('subscribe to the updates to the validation state of all models w/o onValidationChanges', async () => {
const subscriptionA = modelValidationService.subscribe();
subscriptionA.onValidationChanged = Sinon.spy();
const subscriptionB = modelValidationService.subscribe();
await modelValidationService.validate('key1', {});
expect(subscriptionA.onValidationChanged).to.be.calledOnce;
subscriptionB.close();
subscriptionB.close();
});
it('subscribe to the updates of the validation state of several models with one onValidationChanged throwing an error', async () => {
const subscriptionA = modelValidationService.subscribe(
'key1',
'key2',
'key3'
);
subscriptionA.onValidationChanged = (
_modelId: string,
_model: object,
_diagnostic: Diagnostic
) => {
throw new Error('This is an onValidationChanged exception');
};
const subscriptionB = modelValidationService.subscribe(
'key1',
'key2',
'key3'
);
subscriptionB.onValidationChanged = Sinon.spy();
await modelValidationService.validate('key1', {});
const msg1 = `An error occurred within the onValidationChanged callback for 'key1'. Error: This is an onValidationChanged exception. Other subscribers will still be notified ignoring the failed callback`;
expect(
consoleWarnStub.calledWith(
'model-validation/model-validation-service:',
msg1
)
).to.be.true;
await modelValidationService.validate('key2', {});
const msg2 = `An error occurred within the onValidationChanged callback for 'key2'. Error: This is an onValidationChanged exception. Other subscribers will still be notified ignoring the failed callback`;
expect(
consoleWarnStub.calledWith(
'model-validation/model-validation-service:',
msg2
)
).to.be.true;
expect(subscriptionB.onValidationChanged).to.be.calledTwice;
subscriptionA.close();
subscriptionB.close();
});
it('subscribe to the updates of the validation state of all models with one onValidationChanged throwing an error', async () => {
const subscriptionA = modelValidationService.subscribe();
subscriptionA.onValidationChanged = (
_modelId: string,
_model: object,
_diagnostic: Diagnostic
) => {
throw new Error('This is an onValidationChanged exception');
};
const subscriptionB = modelValidationService.subscribe();
subscriptionB.onValidationChanged = Sinon.spy();
await modelValidationService.validate('key1', {});
expect(subscriptionB.onValidationChanged).to.be.calledOnce;
const msg = `An error occurred within the onValidationChanged callback for 'key1'. Error: This is an onValidationChanged exception. Other subscribers will still be notified ignoring the failed callback`;
expect(
consoleWarnStub.calledWith(
'model-validation/model-validation-service:',
msg
)
).to.be.true;
subscriptionA.close();
subscriptionB.close();
});
it('get current validation state in subscription', async () => {
modelValidationService.addValidator(validatorError);
const subscriptionA = modelValidationService.subscribe();
let currentState: Diagnostic | undefined;
subscriptionA.onValidationChanged = (
modelId: string,
_model: object,
_diagnostic: Diagnostic
) => {
currentState = modelValidationService.getValidationState(modelId);
};
const diagnostic = await modelValidationService.validate('key1', {});
expect(currentState).to.equal(diagnostic);
expect(currentState?.severity).to.equal('error');
});
});
});