Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meat #5

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ test/tmp
*.log
*.cache
/coverage
dist
dest
.nyc_output
22 changes: 17 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@
"name": "ava-playback",
"version": "0.0.11",
"description": "Record and playback http requests with nock, integrated into ava",
"main": "dist/index.js",
"main": "dest/index.js",
"repository": "https://github.com/dempfi/ava-playback",
"author": "Ike Ku <[email protected]>",
"license": "Apache-2.0",
"scripts": {
"build": "rm -rf ./dist && tsc",
"watch": "rm -rf ./dist && tsc -w --pretty"
"build": "rm -rf ./dest && tsc",
"pretest": "rm -rf ./dest && tsc",
"test": "ava",
"watch": "rm -rf ./dest && tsc -w --pretty"
},
"ava": {
"files": [
"dest/test/**/*.js"
],
"babel": {}
},
"files": [
"dist",
"dest",
"*.js"
],
"keywords": [
Expand Down Expand Up @@ -45,6 +53,10 @@
"@types/lodash.isplainobject": "^4.0.2",
"@types/lodash.mapvalues": "^4.6.2",
"@types/nock": "^8.2.1",
"@types/node": "^7.0.18"
"@types/node": "^7.0.18",
"@types/request-promise-native": "^1.0.6",
"ava": "^0.22.0",
"request-promise-native": "^1.0.4",
"typescript": "2.4.x"
}
}
44 changes: 44 additions & 0 deletions src/common/chunks-to-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
type Headers = { [k: string]: string }

const isBuffer = Buffer.isBuffer
const isString = (a: any): a is string => typeof a === 'string'

const isContentEncoded = (headers: Headers) => {
const encoding = headers['content-encoding']
return isString(encoding) && encoding !== ''
}

const isBinaryBuffer = (buffer: Buffer) => {
if (!isBuffer(buffer)) return false

const reconstructedBuffer = new Buffer(buffer.toString('utf8'), 'utf8')
if (buffer.length !== reconstructedBuffer.length) return true

for (var i = 0; i < buffer.length; ++i)
if (buffer[i] !== reconstructedBuffer[i])
return true

return false
}

const mergeBuffers = (chunks: Buffer[]) => {
if (chunks.length === 0) return new Buffer(0)
if (!isBuffer(chunks[0])) return new Buffer(chunks.join(''), 'utf8')
return Buffer.concat(chunks)
}

const processEncodedBuffers = (chunks: Buffer[]) => chunks.map(c => {
if (!isBuffer(c) && isString(c)) c = new Buffer(c)
return c.toString('hex')
})

export default (chunks: Buffer[], headers: Headers = {}) => {
if (isContentEncoded(headers)) return processEncodedBuffers(chunks)
const mergedBuffers = mergeBuffers(chunks)
if (isBinaryBuffer(mergedBuffers)) return mergedBuffers.toString('hex')

const stringified = mergedBuffers.toString('utf8')
if (stringified.length === 0) return {}
try { return JSON.parse(stringified) }
catch (errr) { return stringified }
}
35 changes: 35 additions & 0 deletions src/common/normalize-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as url from 'url'
import { Options } from '../interfaces'

const fromString = (str: string): Options => {
const { hostname, port, path } = url.parse(str)
return {
hostname,
method: 'GET',
port: Number(port),
__recording__: false,
__https__: false,
path,
}
}

export default (raw: Options | string, protocol: string): Options => {
const options = typeof raw === 'string' ? fromString(raw) : raw
options.__https__ = protocol === 'https'
options.method = options.method || 'GET'
options.port = options.port || (options.__https__ ? 443 : 80)

if (options.host && !options.hostname) {
if (options.host.split(':').length === 2)
options.hostname = options.host.split(':')[0]
else options.hostname = options.host
}

options.__recording__ = options.__recording__ || false

options.host = (options.hostname || 'localhost') + ':' + options.port
options.hostname = options.hostname.toLowerCase()
options.host = options.host.toLowerCase()

return options
}
14 changes: 14 additions & 0 deletions src/common/override-natives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Overrider, RequestMaker } from '../interfaces'

type Modules = { [p: string]: { request: RequestMaker, get: RequestMaker } }
const modules: Modules = { http: require('http'), https: require('https') }

export default (overrider: Overrider) => {
Object.keys(modules).forEach(protocol => {
const module = modules[protocol]
const overriddenGet = module.get
const overriddenRequest = module.request
module.request = (options, callback) =>
overrider(protocol, overriddenRequest.bind(module), options, callback)
})
}
12 changes: 12 additions & 0 deletions src/common/resolvable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class Resolvable<T extends any> {
reject: (arg?: Error) => void
resolve: (arg?: T) => void
promise: Promise<T>

constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
}
13 changes: 13 additions & 0 deletions src/interfaces.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { RequestOptions, IncomingMessage, ClientRequest } from 'http'

export type Request = ClientRequest
export type Options = RequestOptions & { __recording__: boolean, __https__: boolean }
export type Response = IncomingMessage
export type Callback = (res: Response) => void
export type RequestMaker = (options: Options, cb?: Callback) => Request
export type Overrider = (
protocol: string,
overridenRequest: RequestMaker,
options: string | Options,
callback?: Callback
) => Request
Empty file added src/player/index.ts
Empty file.
18 changes: 18 additions & 0 deletions src/recorder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import record from './record'
import overrideNatives from '../common/override-natives'
import normalizeOptions from '../common/normalize-options'

export default () => overrideNatives(
(protocol, overriddenRequest, rawOptions, callback) => {
const options = normalizeOptions(rawOptions, protocol)
if (options.__recording__) return overriddenRequest(options, callback)
options.__recording__ = true

const request = overriddenRequest(options, response => {
record(protocol, options, request, response)
if (callback) callback(response)
else response.resume()
})

return request
})
38 changes: 38 additions & 0 deletions src/recorder/record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as I from '../interfaces'
import * as Request from './request'
import * as Response from './response'
import chunksToBody from '../common/chunks-to-body'

const getScope = ({ host, __https__, port }: I.Options) => {
let scope = `${__https__ ? 'https' : 'http'}://${host}`
const isDefaultPort = port.toString() === (__https__ ? '443' : '80')
const needPort = !host.includes(':') && port && isDefaultPort
return needPort ? `${scope}:${port}` : scope
}

const generatePlayback = (
req: I.Request,
res: I.Response,
options: I.Options,
requestBody: Buffer[],
responseBody: Buffer[]
) => ({
scope: getScope(options),
path: options.path,
method: options.method || 'GET',
status: res.statusCode,
body: chunksToBody(requestBody),
response: chunksToBody(responseBody, res.headers),
rawHeaders: res.rawHeaders || res.headers
})

export default async (protocol: string, options: I.Options, req: I.Request, res: I.Response) => {
const requestBody: Buffer[] = []
const responseBody: Buffer[] = []
Request.onData(req, requestBody.push.bind(requestBody))
Response.onData(res, responseBody.push.bind(responseBody))
await Response.waitEnd(res)

const playback = generatePlayback(req, res, options, requestBody, responseBody)
console.dir(playback, {colors: true, depth: 5})
}
11 changes: 11 additions & 0 deletions src/recorder/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Request } from '../interfaces'

export const onData = (request: Request, cb: (chunk: Buffer) => any) => {
const originalWrite = request.write
request.write = function(chunk: string | Buffer, encoding?: any) {
if (typeof chunk === 'undefined') return originalWrite.apply(request, arguments)
if (!Buffer.isBuffer(chunk)) cb(new Buffer(chunk, encoding))
else cb(chunk)
return originalWrite.apply(request, arguments)
}
}
25 changes: 25 additions & 0 deletions src/recorder/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Response } from '../interfaces'
import { Resolvable } from '../common/resolvable'

export const onData = (response: Response, cb: (chunk: Buffer) => any) => {
let encoding: string
const originalSetEncoding = response.setEncoding
response.setEncoding = (encoding: string) => {
encoding = encoding
return originalSetEncoding.call(response, encoding)
}

const originalPush = response.push
response.push = (chunk: any) => {
if (!chunk) return originalPush.call(response, chunk, encoding)
if (encoding) cb(new Buffer(chunk, encoding))
else cb(chunk)
return originalPush.call(response, chunk, encoding)
}
}

export const waitEnd = (response: Response) => {
const resolvable = new Resolvable()
response.once('end', () => resolvable.resolve())
return resolvable.promise
}
11 changes: 11 additions & 0 deletions src/test/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import test from 'ava'
import * as requet from 'request-promise-native'
import record from '../recorder'

record()

test('Flow', async t => {
const response = await requet.get('http://api.github.com/repos/octokit/octokit.rb', {
headers: { 'user-agent': 'ava-playback' }, json: true })
t.true(typeof response === 'object')
})
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"baseUrl": "src/",
"target": "es5",
"rootDir": "src/",
"outDir": "dist/",
"outDir": "dest/",
"lib": [
"es2015",
"scripthost",
Expand All @@ -18,6 +18,6 @@
},
"exclude": [
"node_modules",
"dist"
"dest"
]
}
Loading