mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	Differential Revision: https://phabricator.services.mozilla.com/D46260 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			413 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			413 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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));
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |