@contract-case/case-core
Version:
Core functionality for the ContractCase contract testing suite
166 lines (164 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseCaseContract = void 0;
const case_plugin_base_1 = require("@contract-case/case-plugin-base");
const case_plugin_dsl_types_1 = require("@contract-case/case-plugin-dsl-types");
const structure_1 = require("./structure");
const config_1 = require("./config");
const defaultTestId_1 = require("./defaultTestId");
const diffmatch_1 = require("../diffmatch");
const entities_1 = require("../entities");
const plugins_1 = require("./plugins");
class BaseCaseContract {
currentContract;
initialContext;
/**
* The currently executing context. Used to make it easy to do things
* like get state variables during a stripMatchers call inside a test trigger
*/
runningContext;
userProvidedFunctions;
constructor(description, config, defaultConfig, resultPrinter, makeLogger, parentVersions, userProvidedFunctions = {}) {
this.currentContract = (0, structure_1.makeContract)(description);
this.userProvidedFunctions = userProvidedFunctions;
const contractFns = {
lookupMatcher: this.lookupMatcher.bind(this),
saveLookupableMatcher: this.saveLookupableMatcher.bind(this),
addVariable: this.addVariable.bind(this),
lookupVariable: this.lookupVariable.bind(this),
};
const invokeHandle = this.invokeFunctionByHandle.bind(this);
const makeLookup = (context) => ({
lookupMatcher: (uniqueName) => contractFns.lookupMatcher(uniqueName, context),
saveLookupableMatcher: (matcher) => contractFns.saveLookupableMatcher(matcher, context),
addDefaultVariable: (name, stateName, value) => contractFns.addVariable(name, 'default', stateName, value, context),
addStateVariable: (name, stateName, value) => contractFns.addVariable(name, 'state', stateName, value, context),
lookupVariable: (name) => contractFns.lookupVariable(name, context),
invokeFunctionByHandle: (handle, callerArguments) => invokeHandle(handle, callerArguments, context),
});
this.initialContext = (0, case_plugin_base_1.constructMatchContext)(diffmatch_1.traversals, makeLogger, makeLookup, resultPrinter, { ...(0, config_1.configToRunContext)({ ...defaultConfig, ...config }) }, defaultConfig, parentVersions);
this.runningContext = this.initialContext;
if (this.initialContext['_case:currentRun:context:testRunId'] ===
defaultTestId_1.DEFAULT_TEST_ID) {
this.initialContext.logger.warn("The 'testRunId' config property has not been set for this run. It is recommended to set it for each CaseContract() or CaseVerifier() invocation.");
}
this.initialContext.logger.maintainerDebug(`Running on node ${process.version}, arch ${process.arch}`);
(0, plugins_1.loadCorePlugins)(this.initialContext);
}
registerFunction(handle, invokeableFn) {
if (handle in this.userProvidedFunctions) {
const message = `Registering a function with handle '${handle}', but a function with this handle is already registered. Make sure you are only calling registerHandle once.`;
this.initialContext.logger.error(message);
this.initialContext.logger.deepMaintainerDebug('When trying to register the duplicate, the userProvidedFunctions were:', this.userProvidedFunctions);
throw new case_plugin_base_1.CaseConfigurationError(message, 'DONT_ADD_LOCATION');
}
this.initialContext.logger.debug(`Registered a user-provided invokable function with name '${handle}'`);
this.userProvidedFunctions[handle] = invokeableFn;
}
invokeFunctionByHandle(handle, callerArguments, context) {
return Promise.resolve()
.then(() => {
context.logger.maintainerDebug(`Invoking function by handle: '${handle}', with callerArguments: `, callerArguments);
const invokeableFn = this.userProvidedFunctions[handle];
if (invokeableFn == null) {
const message = `Tried to invoke a user-provided function with the handle '${handle}', but it didn't exist\nMake sure you have used registerFunction to define a function with this handle when setting up your test`;
context.logger.error(message);
context.logger.deepMaintainerDebug('When trying to add the duplicate, the userProvidedFunctions were:', this.userProvidedFunctions);
throw new case_plugin_base_1.CaseConfigurationError(message, context);
}
return invokeableFn;
})
.then((invokeableFn) => invokeableFn(...callerArguments));
}
lookupVariable(name, context) {
if (this.currentContract === undefined) {
context.logger.error(`lookupVariable was called without initialising the contract file. This should not be possible.`);
throw new case_plugin_base_1.CaseCoreError('Contract was not initialised at the time that lookupVariable was called');
}
const defaultVariable = (0, structure_1.findVariable)(this.currentContract, name, context);
if (defaultVariable === undefined) {
throw new case_plugin_base_1.CaseConfigurationError(`Variable '${name}' was requested but appears not to be set. Check that the variable is spelt correctly, and the states for this mock are correctly setup`, context, 'BAD_INTERACTION_DEFINITION');
}
return (0, entities_1.coreShapedLike)(defaultVariable);
}
addVariable(name, type, stateName, value, context) {
context.logger.maintainerDebug(`Setting state variable '${name}', in test '${context['_case:currentRun:context:testName']}' to value:`, value);
if (this.currentContract === undefined) {
context.logger.error(`addVariable was called by state '${stateName}' without initialising the contract file. This should not be possible.`);
throw new case_plugin_base_1.CaseCoreError('Contract was not initialised at the time that addVariable was called');
}
this.currentContract = (0, structure_1.addVariable)(this.currentContract, name, type, (0, entities_1.coreShapedLike)(value), context);
return [name, value];
}
saveLookupableMatcher(namedMatcher, context) {
if (this.currentContract === undefined) {
context.logger.error('saveNamedMatcher was called without initialising the contract file. This should not be possible.');
throw new case_plugin_base_1.CaseCoreError('Contract was not initialised at the time that saveNamedMatcher was called');
}
if (!(0, case_plugin_dsl_types_1.isLookupableMatcher)(namedMatcher)) {
context.logger.error('Non-lookup matcher incorrectly received in saveLookupMatcher. Matcher follows', namedMatcher);
throw new case_plugin_base_1.CaseCoreError(`A non-lookup matcher was passed to saveLookupableMatcher. This is an error in a matcher or mock implementation. Please open an issue.`, context);
}
this.currentContract = (0, structure_1.addLookupableMatcher)(this.currentContract, namedMatcher, context);
return this.currentContract;
}
lookupMatcher(uniqueName, context) {
if (!this.currentContract) {
context.logger.error('lookupMatcher was called without initialising the contract file. This should not be possible.');
throw new case_plugin_base_1.CaseCoreError('Contract was not initialised at the time that lookupMatcher was called');
}
// Have to pull this out, because typescript can't see that it's the same result even if we call twice
const possibleMatch = (0, structure_1.findMatcher)(this.currentContract, uniqueName, context);
if (possibleMatch !== undefined) {
return possibleMatch;
}
throw new case_plugin_base_1.CaseConfigurationError(`Contract did not contain a matcher with the name '${uniqueName}'.
This can happen if you have an interaction that asks for a
named matcher before it has been defined.
It can also happen if the named matcher has been misspelt
`, context, 'BAD_INTERACTION_DEFINITION');
}
/**
* Strips the matchers from a (potentially) case matcher and returns
* the concrete example it would match.
*
* Note this isn't the only example it could match - just the default version.
*
* WARNING: If called outside a test, this function runs using the context of
* the last test run. This means that state variables might not be correct.
* Perhaps we should consider making it an error to call this if a test
* isn't running?
*
* @param matcherOrData - the matcher or data to strip the matchers from
* @returns the concrete example
*/
stripMatchers(matcherOrData) {
this.runningContext.logger.maintainerDebug('Stripping matchers from: ', matcherOrData);
this.runningContext.logger.deepMaintainerDebug('At the time of this strip, the current context is', this.runningContext);
this.runningContext.logger.deepMaintainerDebug('And the lookupable variables are:', Object.keys(this.currentContract.matcherLookup).filter((key) => key.startsWith('variable')));
const strippedResult = diffmatch_1.traversals.descendAndStrip(matcherOrData, (0, case_plugin_base_1.applyNodeToContext)(matcherOrData, this.runningContext));
this.runningContext.logger.maintainerDebug('Stripped result was: ', strippedResult);
return strippedResult;
}
/**
* Checks if a matcher matchers some actual data.
*
* This is only exposed for tests at the moment - if it were exposed further,
* we'd need to use the running test context like stripMatchers does.
*
*
* WARNING: If called outside a test, this function runs using the context of
* the last test run. This means that state variables might not be correct.
*
* @param matcherOrData - the matcher or data that is expected
* @param actual - the actual data to match against
* @returns a {@link MatchResult} object with the results of the match
*/
checkMatch(matcherOrData, actual) {
return Promise.resolve()
.then(() => diffmatch_1.traversals.selfVerify(matcherOrData, (0, case_plugin_base_1.applyNodeToContext)(matcherOrData, this.initialContext)))
.then(() => diffmatch_1.traversals.descendAndCheck(matcherOrData, (0, case_plugin_base_1.applyNodeToContext)(matcherOrData, this.initialContext), actual));
}
}
exports.BaseCaseContract = BaseCaseContract;
//# sourceMappingURL=BaseCaseContract.js.map