-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), '.') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, {}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |