diff --git a/cli/src/commands/test.ts b/cli/src/commands/test.ts index 0216ed61f..102d2d046 100644 --- a/cli/src/commands/test.ts +++ b/cli/src/commands/test.ts @@ -1,6 +1,7 @@ import { Command, flags } from "@oclif/command"; import { safeParse } from "../common/safe-parse"; -import { runTest, TestFilter } from "../test-utils/test-runner"; +import { TestFilter } from "../test-utils/common"; +import { TestRunner } from "../test-utils/test-runner"; const ARG_API = "spot_contract"; @@ -27,6 +28,7 @@ export default class Test extends Command { static flags = { help: flags.help({ char: "h" }), + debug: flags.boolean(), url: flags.string({ required: true, char: "u", @@ -44,23 +46,22 @@ export default class Test extends Command { async run() { const { args, flags } = this.parse(Test); - const { url: baseUrl, stateUrl: baseStateUrl, testFilter } = flags; + const { url: baseUrl, stateUrl: baseStateUrl, testFilter, debug } = flags; const { definition } = safeParse.call(this, args[ARG_API]); - const resolvedBaseStateUrl = baseStateUrl - ? baseStateUrl - : `${baseUrl}/state`; - - const filter = testFilter ? parseTestFilter(testFilter) : undefined; - - const allPassed = await runTest( - definition, - resolvedBaseStateUrl, + const testRunnerConfig = { + baseStateUrl: baseStateUrl ? baseStateUrl : `${baseUrl}/state`, baseUrl, - filter - ); + debugMode: debug + }; + const testConfig = { + testFilter: testFilter ? parseTestFilter(testFilter) : undefined + }; + + const testRunner = new TestRunner(testRunnerConfig); + const passed = await testRunner.test(definition, testConfig); - if (!allPassed) { + if (!passed) { this.exit(1); } } diff --git a/cli/src/test-utils/common.ts b/cli/src/test-utils/common.ts new file mode 100644 index 000000000..1be2a00c8 --- /dev/null +++ b/cli/src/test-utils/common.ts @@ -0,0 +1,8 @@ +export interface TestConfig { + testFilter?: TestFilter; +} + +export interface TestFilter { + endpoint: string; + test?: string; +} diff --git a/cli/src/test-utils/test-logger.ts b/cli/src/test-utils/test-logger.ts index a12e42c77..8f380861d 100644 --- a/cli/src/test-utils/test-logger.ts +++ b/cli/src/test-utils/test-logger.ts @@ -1,38 +1,54 @@ import chalk from "chalk"; -export const TestLogger = { - error, - log, - success, - warn -}; - -// tslint:disable:no-console -function error(message: string, opts?: LoggerOpts) { - console.log(chalk.red(transformMessage(message, opts))); -} +export class TestLogger { + /** Prepares an object for printing */ + static formatObject(obj: any): string { + return JSON.stringify(obj, undefined, 2); + } -function log(message: string, opts?: LoggerOpts) { - console.log(chalk.dim.white(transformMessage(message, opts))); -} + private readonly debugMode: boolean; -function success(message: string, opts?: LoggerOpts) { - console.log(chalk.green(transformMessage(message, opts))); -} + constructor(opts?: LoggerOpts) { + this.debugMode = opts ? !!opts.debugMode : false; + } -function warn(message: string, opts?: LoggerOpts) { - console.log(chalk.yellow(transformMessage(message, opts))); -} -// tslint:enable:no-console - -function transformMessage(message: string, customOpts?: LoggerOpts) { - const opts = { - indent: customOpts ? customOpts.indent || 0 : 0 - }; - const indents = "\t".repeat(opts.indent); - return indents + message.replace(/\n/g, `\n${indents}`); + // tslint:disable:no-console + debug(message: string, opts?: LogOpts): void { + if (this.debugMode) { + console.log(chalk.magenta(this.transformMessage(message, opts))); + } + } + + log(message: string, opts?: LogOpts): void { + console.log(chalk.dim.white(this.transformMessage(message, opts))); + } + + success(message: string, opts?: LogOpts): void { + console.log(chalk.green(this.transformMessage(message, opts))); + } + + warn(message: string, opts?: LogOpts): void { + console.log(chalk.yellow(this.transformMessage(message, opts))); + } + + error(message: string, opts?: LogOpts): void { + console.log(chalk.red(this.transformMessage(message, opts))); + } + // tslint:enable:no-console + + private transformMessage(message: string, customOpts?: LogOpts): string { + const opts = { + indent: customOpts ? customOpts.indent || 0 : 0 + }; + const indents = "\t".repeat(opts.indent); + return indents + message.replace(/\n/g, `\n${indents}`); + } } interface LoggerOpts { + debugMode?: boolean; +} + +interface LogOpts { indent?: number; // number of tabs } diff --git a/cli/src/test-utils/test-runner.spec.ts b/cli/src/test-utils/test-runner.spec.ts index 5c84537da..efd44f8a8 100644 --- a/cli/src/test-utils/test-runner.spec.ts +++ b/cli/src/test-utils/test-runner.spec.ts @@ -3,11 +3,18 @@ import { cleanse } from "../../../lib/src/cleansers/cleanser"; import { ContractDefinition } from "../../../lib/src/models/definitions"; import { parse } from "../../../lib/src/parsers/parser"; import { verify } from "../../../lib/src/verifiers/verifier"; -import { runTest } from "./test-runner"; +import { TestRunner } from "./test-runner"; describe("test runner", () => { - const stateUrl = "http://localhost:9988/state"; + const baseStateUrl = "http://localhost:9988/state"; const baseUrl = "http://localhost:9988"; + const testRunnerConfig = { + baseStateUrl, + baseUrl, + debugMode: true + }; + + const testRunner = new TestRunner(testRunnerConfig); afterEach(() => { nock.cleanAll(); @@ -30,7 +37,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(true); }); @@ -53,7 +60,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(true); }); @@ -82,7 +89,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(true); }); @@ -112,7 +119,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scopeA.isDone()).toBe(true); expect(scopeB.isDone()).toBe(true); expect(result).toBe(true); @@ -143,9 +150,8 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl, { - endpoint: "CreateCompany", - test: "badRequestTest" + const result = await testRunner.test(contract, { + testFilter: { endpoint: "CreateCompany", test: "badRequestTest" } }); expect(scopeA.isDone()).toBe(false); @@ -172,7 +178,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(initializeScope.isDone()).toBe(true); expect(tearDownScope.isDone()).toBe(true); expect(setupScope.isDone()).toBe(false); @@ -192,7 +198,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(false); }); @@ -215,7 +221,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(400); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(false); }); @@ -238,7 +244,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(false); }); @@ -260,7 +266,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(false); }); @@ -282,7 +288,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(false); }); @@ -305,7 +311,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(false); }); @@ -329,7 +335,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(true); }); @@ -352,7 +358,7 @@ describe("test runner", () => { .post("/state/teardown") .reply(200); - const result = await runTest(contract, stateUrl, baseUrl); + const result = await testRunner.test(contract); expect(scope.isDone()).toBe(true); expect(result).toBe(true); }); diff --git a/cli/src/test-utils/test-runner.ts b/cli/src/test-utils/test-runner.ts index 344ab8f4b..ffbc3b1a0 100644 --- a/cli/src/test-utils/test-runner.ts +++ b/cli/src/test-utils/test-runner.ts @@ -13,474 +13,473 @@ import { } from "../../../lib/src/models/definitions"; import { TypeNode } from "../../../lib/src/models/nodes"; import { valueFromDataExpression } from "../../../lib/src/utilities/data-expression-utils"; +import { TestConfig } from "./common"; import { TestLogger } from "./test-logger"; import { TestTimer } from "./test-timer"; -/** - * Run the contract test suite for a contract. - * - * @param definition contract definition - * @param baseStateUrl base state change URL - * @param baseUrl base URL - * @param testFilter optional test filter - */ -export async function runTest( - definition: ContractDefinition, - baseStateUrl: string, - baseUrl: string, - testFilter?: TestFilter -): Promise { - const testSuiteStartTime = TestTimer.startTime(); - - let allPassed = true; - - for (const endpoint of definition.endpoints) { - for (const test of endpoint.tests) { - if (testFilter) { - if ( - testFilter.endpoint !== endpoint.name || - (testFilter.test && testFilter.test !== test.name) - ) { - TestLogger.warn(`Test ${endpoint.name}:${test.name} skipped`); - continue; - } - } - const testStartTime = TestTimer.startTime(); +export class TestRunner { + private readonly config: TestRunnerConfig; + private readonly logger: TestLogger; - TestLogger.log(`Testing ${endpoint.name}:${test.name}`); - const correlatedResponse = findCorrelatedResponse(endpoint, test); - const result = await executeTest( - test, - baseStateUrl, - baseUrl, - endpoint, - correlatedResponse, - definition.types - ); + constructor(config: TestRunnerConfig) { + this.config = config; + this.logger = new TestLogger({ debugMode: config.debugMode }); + } - if (result) { - TestLogger.success( - `Test ${endpoint.name}:${test.name} passed (${TestTimer.formattedDiff( - testStartTime - )})`, - { indent: 1 } - ); - } else { - TestLogger.error( - `Test ${endpoint.name}:${test.name} failed (${TestTimer.formattedDiff( - testStartTime - )})`, - { indent: 1 } + /** + * Run the contract test suite for a contract. + * + * @param definition contract definition + * @param config test configuration + */ + async test( + definition: ContractDefinition, + config?: TestConfig + ): Promise { + const testSuiteStartTime = TestTimer.startTime(); + + let allPassed = true; + + for (const endpoint of definition.endpoints) { + for (const test of endpoint.tests) { + if (config && config.testFilter) { + if ( + config.testFilter.endpoint !== endpoint.name || + (config.testFilter.test && config.testFilter.test !== test.name) + ) { + this.logger.warn(`Test ${endpoint.name}:${test.name} skipped`); + continue; + } + } + const testStartTime = TestTimer.startTime(); + + this.logger.log(`Testing ${endpoint.name}:${test.name}`); + const correlatedResponse = this.findCorrelatedResponse(endpoint, test); + const result = await this.executeTest( + test, + endpoint, + correlatedResponse, + definition.types ); + + if (result) { + this.logger.success( + `Test ${endpoint.name}:${ + test.name + } passed (${TestTimer.formattedDiff(testStartTime)})`, + { indent: 1 } + ); + } else { + this.logger.error( + `Test ${endpoint.name}:${ + test.name + } failed (${TestTimer.formattedDiff(testStartTime)})`, + { indent: 1 } + ); + } + allPassed = allPassed && result; } - allPassed = allPassed && result; } - } - TestLogger.log( - `Total time: ${TestTimer.formattedDiff(testSuiteStartTime)}\n` - ); + this.logger.log( + `Total time: ${TestTimer.formattedDiff(testSuiteStartTime)}\n` + ); - return allPassed; -} + return allPassed; + } -/** - * Run a particular contract test. - * - * @param test test definition - * @param baseStateUrl base state change URL - * @param baseUrl base URL - * @param endpoint endpoint definition - * @param correlatedResponse expected test response - * @param typeStore reference type definitions - */ -async function executeTest( - test: TestDefinition, - baseStateUrl: string, - baseUrl: string, - endpoint: EndpointDefinition, - correlatedResponse: DefaultResponseDefinition, - typeStore: TypeNode[] -): Promise { - if ( - (await executeStateInitialization(baseStateUrl)) && - (await executeStateSetup(test, baseStateUrl)) - ) { - const testResult = await executeRequestUnderTest( - endpoint, - test, - baseUrl, - correlatedResponse, - typeStore - ); - const stateTearDownResult = await executeStateTeardown(baseStateUrl); - return testResult && stateTearDownResult; - } else { - await executeStateTeardown(baseStateUrl); - return false; + /** + * Run a particular contract test. + * + * @param test test definition + * @param endpoint endpoint definition + * @param correlatedResponse expected test response + * @param typeStore reference type definitions + */ + private async executeTest( + test: TestDefinition, + endpoint: EndpointDefinition, + correlatedResponse: DefaultResponseDefinition, + typeStore: TypeNode[] + ): Promise { + if ( + (await this.executeStateInitialization()) && + (await this.executeStateSetup(test)) + ) { + const testResult = await this.executeRequestUnderTest( + endpoint, + test, + correlatedResponse, + typeStore + ); + const stateTearDownResult = await this.executeStateTeardown(); + return testResult && stateTearDownResult; + } else { + await this.executeStateTeardown(); + return false; + } } -} -/** - * Find the the response that matches the response status for a particular test. - * If no exact response status is found, the default response is used. Otherwise - * an error it thrown. - * - * @param endpoint endpoint definition - * @param test test definition - */ -function findCorrelatedResponse( - endpoint: EndpointDefinition, - test: TestDefinition -): DefaultResponseDefinition { - const correlatedResponse = - endpoint.responses.find( - response => response.status === test.response.status - ) || endpoint.defaultResponse; - if (!correlatedResponse) { - throw new Error( - `a response with status ${ - test.response.status - } was not found and a default response has not been defined` - ); + /** + * Find the the response that matches the response status for a particular test. + * If no exact response status is found, the default response is used. Otherwise + * an error it thrown. + * + * @param endpoint endpoint definition + * @param test test definition + */ + private findCorrelatedResponse( + endpoint: EndpointDefinition, + test: TestDefinition + ): DefaultResponseDefinition { + const correlatedResponse = + endpoint.responses.find( + response => response.status === test.response.status + ) || endpoint.defaultResponse; + if (!correlatedResponse) { + throw new Error( + `a response with status ${ + test.response.status + } was not found and a default response has not been defined` + ); + } + return correlatedResponse; } - return correlatedResponse; -} -/** - * Generate the axios configuration necessary to execute the request - * under test. All responses statuses are considered valid with this - * configuration. - * - * @param endpoint endpoint definition - * @param test test definition - * @param baseUrl base URL - */ -function generateAxiosConfig( - endpoint: EndpointDefinition, - test: TestDefinition, - baseUrl: string -): AxiosRequestConfig { - const urlPath = endpoint.path - .split("/") - .map(value => { - if (value.startsWith(":")) { - if (test.request) { - const pathParam = test.request.pathParams.find(pathParam => { - return pathParam.name === value.substring(1); - }); - if (pathParam) { - return valueFromDataExpression(pathParam.expression); + /** + * Generate the axios configuration necessary to execute the request + * under test. All responses statuses are considered valid with this + * configuration. + * + * @param endpoint endpoint definition + * @param test test definition + */ + private generateAxiosConfig( + endpoint: EndpointDefinition, + test: TestDefinition + ): AxiosRequestConfig { + const urlPath = endpoint.path + .split("/") + .map(value => { + if (value.startsWith(":")) { + if (test.request) { + const pathParam = test.request.pathParams.find(pathParam => { + return pathParam.name === value.substring(1); + }); + if (pathParam) { + return valueFromDataExpression(pathParam.expression); + } else { + throw new Error( + `Unable to find path param for ${value} in ${endpoint.path}` + ); + } } else { throw new Error( `Unable to find path param for ${value} in ${endpoint.path}` ); } } else { - throw new Error( - `Unable to find path param for ${value} in ${endpoint.path}` - ); + return value; } - } else { - return value; - } - }) - .join("/"); - - const config: AxiosRequestConfig = { - baseURL: baseUrl, - url: urlPath, - method: endpoint.method, - validateStatus: () => true // never invalidate the status - }; - - if (test.request) { - config.headers = test.request.headers.reduce( - (acc, header) => { - acc[header.name] = valueFromDataExpression(header.expression); - return acc; - }, - {} - ); + }) + .join("/"); - config.params = test.request.queryParams.reduce( - (acc, param) => { - acc[param.name] = valueFromDataExpression(param.expression); - return acc; - }, - {} - ); - - config.paramsSerializer = params => { - return qsStringify(params); + const config: AxiosRequestConfig = { + baseURL: this.config.baseUrl, + url: urlPath, + method: endpoint.method, + validateStatus: () => true // never invalidate the status }; - if (test.request.body) { - config.data = valueFromDataExpression(test.request.body); + if (test.request) { + config.headers = test.request.headers.reduce( + (acc, header) => { + acc[header.name] = valueFromDataExpression(header.expression); + return acc; + }, + {} + ); + + config.params = test.request.queryParams.reduce( + (acc, param) => { + acc[param.name] = valueFromDataExpression(param.expression); + return acc; + }, + {} + ); + + config.paramsSerializer = params => { + return qsStringify(params); + }; + + if (test.request.body) { + config.data = valueFromDataExpression(test.request.body); + } } + return config; } - return config; -} -/** - * Executes the request under test. - * - * @param endpoint endpoint definition - * @param test test definition - * @param baseUrl base URL - * @param correlatedResponse expected test response - * @param typeStore reference type definitions - */ -async function executeRequestUnderTest( - endpoint: EndpointDefinition, - test: TestDefinition, - baseUrl: string, - correlatedResponse: DefaultResponseDefinition, - typeStore: TypeNode[] -) { - const testStartTime = process.hrtime(); - - const config = generateAxiosConfig(endpoint, test, baseUrl); - TestLogger.log( - `Performing request under test: ${config.method} ${config.url}`, - { indent: 1 } - ); - TestLogger.log( - `Request complete (${TestTimer.formattedDiff(testStartTime)})`, - { indent: 2 } - ); - const response = await axios.request(config); - const statusResult = verifyStatus(test, response); - // TODO: check headers - const bodyResult = verifyBody(correlatedResponse, response, typeStore); - - return statusResult && bodyResult; -} + /** + * Executes the request under test. + * + * @param endpoint endpoint definition + * @param test test definition + * @param correlatedResponse expected test response + * @param typeStore reference type definitions + */ + private async executeRequestUnderTest( + endpoint: EndpointDefinition, + test: TestDefinition, + correlatedResponse: DefaultResponseDefinition, + typeStore: TypeNode[] + ) { + const testStartTime = process.hrtime(); -/** - * Execute the state initialization request. - * - * @param baseStateUrl base state change URL - */ -async function executeStateInitialization( - baseStateUrl: string -): Promise { - const testInitStartTime = process.hrtime(); - - try { - TestLogger.log("Performing state initialization request", { indent: 1 }); - await axios.post(`${baseStateUrl}/initialize`); - TestLogger.success( - `State initialization request success (${TestTimer.formattedDiff( - testInitStartTime - )})`, + const config = this.generateAxiosConfig(endpoint, test); + this.logger.log( + `Performing request under test: ${config.method} ${config.url}`, + { indent: 1 } + ); + const response = await axios.request(config); + this.logger.log( + `Request complete (${TestTimer.formattedDiff(testStartTime)})`, { indent: 2 } ); - return true; - } catch (error) { - if (error.response) { - TestLogger.error( - `State initialization request failed: received ${ - error.response.status - } status (${TestTimer.formattedDiff( - testInitStartTime - )})\nReceived:\n${JSON.stringify(error.response.data, undefined, 2)}`, - { indent: 2 } - ); - } else if (error.request) { - TestLogger.error( - `State initialization request failed: no response (${TestTimer.formattedDiff( - testInitStartTime - )})`, - { indent: 2 } - ); - } else { - TestLogger.error( - `State initialization request failed: ${ - error.message - } (${TestTimer.formattedDiff(testInitStartTime)})`, - { indent: 2 } - ); - } - return false; + this.logger.debug( + `Received:\n===============\nStatus: ${ + response.status + }\nHeaders: ${TestLogger.formatObject( + response.headers + )}\nBody: ${TestLogger.formatObject(response.data)}\n===============`, + { indent: 2 } + ); + const statusResult = this.verifyStatus(test, response); + // TODO: check headers + const bodyResult = this.verifyBody(correlatedResponse, response, typeStore); + + return statusResult && bodyResult; } -} -/** - * Execute state setup requests defined for a test. - * - * @param test test definition - * @param baseStateUrl base state change URL - */ -async function executeStateSetup( - test: TestDefinition, - baseStateUrl: string -): Promise { - for (const state of test.states) { - const testSetupStartTime = process.hrtime(); - - TestLogger.log(`Performing state setup request: ${state.name}`, { - indent: 1 - }); - const data = { - name: state.name, - params: state.params.reduce((acc, param) => { - acc[param.name] = valueFromDataExpression(param.expression); - return acc; - }, {}) - }; + /** + * Execute the state initialization request. + */ + private async executeStateInitialization(): Promise { + const testInitStartTime = process.hrtime(); + try { - await axios.post(`${baseStateUrl}/setup`, data); - TestLogger.success( - `State setup request (${state.name}) success (${TestTimer.formattedDiff( - testSetupStartTime + this.logger.log("Performing state initialization request", { indent: 1 }); + await axios.post(`${this.config.baseStateUrl}/initialize`); + this.logger.success( + `State initialization request success (${TestTimer.formattedDiff( + testInitStartTime )})`, { indent: 2 } ); + return true; } catch (error) { if (error.response) { - TestLogger.error( - `State change request (${state.name}) failed: received ${ + this.logger.error( + `State initialization request failed: received ${ error.response.status } status (${TestTimer.formattedDiff( - testSetupStartTime - )})\nReceived:\n${JSON.stringify(error.response.data, undefined, 2)}`, + testInitStartTime + )})\nReceived:\n${TestLogger.formatObject(error.response.data)}`, { indent: 2 } ); } else if (error.request) { - TestLogger.error( - `State change request (${ - state.name - }) failed: no response (${TestTimer.formattedDiff( - testSetupStartTime + this.logger.error( + `State initialization request failed: no response (${TestTimer.formattedDiff( + testInitStartTime )})`, { indent: 2 } ); } else { - TestLogger.error( - `State change request (${state.name}) failed: ${ + this.logger.error( + `State initialization request failed: ${ error.message - } (${TestTimer.formattedDiff(testSetupStartTime)})`, + } (${TestTimer.formattedDiff(testInitStartTime)})`, { indent: 2 } ); } return false; } } - return true; -} -/** - * Execute the state teardown request. - * - * @param baseStateUrl base state change URL - */ -async function executeStateTeardown(baseStateUrl: string): Promise { - const testTeardownStartTime = process.hrtime(); - - try { - TestLogger.log("Performing state teardown request", { indent: 1 }); - await axios.post(`${baseStateUrl}/teardown`); - TestLogger.success( - `State teardown request success (${TestTimer.formattedDiff( - testTeardownStartTime - )})`, - { indent: 2 } - ); + /** + * Execute state setup requests defined for a test. + * + * @param test test definition + */ + private async executeStateSetup(test: TestDefinition): Promise { + for (const state of test.states) { + const testSetupStartTime = process.hrtime(); + + this.logger.log(`Performing state setup request: ${state.name}`, { + indent: 1 + }); + const data = { + name: state.name, + params: state.params.reduce((acc, param) => { + acc[param.name] = valueFromDataExpression(param.expression); + return acc; + }, {}) + }; + try { + await axios.post(`${this.config.baseStateUrl}/setup`, data); + this.logger.success( + `State setup request (${ + state.name + }) success (${TestTimer.formattedDiff(testSetupStartTime)})`, + { indent: 2 } + ); + } catch (error) { + if (error.response) { + this.logger.error( + `State change request (${state.name}) failed: received ${ + error.response.status + } status (${TestTimer.formattedDiff( + testSetupStartTime + )})\nReceived:\n${TestLogger.formatObject(error.response.data)}`, + { indent: 2 } + ); + } else if (error.request) { + this.logger.error( + `State change request (${ + state.name + }) failed: no response (${TestTimer.formattedDiff( + testSetupStartTime + )})`, + { indent: 2 } + ); + } else { + this.logger.error( + `State change request (${state.name}) failed: ${ + error.message + } (${TestTimer.formattedDiff(testSetupStartTime)})`, + { indent: 2 } + ); + } + return false; + } + } return true; - } catch (error) { - if (error.response) { - TestLogger.error( - `State teardown request failed: received ${ - error.response.status - } status (${TestTimer.formattedDiff( - testTeardownStartTime - )})\nReceived:\n${JSON.stringify(error.response.data, undefined, 2)}`, - { indent: 2 } - ); - } else if (error.request) { - TestLogger.error( - `State teardown request failed: no response (${TestTimer.formattedDiff( + } + + /** + * Execute the state teardown request. + */ + private async executeStateTeardown(): Promise { + const testTeardownStartTime = process.hrtime(); + + try { + this.logger.log("Performing state teardown request", { indent: 1 }); + await axios.post(`${this.config.baseStateUrl}/teardown`); + this.logger.success( + `State teardown request success (${TestTimer.formattedDiff( testTeardownStartTime )})`, { indent: 2 } ); + return true; + } catch (error) { + if (error.response) { + this.logger.error( + `State teardown request failed: received ${ + error.response.status + } status (${TestTimer.formattedDiff( + testTeardownStartTime + )})\nReceived:\n${TestLogger.formatObject(error.response.data)}`, + { indent: 2 } + ); + } else if (error.request) { + this.logger.error( + `State teardown request failed: no response (${TestTimer.formattedDiff( + testTeardownStartTime + )})`, + { indent: 2 } + ); + } else { + this.logger.error( + `State teardown request failed: ${ + error.message + } (${TestTimer.formattedDiff(testTeardownStartTime)})`, + { indent: 2 } + ); + } + return false; + } + } + + /** + * Check if an axios response status matches the expected status of a test. + * + * @param test test definition + * @param response axios response + */ + private verifyStatus( + test: TestDefinition, + response: AxiosResponse + ): boolean { + if (test.response.status === response.status) { + this.logger.success("Status matched", { indent: 2 }); + return true; } else { - TestLogger.error( - `State teardown request failed: ${ - error.message - } (${TestTimer.formattedDiff(testTeardownStartTime)})`, + this.logger.error( + `Expected status ${test.response.status}, got ${response.status}`, { indent: 2 } ); + return false; } - return false; - } -} - -/** - * Check if an axios response status matches the expected status of a test. - * - * @param test test definition - * @param response axios response - */ -function verifyStatus( - test: TestDefinition, - response: AxiosResponse -): boolean { - if (test.response.status === response.status) { - TestLogger.success("Status matched", { indent: 2 }); - return true; - } else { - TestLogger.error( - `Expected status ${test.response.status}, got ${response.status}`, - { indent: 2 } - ); - return false; } -} -/** - * Check if an exios response body matches the expected body of an expected response definition. - * - * @param expectedResponse expected response - * @param response axios response - * @param typeStore reference type definitions - */ -function verifyBody( - expectedResponse: DefaultResponseDefinition, - response: AxiosResponse, - typeStore: TypeNode[] -): boolean { - if (!expectedResponse.body) { - return true; - } + /** + * Check if an exios response body matches the expected body of an expected response definition. + * + * @param expectedResponse expected response + * @param response axios response + * @param typeStore reference type definitions + */ + private verifyBody( + expectedResponse: DefaultResponseDefinition, + response: AxiosResponse, + typeStore: TypeNode[] + ): boolean { + if (!expectedResponse.body) { + return true; + } - const jsv = new JsonSchemaValidator(); - const schema = { - ...jsonTypeSchema(expectedResponse.body.type), - definitions: typeStore.reduce<{ [key: string]: JsonSchemaType }>( - (defAcc, typeNode) => { - return { [typeNode.name]: jsonTypeSchema(typeNode.type), ...defAcc }; - }, - {} - ) - }; - const validateFn = jsv.compile(schema); - const valid = validateFn(response.data); - if (valid) { - TestLogger.success("Body compliant", { indent: 2 }); - return true; - } else { - TestLogger.error( + const jsv = new JsonSchemaValidator(); + const schema = { + ...jsonTypeSchema(expectedResponse.body.type), + definitions: typeStore.reduce<{ [key: string]: JsonSchemaType }>( + (defAcc, typeNode) => { + return { [typeNode.name]: jsonTypeSchema(typeNode.type), ...defAcc }; + }, + {} + ) + }; + const validateFn = jsv.compile(schema); + const valid = validateFn(response.data); + if (valid) { + this.logger.success("Body compliant", { indent: 2 }); + return true; + } + this.logger.error( `Body is not compliant: ${jsv.errorsText( validateFn.errors - )}\nReceived:\n${JSON.stringify(response.data, undefined, 2)}`, + )}\nReceived:\n${TestLogger.formatObject(response.data)}`, { indent: 2 } ); return false; } } +export interface TestRunnerConfig { + baseStateUrl: string; + baseUrl: string; + debugMode?: boolean; +} + interface AxiosHeaders { [key: string]: string; } @@ -488,8 +487,3 @@ interface AxiosHeaders { interface GenericParams { [key: string]: any; } - -export interface TestFilter { - endpoint: string; - test?: string; -}