Repository: bustlelabs/shep (js)

Issue:
Shep lint
A command which would check the current project against best practices. It would also be run as part of the `shep deploy` command. Both #91 and #127 already implement warnings and I foresee that the list of checks will continue to grow so having a system in place would be nice. Would this be worthwhile?

Rules adding:
- [x] `api.json` has duplicate `cacheNamespace`
- [x] `api.json` is missing `Description`
- [x] `function-name` isn't referenced in `api.json`
- [x] `uri` doesn't include `${stageVariables.functionAlias}`
- [x] `lambda.json` doesn't have description
- [x] Shep version running doesn't match package Shep

Pull request:
Lint
Closes #167 #91 

Todo:
- [x] make warnings pretty
- [x] write tests for rules
- [x] update readme


Patch:
diff --git a/README.md b/README.md
index 3146b01..eff717b 100644
--- a/README.md
+++ b/README.md
@@ -69,205 +69,213 @@ _Coming soon! See [`shep new`](#shep-new)_
 Usage: shep <command> [options]
 
 Commands:
-build [env] [functions]   Builds functions and writes them to disk
-deploy [env] [functions]  Deploy both functions and APIs to AWS. Will create a new API if the ID is not specified
-generate                  Run `shep generate --help` for additional information
-logs [stage] [name]       Streams logs from the specified version of a function
-new [path]                Create a new shep project
-pull                      Pulls a swagger JSON representation of an existing API and writes it to a local file
-push                      Create a new shep project
-run [pattern]             Run a function in your local environemnt
+  build [functions]         Builds functions and writes them to disk
+  config                    Run `shep config --help` for additional information
+  deploy [env] [functions]  Deploy both functions and APIs to AWS. Will create a new API if the ID is not specified
+  doctor                    Checks your projects against best standards
+  generate                  Run `shep generate --help` for additional information
+  logs [stage] [name]       Streams logs from the specified version of a function
+  new [path]                Create a new shep project
+  pull                      Pulls a swagger JSON representation of an existing API and writes it to a local file
+  push                      Create a new shep project
+  run [pattern]             Run a function in your local environemnt
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
 ```
 #### `shep new`
 ```
 shep new [path]
 
 Options:
---version      Show version number                                                                           [boolean]
---help         Show help                                                                                     [boolean]
---path         Location to create the new shep project
---skip-config  Skips configuring shep project                                                                [boolean]
---region       Region for new shep project
---rolename     Name of IAM Role which will be used to execute Lambda functions
+  --version      Show version number                                                                           [boolean]
+  --help         Show help                                                                                     [boolean]
+  --path         Location to create the new shep project
+  --skip-config  Skips configuring shep project                                                                [boolean]
+  --region       Region for new shep project
+  --rolename     Name of IAM Role which will be used to execute Lambda functions
+  -q, --quiet    Don't log anything                                                                     [default: false]
 
 Examples:
-shep new         Launch an interactive CLI
-shep new my-api  Generates a project at `my-api`
+  shep new         Launch an interactive CLI
+  shep new my-api  Generates a project at `my-api`
 ```
 #### `shep pull`
 ```
 shep pull
 
 Options:
---version     Show version number                                                                            [boolean]
---help        Show help                                                                                      [boolean]
---region, -r  AWS region                                                                                    [required]
---stage, -s   AWS API Gateway stage. Read from the shep config in package.json if not provided              [required]
---api-id, -a  AWS API Gateway ID. Read from the shep config in package.json if not provided                 [required]
---output, -o  Path of the file to output                                                         [default: "api.json"]
+  --version     Show version number                                                                            [boolean]
+  --help        Show help                                                                                      [boolean]
+  --region, -r  AWS region                                                                                    [required]
+  --stage, -s   AWS API Gateway stage. Read from the shep config in package.json if not provided              [required]
+  --api-id, -a  AWS API Gateway ID. Read from the shep config in package.json if not provided                 [required]
+  --output, -o  Path of the file to output                                                         [default: "api.json"]
+  -q, --quiet   Don't log anything                                                                      [default: false]
+  --build                                                                                                [default: true]
 
 Examples:
-shep pull                           Download a JSON swagger file for `apiId` in package.json and prompts for stage via
-interactive CLI
-shep pull --api-id foo --stage bar  Downloads a JSON swagger file for stage `bar` of API id `foo`
-shep pull --output other-path.json  Writes the JSON swagger file to `other-path.json`
+  shep pull                           Download a JSON swagger file for `apiId` in package.json and prompts for stage via
+                                      interactive CLI
+  shep pull --api-id foo --stage bar  Downloads a JSON swagger file for stage `bar` of API id `foo`
+  shep pull --output other-path.json  Writes the JSON swagger file to `other-path.json`
 ```
 #### `shep push`
 ```
 shep push
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
---api-id   API Gateway resource id. Read from package.json if not provided                                  [required]
---region   AWS region. Read from package.json if not provided                                               [required]
+  --version    Show version number                                                                             [boolean]
+  --help       Show help                                                                                       [boolean]
+  --api-id     API Gateway resource id. Read from package.json if not provided                                [required]
+  --region     AWS region. Read from package.json if not provided                                             [required]
+  -q, --quiet  Don't log anything                                                                       [default: false]
 
 Examples:
-shep push                                  Pushes the api.json swagger configuration to API Gateway. Does not deploy
-the API.
-shep push --api-id foo --region us-east-1
+  shep push                                  Pushes the api.json swagger configuration to API Gateway. Does not deploy
+                                             the API.
+  shep push --api-id foo --region us-east-1
 ```
 #### `shep run`
 ```
 shep run [pattern]
 
 Options:
---version      Show version number                                                                           [boolean]
---help         Show help                                                                                     [boolean]
---environment  Environment variables to use                                                   [default: "development"]
---event        Event to use
--v             Responses from functions aren't truncated
---build        Build functions before running. Use --no-build to skip this step                        [default: true]
+  --version      Show version number                                                                           [boolean]
+  --help         Show help                                                                                     [boolean]
+  --environment  Environment variables to use                                                   [default: "development"]
+  --event        Event to use
+  -v             Responses from functions aren't truncated
+  --build        Build functions before running. If omitted functions are transpiled by babel on the fly[default: false]
 
 Examples:
-shep run                               Launch an interactive CLI
-shep run foo                           Runs the `foo` function for all events
-shep run foo --no-build                Run the already built `foo` function in the dist folder
-shep run foo --event default           Runs the `foo` function for just the `default` event
-shep run foo --environment production  Runs the `foo` function with production environment
-DB_TABLE=custom shep run foo           Runs the `foo` function with process.env.DB_TABLE assigned to custom (vars
-    declared this way will overwrite vals in your environments/${env}.json file)
-shep run '*'                           Runs all functions for all events
-shep run 'foo-*'                       Runs all functions matching pattern `foo-*`
+  shep run                               Launch an interactive CLI
+  shep run foo                           Runs the `foo` function for all events
+  shep run foo --build                   Builds the `foo` function and then runs it
+  shep run foo --event default           Runs the `foo` function for just the `default` event
+  shep run foo --environment production  Runs the `foo` function with production environment
+  DB_TABLE=custom shep run foo           Runs the `foo` function with process.env.DB_TABLE assigned to custom (vars
+                                         declared this way will overwrite vals in your .env file)
+  shep run '*'                           Runs all functions for all events
+  shep run 'foo-*'                       Runs all functions matching pattern `foo-*`
 ```
 #### `shep deploy`
 ```
 shep deploy [env] [functions]
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
---build    Build functions before deployment. Use --no-build to skip this step                         [default: true]
+  --version    Show version number                                                                             [boolean]
+  --help       Show help                                                                                       [boolean]
+  --build      Build functions before deployment. Use --no-build to skip this step                       [default: true]
+  -q, --quiet  Don't log anything                                                                       [default: false]
 
 Examples:
-shep deploy                         Launch an interactive CLI
-shep deploy production              Deploy all functions with production env variables
-shep deploy beta --no-build         Deploy all functions as currently built in the dist folder
-shep deploy production create-user  Deploy only the create-user function
-shep deploy beta *-user             Deploy only functions matching the pattern *-user
+  shep deploy                         Launch an interactive CLI
+  shep deploy production              Deploy all functions with production env variables
+  shep deploy beta --no-build         Deploy all functions as currently built in the dist folder
+  shep deploy production create-user  Deploy only the create-user function
+  shep deploy beta *-user             Deploy only functions matching the pattern *-user
 ```
 #### `shep build`
 ```
-shep build [env] [functions]
+shep build [functions]
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
+  --version    Show version number                                                                             [boolean]
+  --help       Show help                                                                                       [boolean]
+  -q, --quiet  Don't log anything                                                                       [default: false]
 
 Examples:
-shep build                   Launch an interactive CLI
-shep build beta              Build all functions with beta environment variables
-shep build beta create-user  Build only the create-user function
-shep build beta *-user       Build functions matching the pattern *-user
+  shep build                   Launch an interactive CLI
+  shep build beta              Build all functions with beta environment variables
+  shep build beta create-user  Build only the create-user function
+  shep build beta *-user       Build functions matching the pattern *-user
 ```
 #### `shep logs`
 ```
 shep logs [stage] [name]
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
---stage    Name of stage to use
---name     Name of function to use
---region   Name of region to use, uses region in `package.json` if not given
---stream                                                                                     [boolean] [default: true]
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
+  --stage    Name of stage to use
+  --name     Name of function to use
+  --region   Name of region to use, uses region in `package.json` if not given
+  --stream                                                                                     [boolean] [default: true]
 
 Examples:
-shep logs                 Launch an interactive CLI
-shep logs production foo  Shows logs for the `foo` function in the production environment
+  shep logs                 Launch an interactive CLI
+  shep logs production foo  Shows logs for the `foo` function in the production environment
 ```
-#### `shep config list`
+#### `shep doctor`
 ```
-shep config list [env] [functionName]
+shep doctor
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
+  --version    Show version number                                                                             [boolean]
+  --help       Show help                                                                                       [boolean]
+  --verbose    Logs additional information
+  -q, --quiet  Don't log anything                                                                       [default: false]
 
 Examples:
-shep config list            Launches an interactive CLI to choose env and functionName
-shep config list beta functionName   Lists the environment variables and values for foo with alias beta
-```
-#### `shep config set`
-```
-shep config set [env] [vars...]
-
-Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
-
-Examples:
-shep config set beta FOO=BAR BAR=FOO Adds these environment variables and values to all functions and aliases them to beta
-```
-#### `shep config remove`
-```
-shep config remove [env] [vars...]
-
-Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
-
-Examples:
-shep config remove beta FOO  Removes the environment variable FOO from beta aliased functions
-shep config remove production FOO BAR Removes FOO and BAR environment variables from production functions
+  shep doctor  Runs the doctor on your project
 ```
 #### `shep generate function`
 ```
 shep generate function [name]
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
---name     Function name
+  --version    Show version number                                                                             [boolean]
+  --help       Show help                                                                                       [boolean]
+  --name       Function name
+  -q, --quiet  Don't log anything                                                                       [default: false]
 
 Examples:
-shep generate function      Launch an interactive CLI
-shep generate function foo  Genereate a new functon called "foo"
+  shep generate function      Launch an interactive CLI
+  shep generate function foo  Genereate a new functon called "foo"
 ```
 #### `shep generate endpoint`
 ```
 shep generate endpoint [path]
 
 Options:
---version  Show version number                                                                               [boolean]
---help     Show help                                                                                         [boolean]
---method   HTTP Method                                     [choices: "get", "post", "put", "delete", "options", "any"]
+  --version    Show version number                                                                             [boolean]
+  --help       Show help                                                                                       [boolean]
+  --method     HTTP Method                                   [choices: "get", "post", "put", "delete", "options", "any"]
+  -q, --quiet  Don't log anything                                                                       [default: false]
 ```
 #### `shep generate webpack`
 ```
 shep generate webpack
 
 Options:
---version     Show version number                                                                            [boolean]
---help        Show help                                                                                      [boolean]
---output, -o  Output file                                                               [default: "webpack.config.js"]
+  --version     Show version number                                                                            [boolean]
+  --help        Show help                                                                                      [boolean]
+  --output, -o  Output file                                                               [default: "webpack.config.js"]
 
 Examples:
-shep generate webpack -o foo.js  Writes default webpack configuration to foo.js
+  shep generate webpack -o foo.js  Writes default webpack configuration to foo.js
+```
+#### `shep version`
+```
+Usage: shep <command> [options]
+
+Commands:
+  build [functions]         Builds functions and writes them to disk
+  config                    Run `shep config --help` for additional information
+  deploy [env] [functions]  Deploy both functions and APIs to AWS. Will create a new API if the ID is not specified
+  doctor                    Checks your projects against best standards
+  generate                  Run `shep generate --help` for additional information
+  logs [stage] [name]       Streams logs from the specified version of a function
+  new [path]                Create a new shep project
+  pull                      Pulls a swagger JSON representation of an existing API and writes it to a local file
+  push                      Create a new shep project
+  run [pattern]             Run a function in your local environemnt
+
+Options:
+  --version  Show version number                                                                               [boolean]
+  --help     Show help                                                                                         [boolean]
 ```
 
 ## Lambda Execution Role
diff --git a/src/commands/doctor.js b/src/commands/doctor.js
new file mode 100644
index 0000000..43aa12a
--- /dev/null
+++ b/src/commands/doctor.js
@@ -0,0 +1,14 @@
+import lint from '../doctor'
+
+export const command = 'doctor'
+export const desc = 'Checks your projects against best standards'
+export function builder (yargs) {
+  return yargs
+  .describe('quiet', 'Don\'t log anything')
+  .describe('verbose', 'Logs additional information')
+  .default('quiet', false)
+  .alias('q', 'quiet')
+  .example('shep doctor', 'Runs the doctor on your project')
+}
+
+export const handler = lint
diff --git a/src/doctor/index.js b/src/doctor/index.js
new file mode 100644
index 0000000..027f912
--- /dev/null
+++ b/src/doctor/index.js
@@ -0,0 +1,29 @@
+/* eslint import/namespace: ['error', { allowComputed: true }] */
+import * as rules from './rules'
+
+export default function (opts) {
+  const log = logger(opts.verbose)
+  const ruleNames = Object.keys(rules).filter((name) => name !== 'toString' && name !== 'default')
+  const warnings = ruleNames.map((name) => {
+    return rules[name](opts)
+  })
+  const flatWarnings = warnings.reduce((com, a) => com.concat(a), [])
+
+  if (!opts.quiet) { flatWarnings.forEach(log) }
+
+  const errors = warnings.filter(({ type }) => type === 'error').map(({ message }) => message)
+
+  if (errors.length > 0) {
+    console.error(errors.join('\n'))
+    process.exit(errors.length)
+  }
+
+  return flatWarnings
+}
+
+function logger (verbose) {
+  return ({ rule, message }) => {
+    if (verbose) { console.log(rule) }
+    console.log(message + '\n')
+  }
+}
diff --git a/src/doctor/rules/duplicate-cachenamespaces.js b/src/doctor/rules/duplicate-cachenamespaces.js
new file mode 100644
index 0000000..90a9491
--- /dev/null
+++ b/src/doctor/rules/duplicate-cachenamespaces.js
@@ -0,0 +1,45 @@
+import * as load from '../../util/load'
+import parseApi from '../../util/parse-api'
+
+export default function () {
+  const api = load.api()
+  if (!api) { return [] }
+
+  const parsedApi = parseApi(api)
+
+  const partitionedByCache = parsedApi
+    .filter(hasIntegration)
+    .reduce(partitionByCache, {})
+
+  const duplicateNamespaces = Object.keys(partitionedByCache)
+          .map((cacheName) => partitionedByCache[cacheName])
+          .filter((partition) => partition.length > 1)
+
+  return duplicateNamespaces.map(generateWarnings)
+}
+
+function hasIntegration ({ integration }) {
+  return integration !== undefined
+}
+
+function partitionByCache (acc, endpoint) {
+  const currentNamespace = endpoint.integration.cacheNamespace
+  if (currentNamespace === undefined) return acc
+  if (acc[currentNamespace] === undefined) {
+    acc[currentNamespace] = [endpoint]
+  } else {
+    acc[currentNamespace].push(endpoint)
+  }
+  return acc
+}
+
+function generateWarnings (partition) {
+  const nameSpace = partition[0].integration.cacheNamespace
+  const endpoints = partition.map(({ path, method }) => {
+    return `  ${path} ${method.toUpperCase()}`
+  }).join('\n')
+  return {
+    rule: 'duplicate-cachenamespaces',
+    message: `cacheNamespace '${nameSpace}' is shared by the following: \n${endpoints}`
+  }
+}
diff --git a/src/doctor/rules/index.js b/src/doctor/rules/index.js
new file mode 100644
index 0000000..9499520
--- /dev/null
+++ b/src/doctor/rules/index.js
@@ -0,0 +1,6 @@
+export { default as missingConfig } from './missing-config'
+export { default as unreferencedFunction } from './unreferenced-function'
+export { default as duplicateCacheNamespaces } from './duplicate-cachenamespaces'
+export { default as missingApiDescription } from './missing-api-description'
+export { default as unaliasedUri } from './unaliased-uri'
+export { default as shepVersionMismatch } from './shep-version-mismatch'
diff --git a/src/doctor/rules/missing-api-description.js b/src/doctor/rules/missing-api-description.js
new file mode 100644
index 0000000..bd12f6d
--- /dev/null
+++ b/src/doctor/rules/missing-api-description.js
@@ -0,0 +1,11 @@
+import * as load from '../../util/load'
+
+export default function () {
+  const api = load.api()
+  if (!api || api.info.description) { return [] }
+
+  return [{
+    rule: 'missing-api-description',
+    message: 'api.json has no description in the info object'
+  }]
+}
diff --git a/src/doctor/rules/missing-config.js b/src/doctor/rules/missing-config.js
new file mode 100644
index 0000000..8d9c5f4
--- /dev/null
+++ b/src/doctor/rules/missing-config.js
@@ -0,0 +1,18 @@
+import { funcs, lambdaConfig } from '../../util/load'
+export default function () {
+  const funcNames = funcs('*')
+  const configs = funcNames.map(lambdaConfig)
+  const warnConfigs = configs.filter(missingDescription)
+  return warnConfigs.map(generateWarning)
+}
+
+function missingDescription (config) {
+  return !config.Description
+}
+
+function generateWarning (config) {
+  return {
+    rule: 'missing-description',
+    message: `Function ${config.FunctionName} has no description set`
+  }
+}
diff --git a/src/doctor/rules/shep-version-mismatch.js b/src/doctor/rules/shep-version-mismatch.js
new file mode 100644
index 0000000..b1349a8
--- /dev/null
+++ b/src/doctor/rules/shep-version-mismatch.js
@@ -0,0 +1,15 @@
+import { version } from '../../index'
+import * as load from '../../util/load'
+
+export default function () {
+  const pkgVersion = load.pkg().devDependencies.shep
+  return pkgVersion === version ? [] : generateWarning(version, pkgVersion)
+}
+
+function generateWarning (version, pkgVersion) {
+  return [{
+    rule: 'shep-version-mismatch',
+    type: 'error',
+    message: `shep ${version} is being used, but package.json requires ${pkgVersion}`
+  }]
+}
diff --git a/src/doctor/rules/unaliased-uri.js b/src/doctor/rules/unaliased-uri.js
new file mode 100644
index 0000000..b10b7e2
--- /dev/null
+++ b/src/doctor/rules/unaliased-uri.js
@@ -0,0 +1,24 @@
+import * as load from '../../util/load'
+import parseApi from '../../util/parse-api'
+
+export default function () {
+  const parsedApi = parseApi(load.api())
+  const unaliasedEndpoints = parsedApi.filter(hasNoAlias)
+
+  return unaliasedEndpoints.map(generateWarnings)
+}
+
+function hasNoAlias ({ integration }) {
+  const uri = integration.uri
+  const aliasString = '${stageVariables.functionAlias}' // eslint-disable-line no-template-curly-in-string
+
+  return uri && uri.indexOf(aliasString) === -1
+}
+
+function generateWarnings ({ path, method }) {
+  return {
+    rule: 'unaliased-uri',
+    type: 'error',
+    message: `The integration of ${path} ${method.toUpperCase()} isn't aliased.`
+  }
+}
diff --git a/src/doctor/rules/unreferenced-function.js b/src/doctor/rules/unreferenced-function.js
new file mode 100644
index 0000000..6a9f64a
--- /dev/null
+++ b/src/doctor/rules/unreferenced-function.js
@@ -0,0 +1,30 @@
+import * as load from '../../util/load'
+import parseApi from '../../util/parse-api'
+import generateName from '../../util/generate-name'
+
+export default function () {
+  const api = load.api()
+  if (!api) { return [] }
+
+  const funcNames = load.funcs('*')
+  const parsedApi = parseApi(api)
+  const unreferencedFunctions = funcNames.filter((funcName) => {
+    return !parsedApi.some(({ integration }) => isFuncInUri(funcName, integration.uri))
+  })
+
+  return unreferencedFunctions.map(generateWarning)
+}
+
+function isFuncInUri (funcName, uri) {
+  if (uri === undefined) { return false }
+  const funcRegExp = new RegExp(`:${generateName(funcName).fullName}:`)
+  return funcRegExp.test(uri)
+}
+
+function generateWarning (funcName) {
+  return {
+    rule: 'unreferenced-functions',
+    message: `Function ${funcName} isn't referenced by api.json`
+  }
+}
+
diff --git a/src/index.js b/src/index.js
index 8da35b4..2da9156 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,7 @@ export { default as run } from './run'
 export { default as deploy } from './deploy'
 export { default as build } from './build'
 export { default as logs } from './logs'
+export { default as doctor } from './doctor'
 export { default as generateFunction } from './generate-function'
 export { default as generateEndpoint } from './generate-endpoint'
 export { default as generateWebpack } from './generate-webpack'
diff --git a/src/util/modules/fs.js b/src/util/modules/fs.js
index 1f0a163..84a3d68 100644
--- a/src/util/modules/fs.js
+++ b/src/util/modules/fs.js
@@ -7,3 +7,4 @@ export const writeJSON = fs.writeJSONAsync
 export const readJSON = fs.readJSONAsync
 export const readJSONSync = fs.readJSONSync
 export const readdirSync = fs.readdirSync
+export const readdir = fs.readdirAsync
diff --git a/src/util/parse-api.js b/src/util/parse-api.js
new file mode 100644
index 0000000..a5f02d0
--- /dev/null
+++ b/src/util/parse-api.js
@@ -0,0 +1,19 @@
+export default function (api) {
+  if (!api) { return [] }
+
+  const paths = Object.keys(api.paths)
+
+  const methods = paths.reduce((acc, path) => {
+    const nestedMethods = Object.keys(api.paths[path]).map((method) => {
+      return { path, method }
+    })
+    return acc.concat(nestedMethods)
+  }, [])
+
+  const integrations = methods.map(({ path, method }) => {
+    const integration = api.paths[path][method]['x-amazon-apigateway-integration']
+    return { path, method, integration }
+  })
+
+  return integrations
+}


Test patch:
diff --git a/test/doctor/index.js b/test/doctor/index.js
new file mode 100644
index 0000000..9c25f48
--- /dev/null
+++ b/test/doctor/index.js
@@ -0,0 +1,20 @@
+import test from 'ava'
+import td from '../helpers/testdouble'
+import { isArray } from 'util'
+
+test.afterEach(() => {
+  td.reset()
+})
+
+test('Returns flat array of warnings', (t) => {
+  const fooWarn = 'bad foo'
+  const barWarn = 'bad bar'
+  const rules = td.replace('../../src/doctor/rules', td.object(['foo', 'bar']))
+  td.when(rules.foo(), { ignoreExtraArgs: true }).thenReturn([fooWarn])
+  td.when(rules.bar(), { ignoreExtraArgs: true }).thenReturn([barWarn])
+
+  const warnings = require('../../src/doctor')({ quiet: true })
+  t.truthy(isArray(warnings))
+  t.not(warnings.indexOf(fooWarn), -1)
+  t.not(warnings.indexOf(barWarn), -1)
+})
diff --git a/test/doctor/rules/duplicate-cachenamespaces.js b/test/doctor/rules/duplicate-cachenamespaces.js
new file mode 100644
index 0000000..22a8ccf
--- /dev/null
+++ b/test/doctor/rules/duplicate-cachenamespaces.js
@@ -0,0 +1,35 @@
+import test from 'ava'
+import td from '../../helpers/testdouble'
+import { isArray } from 'util'
+
+const mockApi = {
+  paths: {
+    '/foo': {
+      get: {
+        'x-amazon-apigateway-integration': {
+          cacheNamespace: 'name'
+        }
+      },
+      put: {
+        'x-amazon-apigateway-integration': {
+          cacheNamespace: 'name'
+        }
+      }
+    }
+  }
+}
+
+const load = td.replace('../../../src/util/load', td.object(['api']))
+
+const duplicateCacheNamespaces = require('../../../src/doctor/rules/duplicate-cachenamespaces')
+
+test('No warnings when no api.json', (t) => {
+  const warnings = duplicateCacheNamespaces({})
+  t.true(isArray(warnings))
+  t.is(warnings.length, 0)
+})
+
+test('Warns when duplicate namespaces', (t) => {
+  td.when(load.api()).thenReturn(mockApi)
+  t.is(duplicateCacheNamespaces({}).length, 1)
+})
diff --git a/test/doctor/rules/missing-api-description.js b/test/doctor/rules/missing-api-description.js
new file mode 100644
index 0000000..ceab03b
--- /dev/null
+++ b/test/doctor/rules/missing-api-description.js
@@ -0,0 +1,10 @@
+import test from 'ava'
+import td from '../../helpers/testdouble'
+
+const load = td.replace('../../../src/util/load')
+td.when(load.api()).thenReturn({ info: { description: '' } })
+
+test('Returns warning when no description set', (t) => {
+  const warnings = require('../../../src/doctor/rules/missing-api-description')()
+  t.is(warnings.length, 1)
+})
diff --git a/test/doctor/rules/missing-config.js b/test/doctor/rules/missing-config.js
new file mode 100644
index 0000000..e7c294c
--- /dev/null
+++ b/test/doctor/rules/missing-config.js
@@ -0,0 +1,13 @@
+import test from 'ava'
+import td from '../../helpers/testdouble'
+
+const funcs = ['foo', 'bar']
+
+const load = td.replace('../../../src/util/load')
+td.when(load.funcs('*')).thenReturn(funcs)
+td.when(load.lambdaConfig(), { ignoreExtraArgs: true }).thenReturn({})
+
+test('Reports missing description for lambda configs', (t) => {
+  const warnings = require('../../../src/doctor/rules/missing-config')()
+  t.is(warnings.length, funcs.length)
+})
diff --git a/test/doctor/rules/shep-version-mismatch.js b/test/doctor/rules/shep-version-mismatch.js
new file mode 100644
index 0000000..2afd2fd
--- /dev/null
+++ b/test/doctor/rules/shep-version-mismatch.js
@@ -0,0 +1,11 @@
+import test from 'ava'
+import td from '../../helpers/testdouble'
+
+td.replace('../../../src/index', { version: '1.0.0' })
+const load = td.replace('../../../src/util/load')
+td.when(load.pkg()).thenReturn({ devDependencies: { shep: '1.0.1' } })
+
+test('Warns on mismatch', (t) => {
+  const warnings = require('../../../src/doctor/rules/shep-version-mismatch')()
+  t.is(warnings.length, 1)
+})
diff --git a/test/doctor/rules/unaliased-uri.js b/test/doctor/rules/unaliased-uri.js
new file mode 100644
index 0000000..f5a563d
--- /dev/null
+++ b/test/doctor/rules/unaliased-uri.js
@@ -0,0 +1,27 @@
+import test from 'ava'
+import td from '../../helpers/testdouble'
+
+const api = {
+  paths: {
+    '/foo': {
+      get: {
+        'x-amazon-apigateway-integration': {
+          uri: 'has no alias'
+        }
+      },
+      post: {
+        'x-amazon-apigateway-integration': {
+          uri: '${stageVariables.functionAlias}' // eslint-disable-line no-template-curly-in-string
+        }
+      }
+    }
+  }
+}
+
+const load = td.replace('../../../src/util/load')
+td.when(load.api()).thenReturn(api)
+
+test('Reports unaliased endpoints', (t) => {
+  const warnings = require('../../../src/doctor/rules/unaliased-uri')()
+  t.is(warnings.length, 1)
+})
diff --git a/test/doctor/rules/unreferenced-function.js b/test/doctor/rules/unreferenced-function.js
new file mode 100644
index 0000000..6da20f8
--- /dev/null
+++ b/test/doctor/rules/unreferenced-function.js
@@ -0,0 +1,32 @@
+import test from 'ava'
+import td from '../../helpers/testdouble'
+
+const funcs = ['foo', 'bar']
+
+const load = td.replace('../../../src/util/load', td.object(['funcs', 'api', 'pkg']))
+td.when(load.funcs('*')).thenReturn(funcs)
+td.when(load.pkg()).thenReturn({ name: 'test' })
+
+const unreferencedFunction = require('../../../src/doctor/rules/unreferenced-function')
+
+test('Should report missing functions', (t) => {
+  const api = { paths: {} }
+  td.when(load.api()).thenReturn(api)
+
+  const warnings = unreferencedFunction({})
+  t.is(warnings.length, funcs.length)
+})
+
+test('Should be quiet when missing api', (t) => {
+  td.when(load.api()).thenReturn(null)
+  const warnings = unreferencedFunction({})
+  t.deepEqual(warnings, [])
+})
+
+test('Should report difference', (t) => {
+  const uri = ':test-foo:'
+  const api = { paths: { '/foo': { 'get': { 'x-amazon-apigateway-integration': { uri, type: 'aws_proxy' } } } } }
+  td.when(load.api()).thenReturn(api)
+
+  t.is(unreferencedFunction({}).length, 1)
+})
diff --git a/test/util/parse-api.js b/test/util/parse-api.js
new file mode 100644
index 0000000..e6de3c8
--- /dev/null
+++ b/test/util/parse-api.js
@@ -0,0 +1,49 @@
+import test from 'ava'
+import parseApi from '../../src/util/parse-api'
+import { isArray } from 'util'
+
+const mockApi = {
+  paths: {
+    '/foo': {
+      get: {
+        'x-amazon-apigateway-integration': {
+          uri: 'some-lambda-arn',
+          type: 'aws_proxy'
+        }
+      },
+      post: {
+        'x-amazon-apigateway-integration': {
+          uri: 'some-other-labmda-arn',
+          type: 'aws_proxy'
+        }
+      }
+    }
+  }
+}
+
+test('Should return array', (t) => {
+  t.true(isArray(parseApi()))
+  t.true(isArray(parseApi(mockApi)))
+})
+
+test('Should handle multiple http methods correctly', (t) => {
+  const api = parseApi(mockApi)
+  api.map(({ path }) => path).forEach((path) => t.is(path, '/foo'))
+  t.deepEqual(api.map(({ method }) => method), ['get', 'post'])
+})
+
+test('Should pass entire integration', (t) => {
+  const apiIntegration = { type: 'mock' }
+  const api = {
+    paths: {
+      '/foo': {
+        options: {
+          'x-amazon-apigateway-integration': apiIntegration
+        }
+      }
+    }
+  }
+
+  const parsedApi = parseApi(api)
+  parsedApi.map(({ integration }) => integration).forEach((integration) => t.deepEqual(integration, apiIntegration))
+})