Integration Test Agent
The Integration Test Agent is a Java Agent that provides remote test script execution and scriptable interception of all the User handlers and endpoints on a running server. The agent can be used to mock downstream behaviour and/or rewrite all inbound and outbound data in a deployed Rubris instance, without having to go through the overhead of maintaining full integration test environments or requiring any configuration or code change to the released artifact.
There is no configuration required, other than starting with the -javaagent
in an environment. As the library requires the tools.jar library it must be run with a JDK and not just the JRE.
The usage of the agent is twofold.
- As a means to do comprehensive, but non-invasive integration testing from the client/client scripts without requiring the cost and time overhead of a full downstream test environment or restarts.
- A means to remotely alter the behaviour of the server code so as to exercise different client capabilities during development so we can quickly cycle through code paths based on server actions without needing matching data set up in the dev environments.
The remote scripts must be written in JavaScript (ECMA 5.1 compliant) and are injected into the server over a built in HTTP direct endpoint in the agent, so any mechanism capable of doing a raw HTTP POST can be used to control the scripts. This means you can drive the agent from the command line or many of the popular test tools, or write integrate into your own test tool.
Integration Testing Support
One of the big headaches with any project is the multiple test/dev environments that are required to be kept in sync. Especially as the application becomes more complex and relies on multiple downstream systems. While not a complete replacement for such an environment, the agent can be used to significantly reduce this overhead and complexity and enables comprehensive, repeatable and scripted testing on what is a running server deployed in the exact form of its normal build.
As part of this testing, one would normally expect the creation of a number of scripts for different integration scenarios to be developed and used as a set of repeatable integration tests.
To use the agent in an environment simply use the -javaagent
directive. On startup the agent injects a new direct path into the server under /SERVER_PATH/testsupport/api
. This URL is the target for all our script actions. From a browser or other HTTP client we can then fully control the server and alter endpoint behaviour using just this URL.
The raw URL returns the built in test page. All test scripts should pass an action or endpoint to ensure they are bypassing the test page as shown below.
DO NOT DEPLOY THE AGENT IN ANY PROD OR EXTERNAL FACING ENVIRONMENT. IT IS A HUGE SECURITY HOLE.
Discovering Available Endpoints
If the script does not have pre-knowledge of the endpoints we can use the API URL to request these.
A simple POST or GET to the /SERVER_PATH/testsupport/api?action=LIST_ENDPOINTS
will return a JSON object similar to the following:
{
"action":"OK",
"channels":[
[
"/heartbeat",
"DIRECT"
],
[
"trading",
"PRIVATE"
],
[
"StateOfTheWorld",
"RPC"
],
[
"wildcardPrices",
"SUBSCRIPTION"
],
[
"~CLIENT_CALLBACK~",
"USER"
],
[
"~CLIENT_DESTROY~",
"USER"
],
[
"~CONNECTION_HANDLER~",
"CONNECTION"
],
[
"~CONNECTION_TERMINATION~",
"CONNECTION"
]
]
}
In the above example the first 4 channels are named channels that we have registered. The channels starting “~” are system ones that we can register handlers for, but do not have a name in the system (e.g. the connection handler). The second String in each object can be used to determine the type of the endpoint.
Injecting A Script
Once we know the endpoint we can simply inject our script on that endpoint using a HTTP POST request. For example using JQuery:
$.ajax({ type: "POST", url: /SERVER_PATH/testsupport/api?endpoint=StateOfTheWorld, cache: false, contentType: false, processData: false, data:$SCRIPT_BODY$, success:function (msg) { processResult(msg); } });
The $SCRIPT_BODY$ is just a raw string containing the Javascript script.
Note: The action is not needed on this post as the endpoint body combination will default to the action INJECT
. If you want to keep consistency in the URL then you can use the URL format /SERVER_PATH/testsupport/api?endpoint=StateOfTheWorld&action=INJECT
.
Writing Scripts
The scripts themselves are simple to write and have some very simple guidelines.
- It must be self contained
- It cannot rely on other JS libraries
- It should be compatible with ECMAScript 5.1
- There is no module support or namespacing
- Each injected script is scoped to a single endpoint
- The script can store state in global variables which are scoped to an individual ScriptContext on the endpoint until the script is replaced or removed
- In order to intercept methods on the server, functions in the script should mirror the API functions of the Handler targeted
- Additional functions can be invoked remotely by the client and a response returned
- If a
shutdown
function is included on the script, it will be automatically called when the script is removed or replaced.
At the time of submission to the server the script will be run through the Nashorn compiler and any error will be returned to the server, rather than delaying evaluation until runtime. This enables syntax errors to be picked up early and for your code to be made aware of this. Runtime errors in your script will manifest themselves on the server and client responses as part of the actual endpoint execution path and will not be returned to your code independently.
Intercepting the Java API Handler methods
The functions the scripts must expose is derived from the actual Java API you use on the server to hook into the server functionality.
The difference is that an additional argument is added to the end of the method signature that contains your original registered object. Therefore, all the intercepted functions are of the form:
OriginalJavaAPI(ARG1,..,ARGN,originalHandler);
Where the original Java API method signature is extended to include the original handler as the last argument.
For example, the API for the AsynchronousHandler is:
public interface AsynchronousHandler { /** * Called back on each Request Message. * * @param user - user object originally set on Client session * @param message - the message payload * @param handle - {@link DataHandle} to enable asynchronous reply to message. */ public void onMessage(User user, Object message, DataHandle handle); }
The script equivalent function would be:
function(user,message,handle,originalHandler){
}
The Javascript is able to call methods on the objects dynamically provided they are scoped in the normal way.
The reason for including the originalHandler as an additional argument is that it allows the script to either delegate the call to the original Handler we registered with the endpoint, or use the script to replace arbitrary objects on our handler to mock other downstream behaviour. If all the components in your Handler are accessible via getter/setter methods the script can inject proxies at any point in the call chain. You should bear this in mind when developing.
The simplest functional behaviour we can do is ignore the original Handler the application is deployed with and intercept the method call entirely. Such as:
function onMessage(user, message, handle, originalHandler){ handle.onMessage("intercepted response"); }
The script above is the full script injected to the server and there is no other wrapping function or entry point required.
Now we can do something slightly more complicated and inject an amended script directly to replace the last one. We don’t have to restart the server or do anything else. Simply call the URL /SERVER_PATH/testsupport/api?endpoint=StateOfTheWorld&action=INJECT
with the new script and the current one will be replaced entirely.
function onMessage(user, message, handle, originalHandle){ var dHandle = new com.rubris.api.DataHandle(){ onMessage: function(data) { print("data intercepted:"+data); var original = {orig:data}; handle.onMessage(JSON.stringify(original)); } } originalHandle.onMessage(user,message,dHandle); }
The above script uses an inline function to proxy the onMessage
method of the Handle presented to the original Handler. The original Handler is then called as it would be normally and inside the inline function we print the value we would have sent and decorate the response we would have returned as a JSON String prior to returning it to the client.
When proxying an Object in this way the form is always methodName: function(xxx)
. Under the covers this produces a dynamic proxy that can be used in the Java code. Multiple methods can be exposed inline within the object definition. Nashorn also provides a couple of other ways to achieve this syntactically.
The example above assumes the data payload is a String. If you have already serialised it to a byte[] it can be converted back using the JavaScript to create a new String as one would in Java or alter the bytes directly after casting it to a NativeArray type. Please refer to the Nashorn docs for more details on this.
We can extend the functionality for this endpoint if we want to control one of the other mixin interfaces on the RPC channel, for example the AuthorisationHandler. Note: The methods in the script are in a flat namespace and do not delineate which interface they are from, however as the interfaces that are implementable on each endpoint do not overlap in method name the function name is enough to decide what the target is.
var myVal = 0; function onMessage(user, message, handle, originalHandle){ var dHandle = new com.rubris.api.DataHandle(){ onMessage: function(data) { print("data intercepted:"+data); var original = {orig:data}; handle.onMessage(JSON.stringify(original)); myVal = original; } } originalHandle.onMessage(user,message,dHandle); } function allow(user, originalAuthoriser){ print ("auth for "+ user.getName()); return true; } function getMyVal(){ return myVal; }
We can see that we simply add another function that matches the EndpointAuthorisationHandler.allow(..)
method with the extra originalHandler parameter. Changing true
to false
as the return value will result in an AUTH_FAIL response to the client and the onMessage
function will not be called.
Additionally, this script shows the use of a global variable to store state. This can be retrieved using an action call to the endpoint as shown below.
The state of the variables is accumulated in an isolated ScriptContext on the endpoint instance until the script is replaced or removed from the endpoint.
Other endpoints obey the same rules and in general the rule of thumb is that if you provide a Java Handler it can be mocked and intercepted using a script.
Be aware that you should not run long running functions in the callback methods (i.e a long thread loop). It is event driven and doing so will block the main execution thread in the server. For some testing this may be acceptable, for use with other request it will prevent parallel activity.
Intercepting methods in the direction of your code (i.e in the callbackHandle ) as shown above will occupy your own threads (or the caller thread if done in line). If done in the first example it will block the server thread and will prevent servicing other requests. this is the same restriction as you have when developing server side code. The use of Javascript does not remove this requirement for you.
Although normally one would expect the main use being one of transformation of data inbound or oubound we might want to do something more complicated that does require the creation of other threads to prevent blocking.
The below example replaces the subscribe response completely and simulates timed response messages running in another thread. It uses an ExecutorService internally to run the main execution ans implements the join
method of the SharedSubscriptionMembershipHandler
(assuming your endpoint actually implements this mixin interface - otherwise it will not be called). Note the use of the Future from the Executor stored in the global state against the topic that is used to interrupt the executor thread if it is still active:
var myExecutor; var subscribes ={}; function setup(){ if (!myExecutor){ myExecutor = Java.type('java.util.concurrent.Executors').newFixedThreadPool(1); } } function shutdown(){ print("Shutting down executor"); if (myExecutor){ myExecutor.shutdownNow(); myExecutor =null; } return "Shutdown executor"; } function join(user, sid, topicHandle, originalHandler){ print("Intercept "+ user.getName()); originalHandler.join(user,sid,topicHandle); } function onUnsubscribe(topicHandle, originalHandler){ var future = subscribes[topicHandle.getTopic()]; if (future){ print ("Cancelling future "+topicHandle.getTopic()); future.cancel(true); subscribes[topicHandle.getTopic()] =null; } } function onSubscribe(topicHandle, originalHandler){ setup(); var Printer = Java.extend(Java.type('java.lang.Runnable'), { run: function() { try{ for (i = 0; i < 10; i++) { topicHandle.onMessage("from a separate thread "+ i); Java.type('java.lang.Thread').sleep(1000); } } catch(e){ print("Thread interrupted"); } } }); var future =myExecutor.submit(new Printer()); subscribes[topicHandle.getTopic()]= future; }
We can see some of the Nashorn functionality in the above script in the use of the Executor to drive the response and how one would call a startup/shutdown equivalent. Note: As detailed in the script conditions above, the shutdown
function (if present) is called on the script on replacement or removal. You can also call it yourself (or manage this in some other manner) if you want to return some result, or shutdown the script after a test cycle.
When using threads or executors it is very easy to leave these running when the ScriptContext is removed if a shutdown method is not implemented or you have not set the thread to have a deterministic lifespan. You should be careful that any thread is stoppable using interrupts or variable triggers that you can alter in the global namespace using remote function calls. The server agent cannot detect inside the javascript if you leave a zombie thread running or you have an infinite loop.
It is your responsibility to ensure this behaviour is correct.
Note: One useful point about the script above is that we can use Java functions such as sleep to do things like vary individual timings for messages or behaviour for specific users. The limits on this is essentially set by how you construct your scripts. This is very useful for client developers who can vary timing and data from the server for their own test/development cycles without requiring support from the server developers.
Similarly we use the same approach to be able to intercept and alter endpoints such as the ConnectionHandler so we can change the script below from accept()
to deny()
to change behaviour prior to establishing a connection to a normal endpoint:
function onConnection(remoteAddress, uri, data, isCrossOrigin, isXHR, handle, originalHandler){ print("Connection on "+ uri); handle.accept(); }
These are addressed using the endpoints starting with “~” as shown above.
Direct endpoints, although using URL paths as the name, work in exactly the same way as other endpoints and are exposed in exactly the manner.
e.g: SERVER_PATH/testsupport/api?endpoint=/somedirect
function onRequest(request, response, originalHandler){ response.setBody("intercepted".getBytes()); response.send(); }
As we can replace pretty much any of the interfaces on the server we can also use it to intercept and alter functionality such as the namespaces on an endpoint that implements the NamespaceProvider , without affecting the subscribe or unsubscribe.
e.g: SERVER_PATH/testsupport/api?endpoint=/somenamespaceendpoint
function namespaces(user, originalProvider){ print(" namespaces called "+user); var Provider = Java.extend(Java.type( 'com.rubris.api.subscribe.NamespaceProviderResult'), { allowedNamespaces: function() { return Java.type('java.util.Arrays').asList( Java.to(["/mysub/intercepted/part1","/mysub/intercepted/part2/"], "java.lang.String[]")); } }); return new Provider(); }
It is probably best to keep the scripts small as the syntax can become somewhat verbose and tricky to read (as shown above).
Calling Remote Functions
As we have seen from the examples, the functions in the injected scripts are called by the Rubris Server as part of normal endpoint behaviours. However, sometimes we want to be able to call functions from the test client/browser outside the normal server pathways. The main uses for this is either to use setup/shutdown functions, to set up state or to retrieve data accumulated as part of the testing cycle.
To call a script function we would construct a request like:
$.ajax({ type: "POST", url: /SERVER_PATH/testsupport/api?endpoint=StateOfTheWorld&action=myFunction, cache: false, contentType: false, processData: false, data:$FUNCTION_ARGUMENT$, success:function (msg) { processResult(msg); } });
This would call the function and the response should contain the data as a string (The response is always turned into a String so if you want Object type responses then some serialisable format should be used. If you return a normal Javascript object this will result in a String similar to [Object object]
):
function myFunction(arg){ var data = JSON.parse(arg); // do some stuff return JSON.stringify(data); }
The action name simply must match the function name and the body of the POST (if any) will be converted to a String to be passed as the argument. The best way to convert this into object arguments is to pass a JSON string as the argument and then use Nashorn’s JSON.parse() method to turn it back into a JSObject on the server.
Removing Scripts
After we have finished testing the endpoint we can remove the script from the endpoint. The 2 ways to achieve this are to submit to the endpoint with a null/empty body or to submit with an action of REMOVE
(e.g. /SERVER_PATH/testsupport/api?endpoint=StateOfTheWorld&action=REMOVE
)
Removing a script deletes the currently registered ScriptContext and an ack is returned to the client.
If the script contains a shutdown
function this will be called before the ScriptContext is removed.
Built-in Agent Page
While the functionality described is probably going to be driven via automation tests or client side scripts, the Integration Agent comes with an agent test page which provides a basic javascript editor to enable rapid testing and prototyping of scripts.
The page is also really useful in being able to inject pauses, data changes, errors or other behaviours during development itself, without needing to have data on the server available to drive such behaviours. If you are sharing the server while doing this it is probably better to scope your scripts to the user, topics or individual namespaces for your own user where appropriate.
This is retrieved by calling the API URL with no parameters /SERVER_PATH/testsupport/api
.
Unlike other parts of the server the agent page does not require the use of Engine-IO, nor is there any authorisation or restrictions on calling the page. This allows us to turn off the connection permissions for users while still retaining access to the agent page.
It maybe that you want to develop your own pages to better fit in with any tooling you already use and this is simple to do with straightforward HTML/Javascript knowledge. The only contract is with the API actions. The agent page is purely a mechanism of accessing these without having to write any code.
It is not required that you use the test page, but for developers it avoids having to construct one yourself to take advantage of the API.
On load, the agent page queries the UL to retrieve the available endpoints which populates its drop down menu.
Scripts can be written and edited in the script box. On submission of a script or using the local save the script is stored in the page against the endpoint, so one can keep the state of the script and switch between them using the drop down without having to re-enter them when working on another endpoint.
The agent page also enables arbitrary methods to be called on the scripts and can be used to remove any registered script. This can also be achieved by pressing Inject
with an empty script.
The result of the invocation or the response from the remote function call is displayed in the result box.