1
1
import { expect } from 'chai' ;
2
- import { EventEmitter } from 'events' ;
2
+ import { EventEmitter , once } from 'events' ;
3
3
import { Socket } from 'net' ;
4
4
import * as sinon from 'sinon' ;
5
+ import { Readable } from 'stream' ;
5
6
import { setTimeout } from 'timers' ;
6
7
8
+ import { BinMsg } from '../../../src/cmap/commands' ;
7
9
import { connect } from '../../../src/cmap/connect' ;
8
10
import { Connection , hasSessionSupport } from '../../../src/cmap/connection' ;
9
11
import { MessageStream } from '../../../src/cmap/message_stream' ;
10
- import { MongoNetworkTimeoutError } from '../../../src/error' ;
12
+ import { MongoNetworkTimeoutError , MongoRuntimeError } from '../../../src/error' ;
11
13
import { isHello , ns } from '../../../src/utils' ;
12
14
import * as mock from '../../tools/mongodb-mock/index' ;
13
- import { getSymbolFrom } from '../../tools/utils' ;
15
+ import { generateOpMsgBuffer , getSymbolFrom } from '../../tools/utils' ;
14
16
import { createTimerSandbox } from '../timer_sandbox' ;
15
17
16
18
const connectionOptionsDefaults = {
@@ -22,6 +24,25 @@ const connectionOptionsDefaults = {
22
24
loadBalanced : false
23
25
} ;
24
26
27
+ /** The absolute minimum socket API needed by Connection as of writing this test */
28
+ class FakeSocket extends EventEmitter {
29
+ address ( ) {
30
+ // is never called
31
+ }
32
+ pipe ( ) {
33
+ // does not need to do anything
34
+ }
35
+ destroy ( ) {
36
+ // is called, has no side effects
37
+ }
38
+ get remoteAddress ( ) {
39
+ return 'iLoveJavaScript' ;
40
+ }
41
+ get remotePort ( ) {
42
+ return 123 ;
43
+ }
44
+ }
45
+
25
46
describe ( 'new Connection()' , function ( ) {
26
47
let server ;
27
48
after ( ( ) => mock . cleanup ( ) ) ;
@@ -137,6 +158,189 @@ describe('new Connection()', function () {
137
158
} ) ;
138
159
} ) ;
139
160
161
+ describe ( '#onMessage' , function ( ) {
162
+ context ( 'when the connection is a monitoring connection' , function ( ) {
163
+ let queue : Map < number , OperationDescription > ;
164
+ let driverSocket : FakeSocket ;
165
+ let connection : Connection ;
166
+
167
+ beforeEach ( function ( ) {
168
+ driverSocket = sinon . spy ( new FakeSocket ( ) ) ;
169
+ } ) ;
170
+
171
+ context ( 'when multiple hellos exist on the stream' , function ( ) {
172
+ let callbackSpy ;
173
+ const inputStream = new Readable ( ) ;
174
+ const document = { ok : 1 } ;
175
+ const last = { isWritablePrimary : true } ;
176
+
177
+ beforeEach ( function ( ) {
178
+ callbackSpy = sinon . spy ( ) ;
179
+ const firstHello = generateOpMsgBuffer ( document ) ;
180
+ const secondHello = generateOpMsgBuffer ( document ) ;
181
+ const thirdHello = generateOpMsgBuffer ( last ) ;
182
+ const buffer = Buffer . concat ( [ firstHello , secondHello , thirdHello ] ) ;
183
+
184
+ connection = sinon . spy ( new Connection ( inputStream , connectionOptionsDefaults ) ) ;
185
+ connection . isMonitoringConnection = true ;
186
+ const queueSymbol = getSymbolFrom ( connection , 'queue' ) ;
187
+ queue = connection [ queueSymbol ] ;
188
+
189
+ // Create the operation description.
190
+ const operationDescription : OperationDescription = {
191
+ requestId : 1 ,
192
+ cb : callbackSpy
193
+ } ;
194
+
195
+ // Stick an operation description in the queue.
196
+ queue . set ( 1 , operationDescription ) ;
197
+
198
+ // Push the buffer of 3 hellos to the input stream
199
+ inputStream . push ( buffer ) ;
200
+ inputStream . push ( null ) ;
201
+ } ) ;
202
+
203
+ it ( 'calls the callback with the last hello document' , async function ( ) {
204
+ const messages = await once ( connection , 'message' ) ;
205
+ expect ( messages [ 0 ] . responseTo ) . to . equal ( 0 ) ;
206
+ expect ( callbackSpy ) . to . be . calledOnceWith ( undefined , last ) ;
207
+ } ) ;
208
+ } ) ;
209
+
210
+ context ( 'when requestId/responseTo do not match' , function ( ) {
211
+ let callbackSpy ;
212
+ const document = { ok : 1 } ;
213
+
214
+ beforeEach ( function ( ) {
215
+ callbackSpy = sinon . spy ( ) ;
216
+
217
+ // @ts -expect-error: driverSocket does not fully satisfy the stream type, but that's okay
218
+ connection = sinon . spy ( new Connection ( driverSocket , connectionOptionsDefaults ) ) ;
219
+ connection . isMonitoringConnection = true ;
220
+ const queueSymbol = getSymbolFrom ( connection , 'queue' ) ;
221
+ queue = connection [ queueSymbol ] ;
222
+
223
+ // Create the operation description.
224
+ const operationDescription : OperationDescription = {
225
+ requestId : 1 ,
226
+ cb : callbackSpy
227
+ } ;
228
+
229
+ // Stick an operation description in the queue.
230
+ queue . set ( 1 , operationDescription ) ;
231
+ // Emit a message that won't match the existing operation description.
232
+ const msg = generateOpMsgBuffer ( document ) ;
233
+ const msgHeader : MessageHeader = {
234
+ length : msg . readInt32LE ( 0 ) ,
235
+ requestId : 1 ,
236
+ responseTo : 0 , // This will not match.
237
+ opCode : msg . readInt32LE ( 12 )
238
+ } ;
239
+ const msgBody = msg . subarray ( 16 ) ;
240
+
241
+ const message = new BinMsg ( msg , msgHeader , msgBody ) ;
242
+ connection . onMessage ( message ) ;
243
+ } ) ;
244
+
245
+ it ( 'calls the operation description callback with the document' , function ( ) {
246
+ expect ( callbackSpy ) . to . be . calledOnceWith ( undefined , document ) ;
247
+ } ) ;
248
+ } ) ;
249
+
250
+ context ( 'when requestId/reponseTo match' , function ( ) {
251
+ let callbackSpy ;
252
+ const document = { ok : 1 } ;
253
+
254
+ beforeEach ( function ( ) {
255
+ callbackSpy = sinon . spy ( ) ;
256
+
257
+ // @ts -expect-error: driverSocket does not fully satisfy the stream type, but that's okay
258
+ connection = sinon . spy ( new Connection ( driverSocket , connectionOptionsDefaults ) ) ;
259
+ connection . isMonitoringConnection = true ;
260
+ const queueSymbol = getSymbolFrom ( connection , 'queue' ) ;
261
+ queue = connection [ queueSymbol ] ;
262
+
263
+ // Create the operation description.
264
+ const operationDescription : OperationDescription = {
265
+ requestId : 1 ,
266
+ cb : callbackSpy
267
+ } ;
268
+
269
+ // Stick an operation description in the queue.
270
+ queue . set ( 1 , operationDescription ) ;
271
+ // Emit a message that matches the existing operation description.
272
+ const msg = generateOpMsgBuffer ( document ) ;
273
+ const msgHeader : MessageHeader = {
274
+ length : msg . readInt32LE ( 0 ) ,
275
+ requestId : 2 ,
276
+ responseTo : 1 ,
277
+ opCode : msg . readInt32LE ( 12 )
278
+ } ;
279
+ const msgBody = msg . subarray ( 16 ) ;
280
+
281
+ const message = new BinMsg ( msg , msgHeader , msgBody ) ;
282
+ connection . onMessage ( message ) ;
283
+ } ) ;
284
+
285
+ it ( 'calls the operation description callback with the document' , function ( ) {
286
+ expect ( callbackSpy ) . to . be . calledOnceWith ( undefined , document ) ;
287
+ } ) ;
288
+ } ) ;
289
+
290
+ context ( 'when more than one operation description is in the queue' , function ( ) {
291
+ let spyOne ;
292
+ let spyTwo ;
293
+ const document = { ok : 1 } ;
294
+
295
+ beforeEach ( function ( ) {
296
+ spyOne = sinon . spy ( ) ;
297
+ spyTwo = sinon . spy ( ) ;
298
+
299
+ // @ts -expect-error: driverSocket does not fully satisfy the stream type, but that's okay
300
+ connection = sinon . spy ( new Connection ( driverSocket , connectionOptionsDefaults ) ) ;
301
+ connection . isMonitoringConnection = true ;
302
+ const queueSymbol = getSymbolFrom ( connection , 'queue' ) ;
303
+ queue = connection [ queueSymbol ] ;
304
+
305
+ // Create the operation descriptions.
306
+ const descriptionOne : OperationDescription = {
307
+ requestId : 1 ,
308
+ cb : spyOne
309
+ } ;
310
+ const descriptionTwo : OperationDescription = {
311
+ requestId : 2 ,
312
+ cb : spyTwo
313
+ } ;
314
+
315
+ // Stick an operation description in the queue.
316
+ queue . set ( 2 , descriptionOne ) ;
317
+ queue . set ( 3 , descriptionTwo ) ;
318
+ // Emit a message that matches the existing operation description.
319
+ const msg = generateOpMsgBuffer ( document ) ;
320
+ const msgHeader : MessageHeader = {
321
+ length : msg . readInt32LE ( 0 ) ,
322
+ requestId : 2 ,
323
+ responseTo : 1 ,
324
+ opCode : msg . readInt32LE ( 12 )
325
+ } ;
326
+ const msgBody = msg . subarray ( 16 ) ;
327
+
328
+ const message = new BinMsg ( msg , msgHeader , msgBody ) ;
329
+ connection . onMessage ( message ) ;
330
+ } ) ;
331
+
332
+ it ( 'calls all operation description callbacks with an error' , function ( ) {
333
+ expect ( spyOne ) . to . be . calledOnce ;
334
+ expect ( spyTwo ) . to . be . calledOnce ;
335
+ const errorOne = spyOne . firstCall . args [ 0 ] ;
336
+ const errorTwo = spyTwo . firstCall . args [ 0 ] ;
337
+ expect ( errorOne ) . to . be . instanceof ( MongoRuntimeError ) ;
338
+ expect ( errorTwo ) . to . be . instanceof ( MongoRuntimeError ) ;
339
+ } ) ;
340
+ } ) ;
341
+ } ) ;
342
+ } ) ;
343
+
140
344
describe ( 'onTimeout()' , ( ) => {
141
345
let connection : sinon . SinonSpiedInstance < Connection > ;
142
346
let clock : sinon . SinonFakeTimers ;
@@ -146,25 +350,6 @@ describe('new Connection()', function () {
146
350
let kDelayedTimeoutId : symbol ;
147
351
let NodeJSTimeoutClass : any ;
148
352
149
- /** The absolute minimum socket API needed by Connection as of writing this test */
150
- class FakeSocket extends EventEmitter {
151
- address ( ) {
152
- // is never called
153
- }
154
- pipe ( ) {
155
- // does not need to do anything
156
- }
157
- destroy ( ) {
158
- // is called, has no side effects
159
- }
160
- get remoteAddress ( ) {
161
- return 'iLoveJavaScript' ;
162
- }
163
- get remotePort ( ) {
164
- return 123 ;
165
- }
166
- }
167
-
168
353
beforeEach ( ( ) => {
169
354
timerSandbox = createTimerSandbox ( ) ;
170
355
clock = sinon . useFakeTimers ( ) ;
0 commit comments