Files
websocket-extensions-node/spec/websocket_extensions_spec.js
T

499 lines
19 KiB
JavaScript

var Extensions = require("../lib/websocket_extensions"),
test = require("jstest").Test
FakeClock = test.FakeClock
test.describe("Extensions", function() { with(this) {
before(function() { with(this) {
this.extensions = new Extensions()
this.ext = {name: "deflate", type: "permessage", rsv1: true, rsv2: false, rsv3: false}
this.session = {}
}})
describe("add", function() { with(this) {
it("does not throw on valid extensions", function() { with(this) {
assertNothingThrown(function() { extensions.add(ext) })
}})
it("throws if ext.name is not a string", function() { with(this) {
ext.name = 42
assertThrows(TypeError, function() { extensions.add(ext) })
}})
it("throws if ext.rsv1 is not a boolean", function() { with(this) {
ext.rsv1 = 42
assertThrows(TypeError, function() { extensions.add(ext) })
}})
it("throws if ext.rsv2 is not a boolean", function() { with(this) {
ext.rsv2 = 42
assertThrows(TypeError, function() { extensions.add(ext) })
}})
it("throws if ext.rsv3 is not a boolean", function() { with(this) {
ext.rsv3 = 42
assertThrows(TypeError, function() { extensions.add(ext) })
}})
}})
describe("client sessions", function() { with(this) {
before(function() { with(this) {
this.offer = {mode: "compress"}
stub(ext, "createClientSession").returns(session)
stub(session, "generateOffer").returns(offer)
extensions.add(ext)
this.conflict = {name: "tar", type: "permessage", rsv1: true, rsv2: false, rsv3: false}
this.conflictSession = {}
stub(conflict, "createClientSession").returns(conflictSession)
stub(conflictSession, "generateOffer").returns({gzip: true})
this.nonconflict = {name: "reverse", type: "permessage", rsv1: false, rsv2: true, rsv3: false}
this.nonconflictSession = {}
stub(nonconflict, "createClientSession").returns(nonconflictSession)
stub(nonconflictSession, "generateOffer").returns({utf8: true})
stub(session, "activate").returns(true)
stub(conflictSession, "activate").returns(true)
stub(nonconflictSession, "activate").returns(true)
}})
describe("generateOffer", function() { with(this) {
it("asks the extension to create a client session", function() { with(this) {
expect(ext, "createClientSession").exactly(1).returning(session)
extensions.generateOffer()
}})
it("asks the session to generate an offer", function() { with(this) {
expect(session, "generateOffer").exactly(1).returning(offer)
extensions.generateOffer()
}})
it("does not ask the session to generate an offer if the extension doesn't build a session", function() { with(this) {
stub(ext, "createClientSession").returns(null)
expect(session, "generateOffer").exactly(0)
extensions.generateOffer()
}})
it("returns the serialized offer from the session", function() { with(this) {
assertEqual( "deflate; mode=compress", extensions.generateOffer() )
}})
it("returns a null offer from the session", function() { with(this) {
stub(session, "generateOffer").returns(null)
assertEqual( null, extensions.generateOffer() )
}})
it("returns multiple serialized offers from the session", function() { with(this) {
stub(session, "generateOffer").returns([offer, {}])
assertEqual( "deflate; mode=compress, deflate", extensions.generateOffer() )
}})
it("returns serialized offers from multiple sessions", function() { with(this) {
extensions.add(nonconflict)
assertEqual( "deflate; mode=compress, reverse; utf8", extensions.generateOffer() )
}})
it("generates offers for potentially conflicting extensions", function() { with(this) {
extensions.add(conflict)
assertEqual( "deflate; mode=compress, tar; gzip", extensions.generateOffer() )
}})
}})
describe("activate", function() { with(this) {
before(function() { with(this) {
extensions.add(conflict)
extensions.add(nonconflict)
extensions.generateOffer()
}})
it("throws if given unregistered extensions", function() { with(this) {
assertThrows(Error, function() { extensions.activate("xml") })
}})
it("does not throw if given registered extensions", function() { with(this) {
assertNothingThrown(function() { extensions.activate("deflate") })
}})
it("does not throw if given only one potentially conflicting extension", function() { with(this) {
assertNothingThrown(function() { extensions.activate("tar") })
}})
it("throws if two extensions conflict on RSV bits", function() { with(this) {
assertThrows(Error, function() { extensions.activate("deflate, tar") })
}})
it("does not throw if given two non-conflicting extensions", function() { with(this) {
assertNothingThrown(function() { extensions.activate("deflate, reverse") })
}})
it("activates one session with no params", function() { with(this) {
expect(session, "activate").given({}).exactly(1).returning(true)
extensions.activate("deflate")
}})
it("activates one session with a boolean param", function() { with(this) {
expect(session, "activate").given({gzip: true}).exactly(1).returning(true)
extensions.activate("deflate; gzip")
}})
it("activates one session with a string param", function() { with(this) {
expect(session, "activate").given({mode: "compress"}).exactly(1).returning(true)
extensions.activate("deflate; mode=compress")
}})
it("activates multiple sessions", function() { with(this) {
expect(session, "activate").given({a: true}).exactly(1).returning(true)
expect(nonconflictSession, "activate").given({b: true}).exactly(1).returning(true)
extensions.activate("deflate; a, reverse; b")
}})
it("does not activate sessions not named in the header", function() { with(this) {
expect(session, "activate").exactly(0)
expect(nonconflictSession, "activate").exactly(1).returning(true)
extensions.activate("reverse")
}})
it("throws if session.activate() does not return true", function() { with(this) {
stub(session, "activate").returns("yes")
assertThrows(Error, function() { extensions.activate("deflate") })
}})
}})
describe("processIncomingMessage", function() { with(this) {
before(function() { with(this) {
extensions.add(conflict)
extensions.add(nonconflict)
extensions.generateOffer()
stub(session, "processIncomingMessage", function(message, callback) {
message.frames.push("deflate")
callback(null, message)
})
stub(nonconflictSession, "processIncomingMessage", function(message, callback) {
message.frames.push("reverse")
callback(null, message)
})
}})
it("processes messages in the reverse order given in the server's response", function() { with(this) {
extensions.activate("deflate, reverse")
extensions.processIncomingMessage({frames: []}, function(error, message) {
assertNull(error)
assertEqual( ["reverse", "deflate"], message.frames )
})
}})
it("yields an error if a session yields an error", function() { with(this) {
extensions.activate("deflate")
stub(session, "processIncomingMessage").yields([{message: "ENOENT"}])
extensions.processIncomingMessage({frames: []}, function(error, message) {
assertEqual( "deflate: ENOENT", error.message )
assertNull( message )
})
}})
it("does not call sessions after one has yielded an error", function() { with(this) {
extensions.activate("deflate, reverse")
stub(nonconflictSession, "processIncomingMessage").yields([{message: "ENOENT"}])
expect(session, "processIncomingMessage").exactly(0)
extensions.processIncomingMessage({frames: []}, function() {})
}})
}})
describe("processOutgoingMessage", function() { with(this) {
before(function() { with(this) {
extensions.add(conflict)
extensions.add(nonconflict)
extensions.generateOffer()
stub(session, "processOutgoingMessage", function(message, callback) {
message.frames.push("deflate")
callback(null, message)
})
stub(nonconflictSession, "processOutgoingMessage", function(message, callback) {
message.frames.push("reverse")
callback(null, message)
})
}})
describe("error handling", function() { with(this) {
include(FakeClock)
before(function() { with(this) {
clock.stub()
extensions.activate("deflate, reverse")
stub(session, "processOutgoingMessage", function(message, callback) {
setTimeout(function() { callback(null, message.concat("a")) }, 100)
})
stub(nonconflictSession, "processOutgoingMessage", function(message, callback) {
setTimeout(function() { callback(null, message.concat("b")) }, 100)
})
stub(nonconflictSession, "processIncomingMessage", function(message, callback) {
if (message[0] === 5)
setTimeout(function() { callback(new Error(""), null) }, 10)
else
setTimeout(function() { callback(null, message.concat("c")) }, 50)
})
stub(session, "processIncomingMessage", function(message, callback) {
setTimeout(function() { callback(null, message.concat("d")) }, 100)
})
stub(session, "close")
stub(nonconflictSession, "close")
this.messages = []
var push = function(error, message) {
if (error) extensions.close(function() { messages.push("close") })
messages.push(message)
}
;[1, 2, 3].forEach(function(n) { extensions.processOutgoingMessage([n], push) })
;[4, 5, 6].forEach(function(n) { extensions.processIncomingMessage([n], push) })
clock.tick(200)
}})
it("allows the message before the error through to the end", function() { with(this) {
assertEqual( [4, "c", "d"], messages[0] )
}})
it("yields the error to the end of the pipeline", function() { with(this) {
assertNull( messages[1] )
}})
it("does not yield the message after the error", function() { with(this) {
assertNotEqual( arrayIncluding([6, "c", "d"]), messages )
}})
it("yields all the messages in the direction unaffected by the error", function() { with(this) {
assertEqual( [1, "a", "b"], messages[2] )
assertEqual( [2, "a", "b"], messages[3] )
assertEqual( [3, "a", "b"], messages[4] )
}})
it("closes after all messages are processed", function() { with(this) {
assertEqual( "close", messages[5] )
assertEqual( 6, messages.length )
}})
}})
describe("async processors", function() { with(this) {
include(FakeClock)
before(function() { with(this) {
clock.stub()
var tags = ["a", "b", "c", "d"]
stub(session, "processOutgoingMessage", function(message, callback) {
var time = message.frames.length === 0 ? 100 : 20
message.frames.push(tags.shift())
setTimeout(function() { callback(null, message) }, time)
})
stub(nonconflictSession, "processOutgoingMessage", function(message, callback) {
var time = message.frames.length === 1 ? 100 : 20
message.frames.push(tags.shift())
setTimeout(function() { callback(null, message) }, time)
})
}})
it("processes messages in order even if upstream emits them out of order", function() { with(this) {
extensions.activate("deflate, reverse")
var out = []
extensions.processOutgoingMessage({frames: []}, function(error, message) { out.push(message) })
extensions.processOutgoingMessage({frames: [1]}, function(error, message) { out.push(message) })
clock.tick(200)
assertEqual( [{frames: ["a", "c"]}, {frames: [1, "b", "d"]}], out )
}})
it("defers closing until the extension has finished processing", function() { with(this) {
extensions.activate("deflate")
var closed = false, notified = false
stub(session, "close", function() { closed = true })
extensions.processOutgoingMessage({frames: []}, function() {})
extensions.close(function() { notified = true })
clock.tick(50)
assertNot( closed || notified )
clock.tick(50)
assert( closed && notified )
}})
it("closes each session as soon as it finishes processing", function() { with(this) {
extensions.activate("deflate, reverse")
var closed = [false, false], notified = false
stub(session, "close", function() { closed[0] = true })
stub(nonconflictSession, "close", function() { closed[1] = true })
extensions.processOutgoingMessage({frames: []}, function() {});
extensions.close(function() { notified = true })
clock.tick(50)
assertNot( closed[0] || closed[1] || notified )
clock.tick(100)
assert( closed[0] )
assertNot( closed[1] || notified )
clock.tick(50)
assert( closed[0] && closed[1] && notified )
}})
it("notifies of closure immeidately if already closed", function() { with(this) {
extensions.activate("deflate")
stub(session, "close", function() { closed = true })
extensions.processOutgoingMessage({frames: []}, function() {})
extensions.close()
clock.tick(100)
var notified = false
extensions.close(function() { notified = true })
assert( notified )
}})
}})
it("processes messages in the order given in the server's response", function() { with(this) {
extensions.activate("deflate, reverse")
extensions.processOutgoingMessage({frames: []}, function(error, message) {
assertNull(error)
assertEqual( ["deflate", "reverse"], message.frames )
})
}})
it("processes messages in the server's order, not the client's order", function() { with(this) {
extensions.activate("reverse, deflate")
extensions.processOutgoingMessage({frames: []}, function(error, message) {
assertNull(error)
assertEqual( ["reverse", "deflate"], message.frames )
})
}})
it("yields an error if a session yields an error", function() { with(this) {
extensions.activate("deflate")
stub(session, "processOutgoingMessage").yields([{message: "ENOENT"}])
extensions.processOutgoingMessage({frames: []}, function(error, message) {
assertEqual( "deflate: ENOENT", error.message )
assertNull( message )
})
}})
it("does not call sessions after one has yielded an error", function() { with(this) {
extensions.activate("deflate, reverse")
stub(session, "processOutgoingMessage").yields([{message: "ENOENT"}])
expect(nonconflictSession, "processOutgoingMessage").exactly(0)
extensions.processOutgoingMessage({frames: []}, function() {})
}})
}})
}})
describe("server sessions", function() { with(this) {
before(function() { with(this) {
this.response = {mode: "compress"}
stub(ext, "createServerSession").returns(session)
stub(session, "generateResponse").returns(response)
this.conflict = {name: "tar", type: "permessage", rsv1: true, rsv2: false, rsv3: false}
this.conflictSession = {}
stub(conflict, "createServerSession").returns(conflictSession)
stub(conflictSession, "generateResponse").returns({gzip: true})
this.nonconflict = {name: "reverse", type: "permessage", rsv1: false, rsv2: true, rsv3: false}
this.nonconflictSession = {}
stub(nonconflict, "createServerSession").returns(nonconflictSession)
stub(nonconflictSession, "generateResponse").returns({utf8: true})
extensions.add(ext)
extensions.add(conflict)
extensions.add(nonconflict)
}})
describe("generateResponse", function() { with(this) {
it("asks the extension for a server session with the offer", function() { with(this) {
expect(ext, "createServerSession").given([{flag: true}]).exactly(1).returning(session)
extensions.generateResponse("deflate; flag")
}})
it("asks the extension for a server session with multiple offers", function() { with(this) {
expect(ext, "createServerSession").given([{a: true}, {b: true}]).exactly(1).returning(session)
extensions.generateResponse("deflate; a, deflate; b")
}})
it("asks the session to generate a response", function() { with(this) {
expect(session, "generateResponse").exactly(1).returning(response)
extensions.generateResponse("deflate")
}})
it("asks multiple sessions to generate a response", function() { with(this) {
expect(session, "generateResponse").exactly(1).returning(response)
expect(nonconflictSession, "generateResponse").exactly(1).returning(response)
extensions.generateResponse("deflate, reverse")
}})
it("does not ask the session to generate a response if the extension doesn't build a session", function() { with(this) {
stub(ext, "createServerSession").returns(null)
expect(session, "generateResponse").exactly(0)
extensions.generateResponse("deflate")
}})
it("does not ask the extension to build a session for unoffered extensions", function() { with(this) {
expect(nonconflict, "createServerSession").exactly(0)
extensions.generateResponse("deflate")
}})
it("does not ask the extension to build a session for conflicting extensions", function() { with(this) {
expect(conflict, "createServerSession").exactly(0)
extensions.generateResponse("deflate, tar")
}})
it("returns the serialized response from the session", function() { with(this) {
assertEqual( "deflate; mode=compress", extensions.generateResponse("deflate") )
}})
it("returns serialized responses from multiple sessions", function() { with(this) {
assertEqual( "deflate; mode=compress, reverse; utf8", extensions.generateResponse("deflate, reverse") )
}})
it("returns responses in registration order", function() { with(this) {
assertEqual( "deflate; mode=compress, reverse; utf8", extensions.generateResponse("reverse, deflate") )
}})
it("does not return responses for unoffered extensions", function() { with(this) {
assertEqual( "reverse; utf8", extensions.generateResponse("reverse") )
}})
it("does not return responses for conflicting extensions", function() { with(this) {
assertEqual( "deflate; mode=compress", extensions.generateResponse("deflate, tar") )
}})
it("returns a response for potentially conflicting extensions if their preceeding extensions don't build a session", function() { with(this) {
stub(ext, "createServerSession").returns(null)
assertEqual( "tar; gzip", extensions.generateResponse("deflate, tar") )
}})
}})
}})
}})