Как издеваться над http.ServerResponse и http.IncomingMessage для express.static

У меня не было проблем с тестированием моих собственных обработчиков маршрутов, но в этом случае я хочу проверить экспресс-статический обработчик. Я не могу для жизни понять, почему он висит. Ясно, что есть некоторая обратная связь, которую я пропускаю, или какое-то событие, которое мне нужно исправить.

Я попытался сделать самый маленький пример, который мог бы сделать.

var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');

function MockResponse(callback) {
  stream.Writable.call(this);
  this.headers = {};
  this.statusCode = -1;
  this.body = undefined;

  this.setHeader = function(key, value) {
    this.headers[key] = value;
  }.bind(this);

  this.on('finish', function() {
    console.log("finished response");
    callback();
  });
};

util.inherits(MockResponse, stream.Writable);

MockResponse.prototype._write = function(chunk, encoding, done) {
  if (this.body === undefined) {
    this.body = "";
  }
  this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
  done();
};

function createRequest(req) {
  var emitter = new events.EventEmitter();
  req.on = emitter.on.bind(emitter);
  req.once = emitter.once.bind(emitter);
  req.addListener = emitter.addListener.bind(emitter);
  req.emit = emitter.emit.bind(emitter);
  return req;
};

describe('test', function() {

  var app;

  before(function() {
    app = express();
    app.use(express.static(__dirname));
  });

  it('gets test.js', function(done) {

    var req = createRequest({
        url: "http://foo.com/test.js",
        method: 'GET',
        headers: {
        },
    });
    var res = new MockResponse(responseDone);
    app(req, res);

    function responseDone() {
      console.log("done");
      done();
    }

  });

});

Настройка

mkdir foo
cd foo
mkdir test
cat > test/test.js   # copy and paste code above
^D
npm install express
npm install mocha
node node_modules/mocha/bin/mocha --recursive

это просто время.

Что мне не хватает?

Я также попытался сделать запрос читаемым потоком. Без изменений

var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');

function MockResponse(callback) {
  stream.Writable.call(this);
  this.headers = {};
  this.statusCode = -1;
  this.body = undefined;

  this.setHeader = function(key, value) {
    this.headers[key] = value;
  }.bind(this);

  this.on('finish', function() {
    console.log("finished response");
    callback();
  });
};

util.inherits(MockResponse, stream.Writable);

MockResponse.prototype._write = function(chunk, encoding, done) {
  if (this.body === undefined) {
    this.body = "";
  }
  this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
  done();
};

function MockMessage(req) {
  stream.Readable.call(this);
  var self = this;
  Object.keys(req).forEach(function(key) {
    self[key] = req[key];
  });
}

util.inherits(MockMessage, stream.Readable);

MockMessage.prototype._read = function() {
  this.push(null);
};


describe('test', function() {

  var app;

  before(function() {
    app = express();
    app.use(express.static(__dirname));
  });

  it('gets test.js', function(done) {

    var req = new MockMessage({
        url: "http://foo.com/test.js",
        method: 'GET',
        headers: {
        },
    });
    var res = new MockResponse(responseDone);
    app(req, res);

    function responseDone() {
      console.log("done");
      done();
    }

  });

});

Я все еще рылся. Загляните внутрь static-server. Я вижу, что он создает читаемый поток, вызывая fs.createReadStream. Это эффективно

var s = fs.createReadStream(filename);
s.pipe(res);

Так что стараюсь, чтобы я работал просто отлично

  it('test stream', function(done) {
    var s = fs.createReadStream(__dirname + "/test.js");
    var res = new MockResponse(responseDone);
    s.pipe(res);

    function responseDone() {
      console.log("done");
      done();
    }    
  });

Я подумал, может быть, это что-то вроде выражения, ожидающего окончания входного потока, но это тоже не похоже. Если я использую макет входного потока с ответом, он работает просто отлично

  it('test msg->res', function(done) {
    var req = new MockMessage({});
    var res = new MockResponse(responseDone);
    req.pipe(res);

    function responseDone() {
      console.log("done");
      done();
    }    
  });

Любое понимание того, что я могу пропустить, было бы полезно

Примечание: в то время как предложения для сторонних издевательских библиотек оценены, я все еще очень хочу понять, чего мне не хватает, чтобы сделать это сам. Даже если я в конечном итоге переключусь на какую-то библиотеку, я все еще хочу знать, почему это не работает.

Ответ 1

Я обнаружил две проблемы, которые предотвращают выполнение обратного вызова finish.

  • serve-static использует модуль send, который используется для создания потока чтения файла из пути и передает его на объект res. Но в этом модуле используется модуль on-finished, который проверяет, установлено ли значение атрибута finished false в объекте ответа, в противном случае уничтожает поток чтения файла. Так что в filestream никогда не будет возможности испускать событие данных.

  • выразить инициализация перезаписывает прототип объекта ответа. Таким образом, потоковые методы по умолчанию, такие как метод end(), перезаписываются прототипом ответа HTTP:

    exports.init = function(app){
      return function expressInit(req, res, next){
        ...
        res.__proto__ = app.response;
        ..
      };
    };
    

    Чтобы предотвратить это, я добавил другое промежуточное программное обеспечение прямо перед статическим промежуточным программным обеспечением до reset обратно к прототипу MockResponse:

    app.use(function(req, res, next){
      res.__proto__ = MockResponse.prototype; //change it back to MockResponse prototype
      next();
    });
    

Ниже приведены изменения, сделанные для работы с MockResponse:

...
function MockResponse(callback) {
  ...
  this.finished = false; // so `on-finished` module doesn't emit finish event prematurely

  //required because of 'send' module
  this.getHeader = function(key) {
    return this.headers[key];
  }.bind(this);
  ...
};

...
describe('test', function() {

  var app;

  before(function() {
    app = express();

    //another middleware to reset the res object
    app.use(function(req, res, next){
      res.__proto__ = MockResponse.prototype;
      next();
    });

    app.use(express.static(__dirname));
  });

  ...

});

EDIT:

Как отметил @gman, можно использовать свойство direct вместо метода прототипа. В этом случае дополнительное промежуточное программное обеспечение для перезаписывания прототипа не требуется:

function MockResponse(callback) {
  ...
  this.finished = false; // so `on-finished` module doesn't emit finish event prematurely

  //required because of 'send' module
  this.getHeader = function(key) {
     return this.headers[key];
  }.bind(this);

  ...

  //using direct property for _write, write, end - since all these are changed when prototype is changed
  this._write = function(chunk, encoding, done) {
    if (this.body === undefined) {
      this.body = "";
    }
    this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
    done();
  };

  this.write = stream.Writable.prototype.write;
  this.end = stream.Writable.prototype.end;

};

Ответ 2

Похоже, мой ответ не завершен. По какой-то причине приложение работает только в том случае, если файл не найден. Первое, что нужно отлаживать, - сделать в своей оболочке (или cmd) следующее:

export DEBUG=express:router,send

затем запустите тест, вы получите больше информации.

Между тем, я все еще смотрю на это, пока не проигнорирую свой ответ ниже.

----------- игнорировать это, пока я не проверю, что он работает -----------

Кажется, что выражение static не поддерживает абсолютный путь, который вы ему даете (__dirname).

Try:

app.use(express.static('.'));

и он будет работать. Обратите внимание, что ваш текущий рекордер для бегуна мокки - "test/"

Я должен признать, что это довольно мистерия. Я попробовал "наполнить" его, сделав:

app.use(express.static(__dirname + '/../test')

но все же это не сработало. Даже определение полного пути не решило этого. Странно.