diff --git a/packages/case-core/src/core/BaseCaseContract.ts b/packages/case-core/src/core/BaseCaseContract.ts index a8f144d5..33e0731d 100644 --- a/packages/case-core/src/core/BaseCaseContract.ts +++ b/packages/case-core/src/core/BaseCaseContract.ts @@ -34,6 +34,7 @@ import type { CaseConfig } from './config/types'; import { DEFAULT_TEST_ID } from './defaultTestId'; import { traversals } from '../diffmatch'; import { coreShapedLike } from '../entities'; +import { loadPlugins } from './plugins'; export class BaseCaseContract { currentContract: ContractData; @@ -97,6 +98,8 @@ export class BaseCaseContract { // TODO: put in a URL to the documentation here ); } + + loadPlugins(this.initialContext); } lookupVariable( diff --git a/packages/case-core/src/core/executeExample/index.ts b/packages/case-core/src/core/executeExample/index.ts index 63b7db2f..c2b37e14 100644 --- a/packages/case-core/src/core/executeExample/index.ts +++ b/packages/case-core/src/core/executeExample/index.ts @@ -1 +1,2 @@ export * from './executeExample'; +export * from './setup'; diff --git a/packages/case-core/src/core/executeExample/setup/setupMock/index.ts b/packages/case-core/src/core/executeExample/setup/setupMock/index.ts index b86ec5ed..73426ec8 100644 --- a/packages/case-core/src/core/executeExample/setup/setupMock/index.ts +++ b/packages/case-core/src/core/executeExample/setup/setupMock/index.ts @@ -1,2 +1,2 @@ export * from './setupMock'; -export * from '../../../../diffmatch/plugins/loadPlugin'; +export * from '../../../plugins/mockExecutors'; diff --git a/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/mockExecutor.ts b/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/mockExecutor.ts index b1b5ffe3..3218b5d3 100644 --- a/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/mockExecutor.ts +++ b/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/mockExecutor.ts @@ -10,7 +10,6 @@ import { MockData, } from '@contract-case/case-plugin-base'; -import { loadPlugins } from './loadPlugins'; import { MockSetupFns } from '../../../../../diffmatch/types'; const inferMock = ( @@ -52,8 +51,6 @@ const executeMock = ( ); } - loadPlugins(context); - const executor = MockSetup[mockType]; if (!executor) { throw new CaseCoreError(`Missing setup for mock type '${mockType}'`); diff --git a/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutors.ts b/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutors.ts deleted file mode 100644 index f9ce8412..00000000 --- a/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { MockSetupFns } from '../../../../diffmatch/plugins/types'; - -export const MockExecutors: MockSetupFns = {} as MockSetupFns; diff --git a/packages/case-core/src/core/executeExample/setup/setupMock/setupMock.ts b/packages/case-core/src/core/executeExample/setup/setupMock/setupMock.ts index c05e838c..fab4e145 100644 --- a/packages/case-core/src/core/executeExample/setup/setupMock/setupMock.ts +++ b/packages/case-core/src/core/executeExample/setup/setupMock/setupMock.ts @@ -10,7 +10,7 @@ import { CaseConfigurationError, } from '@contract-case/case-plugin-base'; import { mockExecutor } from './mockExecutor'; -import { MockExecutors } from './mockExecutors'; +import { MockExecutors } from '../../../plugins/mockExecutors'; import { Assertable } from '../../../../entities/types'; export const setupMock = ( diff --git a/packages/case-core/src/core/plugins/index.ts b/packages/case-core/src/core/plugins/index.ts new file mode 100644 index 00000000..e07ba983 --- /dev/null +++ b/packages/case-core/src/core/plugins/index.ts @@ -0,0 +1 @@ +export * from './loadPlugins'; diff --git a/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/loadPlugins.ts b/packages/case-core/src/core/plugins/loadPlugins.ts similarity index 86% rename from packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/loadPlugins.ts rename to packages/case-core/src/core/plugins/loadPlugins.ts index b12a195d..63d7a9a6 100644 --- a/packages/case-core/src/core/executeExample/setup/setupMock/mockExecutor/loadPlugins.ts +++ b/packages/case-core/src/core/plugins/loadPlugins.ts @@ -8,8 +8,8 @@ import { } from '@contract-case/case-core-plugin-http-dsl'; import { MatchContext } from '@contract-case/case-plugin-base'; -import { MockExecutors } from '../mockExecutors'; -import { loadPlugin } from '../../../../../diffmatch'; +import { loadPlugin } from '../../diffmatch'; +import { MockExecutors } from './mockExecutors'; const DEFAULT_PLUGINS = [CoreHttpPlugin] as const; diff --git a/packages/case-core/src/core/plugins/mockExecutors.ts b/packages/case-core/src/core/plugins/mockExecutors.ts new file mode 100644 index 00000000..b469090e --- /dev/null +++ b/packages/case-core/src/core/plugins/mockExecutors.ts @@ -0,0 +1,3 @@ +import type { MockSetupFns } from '../../diffmatch/plugins/types'; + +export const MockExecutors: MockSetupFns = {} as MockSetupFns; diff --git a/packages/case-core/src/diffmatch/matching/MatcherExecutors.ts b/packages/case-core/src/diffmatch/matching/MatcherExecutors.ts index 7682aea8..e4063422 100644 --- a/packages/case-core/src/diffmatch/matching/MatcherExecutors.ts +++ b/packages/case-core/src/diffmatch/matching/MatcherExecutors.ts @@ -26,20 +26,7 @@ import { MatcherExecutor, LOOKUP_MATCHER_TYPE, } from '@contract-case/case-plugin-base'; -import { - HTTP_STATUS_CODE_MATCHER_TYPE, - HTTP_RESPONSE_MATCHER_TYPE, - HTTP_REQUEST_MATCHER_TYPE, - URL_ENCODED_STRING_TYPE, - HTTP_BASIC_AUTH_TYPE, -} from '@contract-case/case-core-plugin-http-dsl'; -import { - HttpStatusCodeMatcher, - HttpResponseMatcher, - HttpRequestMatcher, - UrlEncodedStringMatcher, - HttpBasicAuthMatcher, -} from '@contract-case/case-core-plugin-http'; + import { NumberMatcher, StringMatcher, @@ -70,9 +57,11 @@ import { StringSuffixMatcher, } from './strings'; -export const MatcherExecutors: { +type AllExecutors = { [T in AnyCaseNodeType]: MatcherExecutor>; -} = { +}; + +export const MatcherExecutors: AllExecutors = { [NUMBER_MATCHER_TYPE]: NumberMatcher, [STRING_MATCHER_TYPE]: StringMatcher, [STRING_CONTAINS_TYPE]: StringContainsMatcher, @@ -83,9 +72,6 @@ export const MatcherExecutors: { [NULL_MATCHER_TYPE]: NullMatcher, [SHAPED_ARRAY_MATCHER_TYPE]: ShapedArrayExecutor, [SHAPED_OBJECT_MATCHER_TYPE]: ShapedObjectExecutor, - [HTTP_STATUS_CODE_MATCHER_TYPE]: HttpStatusCodeMatcher, - [HTTP_RESPONSE_MATCHER_TYPE]: HttpResponseMatcher, - [HTTP_REQUEST_MATCHER_TYPE]: HttpRequestMatcher, [LOOKUP_MATCHER_TYPE]: LookupMatcher, [ARRAY_LENGTH_MATCHER_TYPE]: ArrayLengthExecutor, [COMBINE_MATCHERS_TYPE]: AndCombinationMatcher, @@ -95,8 +81,6 @@ export const MatcherExecutors: { [INTEGER_MATCH_TYPE]: IntegerMatcher, [OBJECT_KEYS_MATCH_TYPE]: ObjectEachKeyMatches, [CONTEXT_VARIABLE_TYPE]: ContextVariableMatcher, - [URL_ENCODED_STRING_TYPE]: UrlEncodedStringMatcher, - [HTTP_BASIC_AUTH_TYPE]: HttpBasicAuthMatcher, [JSON_STRINGIFIED_TYPE]: JsonStringifiedString, [BASE64_ENCODED_TYPE]: Base64EncodedStringMatcher, -}; +} as AllExecutors; // TODO: Remove this assertion when we have everything loaded via plugin diff --git a/packages/case-core/src/diffmatch/plugins/loadPlugin.spec.ts b/packages/case-core/src/diffmatch/plugins/loadPlugin.spec.ts new file mode 100644 index 00000000..863f4a57 --- /dev/null +++ b/packages/case-core/src/diffmatch/plugins/loadPlugin.spec.ts @@ -0,0 +1,39 @@ +import { CaseConfigurationError } from '@contract-case/case-plugin-base'; +import { MatcherExecutors } from '../matching/MatcherExecutors'; +import { loadPlugin } from './loadPlugin'; +import { EMPTY_MATCH_CONTEXT } from '../../__tests__/testContext'; +import { MockSetupFns } from './types'; + +describe('plugin loader', () => { + beforeEach(() => { + Object.keys(MatcherExecutors).forEach((key) => { + // clear all executors + delete MatcherExecutors[key as keyof typeof MatcherExecutors]; + }); + }); + + describe('with LOAD_IN_PROGRESS as the version name', () => { + it('throws a configuration error', () => { + expect(() => { + loadPlugin({} as MockSetupFns, EMPTY_MATCH_CONTEXT, { + name: 'Empty test plugin', + version: 'LOAD_IN_PROGRESS', + matcherExecutors: {}, + setupMocks: {}, + }); + }).toThrow(CaseConfigurationError); + }); + }); + describe('with a valid version', () => { + it('loads an empty plugin', () => { + expect(() => { + loadPlugin({} as MockSetupFns, EMPTY_MATCH_CONTEXT, { + name: 'Empty test plugin', + version: '0.0.0', + matcherExecutors: {}, + setupMocks: {}, + }); + }).not.toThrow(); + }); + }); +}); diff --git a/packages/case-core/src/diffmatch/plugins/loadPlugin.ts b/packages/case-core/src/diffmatch/plugins/loadPlugin.ts index f39a4483..977cd3dc 100644 --- a/packages/case-core/src/diffmatch/plugins/loadPlugin.ts +++ b/packages/case-core/src/diffmatch/plugins/loadPlugin.ts @@ -7,15 +7,31 @@ import { LogContext, MockExecutorFn, CORE_PLUGIN_PREFIX, + MatcherExecutor, } from '@contract-case/case-plugin-base'; +import { CoreNumberMatcher } from '@contract-case/case-entities-internal'; import { caseVersion } from '../../entities/caseVersion'; import { MockSetupFns } from './types'; +import { MatcherExecutors } from '../matching/MatcherExecutors'; const typeToPluginName: Record = {}; const loadedPluginVersions: Record = {}; +const isCorePlugin = < + MatchT extends string, + MockT extends string, + MatchD extends IsCaseNodeForType, + MockD extends AnyMockDescriptor, +>( + plugin: ContractCasePlugin, +): boolean => plugin.name.startsWith(CORE_PLUGIN_PREFIX); + +const isCoreType = (type: string): boolean => type.startsWith('_case:'); + +const IN_PROGRESS = 'LOAD_IN_PROGRESS'; + export const loadPlugin = < MatchT extends string, MockT extends string, @@ -26,16 +42,14 @@ export const loadPlugin = < context: LogContext, plugin: ContractCasePlugin, ): void => { + if (plugin.version === IN_PROGRESS) { + throw new CaseConfigurationError( + `The plugin '${plugin.name}' reported its version to be LOAD_IN_PROGRESS, which is not valid. Contact the plugin authors to fix this.`, + ); + } + if (loadedPluginVersions[plugin.name] != null) { if (plugin.version !== loadedPluginVersions[plugin.name]) { - if ( - plugin.name.startsWith(CORE_PLUGIN_PREFIX) && - plugin.version !== caseVersion - ) { - throw new CaseCoreError( - `Core plugin '${plugin.name}' is at version '${plugin.version}', but this is Core version ${caseVersion}. This isn't supposed to happen.`, - ); - } throw new CaseConfigurationError( `Trying to load plugin '${plugin.name}' at version '${plugin.version}', but it was previously loaded as version '${loadedPluginVersions[plugin.name]}'.`, ); @@ -45,7 +59,16 @@ export const loadPlugin = < ); return; } - if (plugin.name.startsWith(CORE_PLUGIN_PREFIX)) { + // We record this at the start, as otherwise failed plugins cause errors every time + loadedPluginVersions[plugin.name] = plugin.version; + + if (isCorePlugin(plugin)) { + if (plugin.version !== caseVersion) { + throw new CaseCoreError( + `Core plugin '${plugin.name}' is at version '${plugin.version}', but this is Core version ${caseVersion}. This isn't supposed to happen.`, + ); + } + context.logger.deepMaintainerDebug(`Loading core plugin '${plugin.name}'`); } else { context.logger.debug( @@ -56,14 +79,24 @@ export const loadPlugin = < Object.entries(plugin.setupMocks).forEach(([mockType, setup]) => { if (mockType in MockExecutors) { throw new CaseConfigurationError( - `Plugin '${plugin.name} @ ${plugin.version}' attempted to load a mock setup function for '${mockType}', but one had already been loaded by plugin '${typeToPluginName[mockType]}'.`, + `Plugin '${plugin.name}' @ ${plugin.version} attempted to load a mock setup function for '${mockType}', but one had already been loaded by plugin '${typeToPluginName[mockType]}'.`, ); } - if (mockType.startsWith(`_case:`)) { + if (isCorePlugin(plugin)) { + if (!isCoreType(mockType)) { + throw new CaseCoreError( + `Core plugin '${plugin.name}' @ ${plugin.version}' tried to load a non-core mock, '${mockType}'`, + ); + } context.logger.deepMaintainerDebug( - `Core plugin '${plugin.name} @ ${plugin.version}' registered a mock setup function with type '${mockType}'`, + `Core plugin '${plugin.name}' @ ${plugin.version}' registered a mock setup function with type '${mockType}'`, ); } else { + if (isCoreType(mockType)) { + throw new CaseConfigurationError( + `Non-core plugin '${plugin.name} @ ${plugin.version}' tried to load a core mock, '${mockType}'. This is an error in the plugin definition, please contact the plugin's authors`, + ); + } context.logger.debug( `Plugin '${plugin.name} @ ${plugin.version}' registered a mock setup function with type '${mockType}'`, ); @@ -81,5 +114,31 @@ export const loadPlugin = < typeToPluginName[mockType] = plugin.name; }); + Object.entries(plugin.matcherExecutors).forEach( + ([pluginExecutorType, pluginExecutor]) => { + if (pluginExecutorType in MatcherExecutors) { + throw new CaseConfigurationError( + `Plugin '${plugin.name} @ ${plugin.version}' attempted to load a matcher executor for '${pluginExecutorType}', but one had already been loaded by plugin '${typeToPluginName[pluginExecutorType]}'.`, + ); + } + if (pluginExecutorType.startsWith(`_case:`)) { + context.logger.deepMaintainerDebug( + `Core plugin '${plugin.name} @ ${plugin.version}' registered a matcher executor with type '${pluginExecutorType}'`, + ); + } else { + context.logger.debug( + `Plugin '${plugin.name} @ ${plugin.version}' registered a matcher executor with type '${pluginExecutorType}'`, + ); + } + + // We cheat the type system here - we can't actually + // do any type checking so we just tell typescript it's any known type. + MatcherExecutors[pluginExecutorType as '_case:MatchNumber'] = + pluginExecutor as MatcherExecutor< + '_case:MatchNumber', + CoreNumberMatcher + >; + }, + ); loadedPluginVersions[plugin.name] = plugin.version; };