11/**
22 * Integration tests for the change-stream observe engine against a REAL MongoDB.
33 *
4- * These require a mongod binary (downloaded on first run by mongodb-memory-server) and start a single-node
5- * replica set (change streams require a replica set). They are slower than the pure-unit tests and live in
6- * the normal suite for now; if they become too slow they can be split into their own jest config.
4+ * Moved out of `server/collections/changeStream/__tests__/` into the shared `integration` jest project:
5+ * instead of booting its own `MongoMemoryReplSet` per file, it connects to the single replica set started
6+ * once by `__mocks__/integration-global-setup.js` (change streams require a replica set). Run via
7+ * `yarn unit:integration`.
78 */
8- import { MongoClient , Collection , ChangeStreamDocument } from 'mongodb'
9- import { MongoMemoryReplSet } from 'mongodb-memory-server'
10- import { protectString , ProtectedString } from '@sofie-automation/corelib/dist/protectedString'
11- import { MongoQuery , MongoFieldSpecifier } from '@sofie-automation/corelib/dist/mongo'
12- import { CollectionChangeFeed , createCollectionChangeStream , ChangeStreamLike } from '../collectionChangeFeed'
13- import { ObserveMultiplexer , ObserveMultiplexerDeps } from '../observeMultiplexer'
14-
15- interface TestDoc {
16- _id : ProtectedString < any >
17- name ?: string
18- val ?: number
19- studioId ?: string
20- }
21- const id = ( s : string ) => protectString < any > ( s )
22-
23- async function waitFor ( predicate : ( ) => boolean , timeoutMs = 5000 ) : Promise < void > {
24- const start = Date . now ( )
25- while ( ! predicate ( ) ) {
26- if ( Date . now ( ) - start > timeoutMs ) throw new Error ( 'waitFor timed out' )
27- await new Promise ( ( r ) => setTimeout ( r , 20 ) )
28- }
29- }
9+ import { MongoClient , ChangeStreamDocument } from 'mongodb'
10+ import {
11+ CollectionChangeFeed ,
12+ createCollectionChangeStream ,
13+ ChangeStreamLike ,
14+ } from '../../collections/changeStream/collectionChangeFeed'
15+ import { ObserveMultiplexer , ObserveMultiplexerDeps } from '../../collections/changeStream/observeMultiplexer'
16+ import { connectSharedMongoClient , freshCollection } from './_integrationDb'
17+ import { TestDoc , id , waitFor , newSink , makeMultiplexer , subscribeChanges } from './_observeHelpers'
3018
3119describe ( 'change-stream engine (integration)' , ( ) => {
32- let replSet : MongoMemoryReplSet
3320 let client : MongoClient
34- let collectionCounter = 0
3521
3622 // Every observe in a test shares this one signal; afterEach aborts it (and beforeEach makes a fresh one),
3723 // which cascades to stopping the underlying change feed (otherwise a closed client would restart-loop forever).
3824 // Future: once we are using vitest, we can use the signal vitest provides, to ensure cleanup happens promptly during a timeout
3925 let observers : AbortController
4026
4127 beforeAll ( async ( ) => {
42- replSet = await MongoMemoryReplSet . create ( { replSet : { count : 1 } } )
43- client = new MongoClient ( replSet . getUri ( ) , { ignoreUndefined : true } )
44- await client . connect ( )
28+ client = await connectSharedMongoClient ( )
4529 } , 120000 )
4630
4731 beforeEach ( ( ) => {
@@ -54,62 +38,13 @@ describe('change-stream engine (integration)', () => {
5438
5539 afterAll ( async ( ) => {
5640 await client ?. close ( )
57- await replSet ?. stop ( )
5841 } )
5942
60- function makeMultiplexer (
61- collection : Collection < any > ,
62- selector : MongoQuery < TestDoc > ,
63- projection : MongoFieldSpecifier < TestDoc > | undefined
64- ) : ObserveMultiplexer < TestDoc > {
65- const feed = new CollectionChangeFeed (
66- collection . collectionName ,
67- ( ) => createCollectionChangeStream ( client , client . db ( ) , collection . collectionName ) ,
68- ( ) => undefined
69- )
70- const deps : ObserveMultiplexerDeps < TestDoc > = {
71- snapshot : async ( ) => collection . find ( selector as any ) . toArray ( ) as unknown as Promise < TestDoc [ ] > ,
72- subscribeFeed : ( onChange , onReconnect ) =>
73- feed . subscribe ( onChange as ( c : ChangeStreamDocument < any > ) => void , onReconnect ) ,
74- }
75- return new ObserveMultiplexer < TestDoc > ( selector , projection , deps , ( ) => undefined )
76- }
77-
78- async function subscribeChanges (
79- m : ObserveMultiplexer < TestDoc > ,
80- sink : {
81- added : Array < [ any , any ] >
82- changed : Array < [ any , any ] >
83- removed : any [ ]
84- }
85- ) {
86- await m . addObserveChangesSubscriber (
87- {
88- added : ( i : any , f : any ) => sink . added . push ( [ i , f ] ) ,
89- changed : ( i : any , f : any ) => sink . changed . push ( [ i , f ] ) ,
90- removed : ( i : any ) => sink . removed . push ( i ) ,
91- } as any ,
92- observers . signal ,
93- false
94- )
95- }
96-
97- function newSink ( ) {
98- return { added : [ ] as Array < [ any , any ] > , changed : [ ] as Array < [ any , any ] > , removed : [ ] as any [ ] }
99- }
100-
101- async function freshCollection ( seed : TestDoc [ ] = [ ] ) : Promise < Collection < any > > {
102- const name = `obs_test_${ collectionCounter ++ } `
103- const collection = client . db ( ) . collection ( name )
104- if ( seed . length ) await collection . insertMany ( seed as any [ ] )
105- return collection
106- }
107-
10843 test ( 'initial snapshot, insert, update, delete' , async ( ) => {
109- const collection = await freshCollection ( [ { _id : id ( 'A' ) , name : 'a' , val : 1 } ] )
44+ const collection = await freshCollection < TestDoc > ( client , [ { _id : id ( 'A' ) , name : 'a' , val : 1 } ] )
11045 const sink = newSink ( )
111- const m = makeMultiplexer ( collection , { } , undefined )
112- await subscribeChanges ( m , sink )
46+ const m = makeMultiplexer ( client , collection , { } , undefined )
47+ await subscribeChanges ( m , sink , observers . signal )
11348
11449 expect ( sink . added ) . toEqual ( [ [ id ( 'A' ) , { name : 'a' , val : 1 } ] ] )
11550
@@ -127,10 +62,10 @@ describe('change-stream engine (integration)', () => {
12762 } )
12863
12964 test ( 'selector enter/leave and projection' , async ( ) => {
130- const collection = await freshCollection ( )
65+ const collection = await freshCollection < TestDoc > ( client )
13166 const sink = newSink ( )
132- const m = makeMultiplexer ( collection , { studioId : 's1' } , { studioId : 1 , val : 1 } )
133- await subscribeChanges ( m , sink )
67+ const m = makeMultiplexer ( client , collection , { studioId : 's1' } , { studioId : 1 , val : 1 } )
68+ await subscribeChanges ( m , sink , observers . signal )
13469
13570 // insert outside selector → nothing; then move it in → added
13671 await collection . insertOne ( { _id : id ( 'X' ) , studioId : 's2' , val : 1 , name : 'secret' } as any )
@@ -152,7 +87,7 @@ describe('change-stream engine (integration)', () => {
15287 } )
15388
15489 test ( 'two observers with identical args share one change stream' , async ( ) => {
155- const collection = await freshCollection ( [ { _id : id ( 'A' ) , val : 1 } ] )
90+ const collection = await freshCollection < TestDoc > ( client , [ { _id : id ( 'A' ) , val : 1 } ] )
15691 let watchCount = 0
15792
15893 const feed = new CollectionChangeFeed (
@@ -192,11 +127,11 @@ describe('change-stream engine (integration)', () => {
192127 } )
193128
194129 test ( 'observe() delivers full documents on add/change/remove' , async ( ) => {
195- const collection = await freshCollection ( [ { _id : id ( 'A' ) , val : 1 } ] )
130+ const collection = await freshCollection < TestDoc > ( client , [ { _id : id ( 'A' ) , val : 1 } ] )
196131 const added : any [ ] = [ ]
197132 const changed : Array < [ any , any ] > = [ ]
198133 const removed : any [ ] = [ ]
199- const m = makeMultiplexer ( collection , { } , undefined )
134+ const m = makeMultiplexer ( client , collection , { } , undefined )
200135 await m . addObserveSubscriber (
201136 {
202137 added : ( d : any ) => added . push ( d ) ,
@@ -222,10 +157,10 @@ describe('change-stream engine (integration)', () => {
222157 } )
223158
224159 test ( 'a replaceOne is observed as a changed transition' , async ( ) => {
225- const collection = await freshCollection ( [ { _id : id ( 'A' ) , name : 'a' , val : 1 } ] )
160+ const collection = await freshCollection < TestDoc > ( client , [ { _id : id ( 'A' ) , name : 'a' , val : 1 } ] )
226161 const sink = newSink ( )
227- const m = makeMultiplexer ( collection , { } , undefined )
228- await subscribeChanges ( m , sink )
162+ const m = makeMultiplexer ( client , collection , { } , undefined )
163+ await subscribeChanges ( m , sink , observers . signal )
229164 expect ( sink . added ) . toEqual ( [ [ id ( 'A' ) , { name : 'a' , val : 1 } ] ] )
230165
231166 await collection . replaceOne ( { _id : id ( 'A' ) } , { name : 'b' , val : 1 } as any )
@@ -234,7 +169,7 @@ describe('change-stream engine (integration)', () => {
234169 } )
235170
236171 test ( 'recovers writes made while the change stream is down (reconnect re-sync)' , async ( ) => {
237- const collection = await freshCollection ( [ { _id : id ( 'A' ) , val : 1 } ] )
172+ const collection = await freshCollection < TestDoc > ( client , [ { _id : id ( 'A' ) , val : 1 } ] )
238173 const sink = newSink ( )
239174
240175 // A controllable stream: it wraps the REAL change stream, but lets the test inject a synthetic
0 commit comments