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

Commit

Permalink
fix(serve): also watch files in the schemaPath folder if provided
Browse files Browse the repository at this point in the history
fix #92
  • Loading branch information
tamj0rd2 committed Apr 29, 2020
1 parent 97bf8df commit e9af622
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 64 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ icon.png
acceptance-tests/serve-fixture/config.yml
acceptance-tests/serve-fixture/responses
acceptance-tests/serve-fixture/types.ts
acceptance-tests/serve-fixture/json-schemas
acceptance-tests/test-fixture/config.yml
acceptance-tests/test-fixture/types.ts
43 changes: 42 additions & 1 deletion acceptance-tests/config-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface Config {
code: number
headers?: OutgoingHttpHeaders
type?: string
body?: unknown
serveBody?: unknown
serveBodyPath?: string
}
Expand Down Expand Up @@ -48,6 +49,17 @@ export class ConfigBuilder {
return this
}

public withBody(body: unknown): ConfigBuilder {
if (!body) {
delete this.config.response.body
return this
}

delete this.config.response.serveBody
this.config.response.body = body
return this
}

public withServeBody(serveBody: unknown): ConfigBuilder {
if (!serveBody) {
delete this.config.response.serveBody
Expand All @@ -58,7 +70,7 @@ export class ConfigBuilder {
return this
}

public withFixture(name = 'response'): ConfigBuilder {
public withServeBodyPath(name = 'response'): ConfigBuilder {
if (this.config.response.serveBodyPath) {
throw new Error('Response serveBodyPath already set to ' + this.config.response.serveBodyPath)
}
Expand Down Expand Up @@ -90,17 +102,20 @@ export abstract class ConfigWrapper {
private configs: Config[] = []
private fixtures: Record<string, object> = {}
private types: Record<string, object> = {}
private schemas: Record<string, object> = {}

private readonly CONFIG_FILE: string
private readonly FIXTURE_FOLDER: string
private readonly TYPES_FILE: string
private readonly RESPONSES_FOLDER: string
public readonly JSON_SCHEMAS_FOLDER: string

constructor(configFile: string, fixtureFolder: string) {
this.CONFIG_FILE = configFile
this.FIXTURE_FOLDER = fixtureFolder
this.TYPES_FILE = `${this.FIXTURE_FOLDER}/types.ts`
this.RESPONSES_FOLDER = `${this.FIXTURE_FOLDER}/responses`
this.JSON_SCHEMAS_FOLDER = `${this.FIXTURE_FOLDER}/json-schemas`

if (existsSync(this.CONFIG_FILE)) this.deleteYaml()

Expand All @@ -109,6 +124,11 @@ export abstract class ConfigWrapper {
}
mkdirSync(this.RESPONSES_FOLDER)

if (existsSync(this.JSON_SCHEMAS_FOLDER)) {
rmdirSync(this.JSON_SCHEMAS_FOLDER, { recursive: true })
}
mkdirSync(this.JSON_SCHEMAS_FOLDER)

writeFileSync(this.TYPES_FILE, '')
}

Expand Down Expand Up @@ -203,6 +223,27 @@ export abstract class ConfigWrapper {
return this
}

public addSchemaFile(name: string, content: object): ConfigWrapper {
if (this.schemas[name]) {
throw new Error(`The schema ${name} is already registered`)
}

this.schemas[name] = content

writeFileSync(`${this.JSON_SCHEMAS_FOLDER}/${name}.json`, JSON.stringify(content, undefined, 2))
return this
}

public editSchemaFile(name: string, content: object): ConfigWrapper {
if (!this.schemas[name]) {
throw new Error(`The schema ${name} is not registered`)
}

this.schemas[name] = content
writeFileSync(`${this.JSON_SCHEMAS_FOLDER}/${name}.json`, JSON.stringify(content, undefined, 2))
return this
}

private commitConfigs(): void {
const yaml = jsyaml.safeDump(this.configs)
writeFileSync(this.CONFIG_FILE, yaml)
Expand Down
2 changes: 1 addition & 1 deletion acceptance-tests/serve-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,5 @@ export const prepareServe = (cleanupTasks: CleanupTask[], timeout = 5) => async
}, timeout).catch(failNicely(`The ncdc server was not contactable at ${SERVE_HOST}/`))

if (checkAvailability) await waitUntilAvailable()
return { getAllOutput: () => getRawOutput(), waitForOutput, waitUntilAvailable }
return { getAllOutput: getRawOutput, waitForOutput, waitUntilAvailable }
}
162 changes: 102 additions & 60 deletions acceptance-tests/serve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
SERVE_HOST,
MESSAGE_RESTARTING,
ServeResult,
MESSAGE_RSTARTING_FAILURE,
MESSAGE_RSTARTING_FAILURE as MESSAGE_RESTARTING_FAILURE,
CONFIG_FILE,
FIXTURE_FOLDER,
} from './serve-wrapper'
Expand Down Expand Up @@ -47,7 +47,7 @@ describe('ncdc serve', () => {
it('serves an endpoint from a fixture file', async () => {
// arrange
new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withServeBody(undefined).withFixture('response').build())
.addConfig(new ConfigBuilder().withServeBody(undefined).withServeBodyPath('response').build())
.addFixture('response', {
title: 'nice meme lol',
ISBN: 'asdf',
Expand All @@ -68,7 +68,7 @@ describe('ncdc serve', () => {
it('logs an error and exists if a fixture file does not exist', async () => {
// arrange
new ServeConfigWrapper().addConfig(
new ConfigBuilder().withServeBody(undefined).withFixture('my-fixture').build(),
new ConfigBuilder().withServeBody(undefined).withServeBodyPath('my-fixture').build(),
)

// act
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('ncdc serve', () => {
const configWrapper = new ServeConfigWrapper().addConfig()
const { waitForOutput } = await serve('--watch')
configWrapper.deleteYaml()
await waitForOutput(MESSAGE_RSTARTING_FAILURE)
await waitForOutput(MESSAGE_RESTARTING_FAILURE)

// act
const newConfig = new ConfigBuilder().withName('Cooks').withCode(404).build()
Expand Down Expand Up @@ -160,7 +160,7 @@ describe('ncdc serve', () => {
// arrange
const fixtureName = 'response'
const configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withServeBody(undefined).withFixture(fixtureName).build())
.addConfig(new ConfigBuilder().withServeBody(undefined).withServeBodyPath(fixtureName).build())
.addFixture(fixtureName, {
title: 'nice meme lol',
ISBN: 'asdf',
Expand All @@ -186,7 +186,7 @@ describe('ncdc serve', () => {
// arrange
const fixtureName = 'crazy-fixture'
const configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withServeBody(undefined).withFixture(fixtureName).build())
.addConfig(new ConfigBuilder().withServeBody(undefined).withServeBodyPath(fixtureName).build())
.addFixture(fixtureName, {
title: 'nice meme lol',
ISBN: 'asdf',
Expand All @@ -207,7 +207,7 @@ describe('ncdc serve', () => {
// arrange
const fixtureName = 'another-fixture'
const configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withServeBody(undefined).withFixture(fixtureName).build())
.addConfig(new ConfigBuilder().withServeBody(undefined).withServeBodyPath(fixtureName).build())
.addFixture(fixtureName, {
title: 'nice meme lol',
ISBN: 'asdf',
Expand All @@ -217,7 +217,7 @@ describe('ncdc serve', () => {

const { waitForOutput, waitUntilAvailable } = await serve('--watch')
configWrapper.deleteFixture(fixtureName)
await waitForOutput(MESSAGE_RSTARTING_FAILURE)
await waitForOutput(MESSAGE_RESTARTING_FAILURE)

// act
configWrapper.addFixture(fixtureName, {
Expand All @@ -236,7 +236,7 @@ describe('ncdc serve', () => {
it('restarts each time a fixture file is changed or deleted', async () => {
const fixtureName = 'MyFixture'
const configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withServeBody(undefined).withFixture(fixtureName).build())
.addConfig(new ConfigBuilder().withServeBody(undefined).withServeBodyPath(fixtureName).build())
.addFixture(fixtureName, { title: 'Freddy' })
const { waitForOutput, waitUntilAvailable } = await serve('--watch')

Expand All @@ -254,7 +254,7 @@ describe('ncdc serve', () => {
configWrapper.editFixture(fixtureName, (f) => ({ ...f, title: 'My' }))
await verifyFixture('My')
configWrapper.deleteFixture(fixtureName)
await waitForOutput(MESSAGE_RSTARTING_FAILURE)
await waitForOutput(MESSAGE_RESTARTING_FAILURE)
configWrapper.addFixture(fixtureName, { title: 'Shorts' })
await verifyFixture('Shorts')
configWrapper.editFixture(fixtureName, (f) => ({ ...f, title: 'Please' }))
Expand All @@ -265,7 +265,7 @@ describe('ncdc serve', () => {
it('handles switching from a fixture file to an inline body', async () => {
const configWrapper = new ServeConfigWrapper()
.addConfig(
new ConfigBuilder().withName('config').withServeBody(undefined).withFixture('fixture').build(),
new ConfigBuilder().withName('config').withServeBody(undefined).withServeBodyPath('fixture').build(),
)
.addFixture('fixture', { hello: 'world' })
const { waitForOutput, waitUntilAvailable } = await serve('--watch')
Expand Down Expand Up @@ -301,67 +301,109 @@ describe('ncdc serve', () => {
})

describe('type checking', () => {
const typecheckingCleanup: CleanupTask[] = []
let serve: ServeResult
let configWrapper: ServeConfigWrapper
describe('with schema loading from json files', () => {
const typecheckingCleanup: CleanupTask[] = []
let serve: ServeResult
let configWrapper: ServeConfigWrapper
const schemaName = 'Book'

afterAll(() => {
typecheckingCleanup.forEach((task) => task())
})

afterAll(() => {
typecheckingCleanup.forEach((task) => task())
})
it('serves when the type matches the body', async () => {
configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withType(schemaName).withBody('Hello!').build())
.addSchemaFile(schemaName, { type: 'string' })

it('it serves when the type matches the body', async () => {
configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withType('Book').build())
.addType('Book', {
ISBN: 'string',
ISBN_13: 'string',
author: 'string',
title: 'string',
})
serve = await prepareServe(typecheckingCleanup)(
`--watch --schemaPath ${configWrapper.JSON_SCHEMAS_FOLDER}`,
)

const res = await fetch('/api/books/123')
expect(res.status).toBe(200)
await expect(res.text()).resolves.toBe('Hello!')
})

serve = await prepareServe(typecheckingCleanup, 10)('--watch')
it('restarts when the json schema changes', async () => {
configWrapper.editSchemaFile(schemaName, { type: 'number' })
await serve.waitForOutput(MESSAGE_RESTARTING_FAILURE)
})

it('can recover if the json schema matches the body again', async () => {
configWrapper.editSchemaFile(schemaName, { type: 'string' })
await serve.waitForOutput(MESSAGE_RESTARTING)
await serve.waitUntilAvailable()

await expect(fetch('/api/books/hello')).resolves.toMatchObject({ status: 200 })
const res = await fetch('/api/books/123')
expect(res.status).toBe(200)
await expect(res.text()).resolves.toBe('Hello!')
})
})

it('stops the server if a body stops matching the type', async () => {
configWrapper.editConfig('Books', (config) => ({
...config,
response: {
...config.response,
serveBody: {
title: 123,
describe('with schema generation', () => {
const typecheckingCleanup: CleanupTask[] = []
let serve: ServeResult
let configWrapper: ServeConfigWrapper

afterAll(() => {
typecheckingCleanup.forEach((task) => task())
})

it('it serves when the type matches the body', async () => {
configWrapper = new ServeConfigWrapper()
.addConfig(new ConfigBuilder().withType('Book').build())
.addType('Book', {
ISBN: 'string',
ISBN_13: 'string',
author: 'string',
title: 'string',
})

serve = await prepareServe(typecheckingCleanup, 10)('--watch')

await expect(fetch('/api/books/hello')).resolves.toMatchObject({ status: 200 })
})

it('stops the server if a body stops matching the type', async () => {
configWrapper.editConfig('Books', (config) => ({
...config,
response: {
...config.response,
serveBody: {
title: 123,
},
},
},
}))
}))

await serve.waitForOutput(MESSAGE_RESTARTING)
await serve.waitForOutput(/Could not restart ncdc server.*due to config errors/)
await expect(fetch('/api/books/aldksj')).rejects.toThrowError()
})
await serve.waitForOutput(MESSAGE_RESTARTING)
await serve.waitForOutput(/Could not restart ncdc server.*due to config errors/)
await expect(fetch('/api/books/aldksj')).rejects.toThrowError()
})

it('can recover from incorrect body type validation', async () => {
configWrapper.editConfig('Books', (config) => ({
...config,
response: {
...config.response,
serveBody: {
ISBN: 'ISBN',
ISBN_13: 'ISBN_13',
author: 'author',
title: 'title',
it('can recover from incorrect body type validation', async () => {
configWrapper.editConfig('Books', (config) => ({
...config,
response: {
...config.response,
serveBody: {
ISBN: 'ISBN',
ISBN_13: 'ISBN_13',
author: 'author',
title: 'title',
},
},
},
}))
}))

await serve.waitForOutput(MESSAGE_RESTARTING)
await serve.waitForOutput(MESSAGE_RESTARTING)

await expect(fetch('/api/books/asdf')).resolves.toMatchObject({ status: 200 })
})
await expect(fetch('/api/books/asdf')).resolves.toMatchObject({ status: 200 })
})

// TODO: oooooh. NCDC could actually have a caching folder!!! Then generate
// would just become the default because why the hell not? :D
// typescript-json-schema getSourceFile could really help with this too
it.todo('restarts when a source file containing types changes')
// TODO: oooooh. NCDC could actually have a caching folder!!! Then generate
// would just become the default because why the hell not? :D
// typescript-json-schema getSourceFile could really help with this too
it.todo('restarts when a source file containing types changes')
})
})
})
7 changes: 5 additions & 2 deletions src/commands/serve/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const createHandler = (
if (!validationResult.validatedConfigs.length) throw new Error('No configs to serve')

const configUsesTypes = validationResult.validatedConfigs.find((c) => c.request.type || c.response.type)
if (!typeValidator && configUsesTypes) {
if (configUsesTypes && (schemaPath || !typeValidator)) {
typeValidator = configUsesTypes && createTypeValidator(tsconfigPath, force, schemaPath)
}

Expand Down Expand Up @@ -126,7 +126,10 @@ const createHandler = (

if (watch) {
const fixturesToWatch = [...result.pathsToWatch]
const configWatcher = chokidar.watch([absoluteConfigPath, ...fixturesToWatch], {
const chokidarWatchPaths = [absoluteConfigPath, ...fixturesToWatch]
if (schemaPath) chokidarWatchPaths.push(resolve(schemaPath))

const configWatcher = chokidar.watch(chokidarWatchPaths, {
ignoreInitial: true,
})

Expand Down

0 comments on commit e9af622

Please sign in to comment.