@contract-case/case-core
Version:
Core functionality for the ContractCase contract testing suite
195 lines • 10.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReadingCaseContract = void 0;
const async_mutex_1 = require("async-mutex");
const case_plugin_base_1 = require("@contract-case/case-plugin-base");
const BaseCaseContract_1 = require("./BaseCaseContract");
const executeExample_1 = require("./executeExample");
const entities_1 = require("../entities");
const slugs_1 = require("./slugs");
/**
* ReadingCaseContract deals with a single contract verification (read).
*
* It is the entry point for the actual verification process of a contract,
* but should be called from a connector. At the time of writing, the
* ContractVerifierConnector is the primary caller.
*
* @internal
*/
class ReadingCaseContract extends BaseCaseContract_1.BaseCaseContract {
mutex;
makeBrokerService;
/**
* The verifier has its own copy of the contract,
* because some of the BaseCaseContract methods modify
* the contract.
*/
contractFileFromDisk;
/**
* What the verification status currently is.
*
* This is the true verification status; during a run it will be UNKNOWN
* if no current interactions have failed. It will be set to FAILED if
* any interaction fails.
*
* It can only be success once endRecord has been called.
*/
status;
/**
* Indicates that the contract has been closed and verification is complete.
* Used to prevent attempts to calculate verification status twice.
*/
contractClosed = false;
/**
* The tests passed back by a call to {@link ReadingCaseContract#getTests}
*
* Will be undefined if getTests has not been called.
*/
verificationTests = undefined;
/**
* Constructs a ReadingCaseContract
*
* @param contractFile - The DownloadedContract to verify
* @param readerDependencies - The dependencies for a contract reader (injected)
* @param config - the CaseConfig for this run
* @param parentVersions - the array of versions of all the ContractCase packages before this one
*/
constructor(contractFile, { resultFormatter: resultPrinter, makeLogger, defaultConfig, makeBrokerService, }, config, parentVersions) {
super(contractFile.contents.description, { throwOnFail: false, testRunId: 'VERIFIER', ...config }, defaultConfig, resultPrinter, makeLogger, parentVersions);
this.currentContract = contractFile.contents;
this.makeBrokerService = makeBrokerService;
this.contractFileFromDisk = contractFile;
this.status = 'UNKNOWN';
this.mutex = new async_mutex_1.Mutex();
}
/**
* Calls the executeExample function for a specific interaction.
*
* @param index - The index of the interaction to execute
* @param invoker - The invoker for this test
* @param completionCallback - A callback to be called before completing the example
* @returns A promise that resolves when the interaction has been executed completely
*/
callExecuteExample(index, invoker, completionCallback = () => { }) {
if (this.contractClosed) {
throw new case_plugin_base_1.CaseConfigurationError('Unable to write more interactions to the contract after endRecord() has been called', this.initialContext, 'UNDOCUMENTED');
}
return this.mutex.runExclusive(() => Promise.resolve()
.then(() => {
const example = this.currentContract.examples[index];
if (example == null) {
this.initialContext.logger.error(`Somehow the example at index ${index} was undefined. This shouldn't happen, as calls to this function are meant to be based off the indexes. Examples follow:`, this.currentContract.examples);
throw new case_plugin_base_1.CaseCoreError(`Somehow the example at index ${index} was undefined. This is a bug, please see the log for details.`);
}
if (example.result !== 'VERIFIED') {
throw new case_plugin_base_1.CaseCoreError(`Attempting to verify an interaction which didn't pass the consumer test ('${example.result}'). This should never happen in normal operation, and might be the result of a corrupted ContractCase file, a file that was not written by ContractCase, or a bug.`);
}
const names = (0, entities_1.exampleToNames)(example, `${index}`);
// Set running context instead of inlining this, so that
// stripMatchers etc have access to the context
this.runningContext = (0, case_plugin_base_1.applyNodeToContext)(example.mock, this.initialContext, {
'_case:currentRun:context:testName': `${index}`,
'_case:currentRun:context:contractMode': 'read',
'_case:currentRun:context:location': [
'verification',
`interaction[${index}]`,
],
});
this.initialContext.logger.maintainerDebug(`Run test callback for ${names.mockName}`);
return (0, executeExample_1.executeExample)({ ...example, result: 'PENDING' }, {
...invoker,
names,
}, this, this.runningContext);
})
.finally(() => {
try {
completionCallback();
}
catch (e) {
this.runningContext.logger.error(`BUG: Error in completion callback: ${e.message}`, e);
}
}));
}
/**
* Gets the tests that can be used later to verify the contract
*
* @param invoker - The invoker for this test
* @returns a list of {@link ContractVerificationTest}s that can be run later
* with the `runTest` callback on the ContractVerificationTest
*/
getTests(invoker) {
this.initialContext.logger.maintainerDebug(`Generating tests for contract: '${this.currentContract.description.consumerName}' -> '${this.currentContract.description.providerName}'`);
this.initialContext.logger.maintainerDebug(`This contract has ${this.currentContract.examples.length} interactions`);
this.verificationTests = this.currentContract.examples.map((example, index) => {
const names = (0, entities_1.exampleToNames)(example, `${index}`);
this.initialContext.logger.maintainerDebug(`Preparing test framework's callback for: ${names.mockName} `);
let isPending = true;
return {
index,
testName: names.mockName,
isPending: () => isPending,
runTest: () => this.callExecuteExample(index, invoker, () => {
isPending = false;
}),
};
});
return this.verificationTests;
}
recordExample(example, currentContext) {
currentContext.logger.deepMaintainerDebug(`recordExample called with`, example);
if (example.result === 'FAILED') {
currentContext.logger.maintainerDebug(`Interaction was a failure, marking verification failed (was '${this.status}')`);
this.status = 'FAILED';
}
else {
currentContext.logger.maintainerDebug(`Interaction was a success, no change to current status of '${this.status}'`);
}
return example;
}
async endRecord() {
return Promise.resolve((0, case_plugin_base_1.addLocation)('PublishingResults', this.initialContext))
.then((publishingContext) => {
this.contractClosed = true;
if (this.verificationTests == null) {
throw new case_plugin_base_1.CaseConfigurationError(`No verification tests had been prepared; you must call prepareVerification before closing the contract with endRecord()
This may be a bug in the language specifc DSL wrapper.`, publishingContext, case_plugin_base_1.ErrorCodes.configuration.INVALID_LIFECYCLE);
}
if (this.status === 'UNKNOWN') {
// No interactions have failed, let's see if we ran them all
const isComplete = this.verificationTests.every((test) => !test.isPending());
if (isComplete) {
publishingContext.logger.maintainerDebug(`All interactions have passed, marking verification successful`);
this.status = 'SUCCESS';
}
else {
publishingContext.logger.error(`Some interactions were still pending! This means that some of the test callbacks were not invoked. List follows:`);
this.verificationTests.forEach((test) => {
publishingContext.logger.error(`Interaction ${test.isPending() ? 'PENDING' : 'COMPLETE'} ${test.testName}`);
});
throw new case_plugin_base_1.CaseConfigurationError(`Some interactions were still pending when verification status was calculated.
This means that some of the test callbacks were not invoked.
See the error logs for details.`, publishingContext, case_plugin_base_1.ErrorCodes.configuration.INVALID_LIFECYCLE);
}
}
if (this.status === 'FAILED') {
// TODO: Print all failures
publishingContext.logger.maintainerDebug('Verification failed');
}
else {
publishingContext.logger.maintainerDebug('Verification successful');
}
publishingContext.logger.maintainerDebug('Calling publishVerificationResults');
return this.makeBrokerService(publishingContext).publishVerificationResults(this.contractFileFromDisk.contents, this.status === 'SUCCESS', (0, case_plugin_base_1.addLocation)(`PublishingVerification(${this.currentContract.description.consumerName} -> ${this.currentContract.description.providerName})`, publishingContext));
})
.then(() => ({
metadata: this.contractFileFromDisk.contents.metadata,
contractPath: this.contractFileFromDisk.filePath,
description: this.contractFileFromDisk.contents.description,
consumerSlug: (0, slugs_1.consumerSlug)(this.contractFileFromDisk.contents),
providerSlug: (0, slugs_1.providerSlug)(this.contractFileFromDisk.contents),
verificationResult: this.status === 'SUCCESS' ? 'COMPATIBILE' : 'INCOMPATIBLE',
}));
}
}
exports.ReadingCaseContract = ReadingCaseContract;
//# sourceMappingURL=ReadingCaseContract.js.map