To start a RoboHydra server, you call robohydra
with a configuration
file as a parameter. The RoboHydra configuration file is a JSON file
with the list of plugins to load, and SSL information if you want
RoboHydra to use the HTTPS protocol when responding to requests. A
configuration file without any plugins looks like this:
{"plugins": []}
Assuming that file is called empty.conf
(you have one such file in
the examples
directory in the RoboHydra distribution), if you start
RoboHydra with robohydra empty.conf
you will get a server that will
return 404 for every URL except the admin interface (available in
/robohydra-admin
).
Each element in the plugins
list in the configuration file can be
one of two things:
name
and config
. The
former specifies the name of the plugin to load, while the latter
specifies a Javascript object with the configuration keys and values
you want to pass to the plugin.config
to an empty object.An example of a RoboHydra configuration file loading a plugin logger
(without special configuration) and a plugin named my-plugin
with
the configuration keys path
and logLevel
could be:
{"plugins": ["logger",
{"name": "my-plugin",
"config": {"path": "/var/log/example.log",
"logLevel": "warn"}}]}
You can load as many plugins as you want. Remember that the order is important: the heads declared in the first will catch requests before any heads defined in further plugins.
If you want RoboHydra to use the HTTPS protocol, you have to set the
secure
property to true
, and the property sslOptions
to an
object with the following two properties: key
and cert
, both being
the paths to the files containing the secret key and the certificate
for the server. An example configuration file for an HTTPS server
could be:
{"secure": true,
"sslOptions": {"key": "my-key.pem",
"cert": "my-cert.pem"},
"plugins": ["logger"]}
RoboHydra has a list of directories where it looks for plugins. That
list contains some system-wide directories, and
robohydra/plugins
. That means that, typically, a plugin called
my-plugin
will be found in robohydra/plugins/my-plugin/
.
If you have your plugins in some other directory, you can add
directories to the RoboHydra load path with the -I
parameter, like
so:
robohydra -I extra-plugins example.conf
If example.conf
had a reference to a plugin named my-plugin
, it
would be searched first under extra-plugins/my-plugin
, then in the
rest of the search directories.
Now, let’s say you have a configuration file like the above, but for a
concrete execution of RoboHydra you want to use tmp/test.log
as the
log file. In that case, you don’t have to modify your configuration
file. Instead, you can pass the path
configuration in the
command-line, like so:
robohydra myapp.conf path=tmp/test.log
When RoboHydra receives a new HTTP request, it goes through all active heads in order. When it finds a head that matches the path of the incoming request, RoboHydra dispatches the request with that head. Otherwise, it returns 404.
Note that heads can call heads below them. See the next
parameter in
the head handling function.
Most uses of RoboHydra need writing custom plugins for the behaviour you want. Writing plugins is easy, as you can use ready-made RoboHydra heads that do most common operations. See the tutorial to get an idea of what’s possible and get started, and below for a detailed description of the capabilities of each type.
A plugin is a directory-based Node package (ie. you can’t have plugins
like robohydra/plugins/my-plugin.js
, it has to be
robohydra/plugins/my-plugin/index.js
or similar) that defines the
symbol getBodyParts
. That symbol must be a function, and that
function will be called when loading the plugin with two parameters:
conf
: this is a Javascript object with all configuration properties
defined in the configuration file and the command-line, plus the
special configuration keys robohydra
(the RoboHydra
object
loading the plugin) and path
(the full path to the plugin
directory, although it’s also available as __dirname
under Node).modules
: this is a Javascript object with special modules for the
plugin to use. Currently there is only one module available,
assert
, used for assertions (see “The ‘assert’ module” below for
more information).The getBodyParts
function must return an object with the following
optional properties (note that for a plugin with zero heads and zero
tests is not valid):
heads
: an array of RoboHydraHead
objects.tests
: an object with test names as properties, and an object as
values.Tests define interesting scenarios that allow you to reproduce certain situations or bugs in your clients. They are a collection of heads that, together, define that scenario. Each test in an object with the following properties:
instructions
(optional): a string explaining what are the steps
to execute the test. If present, it will be shown when activating
the test in the web interface.heads
: an array of heads that will be activated when the test is
running.The assert module defines all functions in Node’s assert
module. However, there
are two key differences between RoboHydra’s assert module and Node’s:
RoboHydra’s assert functions are tied to the RoboHydra server, allowing RoboHydra to fetch and store the results and present them on the web interface.
RoboHydra’s assert functions won’t die when there’s an assert
failure, but instead will return false
. This is much more useful
because it allows you to easily return a normal response to the client
(the same or different than the response the client would get if the
assertion had passed).
The most important type of head is RoboHydraHead
. This is the
generic head that all other heads inherit from. It’s the most generic
because it’s defined by a URL path regular expression and a Javascript
handling function. To create a generic head, you need to pass the
following parameters:
name
(optional): a string with a symbolic name for the head.path
: a string with a regular expression matching the paths this
head should handle. Note that there are implicit “^” and “$” at the
beginning and end of the string, and that GET parameters are
stripped from the incoming request before trying to match with this
regular expression. Also, URL endings are normalised in such a way
that a head with path /foo
or /foo/
will match requests for both
/foo
and /foo/
.handler
: a function that receives three parameters: req
, res
and next
. This function will be called for every request the head
has to handle (it’s not enough that the URL path in the request
matches the path
property, because there could be another head
before handling the request –see “Dispatching requests”). This
function must always call res.end()
to end the request, either
explicitly or implicitly (eg. by passing the res
object to the
next
function).detached
(optional): a boolean specifying if the head should be
detached when starting RoboHydra. If this is true, RoboHydra will
behave as if the head wasn’t there. Heads can be detached and
re-attached from the RoboHydra admin interface.Apart from a normal regular expression, the path string supports the
special syntax :foobar
that matches any URL path fragment (eg. the
path /articles/:id
would match /articles/123
,
/articles/title-of-the-article
and so on, but not
/articles/view/123
). See the documentation for the request object,
below, to see how to access these parameters.
The first parameter of the handling function is the request object. This object contains the following properties:
method
: the HTTP request method (eg. GET
, POST
, …).url
: the URL path of the incoming request, including GET
parameters (eg. /foo
, /bar/qux
, /articles?order=date
).headers
: an object with the request headers. These headers are in
Node-style (ie. lowercased).queryParams
: an object containing the GET parameters for the
request. It was called getParams
before, but now that property is
deprecated.rawBody
: a Buffer
object with the raw body of the request.bodyParams
: an object containing the body parameters, if the body
was parseable. Otherwise, undefined
.params
: an object containing all URL path parameters in the
request. See below for an explanation.When defining URL paths, expressions like :id
or :user
can be used
as part of the regular expression. These expressions will match any
URL path fragment, and the matched contents will be available in the
params
object in the request object. For example, if you have a head
for path /articles/:articleid/view
and you receive a request for
/articles/introduction-to-robohydra/view
, the request object will
have a params
property with a single property, articleid
with
value introduction-to-robohydra
.
When you use the next
function in a RoboHydra head (see
documentation below), you sometimes want to modify the request before
sending it further to the next head. You can modify the request in
place before calling next
, or simply create a new mock request with
the Request
class. To do the latter, call the constructor with an
object containing the request properties. Valid properties are:
url
: the URL path of the request.method
: GET
, POST
, etc. defaults to GET
.headers
: an object with the request headers.rawBody
: a Buffer
object with the request body, if any.The response object is how you send data back to the client. The response object has the following API:
statusCode
: a property containing the HTTP status code to return
to the client (by default 200).headers
: an object with the Node-style (lowercase) response
headers.write
: a method to write content to the body. It accepts a single
parameter, namely a Buffer
or string to append to the current
response body. This method allows a RoboHydra head to write the
response body in chunks, and the response will be sent in chunks to
the client (so you could, say, send data, then wait, then send more
data, wait, then close the connection).end
: a method you must call to end the request.send
: a method to send content to the client. It’s a write
+
end
, so you can call send
with the content to be sent instead of
calling write
, then end
.When you use the next
function in a RoboHydra head (see
documentation below), you often want to pass a mock response so you
can inspect or modify it before sending it back to the client. In that
case you will be interested in these other API methods methods:
Response
class constructor: It receives one optional parameter,
the event listener for the end
event (see the on
method and
event documentation below). Here you would typically inspect or
modify the mock response (this
in the function always points to
the mock response object), then write data to your own response
object, possibly with the help of the methods below. If you don’t
pass any parameters, you can add your own event listeners with the
on
method.on
: It attaches event listeners to the response object. It
receives an event name (see event list below) and a callback
function. The callback function will receive a single parameter,
event
, an object with the property type
set to the event type,
plus different properties according to the event fired. Note that
you can setup more than one event listener for a single event: in
that case, all event listeners will be executed.chain
: It connects a response with the given parameter (another
response) in such a way that all events in the second response will
be duplicated in the first one. It’s intended to be used when
creating a response. As it replicates all events, it keeps
streaming. Note that, as you can setup more than one event listener
for a given event, you can chain a response to another one, then add
extra event listeners for extra processing or logging.copyFrom
: It receives a response as a parameter and copies the
data in the parameter to the current response
(eg. res.copyFrom(res2)
copies res2
into res
). It’s intended
to be used when res2
is finished and won’t change again. Thus, it
breaks streaming.forward
: It receives a response as a parameter and forwards it as
if that had been the original response. In other words,
res.forward(res2)
is equivalent to res.copyFrom(res2);
res.end()
. It’s intended to be used when res2
is finished and
won’t change again. Thus, it breaks streaming.The list of response object events is:
head
: Fired when the header is written. Event objects for this
event contain two properties, statusCode
and headers
.data
: Fired when there is data written in the response
object. Event objects for this event contain a single property,
data
, an instance of Buffer
.end
: Fired when the response is finished. Event objects for this
event contain a single property, response
, the response object
that fired the event.In some circumstances, heads might want to call other heads below it. The main use cases for doing this are:
See examples of both in the tutorials. In order to call other heads,
the handler
function receives a third parameter, next
, a function
that receives two parameters (request and response objects). This
function, when called, will try to dispatch the given request object
with any heads below the current one. Naturally, this processing will
end up with a call to the end
function in the response object.
But note that the head calling the next
function can decide what is
being passed as request and response objects (they need not be the
request and response objects passed to the original head). A common
thing to do is passing a mock response object: when the second head
processes the request, the callback function you passed in the
constructor of the mock response will be called. Thus, you are free to
pass the response as is (by using the forward
method), tweak the
response before sending (eg. modifying it before calling forward
),
retry the request with a different URL if you didn’t like the
response, etc.
Apart from the generic RoboHydra head, there are other classes of
heads available. Namely, RoboHydraHeadStatic
,
RoboHydraHeadFilesystem
, RoboHydraHeadProxy
,
RoboHydraHeadFilter
and RoboHydraHeadWatchdog
.
This head always returns fixed, static content. A head of this class has the following properties:
content
(optional): the content to be sent back for every
request. This can be a string or a plain Javascript object. While
this property is optional, either this or responses
must be
present.responses
(optional): an array of responses to send to the client
(round-robin). Each response in the array can contain the properties
content
, contentType
and statusCode
. For each of these
properties that is not given, the head’s property is used
instead. While this property is optional, either this or content
must be present.path
(optional): the regular expression matching URL paths to be
handled by this head. Defaults to /
if not present.contentType
(optional): the value for the Content-Type
header in
the response. If not present and content is an object, defaults to
application/json
.statusCode
(optional): the status code for the response. By
default it’s 200.This head serves files from the filesystem. It has the following properties:
documentRoot
: the root local filesystem path from which to serve
files.mountPath
(optional): the path to “mount” the head in. This works
a bit differently than the path
property in other heads. In this
case, it’s not a regular expression, but a path under which
everything is considered handled by the head. It defaults to /
.indexFiles
(optional): the list of files that will be considered
“index” (ie. if a request comes for a directory, and that
directory contains one of the index files, the index file is served
instead). Defaults to index.html
, index.htm
, home.html
,
home.htm
. Order matters, first matching file will be used as
index.fs
(optional): an object that behaves like Node’s fs
module. Useful if you need to fake stuff.mime
(optional): an object that behaves like Node’s mime
module. Useful if you need to fake stuff.This head will serve files from the filesystem, taking into account
If-Modified-Since
request headers and sending correct
Last-Modified
headers and 304 status codes when necessary. For
example, a head with documentRoot = /var/www
and mountPath =
/static
that receives a request to /static/css/main.css
will try to
serve the file /var/www/css/main.css
.
This head proxies request to another URL. It has the following properties:
proxyTo
: the root URL the requests are going to be proxied to.mountPath
(optional): the path to “mount” the head in. See the
documentation for RoboHydraHeadFilesystem
.httpCreateClientFunction
(optional): an object that behaves like
Node’s http.createClient
function. Useful if you need to fake
stuff.This head will proxy requests to the given proxyTo
URL. For
example, a head with proxyTo = http://github.com/operasoftware
and
mountPath = /github
that receives a request to /github/robohydra
will proxy the request to the URL
http://github.com/operasoftware/robohydra
.
This head filters a request processed by another head. It has the following properties:
path
(optional): the regular expression matching URL paths to be
handled by this head. Defaults to /.*
if not present.filter
: a function receiving a Buffer
object with the response
body and returning the filtered response to be sent back to the
client. The returned value can be either a string or another
Buffer
object.This head will match certain URLs and pass those requests through for
processing by other heads (see the next
function
documentation). When the response comes back from the next head, the
RoboHydraHeadFilter
head will take the response body, apply the
given filter
function (transparently uncompressing and compressing
back if necessary, and also updating the Content-Length
header, if
present) and send that as a response.
This head acts as a watchdog, looking for requests/responses matching certain criteria and executing a given action. It has the following properties:
path
(optional): the regular expression matching URL paths to be
handled by this head. Defaults to /.*
if not present.watcher
: a function receiving the Request
and Response
objects
for every request. If this function returns true, the reporter
function will be executed.reporter
(optional): a function receiving the Request
and
Response
objects for the requests that make watcher
return true.This head will look for “interesting” request/responses (by checking
if the watcher
function returns true), and when finding one, it will
execute a given action (the reporter
function; by default, printing
some extra output to the console). Note that both these functions will
receive a special Response
object that guarantees that its body
property is always uncompressed. If you need the original (whether it
was compressed or not), you can check the rawBody
property.
In some situations you will want to access or control the RoboHydra
that loaded the plugin. As the config
object passed to
getBodyParts
function has a reference to it, you can call any
methods to get information or change it: starting tests, loading extra
plugins, detaching or re-attaching heads, injecting extra test
results, sending the test results to another server, or any other
thing you can think of. You could also write your own admin interface
if you wanted to, as the stock admin interface is essentially a normal
plugin (see
lib/plugins/admin.js
in the RoboHydra source code).
The following is a list of interesting public RoboHydra API methods and properties:
registerDynamicHead
: receives a single head and
registers it in RoboHydra (at the end of the *dynamic-heads*
pseudo-plugin).registerPluginObject
: receives a plugin as a parameter
and registers it in RoboHydra (at the end). A plugin is a Javascript
object with the following properties:
name
: mandatory, must be made of just ASCII letters, numbers,
underscores and dashes.heads
: optional, an array of heads in the plugin.tests
: optional, a plain object with tests. See getBodyParts
documentation for details.requirePlugin
: It receives a plugin name to load and an object
with the configuration, and returns an object with two properties:
module
(the module loaded through Node) and config
(the given
configuration object plus additional properties –currently, the
robohydra
property and path
, the full path to the loaded
plugin). Normally one would call the getBodyParts
function in the
returned module
, passing the returned config
and
robohydra.getModulesObject()
as parameters; the result of that
getBodyParts
call would then be passed to registerPluginObject
after adding a proper name
for it.getModulesObject
: returns an object with the available modules
(currently only assert
).getPlugins
: returns a list of all current plugins, including
pseudo-plugins.getPluginNames
: returns a list of all current plugin names, including
pseudo-plugins.getPlugin
: receives one parameter (plugin name) and returns the
plugin by that name. If there’s no registered plugin with the given
name it throws a RoboHydraPluginNotFoundException
.headForPath
: given a URL path, it returns the first head that
matches that URL. If given a head as a second parameter
(afterHead
), only heads appearing after afterHead
will be
considered.attachHead
/ detachHead
: given a plugin name and a head name,
they (re-)attach or detach the given head. If no such head exists,
RoboHydraHeadNotFoundException
is thrown. If the head was already
attached/detached, InvalidRoboHydraHeadStateException
is thrown.addPluginLoadPath
: It adds the given path to the list of paths to
search for plugins.startTest
: given a plugin name and test name, start the given
test. If it doesn’t exist, throw InvalidRoboHydraTestException
.stopTest
: stops the current test, if any.currentTest
(property): object with two properties, plugin
and
test
, pointing to the currently running test. If there’s no running
test, it’s *default*
/ *default*
.testResults
(property): object with the current test results. Its
keys are plugin names and its values are objects with test names as
keys. The values of the latter objects are test results: objects
with the keys result
(undefined
if the test doesn’t have any
result yet, or pass
or fail
if at least one assertion has run
for that test), passes
(an array with the description of the
passing assertions) and failures
(ditto for failing assertions).