Skip to content

Commit

Permalink
feat: initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
gr2m committed Feb 11, 2018
1 parent ccbbaf1 commit 5ec06d0
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 0 deletions.
49 changes: 49 additions & 0 deletions bin/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node

const cors = require('cors')
const express = require('express')
const yargs = require('yargs')

const fixtureServereMiddleware = require('..')
const globTofixtures = require('../lib/glob-to-fixtures')

const DEFAULTS = require('../lib/defaults')

const { argv } = yargs.options({
port: {
type: 'number',
default: parseInt(process.env.PORT || DEFAULTS.port, 10)
},
'fixtures-url': {
type: 'string',
default: parseInt(process.env.FIXTURES_URL || DEFAULTS.fixturesUrl, 10)
},
'log-level': {
type: 'string',
describe: 'Set logging level for Express',
default: process.env.LOG_LEVEL || DEFAULTS.logLevel
},
ttl: {
type: 'number',
describe: 'Expiration time for loaded fixtures in ms',
default: parseInt(process.env.TTL || DEFAULTS.ttl, 10)
},
fixtures: {
type: 'string',
description: 'glob path for JSON fixture files created by nock',
default: process.env.FIXTURES || DEFAULTS.fixturesGlob
}
}).help()

const app = express()
app.use(cors())
app.use(fixtureServereMiddleware({
port: argv.port,
fixturesUrl: argv['fixtures-url'],
logLevel: argv['log-level'],
ttl: argv.ttl,
fixtures: globTofixtures(argv.fixtures)
}))

app.listen(argv.port)
console.log(`🌐 http://localhost:${argv.port}`)
67 changes: 67 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module.exports = fixtureServereMiddleware

const {parse: urlParse, resolve: urlResolve} = require('url')

const _ = require('lodash')
const bodyParser = require('body-parser')
const cachimo = require('cachimo')
const express = require('express')
const fixtures = require('@octokit/fixtures')
const Log = require('console-log-level')

const additions = require('./lib/additions')
const proxy = require('./lib/proxy')

const DEFAULTS = require('./lib/defaults')

function fixtureServereMiddleware (options) {
const middleware = express.Router()

const state = _.defaults(_.clone(options), DEFAULTS)

if (!state.fixturesUrl) {
state.fixturesUrl = `http://localhost:${state.port}`
}

state.cachimo = cachimo
state.log = Log({level: state.logLevel === 'silent' ? 'fatal' : state.logLevel})

middleware.post('/fixtures', bodyParser.json(), (request, response) => {
const id = Math.random().toString(36).substr(2)
const requestedFixture = state.fixtures[request.body.scenario]

if (!requestedFixture) {
return response.status(400).json({
error: `Scenario "${request.body.scenario}" not found`
})
}

const mock = fixtures.mock(requestedFixture, fixture => additions(state, {id, fixture}))

cachimo
.put(id, mock, state.ttl)
.then(() => {
state.log.debug(`Deleted fixtures "${id}" (${mock.pending().length} pending)`)
})
// throws error if key was deleted before timeout, safe to ignore
.catch(() => {})

response.status(201).json({
id,
url: urlResolve(state.fixturesUrl, urlParse(requestedFixture[0].scope).hostname)
})
})

// load proxies for all unique scope URLs in fixtures
_.chain(state.fixtures)
.values()
.flatten()
.map('scope')
.uniq()
// remove default ports for http / https, they cause problems for the proxy
.map(url => url.replace(/:(80|443)$/, ''))
.forEach(target => middleware.use(proxy(state, {target})))
.value()

return middleware
}
27 changes: 27 additions & 0 deletions lib/additions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = fixtureAdditions

const mapValuesDeep = require('./map-values-deep')

function fixtureAdditions (state, {id, fixture}) {
fixture.reqheaders['x-fixtures-id'] = id
fixture = mapValuesDeep(fixture, value => {
if (typeof value !== 'string') {
return value
}

// e.g. https://api.github.com/user -> http://localhost/api.github.com/user
return value.replace(/https?:\/\/([^/]+)\//, `${state.fixturesUrl}/$1/`)
})

fixture.headers['content-length'] = String(calculateBodyLength(fixture.response))

return fixture
}

function calculateBodyLength (body) {
if (typeof body === 'string') {
return body.length
}

return JSON.stringify(body).length
}
13 changes: 13 additions & 0 deletions lib/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const {resolve} = require('path')
const resolvePackage = require('resolve-pkg')
const DEFAULT_FIXTURES_GLOB = resolve(resolvePackage('@octokit/fixtures'), 'scenarios/**/normalized-fixture.json')
const globTofixtures = require('./glob-to-fixtures')

module.exports = {
port: 3000,
fixturesUrl: null,
logLevel: 'info',
ttl: 60000,
fixtures: globTofixtures(DEFAULT_FIXTURES_GLOB),
fixturesGlob: DEFAULT_FIXTURES_GLOB.replace(process.cwd(), '.')
}
18 changes: 18 additions & 0 deletions lib/glob-to-fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = globToFixtures

const {dirname, basename, resolve} = require('path')

const glob = require('glob')

function globToFixtures (path) {
return glob.sync(path).reduce((map, path) => {
path = resolve(process.cwd(), path)
const fixture = require(path)
if (/\/normalized-fixture.json$/.test(path)) {
path = dirname(path)
}
const name = basename(path, '.json')
map[name] = fixture
return map
}, {})
}
11 changes: 11 additions & 0 deletions lib/map-values-deep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = mapValuesDeep

const _ = require('lodash')

function mapValuesDeep (v, callback) {
if (_.isObject(v)) {
return _.mapValues(v, v => mapValuesDeep(v, callback))
}

return callback(v)
}
61 changes: 61 additions & 0 deletions lib/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module.exports = proxy

const urlParse = require('url').parse

const express = require('express')

const httpProxyMiddleware = require('http-proxy-middleware')

const validateRequest = require('./request-validation-middleware')

function proxy (state, {target}) {
const middleware = express.Router()
const hostname = urlParse(target).hostname

middleware.use(`/${hostname}`, validateRequest.bind(null, state), httpProxyMiddleware({
target: target,
changeOrigin: true,
logLevel: state.logLevel,
pathRewrite: {
'^/[^/]+/': '/'
},
onError (error, request, response) {
/* istanbul ignore if */
if (error.message.indexOf('Nock: No match for request') !== 0) {
response.writeHead(404, {
'Content-Type': 'application/json; charset=utf-8'
})

return response.end(JSON.stringify({
error: error.message
}))
}

response.writeHead(404, {
'Content-Type': 'application/json; charset=utf-8'
})

const [expected, actual] = error.message
.substr('Nock: No match for request '.length)
.split(' Got instead ')

response.end(JSON.stringify({
error: 'Nock: No match for request',
detail: {
expected: JSON.parse(expected),
actual: JSON.parse(actual)
}
}, null, 2) + '\n')
},
onProxyRes (proxyRes, request, response) {
const fixturesId = request.headers['x-fixtures-id']
const mock = state.cachimo.get(fixturesId)
if (mock.isDone()) {
state.cachimo.remove(fixturesId)
state.log.debug(`Fixtures "${fixturesId}" completed`)
}
}
}))

return middleware
}
39 changes: 39 additions & 0 deletions lib/request-validation-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module.exports = requireFixturesId

const urlParse = require('url').parse

function requireFixturesId (state, req, res, next) {
if (!req.headers['accept']) {
return res.status(400).json({
error: 'Accept header required'
})
}

const fixturesId = req.headers['x-fixtures-id']
if (!fixturesId) {
return res.status(400).json({
error: 'X-Fixtures-Id header required'
})
}

const mock = state.cachimo.get(fixturesId)

if (!mock) {
return res.status(404).json({
error: `Fixture "${fixturesId}" not found`
})
}

const [nextFixture] = mock.pending()

const nextFixtureMethod = nextFixture.split(' ')[0].toUpperCase()
const nextFixturePath = urlParse(nextFixture.substr(nextFixtureMethod.length + 1)).pathname

if (req.method !== nextFixtureMethod || req.path !== nextFixturePath) {
return res.status(404).json({
error: `${req.method} ${req.path} does not match next fixture: ${nextFixtureMethod} ${nextFixturePath}`
})
}

next()
}

0 comments on commit 5ec06d0

Please sign in to comment.