@@ -142,12 +142,13 @@ describe('queryExpiry', () => {
142142 it ( 'should retry and fail if the timestamp is outside the max ingress expiry (with retry)' , async ( ) => {
143143 const timeDiffMsecs = 6 * MINUTE_TO_MSECS ;
144144 const futureDate = new Date ( now . getTime ( ) + timeDiffMsecs ) ;
145+ const retryTimes = 3 ;
145146
146147 const agent = await HttpAgent . create ( {
147148 host : mockReplica . address ,
148149 rootKey : rootSubnetKeyPair . publicKeyDer ,
149150 identity,
150- retryTimes : 3 ,
151+ retryTimes,
151152 } ) ;
152153 const actor = await createActor ( canisterId , { agent } ) ;
153154 const sender = identity . getPrincipal ( ) ;
@@ -177,17 +178,34 @@ describe('queryExpiry', () => {
177178 // advance to go over the max ingress expiry (5 minutes)
178179 advanceTimeByMilliseconds ( timeDiffMsecs ) ;
179180
180- // Get node key from subnet
181- const { responseBody : readStateResponseBody } = await prepareV3ReadStateResponse ( {
181+ // First call executed in parallel with the query
182+ await mockReadStateNodeKeysResponse ( {
183+ mockReplica,
182184 nodeIdentity,
183- canisterRanges : [ [ canisterId . toUint8Array ( ) , canisterId . toUint8Array ( ) ] ] ,
185+ canisterId,
184186 rootSubnetKeyPair,
185- keyPair : subnetKeyPair ,
187+ subnetKeyPair,
186188 date : futureDate , // we don't want this call to fail in this case, so we return the proper date
187189 } ) ;
188- mockReplica . setV3ReadStateSpyImplOnce ( canisterId . toString ( ) , ( _req , res ) => {
189- res . status ( 200 ) . send ( readStateResponseBody ) ;
190- } ) ;
190+
191+ for ( let i = 0 ; i < retryTimes ; i ++ ) {
192+ await mockReadStateNodeKeysResponse ( {
193+ mockReplica,
194+ nodeIdentity,
195+ canisterId,
196+ rootSubnetKeyPair,
197+ subnetKeyPair,
198+ date : futureDate , // we don't want this call to fail in this case, so we return the proper date
199+ } ) ;
200+
201+ await mockSyncTimeResponse ( {
202+ mockReplica,
203+ rootSubnetKeyPair,
204+ keyPair : subnetKeyPair ,
205+ date : futureDate ,
206+ canisterId,
207+ } ) ;
208+ }
191209
192210 expect . assertions ( 5 ) ;
193211
@@ -198,7 +216,7 @@ describe('queryExpiry', () => {
198216 }
199217
200218 expect ( mockReplica . getV3QuerySpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 4 ) ;
201- expect ( mockReplica . getV3ReadStateSpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 1 ) ;
219+ expect ( mockReplica . getV3ReadStateSpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 10 ) ;
202220 } ) ;
203221
204222 it ( 'should not retry if the timestamp is outside the max ingress expiry (verifyQuerySignatures=false)' , async ( ) => {
@@ -388,6 +406,176 @@ describe('queryExpiry', () => {
388406 const req = mockReplica . getV3QueryReq ( canisterId . toString ( ) , 0 ) ;
389407 expect ( requestIdOf ( req . content ) ) . toEqual ( requestId ) ;
390408 } ) ;
409+
410+ it ( 'should sync time when the query response has an outdated certificate and retry succeeds' , async ( ) => {
411+ const timeDiffMsecs = - ( 6 * MINUTE_TO_MSECS ) ;
412+ const replicaDate = new Date ( now . getTime ( ) + timeDiffMsecs ) ;
413+
414+ const agent = await HttpAgent . create ( {
415+ host : mockReplica . address ,
416+ rootKey : rootSubnetKeyPair . publicKeyDer ,
417+ identity,
418+ retryTimes : 1 ,
419+ } ) ;
420+ const actor = await createActor ( canisterId , { agent } ) ;
421+ const sender = identity . getPrincipal ( ) ;
422+
423+ const { responseBody : outdatedResponse } = await prepareV3QueryResponse ( {
424+ canisterId,
425+ methodName : greetMethodName ,
426+ arg : greetArgs ,
427+ sender,
428+ reply : greetReply ,
429+ nodeIdentity,
430+ date : replicaDate ,
431+ } ) ;
432+ mockReplica . setV3QuerySpyImplOnce ( canisterId . toString ( ) , ( _req , res ) => {
433+ res . status ( 200 ) . send ( outdatedResponse ) ;
434+ } ) ;
435+
436+ await mockReadStateNodeKeysResponse ( {
437+ mockReplica,
438+ nodeIdentity,
439+ canisterId,
440+ rootSubnetKeyPair,
441+ subnetKeyPair,
442+ date : now , // we don't want to make the certificate checks sync the time for this call
443+ } ) ;
444+
445+ await mockSyncTimeResponse ( {
446+ mockReplica,
447+ rootSubnetKeyPair,
448+ keyPair : subnetKeyPair ,
449+ canisterId,
450+ date : now ,
451+ } ) ;
452+
453+ const { responseBody : freshResponse , requestId } = await prepareV3QueryResponse ( {
454+ canisterId,
455+ methodName : greetMethodName ,
456+ arg : greetArgs ,
457+ sender,
458+ reply : greetReply ,
459+ nodeIdentity,
460+ date : now ,
461+ } ) ;
462+ mockReplica . setV3QuerySpyImplOnce ( canisterId . toString ( ) , ( _req , res ) => {
463+ res . status ( 200 ) . send ( freshResponse ) ;
464+ } ) ;
465+
466+ const actorResponse = await actor [ greetMethodName ] ( greetReq ) ;
467+
468+ expect ( actorResponse ) . toEqual ( greetRes ) ;
469+ expect ( mockReplica . getV3QuerySpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 2 ) ;
470+ expect ( mockReplica . getV3ReadStateSpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 4 ) ;
471+ expect ( agent . hasSyncedTime ( ) ) . toBe ( true ) ;
472+
473+ const req = mockReplica . getV3QueryReq ( canisterId . toString ( ) , 1 ) ;
474+ expect ( requestIdOf ( req . content ) ) . toEqual ( requestId ) ;
475+ } ) ;
476+
477+ it ( 'should fail if the query response has an outdated certificate and retry fails' , async ( ) => {
478+ const timeDiffMsecs = - ( 6 * MINUTE_TO_MSECS ) ;
479+ const replicaDate = new Date ( now . getTime ( ) + timeDiffMsecs ) ;
480+
481+ const agent = await HttpAgent . create ( {
482+ host : mockReplica . address ,
483+ rootKey : rootSubnetKeyPair . publicKeyDer ,
484+ identity,
485+ retryTimes : 1 ,
486+ } ) ;
487+ const actor = await createActor ( canisterId , { agent } ) ;
488+ const sender = identity . getPrincipal ( ) ;
489+
490+ const { responseBody : outdatedResponse } = await prepareV3QueryResponse ( {
491+ canisterId,
492+ methodName : greetMethodName ,
493+ arg : greetArgs ,
494+ sender,
495+ reply : greetReply ,
496+ nodeIdentity,
497+ date : replicaDate ,
498+ } ) ;
499+ mockReplica . setV3QuerySpyImplOnce ( canisterId . toString ( ) , ( _req , res ) => {
500+ res . status ( 200 ) . send ( outdatedResponse ) ;
501+ } ) ;
502+ mockReplica . setV3QuerySpyImplOnce ( canisterId . toString ( ) , ( _req , res ) => {
503+ res . status ( 200 ) . send ( outdatedResponse ) ;
504+ } ) ;
505+
506+ await mockReadStateNodeKeysResponse ( {
507+ mockReplica,
508+ nodeIdentity,
509+ canisterId,
510+ rootSubnetKeyPair,
511+ subnetKeyPair,
512+ date : now , // we don't want to make the certificate sync the time for this call
513+ } ) ;
514+
515+ await mockSyncTimeResponse ( {
516+ mockReplica,
517+ rootSubnetKeyPair,
518+ keyPair : subnetKeyPair ,
519+ canisterId,
520+ date : now ,
521+ } ) ;
522+
523+ expect . assertions ( 6 ) ;
524+
525+ try {
526+ await actor [ greetMethodName ] ( greetReq ) ;
527+ } catch ( e ) {
528+ expectCertificateOutdatedError ( e ) ;
529+ }
530+
531+ expect ( mockReplica . getV3QuerySpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 2 ) ;
532+ expect ( mockReplica . getV3ReadStateSpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 4 ) ;
533+ expect ( agent . hasSyncedTime ( ) ) . toBe ( true ) ;
534+ } ) ;
535+
536+ it ( 'should not sync time if the query signature verification is disabled' , async ( ) => {
537+ const timeDiffMsecs = - ( 6 * MINUTE_TO_MSECS ) ;
538+ const replicaDate = new Date ( now . getTime ( ) + timeDiffMsecs ) ;
539+
540+ const agent = await HttpAgent . create ( {
541+ host : mockReplica . address ,
542+ rootKey : rootSubnetKeyPair . publicKeyDer ,
543+ identity,
544+ verifyQuerySignatures : false ,
545+ } ) ;
546+ const actor = await createActor ( canisterId , { agent } ) ;
547+ const sender = identity . getPrincipal ( ) ;
548+
549+ const { responseBody } = await prepareV3QueryResponse ( {
550+ canisterId,
551+ methodName : greetMethodName ,
552+ arg : greetArgs ,
553+ sender,
554+ reply : greetReply ,
555+ nodeIdentity,
556+ date : replicaDate ,
557+ } ) ;
558+ mockReplica . setV3QuerySpyImplOnce ( canisterId . toString ( ) , ( _req , res ) => {
559+ res . status ( 200 ) . send ( responseBody ) ;
560+ } ) ;
561+
562+ // Just mock the read state, but we don't expect this to be called
563+ await mockReadStateNodeKeysResponse ( {
564+ mockReplica,
565+ nodeIdentity,
566+ canisterId,
567+ rootSubnetKeyPair,
568+ subnetKeyPair,
569+ date : now ,
570+ } ) ;
571+
572+ const actorResponse = await actor [ greetMethodName ] ( greetReq ) ;
573+
574+ expect ( actorResponse ) . toEqual ( greetRes ) ;
575+ expect ( mockReplica . getV3QuerySpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 1 ) ;
576+ expect ( mockReplica . getV3ReadStateSpy ( canisterId . toString ( ) ) ) . toHaveBeenCalledTimes ( 0 ) ;
577+ expect ( agent . hasSyncedTime ( ) ) . toBe ( false ) ;
578+ } ) ;
391579} ) ;
392580
393581function advanceTimeByMilliseconds ( milliseconds : number ) {
0 commit comments