Come stub https.request response.pipe con sinon.js e promessa e possibili percorsi?
Attualmente sto facendo qualcosa di simile come descritto in Come stub https.request response.pipe con sinon.js?
In realtà sto facendo lo stesso, ma il mio codice è un po 'più complesso poiché ho più percorsi (condizioni) per quanto riguarda la gestione del codice della richiesta e utilizzo una promessa invece di una richiamata. Inoltre, utilizzo l'https invece del modulo di richiesta.
Al momento ho il seguente codice che non riesco a far funzionare:
//utils.js
/**
* Fetches the repository archive
*
* @param url The archive download url
* @param dest The temp directory path
* @param accessToken The access token for the repository (only available if private repository)
*/
exports.fetchArchive = function(url, dest, accessToken) {
let options = {
headers: {}
}
if (accessToken) {
options.headers = {'PRIVATE-TOKEN': accessToken}
}
return new Promise((resolve, reject) => {
https
.get(url, options,(response) => {
const code = response.statusCode;
if (code >= 400) {
reject({ code, message: response.statusMessage });
} else if (code >= 300) {
this.fetchArchive(response.headers.location, dest).then(resolve, reject);
} else {
response
.pipe(fs.createWriteStream(dest))
.on('end', () => resolve(null))
.on('error', () => reject({ code, message: response.statusMessage }));
}
})
});
}
_
//utils.test.js
describe('public fetchArchive', () => {
it(`should have a redirect status code (>= 300) and redirect and thus be called at twice`, () => {
let options = {
headers: {}
}
options.headers = {'PRIVATE-TOKEN': repoPropsPrivate.accessToken}
const mockResponse = `{"data": 123}`;
// //Using a built-in PassThrough stream to emit needed data.
const mockStream = new PassThrough();
mockStream.push(mockResponse);
mockStream.end(); //Mark that we pushed all the data.
sinon
.stub(https, 'get')
.callsFake(function (privateUrl, options, callback) {
callback(mockStream);
return Promise.resolve(null); //Stub end method btw
});
//Finally keep track of how 'pipe' is going to be called
sinon.spy(mockStream, 'pipe');
return utils.fetchArchive(privateArchiveUrl, privateArchiveDest, repoPropsPrivate.accessToken)
.then((res) => {
sinon.assert.calledOnce(mockStream.pipe);
//We can get the stream that we piped to.
let writable = mockStream.pipe.getCall(0).args[0];
assert.equal(writable.path, './output.json');
})
});
});
Non sono sicuro di come adattare il codice dell'altro post per soddisfare le mie esigenze.
Non so come inviare una risposta che includa il codice di richiesta seguito dallo stream, e inoltre come elaborare la promessa nel test.
Apprezzerei molto il tuo aiuto.
Risposte
È possibile creare un flusso scrivibile personalizzato per avere un maggiore controllo sui dati falsificati, come l'aggiunta di statusCodeo headersproprietà sull'oggetto flusso. Creerei qualcosa di simile (questo simula un flusso reale in cui _read()emetterà 4 byte di dati dal dato responseBodyfino a quando non ci sarà più nulla da leggere:
const {Readable} = require('stream');
class ResponseStreamMock extends Readable {
constructor(statusCode, responseBody) {
super();
this.statusCode = statusCode;
this.headers = {location: 'someLocation'};
this.responseData = responseBody !== undefined ? Buffer.from(JSON.stringify(responseBody), "utf8") : Buffer.from([]);
this.bytesRead = 0;
this.offset = 4;
}
_read() {
if (this.bytesRead >= this.responseData.byteLength) {
this.push(null);
} else {
setTimeout(() => {
const buff = this.responseData.toString('utf8', this.bytesRead, this.bytesRead + this.offset);
this.push(Buffer.from(buff));
this.bytesRead += this.offset;
}, Math.random() * 200)
}
}
}
Puoi quindi usarlo in questo modo nel tuo test unitario:
describe('public fetchArchive', () => {
it(`should redirect for status code >= 300, then pipe response into fs-stream`, async function () {
this.timeout(5000)
const httpGetStub = sinon.stub(https, 'get');
let redirectStream = new ResponseStreamMock(300);
let responseStream = new ResponseStreamMock(200, {"someData": {"test": 123}});
httpGetStub.onCall(0).callsFake(function (privateUrl, options, callback) {
redirectStream.resume(); // immediately flush the stream as this one does not get piped
callback(redirectStream);
});
httpGetStub.onCall(1).callsFake(function (privateUrl, options, callback) {
callback(responseStream);
});
sinon.spy(redirectStream, 'pipe');
sinon.spy(responseStream, 'pipe');
const fsWriteStreamStub = sinon
.stub(fs, 'createWriteStream').callsFake(() => process.stdout); // you can replace process.stdout with new PassThrough() if you don't want any output
const result = await fetchArchive("someURL", "someDestination", "")
sinon.assert.calledOnce(fsWriteStreamStub);
sinon.assert.calledOnce(responseStream.pipe);
sinon.assert.notCalled(redirectStream.pipe);
assert.equal(result, null);
});
});
Nota che devi modificare l'implementazione di fetchArchiveun po 'per farlo funzionare:
exports.fetchArchive = function fetchArchive(url, dest, accessToken) {
let options = {
headers: {}
}
if (accessToken) {
options.headers = {'PRIVATE-TOKEN': accessToken}
}
return new Promise((resolve, reject) => {
https
.get(url, options, (response) => {
const code = response.statusCode;
if (code >= 400) {
reject({code, message: response.statusMessage});
} else if (code >= 300) {
fetchArchive(response.headers.location, dest).then(resolve, reject);
} else {
response.on('end', () => resolve(null))
response.on('error', () => reject({code, message: response.statusMessage}));
response.pipe(fs.createWriteStream(dest))
}
})
});
}