diff --git a/src/commands/test/command.ts b/src/commands/test/command.ts index 2f5933df..970ad508 100644 --- a/src/commands/test/command.ts +++ b/src/commands/test/command.ts @@ -3,7 +3,7 @@ import { Argv, CommandModule } from 'yargs' import * as consts from '~commands/consts' import { createHandler, TestArgs } from './handler' import logger from '~logger' -import { testConfigs } from './test' +import { runTests } from './test' import loadConfig from '~config/load' const builder = (yargs: Argv): Argv => @@ -46,6 +46,6 @@ export default function createTestCommand( command: 'test ', describe: 'Tests configured endpoints', builder, - handler: createHandler(handleError, createTypeValidator, logger, testConfigs, loadConfig), + handler: createHandler(handleError, createTypeValidator, logger, runTests, loadConfig), } } diff --git a/src/commands/test/handler.spec.ts b/src/commands/test/handler.spec.ts index f4bbcb5f..2c60baae 100644 --- a/src/commands/test/handler.spec.ts +++ b/src/commands/test/handler.spec.ts @@ -5,7 +5,7 @@ import { resolve } from 'path' import { existsSync } from 'fs' import { TypeValidator } from '~validation' import { NCDCLogger } from '~logger' -import { TestConfigs } from './test' +import { RunTests } from './test' import { LoadConfig, LoadConfigStatus } from '~config/load' import { ValidatedTestConfig, transformConfigs } from './config' import { ConfigBuilder } from '~config/types' @@ -21,7 +21,7 @@ const mockedExistsSync = mocked(existsSync) const mockedResolve = mocked(resolve) const resolvedTsconfigPath = randomString('resolved-tsconfig') const mockedLogger = mockObj({ warn: jest.fn() }) -const mockedTestConfigs = mockFn() +const mockedTestConfigs = mockFn() const mockedLoadConfig = mockFn>() const handler = createHandler( mockedHandleError, @@ -116,7 +116,8 @@ it('calls testConfigs with the correct arguments', async () => { args.baseURL, expect.objectContaining({}), configs, - mockedTypeValidator, + expect.any(Function), + mockedLogger, ) }) @@ -126,11 +127,11 @@ it('handles errors thrown by testConfigs', async () => { absoluteFixturePaths: [], configs: [new ConfigBuilder().build()], }) - mockedTestConfigs.mockRejectedValue(new Error('oops')) + mockedTestConfigs.mockResolvedValue('Failure') await handler(args) - expect(mockedHandleError).toBeCalledWith(expect.objectContaining({ message: 'oops' })) + expect(mockedHandleError).toBeCalledWith(expect.objectContaining({ message: 'Not all tests passed' })) }) // TODO: desired behaviour diff --git a/src/commands/test/handler.ts b/src/commands/test/handler.ts index 006af441..9b9d6390 100644 --- a/src/commands/test/handler.ts +++ b/src/commands/test/handler.ts @@ -1,8 +1,8 @@ import { HandleError, CreateTypeValidator } from '~commands' import { NCDCLogger } from '~logger' -import { TestConfigs } from './test' +import { RunTests } from './test' import { createHttpClient } from './http-client' -import { LoadConfig, LoadConfigStatus } from '~config/load' +import { LoadConfig, LoadConfigStatus, GetTypeValidator } from '~config/load' import { ValidatedTestConfig, transformConfigs } from './config' import { red } from 'chalk' import { TypeValidator } from '~validation' @@ -19,7 +19,7 @@ export const createHandler = ( handleError: HandleError, createTypeValidator: CreateTypeValidator, logger: NCDCLogger, - testConfigs: TestConfigs, + runTests: RunTests, loadConfig: LoadConfig, ) => async (args: TestArgs): Promise => { const { configPath, baseURL, tsconfigPath, schemaPath, force } = args @@ -27,15 +27,12 @@ export const createHandler = ( if (!baseURL) return handleError({ message: 'baseURL must be specified' }) let typeValidator: TypeValidator | undefined + const getTypeValidator: GetTypeValidator = () => { + typeValidator = createTypeValidator(tsconfigPath, force, schemaPath) + return typeValidator + } - const loadResult = await loadConfig( - configPath, - () => { - typeValidator = createTypeValidator(tsconfigPath, force, schemaPath) - return typeValidator - }, - transformConfigs, - ) + const loadResult = await loadConfig(configPath, getTypeValidator, transformConfigs) switch (loadResult.type) { case LoadConfigStatus.Success: @@ -50,9 +47,12 @@ export const createHandler = ( return handleError({ message: 'An unknown error ocurred' }) } - try { - await testConfigs(baseURL, createHttpClient(baseURL), loadResult.configs, typeValidator) - } catch (err) { - return handleError(err) - } + const testResult = await runTests( + baseURL, + createHttpClient(baseURL), + loadResult.configs, + getTypeValidator, + logger, + ) + if (testResult === 'Failure') return handleError({ message: 'Not all tests passed' }) } diff --git a/src/commands/test/test.spec.ts b/src/commands/test/test.spec.ts index da19f51e..248b841b 100644 --- a/src/commands/test/test.spec.ts +++ b/src/commands/test/test.spec.ts @@ -1,84 +1,172 @@ -import { testConfigs } from './test' -import { randomString, mockFn, mockObj, mocked } from '~test-helpers' -import { TestFn, doItAll } from '~commands/test/validators' -import logger from '~logger' +import { runTests } from './test' +import { randomString, mockFn, mockObj, randomNumber } from '~test-helpers' import stripAnsi from 'strip-ansi' -import { ProblemType } from '~problem' import { TestConfig } from './config' import { FetchResource } from './http-client' import { TypeValidator } from '~validation' +import { ConfigBuilder } from '~config/types' +import { Logger } from 'winston' -jest.unmock('./test') -jest.unmock('~messages') -jest.unmock('~commands/shared') +jest.disableAutomock() describe('test configs', () => { - const baseUrl = randomString('base-url') + const baseUrl = randomString('http://example') + '.com' + const mockLogger = mockObj({ error: jest.fn(), info: jest.fn() }) const mockFetchResource = mockFn() - const mockTypeValidator = mockObj({}) - const mockTest = mockFn() - const mockDoItAll = mocked(doItAll) - const mockLogger = mockObj(logger) + const mockTypeValidator = mockObj({ validate: jest.fn() }) + const mockGetTypeValidator = mockFn<() => TypeValidator>() beforeEach(() => { jest.resetAllMocks() - mockDoItAll.mockReturnValue(mockTest) + mockGetTypeValidator.mockReturnValue(mockTypeValidator) }) - it('calls doItAll with the correct args', async () => { - const configs: TestConfig[] = [ - { name: 'yo!', request: { endpoint: randomString('endpoint') } } as TestConfig, - ] - mockTest.mockResolvedValue([]) + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const act = (...configs: TestConfig[]) => + runTests(baseUrl, mockFetchResource, configs, mockGetTypeValidator, mockLogger) - await testConfigs(baseUrl, mockFetchResource, configs, mockTypeValidator) + const getLoggedMessage = (method: 'error' | 'info', callIndex = 0): string => + stripAnsi((mockLogger[method].mock.calls[callIndex][0] as unknown) as string) - expect(mockDoItAll).toBeCalledWith(mockTypeValidator, mockFetchResource) + it('calls fetchResource with the correct args', async () => { + const config = new ConfigBuilder().build() + mockFetchResource.mockResolvedValue({ status: randomNumber() }) + + await act(config) + + expect(mockFetchResource).toBeCalledWith(config) }) - it('logs when a test passes', async () => { - const configs: TestConfig[] = [ - { name: 'yo!', request: { endpoint: randomString('endpoint') } } as TestConfig, - ] - mockTest.mockResolvedValue([]) + it('logs a failure when fetching a resource throws', async () => { + const config = new ConfigBuilder().withName('Bob').withEndpoint('/jim').build() + const errorMessage = randomString('error message') + mockFetchResource.mockRejectedValue(new Error(errorMessage)) - await testConfigs(baseUrl, mockFetchResource, configs, mockTypeValidator) + const result = await act(config) - const expectedMessage = `PASSED: yo! - ${baseUrl}${configs[0].request.endpoint}\n` - expect(mockLogger.info).toBeCalled() - expect(stripAnsi((mockLogger.info.mock.calls[0][0] as unknown) as string)).toEqual(expectedMessage) + expect(getLoggedMessage('error')).toEqual(`FAILED: Bob - ${baseUrl}/jim\n${errorMessage}`) + expect(result).toEqual('Failure') }) - it('logs when a test fail', async () => { - const configs: TestConfig[] = [ - { name: 'yo!', request: { endpoint: randomString('endpoint') } } as TestConfig, - ] - mockTest.mockResolvedValue([{ path: 'some.path', problemType: ProblemType.Request, message: 'message' }]) + it('returns a failed message when the status code does not match', async () => { + const expectedStatus = randomNumber() + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseCode(expectedStatus) + .build() + mockFetchResource.mockResolvedValue({ status: 123 }) - await expect(testConfigs(baseUrl, mockFetchResource, configs, mockTypeValidator)).rejects.toThrowError( - 'Not all tests passed', - ) + const result = await act(config) - const expectedMessage = `FAILED: yo!\nURL: ${baseUrl}${configs[0].request.endpoint}\n` - const expectedError1 = `Request some.path message\n` - expect(mockLogger.error).toBeCalled() - expect(stripAnsi((mockLogger.error.mock.calls[0][0] as unknown) as string)).toEqual( - expectedMessage + expectedError1, + expect(getLoggedMessage('error')).toEqual( + `FAILED: Bob - ${baseUrl}/jim\nExpected status code ${expectedStatus} but got 123`, ) + expect(result).toEqual('Failure') }) - it('logs when a test throws an error', async () => { - const configs: TestConfig[] = [ - { name: 'yo!', request: { endpoint: randomString('endpoint') } } as TestConfig, - ] - mockTest.mockRejectedValue(new Error('woah dude')) + describe('validation when a response body is configured', () => { + it('does not return a failure message when the body matches the specified object', async () => { + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseBody({ hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }) + .build() + mockFetchResource.mockResolvedValue({ + status: 200, + data: { hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }, + }) - await expect(testConfigs(baseUrl, mockFetchResource, configs, mockTypeValidator)).rejects.toThrowError( - 'Not all tests passed', - ) + const result = await act(config) + + expect(getLoggedMessage('info')).toEqual(`PASSED: Bob - ${baseUrl}/jim`) + expect(result).toEqual('Success') + }) + + it('returns a failure message when the response body is undefined', async () => { + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseBody(randomString('body')) + .build() + mockFetchResource.mockResolvedValue({ status: 200 }) + + const result = await act(config) + + expect(getLoggedMessage('error')).toEqual(`FAILED: Bob - ${baseUrl}/jim\nNo response body was received`) + expect(result).toEqual('Failure') + }) + + it('returns a failure message when the body does not match the specified object', async () => { + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseBody({ hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }) + .build() + const actualResponseBody = { hello: 'world' } + mockFetchResource.mockResolvedValue({ status: 200, data: actualResponseBody }) + + const result = await act(config) + + expect(getLoggedMessage('error')).toEqual( + `FAILED: Bob - ${baseUrl}/jim\nThe response body was not deeply equal to your configured fixture\nReceived:\n{ hello: 'world' }`, + ) + expect(result).toEqual('Failure') + }) + }) + + describe('when the config has a response type specified', () => { + it('calls the type validator with the correct params', async () => { + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseType(randomString('res type')) + .withResponseBody(undefined) + .build() + const actualData = randomString('data') + mockFetchResource.mockResolvedValue({ status: 200, data: actualData }) + mockTypeValidator.validate.mockResolvedValue({ success: true }) + + await act(config) + + expect(mockTypeValidator.validate).toBeCalledWith(actualData, config.response.type) + }) + + it('does not return a failure message when the types match', async () => { + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseType(randomString('res type')) + .withResponseBody(undefined) + .build() + mockTypeValidator.validate.mockResolvedValue({ success: true }) + mockFetchResource.mockResolvedValue({ status: 200 }) + + const result = await act(config) + + expect(getLoggedMessage('info')).toEqual(`PASSED: Bob - ${baseUrl}/jim`) + expect(result).toEqual('Success') + }) + + it('returns a failure message when the types do not match', async () => { + const config = new ConfigBuilder() + .withName('Bob') + .withEndpoint('/jim') + .withResponseType(randomString('res type')) + .withResponseBody(undefined) + .build() + const error1 = randomString('error2') + const error2 = randomString('error2') + mockTypeValidator.validate.mockResolvedValue({ success: false, errors: [error1, error2] }) + mockFetchResource.mockResolvedValue({ status: 200 }) + + const result = await act(config) - const expectedMessage = `ERROR: yo!\nURL: ${baseUrl}${configs[0].request.endpoint}\nwoah dude\n` - expect(mockLogger.error).toBeCalled() - expect(stripAnsi((mockLogger.error.mock.calls[0][0] as unknown) as string)).toEqual(expectedMessage) + const expectedPart1 = `FAILED: Bob - ${baseUrl}/jim\n` + const expectedPart2 = `The received body does not match the type ${config.response.type}\n` + const expectedPart3 = `${error1}\n${error2}` + expect(getLoggedMessage('error')).toEqual(`${expectedPart1}${expectedPart2}${expectedPart3}`) + expect(result).toEqual('Failure') + }) }) }) diff --git a/src/commands/test/test.ts b/src/commands/test/test.ts index adcc8943..cff09e9a 100644 --- a/src/commands/test/test.ts +++ b/src/commands/test/test.ts @@ -1,49 +1,94 @@ import { TypeValidator } from '~validation' -import Problem from '~problem' -import { blue } from 'chalk' -import logger from '~logger' -import { testPassed, testFailed, testError } from '~messages' +import { red, green, blue } from 'chalk' import { TestConfig } from './config' -import { FetchResource } from './http-client' -import { doItAll } from './validators' - -// TODO: why is this returning a number? yeah, what the fuck? -// TODO: reuse this at config type validaiton type. nononononoooooo -const logTestResults = (baseUrl: string) => (displayName: string, endpoint: string) => ( - problems: Public[], -): 0 | 1 => { - const displayEndpoint = blue(`${baseUrl}${endpoint}`) - if (!problems.length) { - logger.info(testPassed(displayName, displayEndpoint)) - return 0 - } else { - logger.error(testFailed(displayName, displayEndpoint, problems)) - return 1 +import { inspect } from 'util' +import { Logger } from 'winston' + +export type LoaderResponse = { status: number; data?: Data } +export type FetchResource = (config: TestConfig) => Promise +export type GetTypeValidator = () => TypeValidator + +const isDeeplyEqual = (expected: unknown, actual: unknown): boolean => { + if (typeof expected === 'object') { + if (!expected) return expected === actual + if (typeof actual !== 'object') return false + if (!actual) return false + + for (const key in expected) { + const expectedValue = expected[key as keyof typeof expected] + const actualValue = actual[key as keyof typeof actual] + if (!isDeeplyEqual(expectedValue, actualValue)) return false + } + + return true } + + return expected === actual } -export const testConfigs = async ( - baseURL: string, +export const runTests = async ( + baseUrl: string, fetchResource: FetchResource, configs: TestConfig[], - typeValidator: Optional, -): Promise => { - const test = doItAll(typeValidator, fetchResource) + getTypeValidator: GetTypeValidator, + logger: Logger, +): Promise<'Success' | 'Failure'> => { + // TODO: now I can use the full endpoint if I want to, since baseurl is still currently an argument + + const testTasks2 = configs.map( + async (config): Promise<{ success: boolean; message: string }> => { + const failedLine = red.bold(`FAILED: ${config.name}`) + const endpointSegment = `- ${blue(baseUrl + config.request.endpoint)}` - const resultsLogger = logTestResults(baseURL) + let res: LoaderResponse + try { + res = await fetchResource(config) + } catch (err) { + return { success: false, message: `${failedLine} ${endpointSegment}\n${err.message}` } + } - const testTasks = configs.map((testConfig) => { - return test(testConfig) - .then(resultsLogger(testConfig.name, testConfig.request.endpoint)) - .catch((err) => { - logger.error(testError(testConfig.name, baseURL + testConfig.request.endpoint, err.message)) - return 1 - }) - }) + if (res.status !== config.response.code) { + const message = `Expected status code ${green(config.response.code)} but got ${red(res.status)}` + return { success: false, message: `${failedLine} ${endpointSegment}\n${message}` } + } - const results = Promise.all(testTasks) + const messages: string[] = [] + + if (config.response.body) { + if (res.data === undefined) { + messages.push('No response body was received') + } else if (!isDeeplyEqual(config.response.body, res.data)) { + const message = `The response body was not deeply equal to your configured fixture` + const formattedResponse = inspect(res.data, false, 4, true) + messages.push(`${message}\nReceived:\n${formattedResponse}`) + } + } + + if (config.response.type) { + const validationResult = await getTypeValidator().validate(res.data, config.response.type) + if (!validationResult.success) { + const message = `The received body does not match the type ${config.response.type}` + messages.push(`${message}\n${validationResult.errors.join('\n')}`) + } + } + + if (messages.length) { + return { success: false, message: `${failedLine} ${endpointSegment}\n${messages.join('\n')}` } + } + + return { success: true, message: `${green('PASSED')}: ${config.name} ${endpointSegment}` } + }, + ) + + for (const testTask of testTasks2) { + testTask.then(({ message, success }) => { + if (success) logger.info(message) + else logger.error(message) + }) + } - if ((await results).includes(1)) throw new Error('Not all tests passed') + const allResults = await Promise.all(testTasks2) + return allResults.find((r) => !r.success) ? 'Failure' : 'Success' } -export type TestConfigs = typeof testConfigs +export type RunTests = typeof runTests diff --git a/src/commands/test/validators.spec.ts b/src/commands/test/validators.spec.ts deleted file mode 100644 index 712bb583..00000000 --- a/src/commands/test/validators.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import TypeValidator from '../../validation/type-validator' -import { ConfigBuilder } from '~config/types' -import Problem, { ProblemType } from '~problem' -import * as messages from '~messages' -import { mockObj, mockFn } from '~test-helpers' -import { TestConfig } from '~commands/test/config' -import { FetchResource } from './http-client' -import { doItAll } from './validators' - -jest.unmock('./validators') - -describe('validators', () => { - const mockTypeValidator = mockObj({ getProblems: jest.fn() }) - const mockFetchResource = mockFn() - const mockProblemCtor = mockObj(Problem) - const mockMessages = mockObj(messages) - - beforeEach(() => { - jest.resetAllMocks() - }) - - it('returns an empty list when all validations succeed', async () => { - const config: Partial = { - request: { - endpoint: '/tables/drop', - method: 'POST', - type: 'string', - body: 'drop everything!', - }, - response: { - code: 401, - body: 'not today son', - type: 'string', - }, - } - - mockFetchResource.mockResolvedValue({ status: 401, data: 'not today son' }) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(0) - }) - - it('returns problems when response code does not match expected', async () => { - const config: Partial = { - response: { - code: 200, - }, - } - - mockFetchResource.mockResolvedValue({ status: 302 }) - mockMessages.shouldBe.mockReturnValue('message') - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(1) - expect(mockMessages.shouldBe).toHaveBeenCalledWith('status code', 200, 302) - expect(mockProblemCtor).toHaveBeenCalledWith({ data: 302, message: 'message' }, ProblemType.Response) - }) - - describe('body validation', () => { - describe('when the response body does not match expected', () => { - it('returns problems when the body is a string', async () => { - const config: Partial = { - response: { - body: 'somebody to love', - code: 200, - }, - } - - mockFetchResource.mockResolvedValue({ status: 200, data: 'RIP' }) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(1) - expect(mockProblemCtor).toHaveBeenCalledWith( - { data: 'RIP', message: 'was not deeply equal to the configured fixture' }, - ProblemType.Response, - ) - }) - - it('returns problems when the configured body is an object but actual data is a string', async () => { - const config: Partial = { - response: { - body: { hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }, - code: 200, - }, - } - - mockFetchResource.mockResolvedValue({ status: 200, data: 'RIP' }) - mockMessages.shouldBe.mockReturnValue('message2') - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(1) - expect(mockMessages.shouldBe).toHaveBeenCalledWith('body', 'of type object', 'a string') - expect(mockProblemCtor).toHaveBeenCalledWith( - { data: 'RIP', message: 'message2' }, - ProblemType.Response, - ) - }) - - it('returns problems when the configured body is a string but actual data is an object', async () => { - const config: Partial = { - response: { - body: 'RIP', - code: 200, - }, - } - - mockFetchResource.mockResolvedValue({ status: 200, data: { rip: 'aroo' } }) - mockMessages.shouldBe.mockReturnValue('message2') - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(1) - expect(mockMessages.shouldBe).toHaveBeenCalledWith('body', 'of type string', 'a object') - expect(mockProblemCtor).toHaveBeenCalledWith( - { data: { rip: 'aroo' }, message: 'message2' }, - ProblemType.Response, - ) - }) - - it('returns problems when the configured body and actual date are both objects', async () => { - const config: Partial = { - response: { - body: { hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }, - code: 200, - }, - } - - const responseData = { hello: 'world' } - mockFetchResource.mockResolvedValue({ status: 200, data: responseData }) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(1) - expect(mockProblemCtor).toHaveBeenCalledWith( - { data: responseData, message: 'was not deeply equal to the configured fixture' }, - ProblemType.Response, - ) - }) - }) - - describe('when the response body does match the expected', () => { - it('does not return problems when both are strings', async () => { - const expectedBody = 'coWaBunGa' - const config: Partial = { - response: { - body: expectedBody, - code: 200, - }, - } - mockFetchResource.mockResolvedValue({ status: 200, data: expectedBody }) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(0) - }) - - it('does not return problems when both are objects', async () => { - const config: Partial = { - response: { - body: { hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }, - code: 200, - }, - } - - mockFetchResource.mockResolvedValue({ - status: 200, - data: { hello: ['to', 'the', { world: 'earth' }], cya: 'later', mate: 23 }, - }) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toHaveLength(0) - }) - }) - }) - - it('returns problems when response type does not match expected', async () => { - const config: Partial = { - response: { - body: 'ayy lmao', - code: 404, - type: 'MyFakeType', - }, - } - - mockFetchResource.mockResolvedValue({ status: 404, data: 'ayy lmao' }) - const problem: Partial = { message: 'Yikes' } - mockTypeValidator.getProblems.mockResolvedValue([problem as Problem]) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config as TestConfig) - - expect(results).toStrictEqual([problem]) - }) - - it('does not blow up if an error is thrown while getting a response', async () => { - const config = new ConfigBuilder().build() - mockFetchResource.mockRejectedValue(new Error('whoops')) - - const results = await doItAll(mockTypeValidator, mockFetchResource)(config) - - expect(results).toHaveLength(1) - expect(mockMessages.problemFetching).toHaveBeenCalledWith('whoops') - }) -}) diff --git a/src/commands/test/validators.ts b/src/commands/test/validators.ts deleted file mode 100644 index 25e666b9..00000000 --- a/src/commands/test/validators.ts +++ /dev/null @@ -1,91 +0,0 @@ -import TypeValidator from '../../validation/type-validator' -import Problem, { ProblemType } from '~problem' -import { shouldBe, problemFetching } from '~messages' -import { TestConfig } from '~commands/test/config' -import { FetchResource, LoaderResponse } from './http-client' - -export type TestFn = (config: TestConfig) => Promise[]> - -const isDeeplyEqual = (expected: unknown, actual: unknown): boolean => { - if (typeof expected === 'object') { - if (!expected) return expected === actual - if (typeof actual !== 'object') return false - if (!actual) return false - - for (const key in expected) { - const expectedValue = expected[key as keyof typeof expected] - const actualValue = actual[key as keyof typeof actual] - if (!isDeeplyEqual(expectedValue, actualValue)) return false - } - - return true - } - - return expected === actual -} - -// TODO: get rid of this. it's only used by test mode now -export const doItAll = (typeValidator: Optional, getResponse: FetchResource): TestFn => { - return async (config): Promise => { - const { response: responseConfig } = config - - const problems: Problem[] = [] - let response: LoaderResponse - - try { - response = await getResponse(config) - } catch (err) { - return [ - new Problem( - { - message: problemFetching(err.message), - }, - ProblemType.Response, - ), - ] - } - - if (responseConfig.code && response.status !== responseConfig.code) { - return [ - new Problem( - { - data: response.status, - message: shouldBe('status code', responseConfig.code, response.status), - }, - ProblemType.Response, - ), - ] - } - - if (responseConfig.body !== undefined) { - if (typeof responseConfig.body !== typeof response.data) { - problems.push( - new Problem( - { - data: response.data, - message: shouldBe('body', `of type ${typeof responseConfig.body}`, `a ${typeof response.data}`), - }, - ProblemType.Response, - ), - ) - } else if (!isDeeplyEqual(responseConfig.body, response.data)) { - problems.push( - new Problem( - { - data: response.data, - message: 'was not deeply equal to the configured fixture', - }, - ProblemType.Response, - ), - ) - } - } - - if (typeValidator && responseConfig.type) { - const result = await typeValidator.getProblems(response.data, responseConfig.type, ProblemType.Response) - if (result) problems.push(...result) - } - - return problems - } -} diff --git a/src/config/types.ts b/src/config/types.ts index 98d9c790..cfc4fcfb 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -50,7 +50,7 @@ export class ConfigBuilder { return this } - public withRequestHeaders(headers?: StringDict): ConfigBuilder { + public withRequestHeaders(headers: Optional): ConfigBuilder { this.config.request.headers = headers return this } @@ -60,12 +60,17 @@ export class ConfigBuilder { return this } - public withResponseBody(body?: Data): ConfigBuilder { + public withResponseBody(body: Optional): ConfigBuilder { this.config.response.body = body return this } - public withResponseHeaders(headers?: StringDict): ConfigBuilder { + public withResponseType(type: Optional): ConfigBuilder { + this.config.response.type = type + return this + } + + public withResponseHeaders(headers: Optional): ConfigBuilder { this.config.response.headers = headers return this } diff --git a/src/validation/type-validator.ts b/src/validation/type-validator.ts index 6ebea037..7b9a1042 100644 --- a/src/validation/type-validator.ts +++ b/src/validation/type-validator.ts @@ -54,7 +54,7 @@ export default class TypeValidator { } } - public async validate(data: Data, type: string): Promise { + public async validate(data: Data | undefined, type: string): Promise { const jsonSchema = await this.schemaRetriever.load(type) const validator = this.validator.compile(jsonSchema) const isValid = validator(data)