Skip to content
This repository has been archived by the owner on Jul 4, 2023. It is now read-only.

Commit

Permalink
refactor(test): rewrite test mode validation
Browse files Browse the repository at this point in the history
re #51
  • Loading branch information
tamj0rd2 committed May 8, 2020
1 parent a6de672 commit 898eacc
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 416 deletions.
4 changes: 2 additions & 2 deletions src/commands/test/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestArgs> =>
Expand Down Expand Up @@ -46,6 +46,6 @@ export default function createTestCommand(
command: 'test <configPath> <baseURL>',
describe: 'Tests configured endpoints',
builder,
handler: createHandler(handleError, createTypeValidator, logger, testConfigs, loadConfig),
handler: createHandler(handleError, createTypeValidator, logger, runTests, loadConfig),
}
}
11 changes: 6 additions & 5 deletions src/commands/test/handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,7 +21,7 @@ const mockedExistsSync = mocked(existsSync)
const mockedResolve = mocked(resolve)
const resolvedTsconfigPath = randomString('resolved-tsconfig')
const mockedLogger = mockObj<NCDCLogger>({ warn: jest.fn() })
const mockedTestConfigs = mockFn<TestConfigs>()
const mockedTestConfigs = mockFn<RunTests>()
const mockedLoadConfig = mockFn<LoadConfig<ValidatedTestConfig>>()
const handler = createHandler(
mockedHandleError,
Expand Down Expand Up @@ -116,7 +116,8 @@ it('calls testConfigs with the correct arguments', async () => {
args.baseURL,
expect.objectContaining({}),
configs,
mockedTypeValidator,
expect.any(Function),
mockedLogger,
)
})

Expand All @@ -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
Expand Down
32 changes: 16 additions & 16 deletions src/commands/test/handler.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,23 +19,20 @@ export const createHandler = (
handleError: HandleError,
createTypeValidator: CreateTypeValidator,
logger: NCDCLogger,
testConfigs: TestConfigs,
runTests: RunTests,
loadConfig: LoadConfig<ValidatedTestConfig>,
) => async (args: TestArgs): Promise<void> => {
const { configPath, baseURL, tsconfigPath, schemaPath, force } = args
if (!configPath) return handleError({ message: `configPath must be specified` })
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:
Expand All @@ -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' })
}
196 changes: 142 additions & 54 deletions src/commands/test/test.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Logger>({ error: jest.fn(), info: jest.fn() })
const mockFetchResource = mockFn<FetchResource>()
const mockTypeValidator = mockObj<TypeValidator>({})
const mockTest = mockFn<TestFn>()
const mockDoItAll = mocked(doItAll)
const mockLogger = mockObj(logger)
const mockTypeValidator = mockObj<TypeValidator>({ 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')
})
})
})
Loading

0 comments on commit 898eacc

Please sign in to comment.