Skip to content

Instantly share code, notes, and snippets.

@stellaraccident
Created March 18, 2011 01:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stellaraccident/875455 to your computer and use it in GitHub Desktop.
Save stellaraccident/875455 to your computer and use it in GitHub Desktop.
/**
* Implements the http protocol bindings for the session client api.
* All of the guts are actually passed down to a handler object that
* is passed in at construction time.
*/
var connect=require('connect');
function ClientApiHttp(handler) {
var pageRouter=connect.router(function(app) {
app.post('/registerUserAgent', handleRegisterUserAgent);
app.post('/userData', handleUserData);
});
// -- handler functions
function handleRegisterUserAgent(req, res, next) {
res.writeHead(501);
res.end();
}
function handleUserData(req, res, next) {
res.writeHead(501);
res.end();
}
return pageRouter;
}
module.exports=ClientApiHttp;
var inherits=require('sys').inherits;
var Stream=require('stream').Stream;
/**
* Add a property descriptor to an object for the property that
* will complain if it is accessed.
*/
function mineProperty(obj, prop) {
Object.defineProperty(obj, prop, {
get: function() {
throw new Error('Attempt to access unsupported property "' + prop + '" on mock object.');
},
set: function() {
throw new Error('Attempt to set unsupported property "' + prop + '" on mock object.');
}
});
}
function MockServerRequest(options) {
Stream.call(this);
this.url=options.url;
// Get the request data array in one of a few ways
var body;
if (options.bodyJson) {
if (typeof options.bodyJson === 'string') body=options.bodyJson;
else body=JSON.stringify(options.bodyJson);
} else {
body=options.body;
}
// Data can either be a single anything or an array
// of anything. Any individual data item will either
// be a buffer or coerced to a buffer via an intermediate
// cast to string
// If an array, then this will result in multiple chunks
// being written (ie multiple data events)
if (body===null || body===undefined) this._data=[];
else {
this._data=body;
if (!Array.isArray(this._data)) {
this._data=[this._data];
}
}
this._dataIndex=0;
this._paused=true;
this._dataPending=false;
this._encoding=options.encoding||'utf8';
this.httpVersionMajor=options.httpVersionMajor||1;
this.httpVersionMinor=options.httpVersionMinor||1;
this.httpVersion=this.httpVersionMajor + '.' + this.httpVersionMinor;
this.method=options.method||'GET';
this.headers={};
this.trailers={};
this.readable=true;
// Normalize headers
var headers={};
this.headers=headers;
if (options.headers) {
Object.keys(options.headers).forEach(function(k) {
var v=options.headers[k];
headers[k.toLowerCase()]=v;
});
}
mineProperty(this, 'socket');
mineProperty(this, 'connection');
mineProperty(this, 'client');
mineProperty(this, 'destroy');
}
inherits(MockServerRequest, Stream);
MockServerRequest.prototype.setEncoding=function(encoding) {
this._encoding=encoding;
};
MockServerRequest.prototype.pause=function() {
if (this._paused) throw new Error('Unbalanced call to MockServerRequest.pause()');
this._paused=true;
};
MockServerRequest.prototype.resume=function() {
if (!this._paused) throw new Error('Unbalanced call to MockServerRequest.resume()');
this._paused=false;
if (!this._dataPending) {
this._dataPending=true;
process.nextTick(this._pumpData.bind(this));
}
};
MockServerRequest.prototype._pumpData=function() {
this._dataPending=false;
if (this._paused) return;
if (this._dataIndex>=this._data.length) {
// Already done
return;
}
var chunk=this._data[this._dataIndex++];
if (!Buffer.isBuffer(chunk)) {
chunk=new Buffer(String(chunk), this._encoding);
}
this.emit('data', chunk);
if (this._dataIndex>=this._data.length) {
this.emit('end');
} else if (!this._paused) {
process.nextTick(this._pumpData.bind(this));
}
};
function MockServerResponse(responseState, callback) {
Stream.call(this);
this._callback=callback;
this._responseState=responseState||{};
this._responseState.chunks=[];
this._responseState.trailers={};
this.writable=true;
this.finished=false;
mineProperty(this, 'writeContinue');
mineProperty(this, 'useChunkedEncodingByDefault');
mineProperty(this, 'shouldKeepAlive');
mineProperty(this, 'assignSocket');
mineProperty(this, 'detachSocket');
mineProperty(this, 'destroy');
}
inherits(MockServerResponse, Stream);
MockServerResponse.prototype.writeHead=function(statusCode) {
var responseState=this._responseState;
if (responseState.didWriteHead) {
throw new Error('Duplicate call to MockServerResponse.writeHead');
}
responseState.didWriteHead=true;
var reasonPhrase, headers, headerIndex, normHeaders={};
if (typeof arguments[1] === 'string') {
reasonPhrase=arguments[1];
headerIndex=2;
} else {
reasonPhrase=null;
headerIndex=1;
}
if (typeof arguments[headerIndex] === 'object') {
headers=arguments[headerIndex];
} else {
headers={};
}
responseState.statusCode=statusCode;
responseState.reasonPhrase=reasonPhrase;
responseState.headers=normHeaders;
// Normalize headers and dynamic headers
Object.keys(headers).forEach(function(k) {
var lk=k.toLowerCase();
if (lk in normHeaders) {
throw new Error('Duplicate mixed case headers in call to writeHead: ' + k);
}
normHeaders[lk]=headers[k];
});
if (responseState.dynamicHeaders) {
Object.keys(responseState.dynamicHeaders).forEach(function(lk) {
if (! (lk in normHeaders) ) {
normHeaders[lk]=responseState.dynamicHeaders[lk];
}
});
}
};
MockServerResponse.prototype.writeHeader=function() {
this.writeHead.apply(this, arguments);
};
MockServerResponse.prototype.setHeader=function(name, value) {
var responseState=this._responseState;
if (arguments.length<2) {
throw new Error('name and value are required for setHeader');
}
if (responseState.didWriteHead) {
throw new Error('Cannot manipulate headers after call to writeHead');
}
if (!responseState.dynamicHeaders) responseState.dynamicHeaders={};
var key=name.toLowerCase();
responseState.dynamicHeaders[key]=value;
};
MockServerResponse.prototype.getHeader=function(name) {
var responseState=this._responseState;
if (arguments.length<1) {
throw new Error('name is required for getHeader');
}
if (responseState.didWriteHead) {
throw new Error('Cannot manipulate headers after call to writeHead');
}
var key=name.toLowerCase();
if (responseState.dynamicHeaders) return responseState.dynamicHeaders[key];
else return undefined;
};
MockServerResponse.prototype.removeHeader=function(name) {
var responseState=this._responseState;
if (arguments.length<1) {
throw new Error('name is required for removeHeader');
}
if (responseState.didWriteHead) {
throw new Error('Cannot manipulate headers after call to writeHead');
}
var key=name.toLowerCase();
if (responseState.dynamicHeaders) delete responseState.dynamicHeaders[key];
};
MockServerResponse.prototype.write=function(chunk, encoding) {
var responseState=this._responseState;
if (!responseState.didWriteHead) {
throw new Error('Cannot write until call to writeHead');
}
responseState.chunks.push([chunk, encoding]);
};
MockServerResponse.prototype.addTrailers=function(headers) {
throw new Error('Not implemented');
};
MockServerResponse.prototype.end=function(data, encoding) {
var responseState=this._responseState;
if (responseState.finished) {
throw new Error('Duplicate call to end()');
}
if (!responseState.didWriteHead) {
this.writeHead(200);
}
if (data) {
responseState.chunks.push([data, encoding]);
}
this.finished=true;
responseState.finished=true;
if (this._callback) this._callback();
};
function MockResults(assert) {
this.assert=assert;
}
MockResults.prototype.nextErr=null;
MockResults.prototype.nextInvoked=false;
MockResults.prototype.assertStatusCode=function(expected) {
this.assert.equal(this.statusCode, expected, 'Expected http status code of ' + expected);
};
MockResults.prototype.decodeChunks=function() {
var text=[];
this.chunks.forEach(function(chunk) {
var data=chunk[0], encoding=chunk[1];
if (Buffer.isBuffer(data)) {
text.push(new Buffer(data, encoding));
} else {
text.push(String(data));
}
});
return text.join('');
};
Object.defineProperty(MockResults.prototype, 'bodyText', {
get: function() {
if ('_bodyText' in this) return this._bodyText;
this._bodyText=this.decodeChunks();
return this._bodyText;
}
});
Object.defineProperty(MockResults.prototype, 'bodyJson', {
get: function() {
if ('_bodyJson' in this) return this._bodyJson;
try {
this._bodyJson=JSON.parse(this.bodyText);
} catch (e) {
throw new Error('Mock result expected to be json but was not: ' + this.bodyText);
}
return this._bodyJson;
}
});
function execute(handler, options, callback) {
var results, req, res, next;
var assert=options.assert || require('assert');
function assertFail(msg) {
assert.ok(false, msg);
}
function done() {
assert.ok(results.finished, 'Request did not finish');
callback(results);
}
results=new MockResults(assert);
req=new MockServerRequest(options);
res=new MockServerResponse(results, done);
next=function(err) {
results.nextErr=err;
results.nextInvoked=true;
if (err && !options.expectNextErr) {
// 99% of the time this is a failure - treat it as such
// unless if told not to
assertFail('Mock http execute invoked next with an unexpected error: ' + err);
} else if (!options.expectNext) {
// Most things under test shouldn't invoke next so error by default.
// unless if expectNext is given
assertFail('Mock http execute invoked next unexpectedly (this usually means the middleware opted to not respond to the request)');
} else if (results.didWriteHead) {
// Illegal to call next after having written headers
assertFail('Illegal call to next callback after writing http headers');
}
results.didWriteHead=true;
results.finished=true;
// Otherwise, we're done here
done();
};
process.nextTick(function() {
req.resume();
handler(req, res, next);
});
}
// -- exports
exports.MockServerRequest=MockServerRequest;
exports.MockServerResponse=MockServerResponse;
exports.execute=execute;
require('./setup');
var nodeunit=require('nodeunit');
var ClientApiHttp=require('../lib/clientApiHttp');
var ClientApiHandler=require('../lib/clientApiHandler');
var mockhttp=require('mockhttp');
module.exports=nodeunit.testCase({
setUp: function(callback) {
this.handler=new ClientApiHandler();
this.middleware=new ClientApiHttp(this.handler);
callback();
},
'test for smoke': function(test) {
test.done();
},
'test /registerUserAgent': function(test) {
mockhttp.execute(this.middleware, {
assert: test,
method: 'POST',
url: '/registerUserAgent'
}, function(results) {
results.assertStatusCode(200);
test.done();
});
}
});
@stellaraccident
Copy link
Author

This is a snapshot of some code I was writing to do mock testing of http based handlers and a very trivial example showing usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment