browserchannel
Version:
Google BrowserChannel server for NodeJS
393 lines (312 loc) • 13.3 kB
text/coffeescript
# # Tests for the bare BrowserChannel client.
#
# Run them by first launching
#
# % coffee test/support/runserver.coffee
#
# ... Then browsing to localhost:4321 in your browser or running:
#
# % mocha test/bcsocket.coffee
#
# from the command line. You should do both kinds of testing before pushing.
#
#
# These tests are pretty simple and primitive. The reality is, google's browserchannel
# client library is pretty bloody well tested. (I'm not interested in rewriting that test suite)
#
# However, its important to do some sanity checks on the exported browserchannel bits to
# make sure closure isn't doing anything wacky. Also this acts as a nice little integration
# test for the server, _and_ its useful to make sure that all the browsers node-browserchannel
# supports are behaving themselves.
#
# Oh yeah, and these tests will be run on the nodejs version of browserchannel, which has
# a lot of silly little parts.
#
# These tests will also be useful if the browserchannel protocol ever changes.
#
# Interestingly, most of the benefits of this test suite come from a single test (really, any
# test). If any test passes, they'll all probably pass.
#
#
# ## Known Issues
#
# There's three weird issues with this test suite:
#
# - Sometimes (maybe, 1 in 10) times the test is run from nodejs, it dies in a weird inconsistant
# state.
# - After a test run, 4 sessions are allowed to time out by the server. (Its odd because I'm calling
# disconnect() in tearDown).
#
assert = require 'assert'
if typeof window is 'undefined'
try
require('./runserver').listen 4321
catch e
console.warn e.stack
bc = require '..'
# If coffeescript declares a variable called 'BCSocket' here, it will shadow
# the BCSocket variable that is already defined in the browser. Doing it this
# way is pretty ugly, but it works and the ugliness is constrained to a test.
global.BCSocket = bc.BCSocket
bc.setDefaultLocation 'http://localhost:4321'
# This is a little logging function for old IE. It adds a log() function which
# simply appends HTML messages to the document.
window?.log = log = (str) ->
div = document.createElement 'div'
div.innerHTML = str
document.body.appendChild div
suite 'bcsocket', ->
# IE6 takes about 12 seconds to do the large stress test
@timeout 20000
teardown (callback) ->
if @socket? and @socket.readyState isnt BCSocket.CLOSED
@socket.onclose = -> callback()
@socket.close()
@socket = null
else
callback()
# These match websocket codes
test 'states and errors are mapped', ->
assert.strictEqual BCSocket.CONNECTING, 0
assert.strictEqual BCSocket.OPEN, 1
assert.strictEqual BCSocket.CLOSING, 2
assert.strictEqual BCSocket.CLOSED, 3
assert.strictEqual BCSocket.prototype.CONNECTING, 0
assert.strictEqual BCSocket.prototype.OPEN, 1
assert.strictEqual BCSocket.prototype.CLOSING, 2
assert.strictEqual BCSocket.prototype.CLOSED, 3
assert.strictEqual BCSocket.canSendWhileConnecting, true
assert.strictEqual BCSocket.prototype.canSendWhileConnecting, true
assert.strictEqual BCSocket.canSendJSON, true
assert.strictEqual BCSocket.prototype.canSendJSON, true
# Can we connect to the server?
test 'connect', (done) ->
@socket = new BCSocket '/notify'
assert.strictEqual @socket.readyState, BCSocket.CONNECTING
openCalled = false
@socket.onopen = =>
assert.strictEqual @socket.readyState, BCSocket.OPEN
openCalled = true
@socket.onerror = (reason) ->
throw new Error reason
@socket.onmessage = (message) ->
assert.deepEqual message.data, {appVersion: null}
assert.ok openCalled
done()
# The socket interface exposes browserchannel's app version thingy through
# option arguments
test 'connect sends app version', (done) ->
@socket = new BCSocket '/notify', appVersion: 321
@socket.onmessage = (message) ->
assert.deepEqual message.data, {appVersion:321}
done()
# BrowserChannel's native send method sends a string->string map.
#
# I want to test that I can send and recieve messages both before we've connected
# (they should be sent as soon as the connection is established) and after the
# connection has opened normally.
suite 'send maps', ->
# I'll throw some random unicode characters in here just to make sure...
data = {'foo': 'bar', 'zot': '(◔ ◡ ◔)'}
m = (callback) -> (done) ->
@socket = new BCSocket '/echomap', appVersion: 321
@socket.onmessage = (message) ->
assert.deepEqual message.data, data
done()
callback.apply this
test 'immediately', m ->
@socket.sendMap data
test 'after we have connected', m ->
@socket.onopen = =>
@socket.sendMap data
# I'll also test the normal send method. This is pretty much the same as above, whereby
# I'll do the test two ways.
suite 'can send and receive', ->
test 'unicode', (done) ->
# Vim gets formatting errors with the cat face glyph here. Sad.
data = '⚗☗⚑☯'
@socket = new BCSocket '/echo', appVersion: 321
@socket.onmessage = (message) ->
assert.deepEqual message.data, data
done()
@socket.onopen = =>
@socket.send data
suite 'JSON messages', ->
# Vim gets formatting errors with the cat face glyph here. Sad.
data = [null, 1.5, "hi", {}, [1,2,3], '⚗☗⚑☯']
m = (callback) -> (done) ->
# Using the /echo server not /echomap
@socket = new BCSocket '/echo', appVersion: 321
@socket.onmessage = (message) ->
assert.deepEqual message.data, data
done()
callback.apply this
test 'immediately', m ->
# Calling send() instead of sendMap()
@socket.send data
test 'after we have connected', m ->
@socket.onopen = =>
@socket.send data
suite 'string messages', ->
# Vim gets formatting errors with the cat face glyph here. Sad.
data = ["hi", "", " ", "\n", "\t", '⚗☗⚑☯', "\u2028 \u2029", ('x' for [1..1000]).join()]
# I'm going to send each message in the array in sequence. We should get
# them back in the same sequence.
pos = 0
m = (callback) -> (done) ->
# Using the /echo server not /echomap
@socket = new BCSocket '/echo', appVersion: 321
@socket.onmessage = (message) ->
assert pos < data.length
assert.deepEqual message.data, data[pos++]
done() if pos == data.length
callback.apply this
test 'immediately', m ->
pos = 0
# Calling send() instead of sendMap()
@socket.send str for str in data
return
test 'after we have connected', m ->
pos = 0
@socket.onopen = =>
@socket.send str for str in data
return
# This is a little stress test to make sure I haven't missed anything.
# Sending and recieving this much data pushes the client to use multiple
# forward channel connections. It doesn't use multiple backchannel
# connections - I should probably put some logic there whereby I close the
# backchannel after awhile.
test 'Lots of data', (done) ->
num = 5000
@socket = new BCSocket '/echomap'
received = 0
@socket.onmessage = (message) ->
assert.equal message.data.data, received
received++
done() if received == num
setTimeout =>
# Maps aren't actual JSON. They're just key-value pairs. I don't need to
# encode i as a string here, but thats now its sent anyway.
@socket.sendMap {data:"#{i}", juuuuuuuuuuuuuuuuunnnnnnnnnk:'waaaazzzzzzuuuuuppppppp'} for i in [0...num]
, 0
# I have 2 disconnect servers which have slightly different timing regarding
# when they call close() on the session. If close is called immediately, the
# initial bind request is rejected with a 403 response, before the client
# connects.
test 'disconnecting immediately results in REQUEST_FAILED and a 403', (done) ->
@socket = new BCSocket '/dc1', reconnect: no
@socket.onopen = -> throw new Error 'Socket should not have opened'
onErrorCalled = no
@socket.onerror = (message, errCode) =>
assert.strictEqual message, 'Request failed'
assert.strictEqual errCode, 2
onErrorCalled = yes
@socket.onclose = ->
# This will be called because technically, the websocket does go into the
# close state!
#
# This is exactly what websockets do.
assert.ok onErrorCalled
done()
test 'disconnecting momentarily allows the client to connect, then onclose() is called', (done) ->
@socket = new BCSocket '/dc2', failFast: yes
onErrorCalled = no
@socket.onerror = (message, errCode) =>
# The error code varies here, depending on some timing parameters &
# browser. I've seen NO_DATA, REQUEST_FAILED and UNKNOWN_SESSION_ID.
assert.strictEqual @socket.readyState, @socket.CLOSING
assert.ok message
assert.ok errCode
onErrorCalled = yes
@socket.onclose = (reason, pendingMaps, undeliveredMaps) =>
# The error code varies here, depending on some timing parameters & browser.
# These will probably be undefined, but == will catch that.
assert.strictEqual @socket.readyState, @socket.CLOSED
assert.equal pendingMaps, null
assert.equal undeliveredMaps, null
assert.ok onErrorCalled
done()
test 'passing a previous session will ghost that session', (done) ->
@socket1 = new BCSocket '/echo'
@socket1.onopen = =>
@socket2 = new BCSocket '/echo', prev:@socket1
@socket1.onclose = =>
@socket2.close()
done()
suite 'The client keeps reconnecting', ->
m = (base) -> (done) ->
@socket = new BCSocket base, failFast: yes, reconnect: yes, reconnectTime: 300
openCount = 0
@socket.onopen = =>
throw new Error 'Should not keep trying to open once the test is done' if openCount == 2
assert.strictEqual @socket.readyState, @socket.OPEN
@socket.onclose = (reason, pendingMaps, undeliveredMaps) =>
assert.strictEqual @socket.readyState, @socket.CLOSED
assert openCount < 2
openCount++
if openCount is 2
# Tell the socket to stop trying to connect
@socket.close()
done()
test 'When the connection fails', m('dc1')
# 'When the connection dies': m('dc3')
suite 'stop', ->
makeTest = (base) -> (done) ->
# We don't need failFast for stop.
@socket = new BCSocket base
onErrorCalled = no
@socket.onerror = (message, errCode) =>
assert.strictEqual @socket.readyState, @socket.CLOSING
assert.strictEqual message, 'Stopped by server'
assert.strictEqual errCode, 7
onErrorCalled = yes
@socket.onclose = (reason, pendingMaps, undeliveredMaps) =>
# These will probably be undefined, but == will catch that.
assert.strictEqual @socket.readyState, @socket.CLOSED
assert.equal pendingMaps, null
assert.equal undeliveredMaps, null
assert.strictEqual reason, 'Stopped by server'
assert.ok onErrorCalled
done()
test 'on connect', makeTest 'stop1'
test 'after connect', makeTest 'stop2'
# We need to be able to send \u2028 and \u2029
# http://timelessrepo.com/json-isnt-a-javascript-subset
test 'Line separator and paragraph separators work', (done) ->
@socket = new BCSocket '/utfsep', appVersion: 321
@socket.onmessage = (message) ->
assert.strictEqual message.data, "\u2028 \u2029"
done()
# We should be able to specify GET variables to be sent with every request.
test 'extraParams are passed to the server', (done) ->
@socket = new BCSocket '/extraParams', extraParams: foo: 'bar'
@socket.onmessage = (message) ->
assert.strictEqual message.data.foo, 'bar'
done()
test 'Session affinity tokens are generated by default', (done) ->
@socket = new BCSocket '/extraParams'
affinity = @socket.affinity
@socket.onmessage = (message) ->
assert.strictEqual message.data.a, affinity
done()
test 'Session affinity tokens can be set manually', (done) ->
@socket = new BCSocket '/extraParams', affinity: 'custom token'
@socket.onmessage = (message) ->
assert.strictEqual message.data.a, 'custom token'
done()
test 'Session affinity GET variable can be modified', (done) ->
@socket = new BCSocket '/extraParams', affinityParam: 'avoidConflict'
affinity = @socket.affinity
@socket.onmessage = (message) ->
assert.strictEqual message.data.avoidConflict, affinity
done()
test 'Session affinity tokens can be disabled', (done) ->
@socket = new BCSocket '/extraParams', affinity: null
@socket.onmessage = (message) ->
assert.strictEqual message.data.a, undefined
done()
test 'Extra headers are sent to the server', (done) ->
@socket = new BCSocket '/extraHeaders', extraHeaders: {'X-Style': 'Fabulous'}
@socket.onmessage = (message) ->
assert.strictEqual message.data['x-style'], 'Fabulous'
done()