summaryrefslogtreecommitdiffstats
path: root/testing/xpcshell/node-http2/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/xpcshell/node-http2/test/compressor.js575
-rw-r--r--testing/xpcshell/node-http2/test/connection.js237
-rw-r--r--testing/xpcshell/node-http2/test/endpoint.js41
-rw-r--r--testing/xpcshell/node-http2/test/flow.js260
-rw-r--r--testing/xpcshell/node-http2/test/framer.js395
-rw-r--r--testing/xpcshell/node-http2/test/http.js793
-rw-r--r--testing/xpcshell/node-http2/test/stream.js413
-rw-r--r--testing/xpcshell/node-http2/test/util.js89
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;
+};