diff --git a/src/cmap/commands.ts b/src/cmap/commands.ts index 78e4445f25..a49d184319 100644 --- a/src/cmap/commands.ts +++ b/src/cmap/commands.ts @@ -484,15 +484,15 @@ export class Response { responseTo: number; opCode: number; fromCompressed?: boolean; - responseFlags: number; - cursorId: Long; - startingFrom: number; - numberReturned: number; - documents: (Document | Buffer)[]; - cursorNotFound: boolean; - queryFailure: boolean; - shardConfigStale: boolean; - awaitCapable: boolean; + responseFlags?: number; + cursorId?: Long; + startingFrom?: number; + numberReturned?: number; + documents: (Document | Buffer)[] = new Array(0); + cursorNotFound?: boolean; + queryFailure?: boolean; + shardConfigStale?: boolean; + awaitCapable?: boolean; promoteLongs: boolean; promoteValues: boolean; promoteBuffers: boolean; @@ -522,20 +522,7 @@ export class Response { this.opCode = msgHeader.opCode; this.fromCompressed = msgHeader.fromCompressed; - // Read the message body - this.responseFlags = msgBody.readInt32LE(0); - this.cursorId = new BSON.Long(msgBody.readInt32LE(4), msgBody.readInt32LE(8)); - this.startingFrom = msgBody.readInt32LE(12); - this.numberReturned = msgBody.readInt32LE(16); - - // Preallocate document array - this.documents = new Array(this.numberReturned); - // Flag values - this.cursorNotFound = (this.responseFlags & CURSOR_NOT_FOUND) !== 0; - this.queryFailure = (this.responseFlags & QUERY_FAILURE) !== 0; - this.shardConfigStale = (this.responseFlags & SHARD_CONFIG_STALE) !== 0; - this.awaitCapable = (this.responseFlags & AWAIT_CAPABLE) !== 0; this.promoteLongs = typeof this.opts.promoteLongs === 'boolean' ? this.opts.promoteLongs : true; this.promoteValues = typeof this.opts.promoteValues === 'boolean' ? this.opts.promoteValues : true; @@ -574,6 +561,20 @@ export class Response { // (See https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#wire-op-reply) this.index = 20; + // Read the message body + this.responseFlags = this.data.readInt32LE(0); + this.cursorId = new BSON.Long(this.data.readInt32LE(4), this.data.readInt32LE(8)); + this.startingFrom = this.data.readInt32LE(12); + this.numberReturned = this.data.readInt32LE(16); + + // Preallocate document array + this.documents = new Array(this.numberReturned); + + this.cursorNotFound = (this.responseFlags & CURSOR_NOT_FOUND) !== 0; + this.queryFailure = (this.responseFlags & QUERY_FAILURE) !== 0; + this.shardConfigStale = (this.responseFlags & SHARD_CONFIG_STALE) !== 0; + this.awaitCapable = (this.responseFlags & AWAIT_CAPABLE) !== 0; + // Parse Body for (let i = 0; i < this.numberReturned; i++) { bsonSize = diff --git a/test/unit/cmap/commands.test.js b/test/unit/cmap/commands.test.js new file mode 100644 index 0000000000..65993f3c33 --- /dev/null +++ b/test/unit/cmap/commands.test.js @@ -0,0 +1,111 @@ +const { expect } = require('chai'); +const { Response } = require('../../../src/cmap/commands'); + +describe('commands', function () { + describe('Response', function () { + describe('#parse', function () { + context('when the message body is invalid', function () { + context('when the buffer is empty', function () { + const message = Buffer.from([]); + const header = { + length: 0, + requestId: 0, + responseTo: 0, + opCode: 0 + }; + const body = Buffer.from([]); + + it('throws an exception', function () { + const response = new Response(message, header, body); + expect(() => response.parse()).to.throw(RangeError, /outside buffer bounds/); + }); + }); + + context('when numReturned is invalid', function () { + const message = Buffer.from([]); + const header = { + length: 0, + requestId: 0, + responseTo: 0, + opCode: 0 + }; + const body = Buffer.alloc(5 * 4); + body.writeInt32LE(-1, 16); + + it('throws an exception', function () { + const response = new Response(message, header, body); + expect(() => response.parse()).to.throw(RangeError, /Invalid array length/); + }); + }); + }); + }); + + describe('#constructor', function () { + context('when the message body is invalid', function () { + const message = Buffer.from([]); + const header = { + length: 0, + requestId: 0, + responseTo: 0, + opCode: 0 + }; + const body = Buffer.from([]); + + it('does not throw an exception', function () { + let error; + try { + new Response(message, header, body); + } catch (err) { + error = err; + } + expect(error).to.be.undefined; + }); + + it('initializes the documents to an empty array', function () { + const response = new Response(message, header, body); + expect(response.documents).to.be.empty; + }); + + it('does not set the responseFlags', function () { + const response = new Response(message, header, body); + expect(response.responseFlags).to.be.undefined; + }); + + it('does not set the cursorNotFound flag', function () { + const response = new Response(message, header, body); + expect(response.cursorNotFound).to.be.undefined; + }); + + it('does not set the cursorId', function () { + const response = new Response(message, header, body); + expect(response.cursorId).to.be.undefined; + }); + + it('does not set startingFrom', function () { + const response = new Response(message, header, body); + expect(response.startingFrom).to.be.undefined; + }); + + it('does not set numberReturned', function () { + const response = new Response(message, header, body); + expect(response.numberReturned).to.be.undefined; + }); + + it('does not set queryFailure', function () { + const response = new Response(message, header, body); + expect(response.queryFailure).to.be.undefined; + }); + + it('does not set shardConfigStale', function () { + const response = new Response(message, header, body); + expect(response.shardConfigStale).to.be.undefined; + }); + + it('does not set awaitCapable', function () { + const response = new Response(message, header, body); + expect(response.awaitCapable).to.be.undefined; + }); + }); + }); + }); +});