@@ -14,12 +14,10 @@ import { PRODUCT_RESPONSE_HEADER } from '@kbn/core-elasticsearch-client-server-i
1414import { lazyObject } from '@kbn/lazy-object' ;
1515
1616const omittedProps = [
17- 'diagnostic' ,
1817 'name' ,
1918 'connectionPool' ,
2019 'transport' ,
2120 'serializer' ,
22- 'helpers' ,
2321 'acceptedParams' ,
2422] as Array < PublicKeys < Client > > ;
2523
@@ -57,6 +55,7 @@ export interface ClientApiMockInstance<T, Y extends any[]> extends jest.MockInst
5755 ) : this;
5856}
5957
58+ // Helper to create a jest mock function with response helpers
6059const createMockedApi = <
6160 T = unknown ,
6261 Y extends [ any , TransportRequestOptions ] = [ any , TransportRequestOptions ]
@@ -122,67 +121,176 @@ const createMockedApi = <
122121 return mock ;
123122} ;
124123
124+ // Build a shape of the Elasticsearch client once, using a hoisted real client instance
125125// use jest.requireActual() to prevent weird errors when people mock @elastic/elasticsearch
126126const { Client : UnmockedClient } = jest . requireActual ( '@elastic/elasticsearch' ) ;
127- const createInternalClientMock = ( res ?: Promise < unknown > ) : DeeplyMockedApi < Client > => {
128- // we mimic 'reflection' on a concrete instance of the client to generate the mocked functions.
129- const client = new UnmockedClient ( {
130- node : 'http://127.0.0.1' ,
131- } ) ;
132127
133- const getAllPropertyDescriptors = ( obj : Record < string , any > ) => {
134- const descriptors = Object . entries ( Object . getOwnPropertyDescriptors ( obj ) ) ;
135- let prototype = Object . getPrototypeOf ( obj ) ;
136- while ( prototype != null && prototype !== Object . prototype ) {
137- descriptors . push ( ...Object . entries ( Object . getOwnPropertyDescriptors ( prototype ) ) ) ;
138- prototype = Object . getPrototypeOf ( prototype ) ;
128+ type ShapeNode = { type : 'method' } | { type : 'object' ; props : Record < string , ShapeNode > } ;
129+
130+ let cachedShape : ShapeNode | null = null ;
131+
132+ function getAllPropertyDescriptors ( obj : Record < string , any > ) {
133+ const map : Record < string , PropertyDescriptor > = { } ;
134+ let cur : any = obj ;
135+ while ( cur && cur !== Object . prototype ) {
136+ const descs = Object . getOwnPropertyDescriptors ( cur ) ;
137+ for ( const [ k , d ] of Object . entries ( descs ) ) {
138+ if ( ! ( k in map ) ) map [ k ] = d ;
139139 }
140- return descriptors ;
141- } ;
140+ cur = Object . getPrototypeOf ( cur ) ;
141+ }
142+ return map ;
143+ }
142144
143- const mockify = ( obj : Record < string , any > , omitted : string [ ] = [ ] ) => {
144- // the @elastic /elasticsearch::Client uses prototypical inheritance
145- // so we have to crawl up the prototype chain and get all descriptors
146- // to find everything that we should be mocking
147- const descriptors = getAllPropertyDescriptors ( obj ) ;
148- descriptors
149- . filter ( ( [ key ] ) => ! omitted . includes ( key ) )
150- . forEach ( ( [ key , descriptor ] ) => {
151- if ( typeof descriptor . value === 'function' ) {
152- const mock = createMockedApi ( ) ;
153- mock . mockImplementation ( ( ) => res ?? createSuccessTransportRequestPromise ( { } ) ) ;
154- obj [ key ] = mock ;
155- } else if ( typeof obj [ key ] === 'object' && obj [ key ] != null ) {
156- mockify ( obj [ key ] , omitted ) ;
157- }
158- } ) ;
159- } ;
145+ function buildShapeRecursive ( obj : any , isTopLevel : boolean , seen : WeakSet < object > ) : ShapeNode {
146+ const props : Record < string , ShapeNode > = { } ;
147+ const descriptors = getAllPropertyDescriptors ( obj ) ;
148+ for ( const [ key , desc ] of Object . entries ( descriptors ) ) {
149+ if ( key === 'constructor' ) continue ;
150+ if ( isTopLevel && ( omittedProps as string [ ] ) . includes ( key ) ) continue ;
151+
152+ let value : any ;
153+ if ( 'value' in desc ) value = ( desc as any ) . value ;
154+ else if ( typeof desc . get === 'function' ) {
155+ try {
156+ value = desc . get . call ( obj ) ;
157+ } catch {
158+ value = undefined ;
159+ }
160+ }
160161
161- mockify ( client , omittedProps as string [ ] ) ;
162+ if ( typeof value === 'function' ) {
163+ props [ key ] = { type : 'method' } ;
164+ } else if ( value && typeof value === 'object' ) {
165+ if ( seen . has ( value ) ) continue ;
166+ seen . add ( value ) ;
167+ props [ key ] = buildShapeRecursive ( value , false , seen ) ;
168+ }
169+ }
170+ return { type : 'object' , props } ;
171+ }
172+
173+ function getClientShape ( ) : ShapeNode {
174+ if ( cachedShape ) return cachedShape ;
175+ const client = new UnmockedClient ( { node : 'http://127.0.0.1' } ) ;
176+ try {
177+ const shape = buildShapeRecursive ( client , true , new WeakSet ( ) ) ;
178+ cachedShape = shape ;
179+ return shape ;
180+ } finally {
181+ try {
182+ // ensure we close the actual client instance (ignore errors)
183+ void client . close ( ) ;
184+ } catch {
185+ // ignore
186+ }
187+ }
188+ }
162189
163- client . close = jest . fn ( ) . mockReturnValue ( Promise . resolve ( ) ) ;
164- client . child = jest . fn ( ) . mockImplementation ( ( ) => createInternalClientMock ( ) ) ;
190+ function buildLazyMockFromShape ( shape : ShapeNode , res ?: Promise < unknown > ) : any {
191+ if ( shape . type !== 'object' ) return { } ;
192+ const target : Record < string , any > = { } ;
165193
166- const mockGetter = ( obj : Record < string , any > , propertyName : string ) => {
167- Object . defineProperty ( obj , propertyName , {
194+ const defineLazyMethod = ( obj : Record < string , any > , key : string ) => {
195+ Object . defineProperty ( obj , key , {
168196 configurable : true ,
169- enumerable : false ,
170- get : ( ) => jest . fn ( ) ,
171- set : undefined ,
197+ enumerable : true ,
198+ get ( ) {
199+ const fn = createMockedApi ( ) ;
200+ fn . mockImplementation ( ( ) => res ?? createSuccessTransportRequestPromise ( { } ) ) ;
201+ Object . defineProperty ( obj , key , {
202+ value : fn ,
203+ configurable : true ,
204+ enumerable : true ,
205+ writable : true ,
206+ } ) ;
207+ return fn ;
208+ } ,
209+ set ( value ) {
210+ Object . defineProperty ( obj , key , {
211+ value,
212+ configurable : true ,
213+ enumerable : true ,
214+ writable : true ,
215+ } ) ;
216+ } ,
172217 } ) ;
173218 } ;
174219
175- // `on`, `off`, and `once` are properties without a setter.
176- // We can't `client.diagnostic.on = jest.fn()` because the following error will be thrown:
177- // TypeError: Cannot set property on of #<Client> which has only a getter
178- mockGetter ( client . diagnostic , 'on' ) ;
179- mockGetter ( client . diagnostic , 'off' ) ;
180- mockGetter ( client . diagnostic , 'once' ) ;
181- client . transport = {
182- request : jest . fn ( ) ,
220+ const defineLazyObject = ( obj : Record < string , any > , key : string , childShape : ShapeNode ) => {
221+ Object . defineProperty ( obj , key , {
222+ configurable : true ,
223+ enumerable : true ,
224+ get ( ) {
225+ const child = buildLazyMockFromShape ( childShape , res ) ;
226+ Object . defineProperty ( obj , key , {
227+ value : child ,
228+ configurable : true ,
229+ enumerable : true ,
230+ writable : true ,
231+ } ) ;
232+ return child ;
233+ } ,
234+ } ) ;
183235 } ;
184236
185- return client as DeeplyMockedApi < Client > ;
237+ for ( const [ key , node ] of Object . entries ( shape . props ) ) {
238+ if ( node . type === 'method' ) {
239+ defineLazyMethod ( target , key ) ;
240+ } else if ( node . type === 'object' ) {
241+ defineLazyObject ( target , key , node ) ;
242+ }
243+ }
244+
245+ // Special cases based on prior behavior
246+ Object . defineProperty ( target , 'diagnostic' , {
247+ configurable : true ,
248+ enumerable : false ,
249+ get ( ) {
250+ const d : any = { } ;
251+ for ( const k of [ 'on' , 'off' , 'once' ] ) {
252+ Object . defineProperty ( d , k , {
253+ configurable : true ,
254+ enumerable : false ,
255+ get : ( ) => jest . fn ( ) ,
256+ } ) ;
257+ }
258+ Object . defineProperty ( target , 'diagnostic' , {
259+ value : d ,
260+ configurable : true ,
261+ enumerable : false ,
262+ writable : true ,
263+ } ) ;
264+ return d ;
265+ } ,
266+ } ) ;
267+
268+ Object . defineProperty ( target , 'transport' , {
269+ configurable : true ,
270+ enumerable : true ,
271+ get ( ) {
272+ const t = { request : jest . fn ( ) } as any ;
273+ Object . defineProperty ( target , 'transport' , {
274+ value : t ,
275+ configurable : true ,
276+ enumerable : true ,
277+ writable : true ,
278+ } ) ;
279+ return t ;
280+ } ,
281+ } ) ;
282+
283+ return target ;
284+ }
285+
286+ const createInternalClientMock = ( res ?: Promise < unknown > ) : DeeplyMockedApi < Client > => {
287+ const shape = getClientShape ( ) ;
288+ const mockClient : any = buildLazyMockFromShape ( shape , res ) ;
289+
290+ mockClient . close = jest . fn ( ) . mockReturnValue ( Promise . resolve ( ) ) ;
291+ mockClient . child = jest . fn ( ) . mockImplementation ( ( ) => createInternalClientMock ( ) ) ;
292+
293+ return mockClient as DeeplyMockedApi < Client > ;
186294} ;
187295
188296export type ElasticsearchClientMock = DeeplyMockedApi < ElasticsearchClient > ;
@@ -226,7 +334,7 @@ const createCustomClusterClientMock = () => {
226334 const mock : CustomClusterClientMock = lazyObject ( {
227335 asInternalUser : createClientMock ( ) ,
228336 asScoped : jest . fn ( ) . mockReturnValue ( createScopedClusterClientMock ( ) ) ,
229- close : jest . fn ( ) . mockResolvedValue ( Promise . resolve ( ) ) ,
337+ close : jest . fn ( ) . mockReturnValue ( Promise . resolve ( ) ) ,
230338 } ) ;
231339
232340 return mock ;
0 commit comments