From 299402c97a1ac68a04a54cc1a0d77fa636f9bc6b Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Fri, 20 Jun 2014 15:28:26 -0700 Subject: [PATCH 1/2] [TEST] Switched from `should` to `expect` Chai's `expect` more cleanly handles null and undefined values --- package.json | 5 +++-- test/sandbox.js | 50 ++++++++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 98ba34d..0900954 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,9 @@ ], "devDependencies": { "mocha": "1.20.1", - "should": "0.6.3", - "sinon": "~1.10.2" + "chai": "1.9.1", + "sinon": "~1.10.2", + "sinon-chai": "2.5.0" }, "repository": { "type": "git", diff --git a/test/sandbox.js b/test/sandbox.js index 73ca174..e0b9f55 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -2,9 +2,14 @@ // Init //----------------------------------------------------------------------------- -var should = require('should'); -var sinon = require('sinon'); -var Sandbox = require('../lib/sandbox'); +// Using chai/expect instead of should because it handles +// null and undefined values more cleanly +var chai = require('chai'); +var expect = chai.expect; +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +chai.use(sinonChai); +var Sandbox = require('../lib/sandbox'); //----------------------------------------------------------------------------- // Tests @@ -18,60 +23,63 @@ describe('Sandbox', function() { it('should execute basic javascript', function(done) { sb.run('1 + 1', function(output) { - output.result.should.eql('2'); + expect(output.result).to.equal('2'); done(); }); }); it('should gracefully handle syntax errors', function(done) { sb.run('hi )there', function(output) { - output.result.should.eql("'SyntaxError: Unexpected token )'"); + expect(output.result).to.equal("'SyntaxError: Unexpected token )'"); done(); }); }); it('should effectively prevent code from accessing node', function(done) { sb.run('process.platform', function(output) { - output.result.should.eql("null"); + expect(output.result).to.equal("null"); done(); }); }); it('should effectively prevent code from circumventing the sandbox', function(done) { sb.run("var sys=require('sys'); sys.puts('Up in your fridge')", function(output) { - output.result.should.eql("'ReferenceError: require is not defined'"); + expect(output.result).to.equal("'ReferenceError: require is not defined'"); done(); }); }); it('should timeout on infinite loops', function(done) { sb.run('while ( true ) {}', function(output) { - output.result.should.eql('TimeoutError'); + expect(output.result).to.equal('TimeoutError'); done(); }); }); it('should allow console output via `console.log`', function(done) { sb.run('console.log(7); 42', function(output) { - output.result.should.eql('42'); - output.console[0].should.eql(7); + expect(output.result).to.equal('42'); + expect(output.console).to.have.length(1); + expect(output.console[0]).to.equal(7); done(); }); }); it('should allow console output via `print`', function(done) { sb.run('print(7); 42', function(output) { - output.result.should.eql('42'); - output.console[0].should.eql(7); + expect(output.result).to.equal('42'); + expect(output.console).to.have.length(1); + expect(output.console[0]).to.equal(7); done(); }); }); it('should maintain the order of sync. console output', function(done) { sb.run('console.log("first"); console.log("second"); 42', function(output) { - output.result.should.eql('42'); - output.console[0].should.eql('first'); - output.console[1].should.eql('second'); + expect(output.result).to.equal('42'); + expect(output.console).to.have.length(2); + expect(output.console[0]).to.equal('first'); + expect(output.console[1]).to.equal('second'); done(); }); }); @@ -80,8 +88,8 @@ describe('Sandbox', function() { var messageHandler = sinon.spy(); sb.on('message', messageHandler); sb.run('postMessage("Hello World!");', function(output){ - messageHandler.calledOnce.should.eql(true); - messageHandler.calledWith('Hello World!').should.eql(true); + expect(messageHandler.calledOnce).to.be.true; + expect(messageHandler).to.be.calledWith('Hello World!'); done(); }); }); @@ -93,8 +101,8 @@ describe('Sandbox', function() { sb.postMessage('Hello World!'); }); sb.run('onmessage = function (msg) { postMessage(msg); };', function(output) { - messageHandler.callCount.should.eql(1); - messageHandler.calledWith('Hello World!').should.eql(true); + expect(messageHandler).to.be.calledOnce; + expect(messageHandler).to.be.calledWith('Hello World!'); done(); }); }); @@ -107,8 +115,8 @@ describe('Sandbox', function() { }, 1); sb.on('message', messageHandler); sb.run('onmessage = function (msg) { postMessage(msg); };', function(output) { - messageHandler.callCount.should.eql(num_messages_sent); - num_messages_sent.should.be.greaterThan(0); + expect(messageHandler.callCount).to.equal(num_messages_sent); + expect(num_messages_sent).to.be.greaterThan(0); done(); }); sb.on('ready', function(){ From 5d5ecbb0bc28974f478693070ab4d1f3f8546ed7 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Fri, 20 Jun 2014 16:25:03 -0700 Subject: [PATCH 2/2] [FEATURE] Replace hollabacks with callbacks Sandbox.run now accepts a callback of the form function(error, result){...} Sandboxed code's process.stdout is piped directly to parents Exposed `process.exit()` function to sandboxed code Added tests Updated documentation and examples Bump version --- README.md | 25 +++----- example/example.js | 48 +++++++------- lib/sandbox.js | 109 ++++++++++++++++++++------------ lib/shovel.js | 37 ++++++----- package.json | 5 +- test/sandbox.js | 152 ++++++++++++++++++++++++++++++++++++--------- 6 files changed, 254 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 34843af..f327fc2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A nifty javascript sandbox for node.js. - Handles errors gracefully - Restricted code (cannot access node.js methods) - Supports `console.log` and `print` utility methods -- Supports interprocess messaging with the sandboxed code +- Supports interprocess (IPC) messaging with the sandboxed code ## Example @@ -28,13 +28,11 @@ s.run('1 + 1 + " apples"', function(output) { ## Documentation -### `Sandbox`#`run`(`code`, `hollaback`) +### `Sandbox`#`run`(`code`, `callback`) * `code` {`String`} — string of Javascript to be executed. -* `hollaback` {`Function`} — called after execution with a single argument, `output`. - - `output` is an object with two properties: `result` and `console`. The `result` - property is an inspected string of the return value of the code. The `console` - property is an array of all console output. +* `callback` {`Function`} — called after execution with two arguments, `error` and + `result` For example, given the following code: @@ -48,14 +46,8 @@ function add(a, b){ add(20, 22); ``` -The resulting output object is: +The callback will be called with `(null, '42')` -```javascript -{ - result: "42", - console: ["20", "22"] -} -``` ### `Sandbox`#`postMessage`(`message`) @@ -86,9 +78,10 @@ sandbox.on('message', function(message){ sandbox.postMessage('hello from outside'); ``` -The process will ONLY be considered finished if `onmessage` is NOT a function. -If `onmessage` is defined the sandbox will assume that it is waiting for an -incoming message. +The process will ONLY be considered finished if `onmessage` is NOT a function or +`process.exit()` is called. If `onmessage` is defined the sandbox will assume that +it is waiting for an incoming message. Note, however, that the timeout will still +cause asynchronous sandboxed code to result in a `TimeoutError` if it takes too long. ## Installation & Running diff --git a/example/example.js b/example/example.js index 7ed1810..343d52d 100644 --- a/example/example.js +++ b/example/example.js @@ -2,58 +2,59 @@ var Sandbox = require("../lib/sandbox") , s = new Sandbox() // Example 1 - Standard JS -s.run( "1 + 1", function( output ) { - console.log( "Example 1: " + output.result + "\n" ) +s.run( "1 + 1", function(error, result) { + console.log( "Example 1: " + result + "\n" ) }) // Example 2 - Something slightly more complex -s.run( "(function(name) { return 'Hi there, ' + name + '!'; })('Fabio')", function( output ) { - console.log( "Example 2: " + output.result + "\n" ) +s.run( "(function(name) { return 'Hi there, ' + name + '!'; })('Fabio')", function(error, result) { + console.log( "Example 2: " + result + "\n" ) }) // Example 3 - Syntax error -s.run( "lol)hai", function( output ) { - console.log( "Example 3: " + output.result + "\n" ) +s.run( "lol)hai", function(error, result) { + console.log( "Example 3: " + result + "\n" ) }); // Example 4 - Restricted code -s.run( "process.platform", function( output ) { - console.log( "Example 4: " + output.result + "\n" ) +s.run( "process.platform", function(error, result) { + console.log( "Example 4: " + result + "\n" ) }) // Example 5 - Infinite loop -s.run( "while (true) {}", function( output ) { - console.log( "Example 5: " + output.result + "\n" ) +// A different sandbox is used for this example because the following ones +// end up being run before this one is finished otherwise +var sb = new Sandbox(); +sb.run( "while (true) {}", function(error, result) { + console.log( "Example 5: " + error + "\n" ) }) // Example 6 - Caller Attack Failure -s.run( "(function foo() {return foo.caller.caller;})()", function( output ) { - console.log( "Example 6: " + output.result + "\n" ) +s.run( "(function foo() {return foo.caller.caller;})()", function(error, result) { + console.log( "Example 6: " + result + "\n" ) }) // Example 7 - Argument Attack Failure -s.run( "(function foo() {return [].slice.call(foo.caller.arguments);})()", function( output ) { - console.log( "Example 7: " + output.result + "\n" ) +s.run( "(function foo() {return [].slice.call(foo.caller.arguments);})()", function(error, result) { + console.log( "Example 7: " + result + "\n" ) }) // Example 8 - Type Coersion Attack Failure -s.run( "(function foo() {return {toJSON:function x(){return x.caller.caller.name}}})()", function( output ) { - console.log( "Example 8: " + output.result + "\n" ) +s.run( "(function foo() {return {toJSON:function x(){return x.caller.caller.name}}})()", function(error, result) { + console.log( "Example 8: " + result + "\n" ) }) // Example 9 - Global Attack Failure -s.run( "x=1;(function() {return this})().console.log.constructor('return this')()", function( output ) { - console.log( "Example 9: " + output.result + "\n" ) +s.run( "x=1;(function() {return this})().console.log.constructor('return this')()", function(error, result) { + console.log( "Example 9: " + result + "\n" ) }) // Example 10 - Console Log -s.run( "var x = 5; console.log(x * x); x", function( output ) { - console.log( "Example 10: " + output.console + "\n" ) -}) +s.run( "var x = 5; console.log('Example 10: ' + (x * x)); x", function(error, result) {}) // Example 11 - IPC Messaging -s.run( "onmessage = function(message){ if (message === 'hello from outside') { postMessage('hello from inside'); };", function(output){ - +s.run( "onmessage = function(message){ if (message === 'hello from outside') { postMessage('hello from inside'); process.exit(); };", function(error, result){ + }) s.on('message', function(message){ console.log("Example 11: received message sent from inside the sandbox '" + message + "'\n") @@ -61,4 +62,3 @@ s.on('message', function(message){ var test_message = "hello from outside"; console.log("Example 11: sending message into the sandbox '" + test_message + "'"); s.postMessage(test_message); - diff --git a/lib/sandbox.js b/lib/sandbox.js index 0a36203..b16afd2 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -7,6 +7,7 @@ var path = require('path'); var spawn = require('child_process').spawn; var util = require('util'); var EventEmitter = require('events').EventEmitter; +var concat = require('concat-stream'); //----------------------------------------------------------------------------- // Constructor @@ -14,12 +15,16 @@ var EventEmitter = require('events').EventEmitter; function Sandbox(options) { var self = this; - + // message_queue is used to store messages that are meant to be sent // to the sandbox before the sandbox is ready to process them self._ready = false; self._message_queue = []; - + + // Instance keeps a reference to stdout so it can be + // overwritten for testing purposes + self._stdout = process.stdout; + self.options = { timeout: 500, node: 'node', @@ -37,79 +42,105 @@ util.inherits(Sandbox, EventEmitter); // Instance Methods //----------------------------------------------------------------------------- -Sandbox.prototype.run = function(code, hollaback) { +Sandbox.prototype.run = function(code, callback) { var self = this; var timer; - var stdout = ''; + var result; + var error = null; + + // Spawn child process self.child = spawn(this.options.node, [this.options.shovel], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }); - var output = function(data) { - if (!!data) { - stdout += data; - } - }; - if (typeof hollaback == 'undefined') { - hollaback = console.log; - } else { - hollaback = hollaback.bind(this); - } + // Pass data written to stdout directly to this process' stdout + function stdoutHandler(data){ + var lines = String(data).split('\n'); + lines.forEach(function(line, index){ + // String.split will result in an extra empty string at the end + if (index !== lines.length - 1 || line) { + self._stdout.write(line + '\n'); + } + }); + }; + self.child.stdout.on('data', stdoutHandler); - // Listen - self.child.stdout.on('data', output); + // Listen for errors and call the callback immediately + function stderrHandler(data) { + if (data && data.length > 0) { + error = String(data); + } + }; + self.child.stderr.pipe(concat(stderrHandler)); // Pass messages out from child process // These messages can be handled by Sandbox.on('message', function(message){...}); self.child.on('message', function(message){ - if (message === '__sandbox_inner_ready__') { - - self.emit('ready'); + if (typeof message !== 'object' || typeof message.type !== 'string') { + throw new Error('Bad IPC Message: ' + JSON.stringify(message)); + } + + if (message.type === 'ready') { + self._ready = true; - + self.emit('ready'); + // Process the _message_queue while(self._message_queue.length > 0) { self.postMessage(self._message_queue.shift()); } + } else if (message.type === 'result') { + + // Should this be stringified? + result = String(message.data); + + // Special case null and undefined so that the result does not + // end up as a stringified version (i.e. "null") + if (result === 'null') { + result = null; + } else if (result === 'undefined') { + result = undefined; + } + + + } else if (message.type === 'message') { + + self.emit('message', message.data); + } else { - self.emit('message', message); + throw new Error('Bad IPC Message: ' + JSON.stringify(message)); } }); - - self.child.on('exit', function(code) { + + // This function should be the only one that calls the hollback + function onExit(code) { clearTimeout(timer); setImmediate(function(){ - if (!stdout) { - hollaback({ result: 'Error', console: [] }); - } else { - var ret; - try { - ret = JSON.parse(stdout); - } catch (e) { - ret = { result: 'JSON Error (data was "'+stdout+'")', console: [] } - } - hollaback(ret); + if (typeof callback === 'function') { + callback(error, result); } }); - }); + }; + self.child.on('exit', onExit); + // Go self.child.stdin.write(code); self.child.stdin.end(); - + timer = setTimeout(function() { - self.child.stdout.removeListener('output', output); - stdout = JSON.stringify({ result: 'TimeoutError', console: [] }); + self.child.stdout.removeListener('data', stdoutHandler); + error = 'TimeoutError'; self.child.kill('SIGKILL'); }, self.options.timeout); }; // Send a message to the code running inside the sandbox -// This message will be passed to the sandboxed +// This message will be passed to the sandboxed // code's `onmessage` function, if defined. // Messages posted before the sandbox is ready will be queued Sandbox.prototype.postMessage = function(message) { var self = this; - + if (self._ready) { self.child.send(message); } else { diff --git a/lib/shovel.js b/lib/shovel.js index 266a3a4..730366f 100644 --- a/lib/shovel.js +++ b/lib/shovel.js @@ -8,7 +8,6 @@ var vm = require('vm'); var code = ''; var stdin = process.openStdin(); var result; -var console = []; // Get code stdin.on('data', function(data) { @@ -34,11 +33,16 @@ function getSafeRunner() { // comm.send(event, JSON.stringify([].slice.call(arguments,1))); }; + var exit = function exit(){ + onmessage = null; + comm.exit(); + }; global.print = send.bind(global, 'stdout'); global.console = { log: send.bind(global, 'stdout') }; - global.process = { - stdout: { write: send.bind(global, 'stdout') } + global.process = { + stdout: { write: send.bind(global, 'stdout') }, + exit: exit }; global.postMessage = send.bind(global, 'message'); @@ -53,7 +57,7 @@ function run() { var context = vm.createContext(); var safeRunner = vm.runInContext('('+getSafeRunner.toString()+')()', context); - + try { safeRunner({ send: function (event, value) { @@ -61,13 +65,13 @@ function run() { switch (event) { case 'stdout': - console.push(JSON.parse(value)[0]); + process.stdout.write(String(JSON.parse(value)[0]) + '\n'); break; case 'end': result = JSON.parse(value)[0]; break; case 'message': - process.send(JSON.parse(value)[0]); + process.send({ type: 'message', data: JSON.parse(value)[0] }); break; default: throw new Error('Unknown event type'); @@ -79,35 +83,40 @@ function run() { }, code); } catch (e) { - result = e.name + ': ' + e.message; - // throw e; + process.stderr.write(String(e.name) + ': ' + String(e.message)); + process.stderr.on('drain', processExit); + return; } process.on('message', processMessageListener.bind(null, context)); - - process.send('__sandbox_inner_ready__'); + + process.send({ type: 'ready' }); // This will exit the process if onmessage was not defined checkIfProcessFinished(context); }; +// If the sandboxed code has defined an `onmessage` function, pass the message +// we received to it. Note that we are still worried about what external users +// might pass in to the sandboxed code function processMessageListener(context, message){ vm.runInContext('if (typeof onmessage === "function") { onmessage('+ JSON.stringify(String(message)) + '); }', context); checkIfProcessFinished(context); }; +// The process should only be considered finished if `onmessage` is not set +// or if the sandboxed code calls `process.exit()` explicitly function checkIfProcessFinished(context) { if(vm.runInContext('typeof onmessage', context) !== 'function') { processExit(); } }; +// Send the result to the parent process and exit this process function processExit() { process.removeListener('message', processMessageListener); - process.stdout.on('finish', function() { - process.exit(0); - }); + process.send({ type: 'result', data: result }); - process.stdout.end(JSON.stringify({ result: util.inspect(result), console: console })); + process.exit(0); }; diff --git a/package.json b/package.json index 0900954..9582353 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "Bradley Meck ", "Dominic Tarr (http://cyber-hobo.blogspot.com)" ], - "version": "0.8.5", + "version": "0.9.0", "main": "./lib/sandbox", "directories": { "lib": "./lib" @@ -32,5 +32,8 @@ "license": { "type": "Public Domain", "url": "http://github.com/gf3/sandbox/raw/master/UNLICENSE" + }, + "dependencies": { + "concat-stream": "^1.4.6" } } diff --git a/test/sandbox.js b/test/sandbox.js index e0b9f55..04246e9 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -22,64 +22,95 @@ describe('Sandbox', function() { }); it('should execute basic javascript', function(done) { - sb.run('1 + 1', function(output) { - expect(output.result).to.equal('2'); + sb.run('1 + 1', function(error, result) { + expect(error).not.to.exist; + expect(result).to.equal('2'); + done(); + }); + }); + + it('should call the callback with (null, null) if there is no error or result', function(done){ + sb.run('', function(error, result){ + expect(error).not.to.exist; + expect(result).not.to.exist; done(); }); }); it('should gracefully handle syntax errors', function(done) { - sb.run('hi )there', function(output) { - expect(output.result).to.equal("'SyntaxError: Unexpected token )'"); + sb.run('hi )there', function(error, result) { + expect(error).to.equal('SyntaxError: Unexpected token )'); + expect(result).not.to.exist; done(); }); }); it('should effectively prevent code from accessing node', function(done) { - sb.run('process.platform', function(output) { - expect(output.result).to.equal("null"); + sb.run('process.platform', function(error, result) { + expect(error).not.to.exist; + expect(result).not.to.exist; done(); }); }); it('should effectively prevent code from circumventing the sandbox', function(done) { - sb.run("var sys=require('sys'); sys.puts('Up in your fridge')", function(output) { - expect(output.result).to.equal("'ReferenceError: require is not defined'"); + sb.run("var sys=require('sys'); sys.puts('Up in your fridge')", function(error, result) { + expect(error).to.equal('ReferenceError: require is not defined'); + expect(result).not.to.exist; done(); }); }); it('should timeout on infinite loops', function(done) { - sb.run('while ( true ) {}', function(output) { - expect(output.result).to.equal('TimeoutError'); + sb.run('while ( true ) {}', function(error, result) { + expect(error).to.equal('TimeoutError'); + expect(result).not.to.exist; done(); }); }); it('should allow console output via `console.log`', function(done) { - sb.run('console.log(7); 42', function(output) { - expect(output.result).to.equal('42'); - expect(output.console).to.have.length(1); - expect(output.console[0]).to.equal(7); + var stdout_spy = sinon.spy(); + var normal_stdout = sb._stdout; + sb._stdout = { write: stdout_spy }; + sb.run('console.log(7); 42', function(error, result) { + expect(error).not.to.exist; + expect(result).to.equal('42'); + expect(stdout_spy).to.be.calledOnce; + expect(stdout_spy).to.be.calledWith('7\n'); + + sb._stdout = normal_stdout; done(); }); }); it('should allow console output via `print`', function(done) { - sb.run('print(7); 42', function(output) { - expect(output.result).to.equal('42'); - expect(output.console).to.have.length(1); - expect(output.console[0]).to.equal(7); + var stdout_spy = sinon.spy(); + var normal_stdout = sb._stdout; + sb._stdout = { write: stdout_spy }; + sb.run('print(7); 42', function(error, result) { + expect(error).not.to.exist; + expect(result).to.equal('42'); + expect(stdout_spy).to.be.calledOnce; + expect(stdout_spy).to.be.calledWith('7\n'); + + sb._stdout = normal_stdout; done(); }); }); it('should maintain the order of sync. console output', function(done) { - sb.run('console.log("first"); console.log("second"); 42', function(output) { - expect(output.result).to.equal('42'); - expect(output.console).to.have.length(2); - expect(output.console[0]).to.equal('first'); - expect(output.console[1]).to.equal('second'); + var stdout_spy = sinon.spy(); + var normal_stdout = sb._stdout; + sb._stdout = { write: stdout_spy }; + sb.run('console.log("first"); console.log("second"); 42', function(error, result) { + expect(error).not.to.exist; + expect(result).to.equal('42'); + expect(stdout_spy).to.be.calledTwice; + expect(stdout_spy.firstCall).to.be.calledWith('first\n'); + expect(stdout_spy.secondCall).to.be.calledWith('second\n'); + + sb._stdout = normal_stdout; done(); }); }); @@ -87,9 +118,12 @@ describe('Sandbox', function() { it('should expose the postMessage command to the sandboxed code', function(done){ var messageHandler = sinon.spy(); sb.on('message', messageHandler); - sb.run('postMessage("Hello World!");', function(output){ + sb.run('postMessage("Hello World!");', function(error, result){ + expect(error).not.to.exist; expect(messageHandler.calledOnce).to.be.true; expect(messageHandler).to.be.calledWith('Hello World!'); + + sb.removeListener('message', messageHandler); done(); }); }); @@ -100,13 +134,16 @@ describe('Sandbox', function() { sb.on('ready', function () { sb.postMessage('Hello World!'); }); - sb.run('onmessage = function (msg) { postMessage(msg); };', function(output) { + sb.run('onmessage = function (msg) { postMessage(msg); };', function(error, result) { + // expect(error).not.to.exist; expect(messageHandler).to.be.calledOnce; expect(messageHandler).to.be.calledWith('Hello World!'); + + sb.removeListener('message', messageHandler); done(); }); }); - + it('should queue messages posted before the sandbox is ready and process them once it is', function(done){ var messageHandler = sinon.spy(); var num_messages_sent = 0; @@ -114,9 +151,12 @@ describe('Sandbox', function() { sb.postMessage(++num_messages_sent); }, 1); sb.on('message', messageHandler); - sb.run('onmessage = function (msg) { postMessage(msg); };', function(output) { + sb.run('onmessage = function (msg) { postMessage(msg); };', function(error, result) { + // expect(error).not.to.exist; expect(messageHandler.callCount).to.equal(num_messages_sent); expect(num_messages_sent).to.be.greaterThan(0); + + sb.removeListener('message', messageHandler); done(); }); sb.on('ready', function(){ @@ -124,4 +164,60 @@ describe('Sandbox', function() { }); }); -}); \ No newline at end of file + it('should expose the `process.exit` function to the sandboxed code', function(done){ + var messageHandler = sinon.spy(); + sb.on('message', messageHandler); + sb.run('onmessage = function (msg) { postMessage(msg); }; process.exit();', function(error, result) { + expect(error).not.to.exist; + expect(messageHandler).not.to.be.called; + + sb.removeListener('message', messageHandler); + done(); + }); + sb.on('ready', function(){ + sb.postMessage('this shouldnt get sent'); + }); + }); + + it('should call the callback if `onmessage` is set to null after it receives one message', function(done){ + var messageHandler = sinon.spy(); + sb.on('message', messageHandler); + sb.run('var msgfunction = function(msg) { postMessage(msg); onmessage = null; }; onmessage = msgfunction;', function(error, result){ + expect(error).not.to.exist; + expect(messageHandler).to.be.called; + + sb.removeListener('message', messageHandler); + done(); + }); + sb.postMessage('Hello World!'); + }); + + it('should only base the `result` on the synchronous part of the code, not on the async `onmessage` function', function(done){ + var messageHandler = sinon.spy(); + sb.on('message', messageHandler); + sb.run('var msgfunction = function(msg) { postMessage(msg); onmessage = null; return 42; }; onmessage = msgfunction;', function(error, result){ + expect(error).not.to.exist; + expect(messageHandler).to.be.called; + expect(result).not.to.exist; + + sb.removeListener('message', messageHandler); + done(); + }); + sb.postMessage('Hello World!'); + }); + + it('should result in a TimeoutError if `onmessage` is used but `process.exit` is never called and `onmessage` is not set to null', function(done){ + var messageHandler = sinon.spy(); + sb.on('message', messageHandler); + sb.run('var msgfunction = function(msg) { postMessage(msg); 42 }; onmessage = msgfunction;', function(error, result){ + expect(error).to.equal('TimeoutError'); + expect(messageHandler).to.be.called; + expect(result).not.to.exist; + + sb.removeListener('message', messageHandler); + done(); + }); + sb.postMessage('Hello World!'); + }); + +});