import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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: new Buffer([10])
}, {
N: 0,
I: 10,
buffer: new Buffer([10])
}, {
N: 5,
I: 1337,
buffer: new Buffer([31, 128 + 26, 10])
}, {
N: 0,
I: 1337,
buffer: new Buffer([128 + 57, 10])
}];
var test_strings = [{
string: 'www.foo.com',
buffer: new Buffer('89f1e3c2f29ceb90f4ff', 'hex')
}, {
string: 'éáűőúöüó€',
buffer: new Buffer('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: new Buffer('82', 'hex')
}, {
// index
header: {
name: 5,
value: 5,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('86', 'hex')
}, {
// index
header: {
name: 3,
value: 3,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('84', 'hex')
}, {
// literal w/index, name index
header: {
name: 0,
value: 'www.foo.com',
index: true,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('41' + '89f1e3c2f29ceb90f4ff', 'hex')
}, {
// indexed
header: {
name: 1,
value: 1,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('82', 'hex')
}, {
// indexed
header: {
name: 6,
value: 6,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('87', 'hex')
}, {
// indexed
header: {
name: 3,
value: 3,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('84', 'hex')
}, {
// literal w/index, name index
header: {
name: 0,
value: 'www.bar.com',
index: true,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('41' + '89f1e3c2f18ec5c87a7f', 'hex')
}, {
// literal w/index, name index
header: {
name: 23,
value: 'no-cache',
index: true,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('58' + '86a8eb10649cbf', 'hex')
}, {
// index
header: {
name: 1,
value: 1,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('82', 'hex')
}, {
// index
header: {
name: 6,
value: 6,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('87', 'hex')
}, {
// literal w/index, name index
header: {
name: 3,
value: '/custom-path.css',
index: true,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('44' + '8b6096a127a56ac699d72211', 'hex')
}, {
// index
header: {
name: 63,
value: 63,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('C0', 'hex')
}, {
// literal w/index, new name & value
header: {
name: 'custom-key',
value: 'custom-value',
index: true,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('40' + '8825a849e95ba97d7f' + '8925a849e95bb8e8b4bf', 'hex')
}, {
// index
header: {
name: 1,
value: 1,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('82', 'hex')
}, {
// index
header: {
name: 6,
value: 6,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('87', 'hex')
}, {
// index
header: {
name: 62,
value: 62,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('BF', 'hex')
}, {
// index
header: {
name: 65,
value: 65,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('C2', 'hex')
}, {
// index
header: {
name: 64,
value: 64,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('C1', 'hex')
}, {
// index
header: {
name: 61,
value: 61,
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('BE', 'hex')
}, {
// Literal w/o index, name index
header: {
name: 6,
value: "whatever",
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('07' + '86f138d25ee5b3', 'hex')
}, {
// Literal w/o index, new name & value
header: {
name: "foo",
value: "bar",
index: false,
mustNeverIndex: false,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('00' + '8294e7' + '03626172', 'hex')
}, {
// Literal never indexed, name index
header: {
name: 6,
value: "whatever",
index: false,
mustNeverIndex: true,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('17' + '86f138d25ee5b3', 'hex')
}, {
// Literal never indexed, new name & value
header: {
name: "foo",
value: "bar",
index: false,
mustNeverIndex: true,
contextUpdate: false,
newMaxSize: 0
},
buffer: new Buffer('10' + '8294e7' + '03626172', 'hex')
}, {
header: {
name: -1,
value: -1,
index: false,
mustNeverIndex: false,
contextUpdate: true,
newMaxSize: 100
},
buffer: new Buffer('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(new Buffer(decoded)).toString('hex')).to.equal(encoded);
}
table = HuffmanTable.huffmanTable;
for (decoded in test_huffman_response) {
encoded = test_huffman_response[decoded];
expect(table.encode(new Buffer(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(new Buffer(encoded, 'hex')).toString()).to.equal(decoded);
}
table = HuffmanTable.huffmanTable;
for (decoded in test_huffman_response) {
encoded = test_huffman_response[decoded];
expect(table.decode(new Buffer(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: new Buffer(5)
});
decompressor.write({
type: 'DATA',
flags: {},
data: new Buffer(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 = new Buffer(buffer);
var table = HuffmanTable.huffmanTable;
var result = table.decode(table.encode(buffer));
expect(result).to.deep.equal(buffer);
}
});
});
});
});

View file

@ -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: new Buffer(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 = new Buffer(0);
var response_headers = {
':status': '200'
};
var response_data = new Buffer('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 = new Buffer(10);
var push_content = new Buffer(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();
});
});
});
});

View file

@ -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));
});
});
});
});

View file

@ -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 = new Buffer(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 = new Buffer(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: new Buffer(0) });
this.push(null);
} else {
var buffer = new Buffer(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: new Buffer(0) });
this.push(null);
} else {
var buffer = new Buffer(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);
});
});
});
});

View file

@ -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: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + content
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
buffer: new Buffer('000009' + '01' + '20' + '0000000F' + '8000000A' + '05' + '12345678', 'hex')
}, {
frame: {
type: 'PRIORITY',
flags: { },
stream: 10,
priorityDependency: 9,
priorityWeight: 5,
exclusiveDependency: false
},
buffer: new Buffer('000005' + '02' + '00' + '0000000A' + '00000009' + '05', 'hex')
}, {
frame: {
type: 'PRIORITY',
flags: { },
stream: 10,
priorityDependency: 9,
priorityWeight: 5,
exclusiveDependency: true
},
buffer: new Buffer('000005' + '02' + '00' + '0000000A' + '80000009' + '05', 'hex')
}, {
frame: {
type: 'RST_STREAM',
flags: { },
stream: 10,
error: 'INTERNAL_ERROR'
},
buffer: new Buffer('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: new Buffer('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: new Buffer('12345678', 'hex')
},
buffer: new Buffer('000008' + '05' + '00' + '0000000F' + '00000003' + '12345678', 'hex')
}, {
frame: {
type: 'PING',
flags: { ACK: false },
stream: 15,
data: new Buffer('1234567887654321', 'hex')
},
buffer: new Buffer('000008' + '06' + '00' + '0000000F' + '1234567887654321', 'hex')
}, {
frame: {
type: 'GOAWAY',
flags: { },
stream: 10,
last_stream: 0x12345678,
error: 'PROTOCOL_ERROR'
},
buffer: new Buffer('000008' + '07' + '00' + '0000000A' + '12345678' + '00000001', 'hex')
}, {
frame: {
type: 'WINDOW_UPDATE',
flags: { },
stream: 10,
window_size: 0x12345678
},
buffer: new Buffer('000004' + '08' + '00' + '0000000A' + '12345678', 'hex')
}, {
frame: {
type: 'CONTINUATION',
flags: { RESERVED1: false, RESERVED2: false, END_HEADERS: true },
stream: 10,
data: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + content
buffer: new Buffer('000004' + '09' + '04' + '0000000A' + '12345678', 'hex')
}, {
frame: {
type: 'ALTSVC',
flags: { },
stream: 0,
maxAge: 31536000,
port: 4443,
protocolID: "h2",
host: "altsvc.example.com",
origin: ""
},
buffer: new Buffer(new Buffer('00002B' + '0A' + '00' + '00000000' + '0000', 'hex') + new Buffer('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: new Buffer(new Buffer('000045' + '0A' + '00' + '00000000' + '001A', 'hex') + new Buffer('https://onlyme.example.comh2="altsvc.example.com:4443"; ma=31536000', 'ascii'))
}, {
frame: {
type: 'BLOCKED',
flags: { },
stream: 10
},
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + pad length + content + padding
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + pad length + data + padding
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + pad length + priority dependency + priority weight + data + padding
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + pad length + priority dependency + priority weight + data + padding
buffer: new Buffer('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: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + pad length + promised stream + data + padding
buffer: new Buffer('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 = new Buffer(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: new Buffer(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));
});
});
});
});

View file

@ -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);
});
});
});
});
});
});
});

View file

@ -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: new Buffer(5) },
{ type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log, null) }
];
var invalid_incoming_frames = {
IDLE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'RST_STREAM', flags: {}, error: 'CANCEL' }
],
RESERVED_LOCAL: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
RESERVED_REMOTE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
OPEN: [
],
HALF_CLOSED_LOCAL: [
],
HALF_CLOSED_REMOTE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} }
]
};
var invalid_outgoing_frames = {
IDLE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} }
],
RESERVED_LOCAL: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
RESERVED_REMOTE: [
{ type: 'DATA', flags: {}, data: new Buffer(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: new Buffer(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: new Buffer(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: new Buffer(0) } },
{ wait : 10 },
{ incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
{ incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(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 = new Buffer(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: new Buffer(5) } },
{ incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(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 = new Buffer(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 = new Buffer(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));
});
});
});
});

View file

@ -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 = new Buffer(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;
};