1b77290e82
This is designed to address these issues: * https://github.com/faye/permessage-deflate-node/issues/1 * https://github.com/faye/permessage-deflate-node/pull/3 * https://github.com/faye/websocket-driver-node/issues/11
499 lines
19 KiB
JavaScript
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") )
|
|
}})
|
|
}})
|
|
}})
|
|
}})
|