diff options
Diffstat (limited to 'testing/xpcshell/node-http2/test')
-rw-r--r-- | testing/xpcshell/node-http2/test/compressor.js | 575 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/connection.js | 237 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/endpoint.js | 41 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/flow.js | 260 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/framer.js | 395 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/http.js | 793 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/stream.js | 413 | ||||
-rw-r--r-- | testing/xpcshell/node-http2/test/util.js | 89 |
8 files changed, 2803 insertions, 0 deletions
diff --git a/testing/xpcshell/node-http2/test/compressor.js b/testing/xpcshell/node-http2/test/compressor.js new file mode 100644 index 0000000000..4588b8a3fd --- /dev/null +++ b/testing/xpcshell/node-http2/test/compressor.js @@ -0,0 +1,575 @@ +var expect = require('chai').expect; +var util = require('./util'); + +var compressor = require('../lib/protocol/compressor'); +var HeaderTable = compressor.HeaderTable; +var HuffmanTable = compressor.HuffmanTable; +var HeaderSetCompressor = compressor.HeaderSetCompressor; +var HeaderSetDecompressor = compressor.HeaderSetDecompressor; +var Compressor = compressor.Compressor; +var Decompressor = compressor.Decompressor; + +var test_integers = [{ + N: 5, + I: 10, + buffer: Buffer.from([10]) +}, { + N: 0, + I: 10, + buffer: Buffer.from([10]) +}, { + N: 5, + I: 1337, + buffer: Buffer.from([31, 128 + 26, 10]) +}, { + N: 0, + I: 1337, + buffer: Buffer.from([128 + 57, 10]) +}]; + +var test_strings = [{ + string: 'www.foo.com', + buffer: Buffer.from('89f1e3c2f29ceb90f4ff', 'hex') +}, { + string: 'éáűőúöüó€', + buffer: Buffer.from('13c3a9c3a1c5b1c591c3bac3b6c3bcc3b3e282ac', 'hex') +}]; + +test_huffman_request = { + 'GET': 'c5837f', + 'http': '9d29af', + '/': '63', + 'www.foo.com': 'f1e3c2f29ceb90f4ff', + 'https': '9d29ad1f', + 'www.bar.com': 'f1e3c2f18ec5c87a7f', + 'no-cache': 'a8eb10649cbf', + '/custom-path.css': '6096a127a56ac699d72211', + 'custom-key': '25a849e95ba97d7f', + 'custom-value': '25a849e95bb8e8b4bf' +}; + +test_huffman_response = { + '302': '6402', + 'private': 'aec3771a4b', + 'Mon, 21 OCt 2013 20:13:21 GMT': 'd07abe941054d5792a0801654102e059b820a98b46ff', + ': https://www.bar.com': 'b8a4e94d68b8c31e3c785e31d8b90f4f', + '200': '1001', + 'Mon, 21 OCt 2013 20:13:22 GMT': 'd07abe941054d5792a0801654102e059b821298b46ff', + 'https://www.bar.com': '9d29ad171863c78f0bc63b1721e9', + 'gzip': '9bd9ab', + 'foo=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ +AAAAAAAAAAAAAAAAAAAAAAAAAALASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKHQWOEIUAL\ +QWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKH\ +QWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEO\ +IUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOP\ +IUAXQWEOIUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234ZZZZZZZZZZ\ +ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ1234 m\ +ax-age=3600; version=1': '94e7821861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861873c3bafe5cd8f666bbfbf9ab672c1ab5e4e10fe6ce583564e10fe67cb9b1ece5ab064e10e7d9cb06ac9c21fccfb307087f33e7cd961dd7f672c1ab86487f34844cb59e1dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab27087f33e5cd8f672d583270873ece583564e10fe67d983843f99f3e6cb0eebfb3960d5c3243f9a42265acf0eebf97363d99aefefe6ad9cb06ad793843f9b3960d593843f99f2e6c7b396ac1938439f672c1ab27087f33ecc1c21fccf9f3658775fd9cb06ae1921fcd21132d678775fcb9b1eccd77f7f356ce58356bc9c21fcd9cb06ac9c21fccf97363d9cb560c9c21cfb3960d593843f99f660e10fe67cf9b2c3bafece583570c90fe6908996bf7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f42265a5291f9587316065c003ed4ee5b1063d5007f', + 'foo=ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ\ +ZZZZZZZZZZZZZZZZZZZZZZZZZZLASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKHQWOEIUAL\ +QWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKH\ +QWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEO\ +IUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOP\ +IUAXQWEOIUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234AAAAAAAAAA\ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234 m\ +ax-age=3600; version=1': '94e783f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f73c3bafe5cd8f666bbfbf9ab672c1ab5e4e10fe6ce583564e10fe67cb9b1ece5ab064e10e7d9cb06ac9c21fccfb307087f33e7cd961dd7f672c1ab86487f34844cb59e1dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab27087f33e5cd8f672d583270873ece583564e10fe67d983843f99f3e6cb0eebfb3960d5c3243f9a42265acf0eebf97363d99aefefe6ad9cb06ad793843f9b3960d593843f99f2e6c7b396ac1938439f672c1ab27087f33ecc1c21fccf9f3658775fd9cb06ae1921fcd21132d678775fcb9b1eccd77f7f356ce58356bc9c21fcd9cb06ac9c21fccf97363d9cb560c9c21cfb3960d593843f99f660e10fe67cf9b2c3bafece583570c90fe6908996a1861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861842265a5291f9587316065c003ed4ee5b1063d5007f' +}; + +var test_headers = [{ + // index + header: { + name: 1, + value: 1, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('82', 'hex') +}, { + // index + header: { + name: 5, + value: 5, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('86', 'hex') +}, { + // index + header: { + name: 3, + value: 3, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('84', 'hex') +}, { + // literal w/index, name index + header: { + name: 0, + value: 'www.foo.com', + index: true, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('41' + '89f1e3c2f29ceb90f4ff', 'hex') +}, { + // indexed + header: { + name: 1, + value: 1, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('82', 'hex') +}, { + // indexed + header: { + name: 6, + value: 6, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('87', 'hex') +}, { + // indexed + header: { + name: 3, + value: 3, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('84', 'hex') +}, { + // literal w/index, name index + header: { + name: 0, + value: 'www.bar.com', + index: true, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('41' + '89f1e3c2f18ec5c87a7f', 'hex') +}, { + // literal w/index, name index + header: { + name: 23, + value: 'no-cache', + index: true, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('58' + '86a8eb10649cbf', 'hex') +}, { + // index + header: { + name: 1, + value: 1, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('82', 'hex') +}, { + // index + header: { + name: 6, + value: 6, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('87', 'hex') +}, { + // literal w/index, name index + header: { + name: 3, + value: '/custom-path.css', + index: true, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('44' + '8b6096a127a56ac699d72211', 'hex') +}, { + // index + header: { + name: 63, + value: 63, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('C0', 'hex') +}, { + // literal w/index, new name & value + header: { + name: 'custom-key', + value: 'custom-value', + index: true, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('40' + '8825a849e95ba97d7f' + '8925a849e95bb8e8b4bf', 'hex') +}, { + // index + header: { + name: 1, + value: 1, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('82', 'hex') +}, { + // index + header: { + name: 6, + value: 6, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('87', 'hex') +}, { + // index + header: { + name: 62, + value: 62, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('BF', 'hex') +}, { + // index + header: { + name: 65, + value: 65, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('C2', 'hex') +}, { + // index + header: { + name: 64, + value: 64, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('C1', 'hex') +}, { + // index + header: { + name: 61, + value: 61, + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('BE', 'hex') +}, { + // Literal w/o index, name index + header: { + name: 6, + value: "whatever", + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('07' + '86f138d25ee5b3', 'hex') +}, { + // Literal w/o index, new name & value + header: { + name: "foo", + value: "bar", + index: false, + mustNeverIndex: false, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('00' + '8294e7' + '03626172', 'hex') +}, { + // Literal never indexed, name index + header: { + name: 6, + value: "whatever", + index: false, + mustNeverIndex: true, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('17' + '86f138d25ee5b3', 'hex') +}, { + // Literal never indexed, new name & value + header: { + name: "foo", + value: "bar", + index: false, + mustNeverIndex: true, + contextUpdate: false, + newMaxSize: 0 + }, + buffer: Buffer.from('10' + '8294e7' + '03626172', 'hex') +}, { + header: { + name: -1, + value: -1, + index: false, + mustNeverIndex: false, + contextUpdate: true, + newMaxSize: 100 + }, + buffer: Buffer.from('3F45', 'hex') +}]; + +var test_header_sets = [{ + headers: { + ':method': 'GET', + ':scheme': 'http', + ':path': '/', + ':authority': 'www.foo.com' + }, + buffer: util.concat(test_headers.slice(0, 4).map(function(test) { return test.buffer; })) +}, { + headers: { + ':method': 'GET', + ':scheme': 'https', + ':path': '/', + ':authority': 'www.bar.com', + 'cache-control': 'no-cache' + }, + buffer: util.concat(test_headers.slice(4, 9).map(function(test) { return test.buffer; })) +}, { + headers: { + ':method': 'GET', + ':scheme': 'https', + ':path': '/custom-path.css', + ':authority': 'www.bar.com', + 'custom-key': 'custom-value' + }, + buffer: util.concat(test_headers.slice(9, 14).map(function(test) { return test.buffer; })) +}, { + headers: { + ':method': 'GET', + ':scheme': 'https', + ':path': '/custom-path.css', + ':authority': ['www.foo.com', 'www.bar.com'], + 'custom-key': 'custom-value' + }, + buffer: util.concat(test_headers.slice(14, 19).map(function(test) { return test.buffer; })) +}]; + +describe('compressor.js', function() { + describe('HeaderTable', function() { + }); + + describe('HuffmanTable', function() { + describe('method encode(buffer)', function() { + it('should return the Huffman encoded version of the input buffer', function() { + var table = HuffmanTable.huffmanTable; + for (var decoded in test_huffman_request) { + var encoded = test_huffman_request[decoded]; + expect(table.encode(Buffer.from(decoded)).toString('hex')).to.equal(encoded); + } + table = HuffmanTable.huffmanTable; + for (decoded in test_huffman_response) { + encoded = test_huffman_response[decoded]; + expect(table.encode(Buffer.from(decoded)).toString('hex')).to.equal(encoded); + } + }); + }); + describe('method decode(buffer)', function() { + it('should return the Huffman decoded version of the input buffer', function() { + var table = HuffmanTable.huffmanTable; + for (var decoded in test_huffman_request) { + var encoded = test_huffman_request[decoded]; + expect(table.decode(Buffer.from(encoded, 'hex')).toString()).to.equal(decoded); + } + table = HuffmanTable.huffmanTable; + for (decoded in test_huffman_response) { + encoded = test_huffman_response[decoded]; + expect(table.decode(Buffer.from(encoded, 'hex')).toString()).to.equal(decoded); + } + }); + }); + }); + + describe('HeaderSetCompressor', function() { + describe('static method .integer(I, N)', function() { + it('should return an array of buffers that represent the N-prefix coded form of the integer I', function() { + for (var i = 0; i < test_integers.length; i++) { + var test = test_integers[i]; + test.buffer.cursor = 0; + expect(util.concat(HeaderSetCompressor.integer(test.I, test.N))).to.deep.equal(test.buffer); + } + }); + }); + describe('static method .string(string)', function() { + it('should return an array of buffers that represent the encoded form of the string', function() { + var table = HuffmanTable.huffmanTable; + for (var i = 0; i < test_strings.length; i++) { + var test = test_strings[i]; + expect(util.concat(HeaderSetCompressor.string(test.string, table))).to.deep.equal(test.buffer); + } + }); + }); + describe('static method .header({ name, value, index })', function() { + it('should return an array of buffers that represent the encoded form of the header', function() { + var table = HuffmanTable.huffmanTable; + for (var i = 0; i < test_headers.length; i++) { + var test = test_headers[i]; + expect(util.concat(HeaderSetCompressor.header(test.header, table))).to.deep.equal(test.buffer); + } + }); + }); + }); + + describe('HeaderSetDecompressor', function() { + describe('static method .integer(buffer, N)', function() { + it('should return the parsed N-prefix coded number and increase the cursor property of buffer', function() { + for (var i = 0; i < test_integers.length; i++) { + var test = test_integers[i]; + test.buffer.cursor = 0; + expect(HeaderSetDecompressor.integer(test.buffer, test.N)).to.equal(test.I); + expect(test.buffer.cursor).to.equal(test.buffer.length); + } + }); + }); + describe('static method .string(buffer)', function() { + it('should return the parsed string and increase the cursor property of buffer', function() { + var table = HuffmanTable.huffmanTable; + for (var i = 0; i < test_strings.length; i++) { + var test = test_strings[i]; + test.buffer.cursor = 0; + expect(HeaderSetDecompressor.string(test.buffer, table)).to.equal(test.string); + expect(test.buffer.cursor).to.equal(test.buffer.length); + } + }); + }); + describe('static method .header(buffer)', function() { + it('should return the parsed header and increase the cursor property of buffer', function() { + var table = HuffmanTable.huffmanTable; + for (var i = 0; i < test_headers.length; i++) { + var test = test_headers[i]; + test.buffer.cursor = 0; + expect(HeaderSetDecompressor.header(test.buffer, table)).to.deep.equal(test.header); + expect(test.buffer.cursor).to.equal(test.buffer.length); + } + }); + }); + }); + describe('Decompressor', function() { + describe('method decompress(buffer)', function() { + it('should return the parsed header set in { name1: value1, name2: [value2, value3], ... } format', function() { + var decompressor = new Decompressor(util.log, 'REQUEST'); + for (var i = 0; i < test_header_sets.length - 1; i++) { + var header_set = test_header_sets[i]; + expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers); + } + }); + }); + describe('transform stream', function() { + it('should emit an error event if a series of header frames is interleaved with other frames', function() { + var decompressor = new Decompressor(util.log, 'REQUEST'); + var error_occured = false; + decompressor.on('error', function() { + error_occured = true; + }); + decompressor.write({ + type: 'HEADERS', + flags: { + END_HEADERS: false + }, + data: Buffer.alloc(5) + }); + decompressor.write({ + type: 'DATA', + flags: {}, + data: Buffer.alloc(5) + }); + expect(error_occured).to.be.equal(true); + }); + }); + }); + + describe('invariant', function() { + describe('decompressor.decompress(compressor.compress(headerset)) === headerset', function() { + it('should be true for any header set if the states are synchronized', function() { + var compressor = new Compressor(util.log, 'REQUEST'); + var decompressor = new Decompressor(util.log, 'REQUEST'); + var n = test_header_sets.length; + for (var i = 0; i < 10; i++) { + var headers = test_header_sets[i%n].headers; + var compressed = compressor.compress(headers); + var decompressed = decompressor.decompress(compressed); + expect(decompressed).to.deep.equal(headers); + expect(compressor._table).to.deep.equal(decompressor._table); + } + }); + }); + describe('source.pipe(compressor).pipe(decompressor).pipe(destination)', function() { + it('should behave like source.pipe(destination) for a stream of frames', function(done) { + var compressor = new Compressor(util.log, 'RESPONSE'); + var decompressor = new Decompressor(util.log, 'RESPONSE'); + var n = test_header_sets.length; + compressor.pipe(decompressor); + for (var i = 0; i < 10; i++) { + compressor.write({ + type: i%2 ? 'HEADERS' : 'PUSH_PROMISE', + flags: {}, + headers: test_header_sets[i%n].headers + }); + } + setTimeout(function() { + for (var j = 0; j < 10; j++) { + expect(decompressor.read().headers).to.deep.equal(test_header_sets[j%n].headers); + } + done(); + }, 10); + }); + }); + describe('huffmanTable.decompress(huffmanTable.compress(buffer)) === buffer', function() { + it('should be true for any buffer', function() { + for (var i = 0; i < 10; i++) { + var buffer = []; + while (Math.random() > 0.1) { + buffer.push(Math.floor(Math.random() * 256)) + } + buffer = Buffer.from(buffer); + var table = HuffmanTable.huffmanTable; + var result = table.decode(table.encode(buffer)); + expect(result).to.deep.equal(buffer); + } + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/connection.js b/testing/xpcshell/node-http2/test/connection.js new file mode 100644 index 0000000000..21785d620a --- /dev/null +++ b/testing/xpcshell/node-http2/test/connection.js @@ -0,0 +1,237 @@ +var expect = require('chai').expect; +var util = require('./util'); + +var Connection = require('../lib/protocol/connection').Connection; + +var settings = { + SETTINGS_MAX_CONCURRENT_STREAMS: 100, + SETTINGS_INITIAL_WINDOW_SIZE: 100000 +}; + +var MAX_PRIORITY = Math.pow(2, 31) - 1; +var MAX_RANDOM_PRIORITY = 10; + +function randomPriority() { + return Math.floor(Math.random() * (MAX_RANDOM_PRIORITY + 1)); +} + +function expectPriorityOrder(priorities) { + priorities.forEach(function(bucket, priority) { + bucket.forEach(function(stream) { + expect(stream._priority).to.be.equal(priority); + }); + }); +} + +describe('connection.js', function() { + describe('Connection class', function() { + describe('method ._insert(stream)', function() { + it('should insert the stream in _streamPriorities in a place determined by stream._priority', function() { + var streams = []; + var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }}); + var streamCount = 10; + + for (var i = 0; i < streamCount; i++) { + var stream = { _priority: randomPriority() }; + connection._insert(stream, stream._priority); + expect(connection._streamPriorities[stream._priority]).to.include(stream); + } + + expectPriorityOrder(connection._streamPriorities); + }); + }); + describe('method ._reprioritize(stream)', function() { + it('should eject and then insert the stream in _streamPriorities in a place determined by stream._priority', function() { + var streams = []; + var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }}); + var streamCount = 10; + var oldPriority, newPriority, stream; + + for (var i = 0; i < streamCount; i++) { + oldPriority = randomPriority(); + while ((newPriority = randomPriority()) === oldPriority); + stream = { _priority: oldPriority }; + connection._insert(stream, oldPriority); + connection._reprioritize(stream, newPriority); + stream._priority = newPriority; + + expect(connection._streamPriorities[newPriority]).to.include(stream); + expect(connection._streamPriorities[oldPriority] || []).to.not.include(stream); + } + + expectPriorityOrder(streams); + }); + }); + describe('invalid operation', function() { + describe('unsolicited ping answer', function() { + it('should be ignored', function() { + var connection = new Connection(util.log, 1, settings); + + connection._receivePing({ + stream: 0, + type: 'PING', + flags: { + 'PONG': true + }, + data: Buffer.alloc(8) + }); + }); + }); + }); + }); + describe('test scenario', function() { + var c, s; + beforeEach(function() { + c = new Connection(util.log.child({ role: 'client' }), 1, settings); + s = new Connection(util.log.child({ role: 'client' }), 2, settings); + c.pipe(s).pipe(c); + }); + + describe('connection setup', function() { + it('should work as expected', function(done) { + setTimeout(function() { + // If there are no exception until this, then we're done + done(); + }, 10); + }); + }); + describe('sending/receiving a request', function() { + it('should work as expected', function(done) { + // Request and response data + var request_headers = { + ':method': 'GET', + ':path': '/' + }; + var request_data = Buffer.alloc(0); + var response_headers = { + ':status': '200' + }; + var response_data = Buffer.from('12345678', 'hex'); + + // Setting up server + s.on('stream', function(server_stream) { + server_stream.on('headers', function(headers) { + expect(headers).to.deep.equal(request_headers); + server_stream.headers(response_headers); + server_stream.end(response_data); + }); + }); + + // Sending request + var client_stream = c.createStream(); + client_stream.headers(request_headers); + client_stream.end(request_data); + + // Waiting for answer + done = util.callNTimes(2, done); + client_stream.on('headers', function(headers) { + expect(headers).to.deep.equal(response_headers); + done(); + }); + client_stream.on('data', function(data) { + expect(data).to.deep.equal(response_data); + done(); + }); + }); + }); + describe('server push', function() { + it('should work as expected', function(done) { + var request_headers = { ':method': 'get', ':path': '/' }; + var response_headers = { ':status': '200' }; + var push_request_headers = { ':method': 'get', ':path': '/x' }; + var push_response_headers = { ':status': '200' }; + var response_content = Buffer.alloc(10); + var push_content = Buffer.alloc(10); + + done = util.callNTimes(5, done); + + s.on('stream', function(response) { + response.headers(response_headers); + + var pushed = response.promise(push_request_headers); + pushed.headers(push_response_headers); + pushed.end(push_content); + + response.end(response_content); + }); + + var request = c.createStream(); + request.headers(request_headers); + request.end(); + request.on('headers', function(headers) { + expect(headers).to.deep.equal(response_headers); + done(); + }); + request.on('data', function(data) { + expect(data).to.deep.equal(response_content); + done(); + }); + request.on('promise', function(pushed, headers) { + expect(headers).to.deep.equal(push_request_headers); + pushed.on('headers', function(headers) { + expect(headers).to.deep.equal(response_headers); + done(); + }); + pushed.on('data', function(data) { + expect(data).to.deep.equal(push_content); + done(); + }); + pushed.on('end', done); + }); + }); + }); + describe('ping from client', function() { + it('should work as expected', function(done) { + c.ping(function() { + done(); + }); + }); + }); + describe('ping from server', function() { + it('should work as expected', function(done) { + s.ping(function() { + done(); + }); + }); + }); + describe('creating two streams and then using them in reverse order', function() { + it('should not result in non-monotonous local ID ordering', function() { + var s1 = c.createStream(); + var s2 = c.createStream(); + s2.headers({ ':method': 'get', ':path': '/' }); + s1.headers({ ':method': 'get', ':path': '/' }); + }); + }); + describe('creating two promises and then using them in reverse order', function() { + it('should not result in non-monotonous local ID ordering', function(done) { + s.on('stream', function(response) { + response.headers({ ':status': '200' }); + + var p1 = s.createStream(); + var p2 = s.createStream(); + response.promise(p2, { ':method': 'get', ':path': '/p2' }); + response.promise(p1, { ':method': 'get', ':path': '/p1' }); + p2.headers({ ':status': '200' }); + p1.headers({ ':status': '200' }); + }); + + var request = c.createStream(); + request.headers({ ':method': 'get', ':path': '/' }); + + done = util.callNTimes(2, done); + request.on('promise', function() { + done(); + }); + }); + }); + describe('closing the connection on one end', function() { + it('should result in closed streams on both ends', function(done) { + done = util.callNTimes(2, done); + c.on('end', done); + s.on('end', done); + + c.close(); + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/endpoint.js b/testing/xpcshell/node-http2/test/endpoint.js new file mode 100644 index 0000000000..bdd2569d42 --- /dev/null +++ b/testing/xpcshell/node-http2/test/endpoint.js @@ -0,0 +1,41 @@ +var expect = require('chai').expect; +var util = require('./util'); + +var endpoint = require('../lib/protocol/endpoint'); +var Endpoint = endpoint.Endpoint; + +var settings = { + SETTINGS_MAX_CONCURRENT_STREAMS: 100, + SETTINGS_INITIAL_WINDOW_SIZE: 100000 +}; + +describe('endpoint.js', function() { + describe('scenario', function() { + describe('connection setup', function() { + it('should work as expected', function(done) { + var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings); + var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings); + + util.log.debug('Test initialization over, starting piping.'); + c.pipe(s).pipe(c); + + setTimeout(function() { + // If there are no exception until this, then we're done + done(); + }, 10); + }); + }); + }); + describe('bunyan serializer', function() { + describe('`e`', function() { + var format = endpoint.serializers.e; + it('should assign a unique ID to each endpoint', function() { + var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings); + var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings); + expect(format(c)).to.not.equal(format(s)); + expect(format(c)).to.equal(format(c)); + expect(format(s)).to.equal(format(s)); + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/flow.js b/testing/xpcshell/node-http2/test/flow.js new file mode 100644 index 0000000000..a64ab010c9 --- /dev/null +++ b/testing/xpcshell/node-http2/test/flow.js @@ -0,0 +1,260 @@ +var expect = require('chai').expect; +var util = require('./util'); + +var Flow = require('../lib/protocol/flow').Flow; + +var MAX_PAYLOAD_SIZE = 16384; + +function createFlow(log) { + var flowControlId = util.random(10, 100); + var flow = new Flow(flowControlId); + flow._log = util.log.child(log || {}); + return flow; +} + +describe('flow.js', function() { + describe('Flow class', function() { + var flow; + beforeEach(function() { + flow = createFlow(); + }); + + describe('._receive(frame, callback) method', function() { + it('is called when there\'s a frame in the input buffer to be consumed', function(done) { + var frame = { type: 'PRIORITY', flags: {}, priority: 1 }; + flow._receive = function _receive(receivedFrame, callback) { + expect(receivedFrame).to.equal(frame); + callback(); + }; + flow.write(frame, done); + }); + it('has to be overridden by the child class, otherwise it throws', function() { + expect(flow._receive.bind(flow)).to.throw(Error); + }); + }); + describe('._send() method', function() { + it('is called when the output buffer should be filled with more frames and the flow' + + 'control queue is empty', function() { + var notFlowControlledFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; + flow._send = function _send() { + this.push(notFlowControlledFrame); + }; + expect(flow.read()).to.equal(notFlowControlledFrame); + + flow._window = 0; + flow._queue.push({ type: 'DATA', flags: {}, data: { length: 1 } }); + var frame = flow.read(); + while (frame.type === notFlowControlledFrame.type) frame = flow.read(); + expect(frame.type).to.equal('BLOCKED'); + expect(flow.read()).to.equal(null); + }); + it('has to be overridden by the child class, otherwise it throws', function() { + expect(flow._send.bind(flow)).to.throw(Error); + }); + }); + describe('._increaseWindow(size) method', function() { + it('should increase `this._window` by `size`', function() { + flow._send = util.noop; + flow._window = 0; + + var increase1 = util.random(0,100); + var increase2 = util.random(0,100); + flow._increaseWindow(increase1); + flow._increaseWindow(increase2); + expect(flow._window).to.equal(increase1 + increase2); + + flow._increaseWindow(Infinity); + expect(flow._window).to.equal(Infinity); + }); + it('should emit error when increasing with a finite `size` when `_window` is infinite', function() { + flow._send = util.noop; + flow._increaseWindow(Infinity); + var increase = util.random(1,100); + + expect(flow._increaseWindow.bind(flow, increase)).to.throw('Uncaught, unspecified "error" event.'); + }); + it('should emit error when `_window` grows over the window limit', function() { + var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1; + flow._send = util.noop; + flow._window = 0; + + flow._increaseWindow(WINDOW_SIZE_LIMIT); + expect(flow._increaseWindow.bind(flow, 1)).to.throw('Uncaught, unspecified "error" event.'); + + }); + }); + describe('.read() method', function() { + describe('when the flow control queue is not empty', function() { + it('should return the first item in the queue if the window is enough', function() { + var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; + var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } }; + flow._send = util.noop; + flow._window = 10; + flow._queue = [priorityFrame, dataFrame]; + + expect(flow.read()).to.equal(priorityFrame); + expect(flow.read()).to.equal(dataFrame); + }); + it('should also split DATA frames when needed', function() { + var buffer = Buffer.alloc(10); + var dataFrame = { type: 'DATA', flags: {}, stream: util.random(0, 100), data: buffer }; + flow._send = util.noop; + flow._window = 5; + flow._queue = [dataFrame]; + + var expectedFragment = { flags: {}, type: 'DATA', stream: dataFrame.stream, data: buffer.slice(0,5) }; + expect(flow.read()).to.deep.equal(expectedFragment); + expect(dataFrame.data).to.deep.equal(buffer.slice(5)); + }); + }); + }); + describe('.push(frame) method', function() { + it('should push `frame` into the output queue or the flow control queue', function() { + var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; + var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } }; + flow._window = 10; + + flow.push(dataFrame); // output queue + flow.push(dataFrame); // flow control queue, because of depleted window + flow.push(priorityFrame); // flow control queue, because it's not empty + + expect(flow.read()).to.be.equal(dataFrame); + expect(flow._queue[0]).to.be.equal(dataFrame); + expect(flow._queue[1]).to.be.equal(priorityFrame); + }); + }); + describe('.write() method', function() { + it('call with a DATA frame should trigger sending WINDOW_UPDATE if remote flow control is not' + + 'disabled', function(done) { + flow._window = 100; + flow._send = util.noop; + flow._receive = function(frame, callback) { + callback(); + }; + + var buffer = Buffer.alloc(util.random(10, 100)); + flow.write({ type: 'DATA', flags: {}, data: buffer }); + flow.once('readable', function() { + expect(flow.read()).to.be.deep.equal({ + type: 'WINDOW_UPDATE', + flags: {}, + stream: flow._flowControlId, + window_size: buffer.length + }); + done(); + }); + }); + }); + }); + describe('test scenario', function() { + var flow1, flow2; + beforeEach(function() { + flow1 = createFlow({ flow: 1 }); + flow2 = createFlow({ flow: 2 }); + flow1._flowControlId = flow2._flowControlId; + flow1._send = flow2._send = util.noop; + flow1._receive = flow2._receive = function(frame, callback) { callback(); }; + }); + + describe('sending a large data stream', function() { + it('should work as expected', function(done) { + // Sender side + var frameNumber = util.random(5, 8); + var input = []; + flow1._send = function _send() { + if (input.length >= frameNumber) { + this.push({ type: 'DATA', flags: { END_STREAM: true }, data: Buffer.alloc(0) }); + this.push(null); + } else { + var buffer = Buffer.allocUnsafe(util.random(1000, 100000)); + input.push(buffer); + this.push({ type: 'DATA', flags: {}, data: buffer }); + } + }; + + // Receiver side + var output = []; + flow2._receive = function _receive(frame, callback) { + if (frame.type === 'DATA') { + expect(frame.data.length).to.be.lte(MAX_PAYLOAD_SIZE); + output.push(frame.data); + } + if (frame.flags.END_STREAM) { + this.emit('end_stream'); + } + callback(); + }; + + // Checking results + flow2.on('end_stream', function() { + input = util.concat(input); + output = util.concat(output); + + expect(input).to.deep.equal(output); + + done(); + }); + + // Start piping + flow1.pipe(flow2).pipe(flow1); + }); + }); + + describe('when running out of window', function() { + it('should send a BLOCKED frame', function(done) { + // Sender side + var frameNumber = util.random(5, 8); + var input = []; + flow1._send = function _send() { + if (input.length >= frameNumber) { + this.push({ type: 'DATA', flags: { END_STREAM: true }, data: Buffer.alloc(0) }); + this.push(null); + } else { + var buffer = Buffer.allocUnsafe(util.random(1000, 100000)); + input.push(buffer); + this.push({ type: 'DATA', flags: {}, data: buffer }); + } + }; + + // Receiver side + // Do not send WINDOW_UPDATESs except when the other side sends BLOCKED + var output = []; + flow2._restoreWindow = util.noop; + flow2._receive = function _receive(frame, callback) { + if (frame.type === 'DATA') { + expect(frame.data.length).to.be.lte(MAX_PAYLOAD_SIZE); + output.push(frame.data); + } + if (frame.flags.END_STREAM) { + this.emit('end_stream'); + } + if (frame.type === 'BLOCKED') { + setTimeout(function() { + this._push({ + type: 'WINDOW_UPDATE', + flags: {}, + stream: this._flowControlId, + window_size: this._received + }); + this._received = 0; + }.bind(this), 20); + } + callback(); + }; + + // Checking results + flow2.on('end_stream', function() { + input = util.concat(input); + output = util.concat(output); + + expect(input).to.deep.equal(output); + + done(); + }); + + // Start piping + flow1.pipe(flow2).pipe(flow1); + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/framer.js b/testing/xpcshell/node-http2/test/framer.js new file mode 100644 index 0000000000..e2eb693665 --- /dev/null +++ b/testing/xpcshell/node-http2/test/framer.js @@ -0,0 +1,395 @@ +var expect = require('chai').expect; +var util = require('./util'); + +var framer = require('../lib/protocol/framer'); +var Serializer = framer.Serializer; +var Deserializer = framer.Deserializer; + +var frame_types = { + DATA: ['data'], + HEADERS: ['priority_information', 'data'], + PRIORITY: ['priority_information'], + RST_STREAM: ['error'], + SETTINGS: ['settings'], + PUSH_PROMISE: ['promised_stream', 'data'], + PING: ['data'], + GOAWAY: ['last_stream', 'error'], + WINDOW_UPDATE: ['window_size'], + CONTINUATION: ['data'], + ALTSVC: ['protocolID', 'host', 'port', 'origin', 'maxAge'] +}; + +var test_frames = [{ + frame: { + type: 'DATA', + flags: { END_STREAM: false, RESERVED2: false, RESERVED4: false, + PADDED: false }, + stream: 10, + + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + content + buffer: Buffer.from('000004' + '00' + '00' + '0000000A' + '12345678', 'hex') + +}, { + frame: { + type: 'HEADERS', + flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, + PADDED: false, RESERVED5: false, PRIORITY: false }, + stream: 15, + + data: Buffer.from('12345678', 'hex') + }, + buffer: Buffer.from('000004' + '01' + '00' + '0000000F' + '12345678', 'hex') + +}, { + frame: { + type: 'HEADERS', + flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, + PADDED: false, RESERVED5: false, PRIORITY: true }, + stream: 15, + priorityDependency: 10, + priorityWeight: 5, + exclusiveDependency: false, + + data: Buffer.from('12345678', 'hex') + }, + buffer: Buffer.from('000009' + '01' + '20' + '0000000F' + '0000000A' + '05' + '12345678', 'hex') + + +}, { + frame: { + type: 'HEADERS', + flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, + PADDED: false, RESERVED5: false, PRIORITY: true }, + stream: 15, + priorityDependency: 10, + priorityWeight: 5, + exclusiveDependency: true, + + data: Buffer.from('12345678', 'hex') + }, + buffer: Buffer.from('000009' + '01' + '20' + '0000000F' + '8000000A' + '05' + '12345678', 'hex') + +}, { + frame: { + type: 'PRIORITY', + flags: { }, + stream: 10, + + priorityDependency: 9, + priorityWeight: 5, + exclusiveDependency: false + }, + buffer: Buffer.from('000005' + '02' + '00' + '0000000A' + '00000009' + '05', 'hex') + +}, { + frame: { + type: 'PRIORITY', + flags: { }, + stream: 10, + + priorityDependency: 9, + priorityWeight: 5, + exclusiveDependency: true + }, + buffer: Buffer.from('000005' + '02' + '00' + '0000000A' + '80000009' + '05', 'hex') + +}, { + frame: { + type: 'RST_STREAM', + flags: { }, + stream: 10, + + error: 'INTERNAL_ERROR' + }, + buffer: Buffer.from('000004' + '03' + '00' + '0000000A' + '00000002', 'hex') + +}, { + frame: { + type: 'SETTINGS', + flags: { ACK: false }, + stream: 10, + + settings: { + SETTINGS_HEADER_TABLE_SIZE: 0x12345678, + SETTINGS_ENABLE_PUSH: true, + SETTINGS_MAX_CONCURRENT_STREAMS: 0x01234567, + SETTINGS_INITIAL_WINDOW_SIZE: 0x89ABCDEF, + SETTINGS_MAX_FRAME_SIZE: 0x00010000 + } + }, + buffer: Buffer.from('00001E' + '04' + '00' + '0000000A' + '0001' + '12345678' + + '0002' + '00000001' + + '0003' + '01234567' + + '0004' + '89ABCDEF' + + '0005' + '00010000', 'hex') + +}, { + frame: { + type: 'PUSH_PROMISE', + flags: { RESERVED1: false, RESERVED2: false, END_PUSH_PROMISE: false, + PADDED: false }, + stream: 15, + + promised_stream: 3, + data: Buffer.from('12345678', 'hex') + }, + buffer: Buffer.from('000008' + '05' + '00' + '0000000F' + '00000003' + '12345678', 'hex') + +}, { + frame: { + type: 'PING', + flags: { ACK: false }, + stream: 15, + + data: Buffer.from('1234567887654321', 'hex') + }, + buffer: Buffer.from('000008' + '06' + '00' + '0000000F' + '1234567887654321', 'hex') + +}, { + frame: { + type: 'GOAWAY', + flags: { }, + stream: 10, + + last_stream: 0x12345678, + error: 'PROTOCOL_ERROR' + }, + buffer: Buffer.from('000008' + '07' + '00' + '0000000A' + '12345678' + '00000001', 'hex') + +}, { + frame: { + type: 'WINDOW_UPDATE', + flags: { }, + stream: 10, + + window_size: 0x12345678 + }, + buffer: Buffer.from('000004' + '08' + '00' + '0000000A' + '12345678', 'hex') +}, { + frame: { + type: 'CONTINUATION', + flags: { RESERVED1: false, RESERVED2: false, END_HEADERS: true }, + stream: 10, + + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + content + buffer: Buffer.from('000004' + '09' + '04' + '0000000A' + '12345678', 'hex') +}, { + frame: { + type: 'ALTSVC', + flags: { }, + stream: 0, + + maxAge: 31536000, + port: 4443, + protocolID: "h2", + host: "altsvc.example.com", + origin: "" + }, + buffer: Buffer.from(Buffer.from('00002B' + '0A' + '00' + '00000000' + '0000', 'hex') + Buffer.from('h2="altsvc.example.com:4443"; ma=31536000', 'ascii')) +}, { + frame: { + type: 'ALTSVC', + flags: { }, + stream: 0, + + maxAge: 31536000, + port: 4443, + protocolID: "h2", + host: "altsvc.example.com", + origin: "https://onlyme.example.com" + }, + buffer: Buffer.from(Buffer.from('000045' + '0A' + '00' + '00000000' + '001A', 'hex') + Buffer.from('https://onlyme.example.comh2="altsvc.example.com:4443"; ma=31536000', 'ascii')) + +}, { + frame: { + type: 'BLOCKED', + flags: { }, + stream: 10 + }, + buffer: Buffer.from('000000' + '0B' + '00' + '0000000A', 'hex') +}]; + +var deserializer_test_frames = test_frames.slice(0); +var padded_test_frames = [{ + frame: { + type: 'DATA', + flags: { END_STREAM: false, RESERVED2: false, RESERVED4: false, + PADDED: true }, + stream: 10, + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + pad length + content + padding + buffer: Buffer.from('00000B' + '00' + '08' + '0000000A' + '06' + '12345678' + '000000000000', 'hex') + +}, { + frame: { + type: 'HEADERS', + flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, + PADDED: true, RESERVED5: false, PRIORITY: false }, + stream: 15, + + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + pad length + data + padding + buffer: Buffer.from('00000B' + '01' + '08' + '0000000F' + '06' + '12345678' + '000000000000', 'hex') + +}, { + frame: { + type: 'HEADERS', + flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, + PADDED: true, RESERVED5: false, PRIORITY: true }, + stream: 15, + priorityDependency: 10, + priorityWeight: 5, + exclusiveDependency: false, + + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + pad length + priority dependency + priority weight + data + padding + buffer: Buffer.from('000010' + '01' + '28' + '0000000F' + '06' + '0000000A' + '05' + '12345678' + '000000000000', 'hex') + +}, { + frame: { + type: 'HEADERS', + flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, + PADDED: true, RESERVED5: false, PRIORITY: true }, + stream: 15, + priorityDependency: 10, + priorityWeight: 5, + exclusiveDependency: true, + + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + pad length + priority dependency + priority weight + data + padding + buffer: Buffer.from('000010' + '01' + '28' + '0000000F' + '06' + '8000000A' + '05' + '12345678' + '000000000000', 'hex') + +}, { + frame: { + type: 'PUSH_PROMISE', + flags: { RESERVED1: false, RESERVED2: false, END_PUSH_PROMISE: false, + PADDED: true }, + stream: 15, + + promised_stream: 3, + data: Buffer.from('12345678', 'hex') + }, + // length + type + flags + stream + pad length + promised stream + data + padding + buffer: Buffer.from('00000F' + '05' + '08' + '0000000F' + '06' + '00000003' + '12345678' + '000000000000', 'hex') + +}]; +for (var idx = 0; idx < padded_test_frames.length; idx++) { + deserializer_test_frames.push(padded_test_frames[idx]); +} + + +describe('framer.js', function() { + describe('Serializer', function() { + describe('static method .commonHeader({ type, flags, stream }, buffer_array)', function() { + it('should add the appropriate 9 byte header buffer in front of the others', function() { + for (var i = 0; i < test_frames.length; i++) { + var test = test_frames[i]; + var buffers = [test.buffer.slice(9)]; + var header_buffer = test.buffer.slice(0,9); + Serializer.commonHeader(test.frame, buffers); + expect(buffers[0]).to.deep.equal(header_buffer); + } + }); + }); + + Object.keys(frame_types).forEach(function(type) { + var tests = test_frames.filter(function(test) { return test.frame.type === type; }); + var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; + describe('static method .' + type + '(' + frame_shape + ', buffer_array)', function() { + it('should push buffers to the array that make up a ' + type + ' type payload', function() { + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + var buffers = []; + Serializer[type](test.frame, buffers); + expect(util.concat(buffers)).to.deep.equal(test.buffer.slice(9)); + } + }); + }); + }); + + describe('transform stream', function() { + it('should transform frame objects to appropriate buffers', function() { + var stream = new Serializer(util.log); + + for (var i = 0; i < test_frames.length; i++) { + var test = test_frames[i]; + stream.write(test.frame); + var chunk, buffer = Buffer.alloc(0); + while (chunk = stream.read()) { + buffer = util.concat([buffer, chunk]); + } + expect(buffer).to.be.deep.equal(test.buffer); + } + }); + }); + }); + + describe('Deserializer', function() { + describe('static method .commonHeader(header_buffer, frame)', function() { + it('should augment the frame object with these properties: { type, flags, stream })', function() { + for (var i = 0; i < deserializer_test_frames.length; i++) { + var test = deserializer_test_frames[i], frame = {}; + Deserializer.commonHeader(test.buffer.slice(0,9), frame); + expect(frame).to.deep.equal({ + type: test.frame.type, + flags: test.frame.flags, + stream: test.frame.stream + }); + } + }); + }); + + Object.keys(frame_types).forEach(function(type) { + var tests = deserializer_test_frames.filter(function(test) { return test.frame.type === type; }); + var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; + describe('static method .' + type + '(payload_buffer, frame)', function() { + it('should augment the frame object with these properties: ' + frame_shape, function() { + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + var frame = { + type: test.frame.type, + flags: test.frame.flags, + stream: test.frame.stream + }; + Deserializer[type](test.buffer.slice(9), frame); + expect(frame).to.deep.equal(test.frame); + } + }); + }); + }); + + describe('transform stream', function() { + it('should transform buffers to appropriate frame object', function() { + var stream = new Deserializer(util.log); + + var shuffled = util.shuffleBuffers(deserializer_test_frames.map(function(test) { return test.buffer; })); + shuffled.forEach(stream.write.bind(stream)); + + for (var j = 0; j < deserializer_test_frames.length; j++) { + expect(stream.read()).to.be.deep.equal(deserializer_test_frames[j].frame); + } + }); + }); + }); + + describe('bunyan formatter', function() { + describe('`frame`', function() { + var format = framer.serializers.frame; + it('should assign a unique ID to each frame', function() { + var frame1 = { type: 'DATA', data: Buffer.alloc(10) }; + var frame2 = { type: 'PRIORITY', priority: 1 }; + expect(format(frame1).id).to.be.equal(format(frame1)); + expect(format(frame2).id).to.be.equal(format(frame2)); + expect(format(frame1)).to.not.be.equal(format(frame2)); + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/http.js b/testing/xpcshell/node-http2/test/http.js new file mode 100644 index 0000000000..95a074e4a0 --- /dev/null +++ b/testing/xpcshell/node-http2/test/http.js @@ -0,0 +1,793 @@ +var expect = require('chai').expect; +var util = require('./util'); +var fs = require('fs'); +var path = require('path'); +var url = require('url'); +var net = require('net'); + +var http2 = require('../lib/http'); +var https = require('https'); + +var serverOptions = { + key: fs.readFileSync(path.join(__dirname, '../example/localhost.key')), + cert: fs.readFileSync(path.join(__dirname, '../example/localhost.crt')), + rejectUnauthorized: true, + log: util.serverLog +}; + +var agentOptions = { + key: serverOptions.key, + ca: serverOptions.cert, + rejectUnauthorized: true, + log: util.clientLog +}; + +var globalAgent = new http2.Agent(agentOptions); + +describe('http.js', function() { + beforeEach(function() { + http2.globalAgent = globalAgent; + }); + describe('Server', function() { + describe('new Server(options)', function() { + it('should throw if called without \'plain\' or TLS options', function() { + expect(function() { + new http2.Server(); + }).to.throw(Error); + expect(function() { + http2.createServer(util.noop); + }).to.throw(Error); + }); + }); + describe('method `listen()`', function () { + it('should emit `listening` event', function (done) { + var server = http2.createServer(serverOptions); + + server.on('listening', function () { + server.close(); + + done(); + }) + + server.listen(0); + }); + it('should emit `error` on failure', function (done) { + var server = http2.createServer(serverOptions); + + // This TCP server is used to explicitly take a port to make + // server.listen() fails. + var net = require('net').createServer(); + + server.on('error', function () { + net.close() + + done(); + }); + + net.listen(0, function () { + server.listen(this.address().port); + }); + }); + }); + describe('property `timeout`', function() { + it('should be a proxy for the backing HTTPS server\'s `timeout` property', function() { + var server = new http2.Server(serverOptions); + var backingServer = server._server; + var newTimeout = 10; + server.timeout = newTimeout; + expect(server.timeout).to.be.equal(newTimeout); + expect(backingServer.timeout).to.be.equal(newTimeout); + }); + }); + describe('method `setTimeout(timeout, [callback])`', function() { + it('should be a proxy for the backing HTTPS server\'s `setTimeout` method', function() { + var server = new http2.Server(serverOptions); + var backingServer = server._server; + var newTimeout = 10; + var newCallback = util.noop; + backingServer.setTimeout = function(timeout, callback) { + expect(timeout).to.be.equal(newTimeout); + expect(callback).to.be.equal(newCallback); + }; + server.setTimeout(newTimeout, newCallback); + }); + }); + }); + describe('Agent', function() { + describe('property `maxSockets`', function() { + it('should be a proxy for the backing HTTPS agent\'s `maxSockets` property', function() { + var agent = new http2.Agent({ log: util.clientLog }); + var backingAgent = agent._httpsAgent; + var newMaxSockets = backingAgent.maxSockets + 1; + agent.maxSockets = newMaxSockets; + expect(agent.maxSockets).to.be.equal(newMaxSockets); + expect(backingAgent.maxSockets).to.be.equal(newMaxSockets); + }); + }); + describe('method `request(options, [callback])`', function() { + it('should use a new agent for request-specific TLS settings', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1234, function() { + var options = url.parse('https://localhost:1234' + path); + options.key = agentOptions.key; + options.ca = agentOptions.ca; + options.rejectUnauthorized = true; + + http2.globalAgent = new http2.Agent({ log: util.clientLog }); + http2.get(options, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + }); + }); + it('should throw when trying to use with \'http\' scheme', function() { + expect(function() { + var agent = new http2.Agent({ log: util.clientLog }); + agent.request({ protocol: 'http:' }); + }).to.throw(Error); + }); + }); + }); + describe('OutgoingRequest', function() { + function testFallbackProxyMethod(name, originalArguments, done) { + var request = new http2.OutgoingRequest(); + + // When in HTTP/2 mode, this call should be ignored + request.stream = { reset: util.noop }; + request[name].apply(request, originalArguments); + delete request.stream; + + // When in fallback mode, this call should be forwarded + request[name].apply(request, originalArguments); + var mockFallbackRequest = { on: util.noop }; + mockFallbackRequest[name] = function() { + expect(Array.prototype.slice.call(arguments)).to.deep.equal(originalArguments); + done(); + }; + request._fallback(mockFallbackRequest); + } + describe('method `setNoDelay(noDelay)`', function() { + it('should act as a proxy for the backing HTTPS agent\'s `setNoDelay` method', function(done) { + testFallbackProxyMethod('setNoDelay', [true], done); + }); + }); + describe('method `setSocketKeepAlive(enable, initialDelay)`', function() { + it('should act as a proxy for the backing HTTPS agent\'s `setSocketKeepAlive` method', function(done) { + testFallbackProxyMethod('setSocketKeepAlive', [true, util.random(10, 100)], done); + }); + }); + describe('method `setTimeout(timeout, [callback])`', function() { + it('should act as a proxy for the backing HTTPS agent\'s `setTimeout` method', function(done) { + testFallbackProxyMethod('setTimeout', [util.random(10, 100), util.noop], done); + }); + }); + describe('method `abort()`', function() { + it('should act as a proxy for the backing HTTPS agent\'s `abort` method', function(done) { + testFallbackProxyMethod('abort', [], done); + }); + }); + }); + describe('OutgoingResponse', function() { + it('should throw error when writeHead is called multiple times on it', function() { + var called = false; + var stream = { _log: util.log, headers: function () { + if (called) { + throw new Error('Should not send headers twice'); + } else { + called = true; + } + }, once: util.noop }; + var response = new http2.OutgoingResponse(stream); + + response.writeHead(200); + response.writeHead(404); + }); + it('field finished should be Boolean', function(){ + var stream = { _log: util.log, headers: function () {}, once: util.noop }; + var response = new http2.OutgoingResponse(stream); + expect(response.finished).to.be.a('Boolean'); + }); + it('field finished should initially be false and then go to true when response completes',function(done){ + var res; + var server = http2.createServer(serverOptions, function(request, response) { + res = response; + expect(res.finished).to.be.false; + response.end('HiThere'); + }); + server.listen(1236, function() { + http2.get('https://localhost:1236/finished-test', function(response) { + response.on('data', function(data){ + var sink = data; // + }); + response.on('end',function(){ + expect(res.finished).to.be.true; + server.close(); + done(); + }); + }); + }); + }); + }); + describe('test scenario', function() { + describe('simple request', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1234, function() { + http2.get('https://localhost:1234' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + }); + }); + }); + describe('2 simple request in parallel', function() { + it('should work as expected', function(originalDone) { + var path = '/x'; + var message = 'Hello world'; + var done = util.callNTimes(2, function() { + server.close(); + originalDone(); + }); + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1234, function() { + http2.get('https://localhost:1234' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + http2.get('https://localhost:1234' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + }); + }); + }); + describe('100 simple request in a series', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + var n = 100; + server.listen(1242, function() { + doRequest(); + function doRequest() { + http2.get('https://localhost:1242' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + if (n) { + n -= 1; + doRequest(); + } else { + server.close(); + done(); + } + }); + }); + } + }); + }); + }); + describe('request with payload', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + request.once('data', function(data) { + expect(data.toString()).to.equal(message); + response.end(); + }); + }); + + server.listen(1240, function() { + var request = http2.request({ + host: 'localhost', + port: 1240, + path: path + }); + request.write(message); + request.end(); + request.on('response', function() { + server.close(); + done(); + }); + }); + }); + }); + describe('request with custom status code and headers', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + var headerName = 'name'; + var headerValue = 'value'; + + var server = http2.createServer(serverOptions, function(request, response) { + // Request URL and headers + expect(request.url).to.equal(path); + expect(request.headers[headerName]).to.equal(headerValue); + + // A header to be overwritten later + response.setHeader(headerName, 'to be overwritten'); + expect(response.getHeader(headerName)).to.equal('to be overwritten'); + + // A header to be deleted + response.setHeader('nonexistent', 'x'); + response.removeHeader('nonexistent'); + expect(response.getHeader('nonexistent')).to.equal(undefined); + + // A set-cookie header which should always be an array + response.setHeader('set-cookie', 'foo'); + + // Don't send date + response.sendDate = false; + + // Specifying more headers, the status code and a reason phrase with `writeHead` + var moreHeaders = {}; + moreHeaders[headerName] = headerValue; + response.writeHead(600, 'to be discarded', moreHeaders); + expect(response.getHeader(headerName)).to.equal(headerValue); + + // Empty response body + response.end(message); + }); + + server.listen(1239, function() { + var headers = {}; + headers[headerName] = headerValue; + var request = http2.request({ + host: 'localhost', + port: 1239, + path: path, + headers: headers + }); + request.end(); + request.on('response', function(response) { + expect(response.headers[headerName]).to.equal(headerValue); + expect(response.headers['nonexistent']).to.equal(undefined); + expect(response.headers['set-cookie']).to.an.instanceof(Array) + expect(response.headers['set-cookie']).to.deep.equal(['foo']) + expect(response.headers['date']).to.equal(undefined); + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + }); + }); + }); + describe('request over plain TCP', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.raw.createServer({ + log: util.serverLog + }, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1237, function() { + var request = http2.raw.request({ + plain: true, + host: 'localhost', + port: 1237, + path: path + }, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + request.end(); + }); + }); + }); + describe('get over plain TCP', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.raw.createServer({ + log: util.serverLog + }, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1237, function() { + var request = http2.raw.get('http://localhost:1237/x', function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + server.close(); + done(); + }); + }); + request.end(); + }); + }); + }); + describe('request to an HTTPS/1 server', function() { + it('should fall back to HTTPS/1 successfully', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = https.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(5678, function() { + http2.get('https://localhost:5678' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + }); + }); + }); + describe('2 parallel request to an HTTPS/1 server', function() { + it('should fall back to HTTPS/1 successfully', function(originalDone) { + var path = '/x'; + var message = 'Hello world'; + var done = util.callNTimes(2, function() { + server.close(); + originalDone(); + }); + + var server = https.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(6789, function() { + http2.get('https://localhost:6789' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + http2.get('https://localhost:6789' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + }); + }); + }); + describe('HTTPS/1 request to a HTTP/2 server', function() { + it('should fall back to HTTPS/1 successfully', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1236, function() { + var options = url.parse('https://localhost:1236' + path); + options.agent = new https.Agent(agentOptions); + https.get(options, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + }); + }); + }); + describe('two parallel request', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1237, function() { + done = util.callNTimes(2, done); + // 1. request + http2.get('https://localhost:1237' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + // 2. request + http2.get('https://localhost:1237' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + }); + }); + }); + describe('two subsequent request', function() { + it('should use the same HTTP/2 connection', function(done) { + var path = '/x'; + var message = 'Hello world'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + response.end(message); + }); + + server.listen(1238, function() { + // 1. request + http2.get('https://localhost:1238' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + + // 2. request + http2.get('https://localhost:1238' + path, function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + }); + }); + }); + }); + }); + }); + describe('https server node module specification conformance', function() { + it('should provide API for remote HTTP 1.1 client address', function(done) { + var remoteAddress = null; + var remotePort = null; + + var server = http2.createServer(serverOptions, function(request, response) { + // HTTPS 1.1 client with Node 0.10 server + if (!request.remoteAddress) { + if (request.socket.socket) { + remoteAddress = request.socket.socket.remoteAddress; + remotePort = request.socket.socket.remotePort; + } else { + remoteAddress = request.socket.remoteAddress; + remotePort = request.socket.remotePort; + } + } else { + // HTTPS 1.1/2.0 client with Node 0.12 server + remoteAddress = request.remoteAddress; + remotePort = request.remotePort; + } + response.write('Pong'); + response.end(); + }); + + server.listen(1259, 'localhost', function() { + var request = https.request({ + host: 'localhost', + port: 1259, + path: '/', + ca: serverOptions.cert + }); + request.write('Ping'); + request.end(); + request.on('response', function(response) { + response.on('data', function(data) { + var localAddress = response.socket.address(); + expect(remoteAddress).to.equal(localAddress.address); + expect(remotePort).to.equal(localAddress.port); + server.close(); + done(); + }); + }); + }); + }); + it('should provide API for remote HTTP 2.0 client address', function(done) { + var remoteAddress = null; + var remotePort = null; + var localAddress = null; + + var server = http2.createServer(serverOptions, function(request, response) { + remoteAddress = request.remoteAddress; + remotePort = request.remotePort; + response.write('Pong'); + response.end(); + }); + + server.listen(1258, 'localhost', function() { + var request = http2.request({ + host: 'localhost', + port: 1258, + path: '/' + }); + request.write('Ping'); + globalAgent.on('false:localhost:1258', function(endpoint) { + localAddress = endpoint.socket.address(); + }); + request.end(); + request.on('response', function(response) { + response.on('data', function(data) { + expect(remoteAddress).to.equal(localAddress.address); + expect(remotePort).to.equal(localAddress.port); + server.close(); + done(); + }); + }); + }); + }); + it('should expose net.Socket as .socket and .connection', function(done) { + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.socket).to.equal(request.connection); + expect(request.socket).to.be.instanceof(net.Socket); + response.write('Pong'); + response.end(); + done(); + }); + + server.listen(1248, 'localhost', function() { + var request = https.request({ + host: 'localhost', + port: 1248, + path: '/', + ca: serverOptions.cert + }); + request.write('Ping'); + request.end(); + }); + }); + }); + describe('request and response with trailers', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + var requestTrailers = { 'content-md5': 'x' }; + var responseTrailers = { 'content-md5': 'y' }; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + request.on('data', util.noop); + request.once('end', function() { + expect(request.trailers).to.deep.equal(requestTrailers); + response.write(message); + response.addTrailers(responseTrailers); + response.end(); + }); + }); + + server.listen(1241, function() { + var request = http2.request('https://localhost:1241' + path); + request.addTrailers(requestTrailers); + request.end(); + request.on('response', function(response) { + response.on('data', util.noop); + response.once('end', function() { + expect(response.trailers).to.deep.equal(responseTrailers); + done(); + }); + }); + }); + }); + }); + describe('Handle socket error', function () { + it('HTTPS on Connection Refused error', function (done) { + var path = '/x'; + var request = http2.request('https://127.0.0.1:6666' + path); + + request.on('error', function (err) { + expect(err.errno).to.equal('ECONNREFUSED'); + done(); + }); + + request.on('response', function (response) { + server._server._handle.destroy(); + + response.on('data', util.noop); + + response.once('end', function () { + done(new Error('Request should have failed')); + }); + }); + + request.end(); + + }); + it('HTTP on Connection Refused error', function (done) { + var path = '/x'; + + var request = http2.raw.request('http://127.0.0.1:6666' + path); + + request.on('error', function (err) { + expect(err.errno).to.equal('ECONNREFUSED'); + done(); + }); + + request.on('response', function (response) { + server._server._handle.destroy(); + + response.on('data', util.noop); + + response.once('end', function () { + done(new Error('Request should have failed')); + }); + }); + + request.end(); + }); + }); + describe('server push', function() { + it('should work as expected', function(done) { + var path = '/x'; + var message = 'Hello world'; + var pushedPath = '/y'; + var pushedMessage = 'Hello world 2'; + + var server = http2.createServer(serverOptions, function(request, response) { + expect(request.url).to.equal(path); + var push1 = response.push('/y'); + push1.end(pushedMessage); + var push2 = response.push({ path: '/y', protocol: 'https:' }); + push2.end(pushedMessage); + response.end(message); + }); + + server.listen(1235, function() { + var request = http2.get('https://localhost:1235' + path); + done = util.callNTimes(5, done); + + request.on('response', function(response) { + response.on('data', function(data) { + expect(data.toString()).to.equal(message); + done(); + }); + response.on('end', done); + }); + + request.on('push', function(promise) { + expect(promise.url).to.be.equal(pushedPath); + promise.on('response', function(pushStream) { + pushStream.on('data', function(data) { + expect(data.toString()).to.equal(pushedMessage); + done(); + }); + pushStream.on('end', done); + }); + }); + }); + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/stream.js b/testing/xpcshell/node-http2/test/stream.js new file mode 100644 index 0000000000..9e60932b8e --- /dev/null +++ b/testing/xpcshell/node-http2/test/stream.js @@ -0,0 +1,413 @@ +var expect = require('chai').expect; +var util = require('./util'); + +var stream = require('../lib/protocol/stream'); +var Stream = stream.Stream; + +function createStream() { + var stream = new Stream(util.log, null); + stream.upstream._window = Infinity; + return stream; +} + +// Execute a list of commands and assertions +var recorded_events = ['state', 'error', 'window_update', 'headers', 'promise']; +function execute_sequence(stream, sequence, done) { + if (!done) { + done = sequence; + sequence = stream; + stream = createStream(); + } + + var outgoing_frames = []; + + var emit = stream.emit, events = []; + stream.emit = function(name) { + if (recorded_events.indexOf(name) !== -1) { + events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) }); + } + return emit.apply(this, arguments); + }; + + var commands = [], checks = []; + sequence.forEach(function(step) { + if ('method' in step || 'incoming' in step || 'outgoing' in step || 'wait' in step || 'set_state' in step) { + commands.push(step); + } + + if ('outgoing' in step || 'event' in step || 'active' in step) { + checks.push(step); + } + }); + + var activeCount = 0; + function count_change(change) { + activeCount += change; + } + + function execute(callback) { + var command = commands.shift(); + if (command) { + if ('method' in command) { + var value = stream[command.method.name].apply(stream, command.method.arguments); + if (command.method.ret) { + command.method.ret(value); + } + execute(callback); + } else if ('incoming' in command) { + command.incoming.count_change = count_change; + stream.upstream.write(command.incoming); + execute(callback); + } else if ('outgoing' in command) { + outgoing_frames.push(stream.upstream.read()); + execute(callback); + } else if ('set_state' in command) { + stream.state = command.set_state; + execute(callback); + } else if ('wait' in command) { + setTimeout(execute.bind(null, callback), command.wait); + } else { + throw new Error('Invalid command', command); + } + } else { + setTimeout(callback, 5); + } + } + + function check() { + checks.forEach(function(check) { + if ('outgoing' in check) { + var frame = outgoing_frames.shift(); + for (var key in check.outgoing) { + expect(frame).to.have.property(key).that.deep.equals(check.outgoing[key]); + } + count_change(frame.count_change); + } else if ('event' in check) { + var event = events.shift(); + expect(event.name).to.be.equal(check.event.name); + check.event.data.forEach(function(data, index) { + expect(event.data[index]).to.deep.equal(data); + }); + } else if ('active' in check) { + expect(activeCount).to.be.equal(check.active); + } else { + throw new Error('Invalid check', check); + } + }); + done(); + } + + setImmediate(execute.bind(null, check)); +} + +var example_frames = [ + { type: 'PRIORITY', flags: {}, priority: 1 }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, + { type: 'RST_STREAM', flags: {}, error: 'CANCEL' }, + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log, null) } +]; + +var invalid_incoming_frames = { + IDLE: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, + { type: 'RST_STREAM', flags: {}, error: 'CANCEL' } + ], + RESERVED_LOCAL: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } + ], + RESERVED_REMOTE: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } + ], + OPEN: [ + ], + HALF_CLOSED_LOCAL: [ + ], + HALF_CLOSED_REMOTE: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} } + ] +}; + +var invalid_outgoing_frames = { + IDLE: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} } + ], + RESERVED_LOCAL: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } + ], + RESERVED_REMOTE: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} }, + { type: 'WINDOW_UPDATE', flags: {}, settings: {} } + ], + OPEN: [ + ], + HALF_CLOSED_LOCAL: [ + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, + { type: 'PUSH_PROMISE', flags: {}, headers: {} } + ], + HALF_CLOSED_REMOTE: [ + ], + CLOSED: [ + { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, + { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, + { type: 'DATA', flags: {}, data: Buffer.alloc(5) }, + { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log, null) } + ] +}; + +describe('stream.js', function() { + describe('Stream class', function() { + describe('._transition(sending, frame) method', function() { + it('should emit error, and answer RST_STREAM for invalid incoming frames', function() { + Object.keys(invalid_incoming_frames).forEach(function(state) { + invalid_incoming_frames[state].forEach(function(invalid_frame) { + var stream = createStream(); + var connectionErrorHappened = false; + stream.state = state; + stream.once('connectionError', function() { connectionErrorHappened = true; }); + stream._transition(false, invalid_frame); + expect(connectionErrorHappened); + }); + }); + + // CLOSED state as a result of incoming END_STREAM (or RST_STREAM) + var stream = createStream(); + stream.headers({}); + stream.end(); + stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); + example_frames.slice(2).forEach(function(invalid_frame) { + invalid_frame.count_change = util.noop; + expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); + }); + + // CLOSED state as a result of outgoing END_STREAM + stream = createStream(); + stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); + stream.headers({}); + stream.end(); + example_frames.slice(3).forEach(function(invalid_frame) { + invalid_frame.count_change = util.noop; + expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); + }); + }); + it('should throw exception for invalid outgoing frames', function() { + Object.keys(invalid_outgoing_frames).forEach(function(state) { + invalid_outgoing_frames[state].forEach(function(invalid_frame) { + var stream = createStream(); + stream.state = state; + expect(stream._transition.bind(stream, true, invalid_frame)).to.throw(Error); + }); + }); + }); + it('should close the stream when there\'s an incoming or outgoing RST_STREAM', function() { + [ + 'RESERVED_LOCAL', + 'RESERVED_REMOTE', + 'OPEN', + 'HALF_CLOSED_LOCAL', + 'HALF_CLOSED_REMOTE' + ].forEach(function(state) { + [true, false].forEach(function(sending) { + var stream = createStream(); + stream.state = state; + stream._transition(sending, { type: 'RST_STREAM', flags: {} }); + expect(stream.state).to.be.equal('CLOSED'); + }); + }); + }); + it('should ignore any incoming frame after sending reset', function() { + var stream = createStream(); + stream.reset(); + example_frames.forEach(stream._transition.bind(stream, false)); + }); + it('should ignore certain incoming frames after closing the stream with END_STREAM', function() { + var stream = createStream(); + stream.upstream.write({ type: 'HEADERS', flags: { END_STREAM: true }, headers:{} }); + stream.headers({}); + stream.end(); + example_frames.slice(0,3).forEach(function(frame) { + frame.count_change = util.noop; + stream._transition(false, frame); + }); + }); + }); + }); + describe('test scenario', function() { + describe('sending request', function() { + it('should trigger the appropriate state transitions and outgoing frames', function(done) { + execute_sequence([ + { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, + { event : { name: 'state', data: ['OPEN'] } }, + + { wait : 5 }, + { method : { name: 'end', arguments: [] } }, + { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: Buffer.alloc(0) } }, + + { wait : 10 }, + { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, + { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: Buffer.alloc(5) } }, + { event : { name: 'headers', data: [{ ':status': 200 }] } }, + { event : { name: 'state', data: ['CLOSED'] } }, + + { active : 0 } + ], done); + }); + }); + describe('answering request', function() { + it('should trigger the appropriate state transitions and outgoing frames', function(done) { + var payload = Buffer.alloc(5); + execute_sequence([ + { incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, + { event : { name: 'state', data: ['OPEN'] } }, + { event : { name: 'headers', data: [{ ':path': '/' }] } }, + + { wait : 5 }, + { incoming: { type: 'DATA', flags: { }, data: Buffer.alloc(5) } }, + { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: Buffer.alloc(10) } }, + { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, + + { wait : 5 }, + { method : { name: 'headers', arguments: [{ ':status': 200 }] } }, + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, + + { wait : 5 }, + { method : { name: 'end', arguments: [payload] } }, + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, + { event : { name: 'state', data: ['CLOSED'] } }, + + { active : 0 } + ], done); + }); + }); + describe('sending push stream', function() { + it('should trigger the appropriate state transitions and outgoing frames', function(done) { + var payload = Buffer.alloc(5); + var pushStream; + + execute_sequence([ + // receiving request + { incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, + { event : { name: 'state', data: ['OPEN'] } }, + { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, + { event : { name: 'headers', data: [{ ':path': '/' }] } }, + + // sending response headers + { wait : 5 }, + { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, + + // sending push promise + { method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } }, + { outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } }, + + // sending response data + { method : { name: 'end', arguments: [payload] } }, + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, + { event : { name: 'state', data: ['CLOSED'] } }, + + { active : 0 } + ], function() { + // initial state of the promised stream + expect(pushStream.state).to.equal('RESERVED_LOCAL'); + + execute_sequence(pushStream, [ + // push headers + { wait : 5 }, + { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, + { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, + { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, + + // push data + { method : { name: 'end', arguments: [payload] } }, + { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, + { event : { name: 'state', data: ['CLOSED'] } }, + + { active : 1 } + ], done); + }); + }); + }); + describe('receiving push stream', function() { + it('should trigger the appropriate state transitions and outgoing frames', function(done) { + var payload = Buffer.alloc(5); + var original_stream = createStream(); + var promised_stream = createStream(); + + done = util.callNTimes(2, done); + + execute_sequence(original_stream, [ + // sending request headers + { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, + { method : { name: 'end', arguments: [] } }, + { outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, + { event : { name: 'state', data: ['OPEN'] } }, + { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, + + // receiving response headers + { wait : 10 }, + { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, + { event : { name: 'headers', data: [{ ':status': 200 }] } }, + + // receiving push promise + { incoming: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/2.html' }, promised_stream: promised_stream } }, + { event : { name: 'promise', data: [promised_stream, { ':path': '/2.html' }] } }, + + // receiving response data + { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } }, + { event : { name: 'state', data: ['CLOSED'] } }, + + { active : 0 } + ], done); + + execute_sequence(promised_stream, [ + // initial state of the promised stream + { event : { name: 'state', data: ['RESERVED_REMOTE'] } }, + + // push headers + { wait : 10 }, + { incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } }, + { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, + { event : { name: 'headers', data: [{ ':status': 200 }] } }, + + // push data + { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, + { event : { name: 'state', data: ['CLOSED'] } }, + + { active : 0 } + ], done); + }); + }); + }); + + describe('bunyan formatter', function() { + describe('`s`', function() { + var format = stream.serializers.s; + it('should assign a unique ID to each frame', function() { + var stream1 = createStream(); + var stream2 = createStream(); + expect(format(stream1)).to.be.equal(format(stream1)); + expect(format(stream2)).to.be.equal(format(stream2)); + expect(format(stream1)).to.not.be.equal(format(stream2)); + }); + }); + }); +}); diff --git a/testing/xpcshell/node-http2/test/util.js b/testing/xpcshell/node-http2/test/util.js new file mode 100644 index 0000000000..c612e38b14 --- /dev/null +++ b/testing/xpcshell/node-http2/test/util.js @@ -0,0 +1,89 @@ +var path = require('path'); +var fs = require('fs'); +var spawn = require('child_process').spawn; + +function noop() {} +exports.noop = noop; + +if (process.env.HTTP2_LOG) { + var logOutput = process.stderr; + if (process.stderr.isTTY) { + var bin = path.resolve(path.dirname(require.resolve('bunyan')), '..', 'bin', 'bunyan'); + if(bin && fs.existsSync(bin)) { + logOutput = spawn(bin, ['-o', 'short'], { + stdio: [null, process.stderr, process.stderr] + }).stdin; + } + } + exports.createLogger = function(name) { + return require('bunyan').createLogger({ + name: name, + stream: logOutput, + level: process.env.HTTP2_LOG, + serializers: require('../lib/http').serializers + }); + }; + exports.log = exports.createLogger('test'); + exports.clientLog = exports.createLogger('client'); + exports.serverLog = exports.createLogger('server'); +} else { + exports.createLogger = function() { + return exports.log; + }; + exports.log = exports.clientLog = exports.serverLog = { + fatal: noop, + error: noop, + warn : noop, + info : noop, + debug: noop, + trace: noop, + + child: function() { return this; } + }; +} + +exports.callNTimes = function callNTimes(limit, done) { + if (limit === 0) { + done(); + } else { + var i = 0; + return function() { + i += 1; + if (i === limit) { + done(); + } + }; + } +}; + +// Concatenate an array of buffers into a new buffer +exports.concat = function concat(buffers) { + var size = 0; + for (var i = 0; i < buffers.length; i++) { + size += buffers[i].length; + } + + var concatenated = Buffer.alloc(size); + for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { + buffers[j].copy(concatenated, cursor); + } + + return concatenated; +}; + +exports.random = function random(min, max) { + return min + Math.floor(Math.random() * (max - min + 1)); +}; + +// Concatenate an array of buffers and then cut them into random size buffers +exports.shuffleBuffers = function shuffleBuffers(buffers) { + var concatenated = exports.concat(buffers), output = [], written = 0; + + while (written < concatenated.length) { + var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20)); + output.push(concatenated.slice(written, written + chunk_size)); + written += chunk_size; + } + + return output; +}; |