@@ -227,7 +227,7 @@ describe('CredentialProvider', () => {
227227 } ) ;
228228
229229 await provider . syncCredentialStatus ( { forceFetch : true } ) ;
230-
230+
231231 // Clear mocks
232232 jest . clearAllMocks ( ) ;
233233
@@ -255,6 +255,196 @@ describe('CredentialProvider', () => {
255255 expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 2 ) ;
256256 } ) ;
257257
258+ it ( 'should always refetch credentials with Invalid status even when cached' , async ( ) => {
259+ // First, create credentials with Invalid status
260+ jest
261+ . spyOn ( credentialServiceRPC , 'verifyCredential' )
262+ . mockImplementation ( async ( ) => {
263+ return {
264+ verified : false ,
265+ error : 'Credential validation failed' ,
266+ } ;
267+ } ) ;
268+
269+ await provider . syncCredentialStatus ( { forceFetch : true } ) ;
270+
271+ // Verify initial calls
272+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 2 ) ;
273+
274+ // Check that status is set to Invalid
275+ const initialStatusDoc = await wallet . getDocumentById (
276+ `${ customerCredential . id } #status` ,
277+ ) ;
278+ expect ( initialStatusDoc . status ) . toBe ( CredentialStatus . Invalid ) ;
279+
280+ // Clear mocks to track only subsequent calls
281+ jest . clearAllMocks ( ) ;
282+
283+ // Now mock successful verification
284+ jest
285+ . spyOn ( credentialServiceRPC , 'verifyCredential' )
286+ . mockImplementation ( async ( ) => {
287+ return {
288+ verified : true ,
289+ } ;
290+ } ) ;
291+
292+ // Call syncCredentialStatus again WITHOUT forceFetch
293+ // Invalid status should trigger refetch even without forceFetch
294+ const statusDocs = await provider . syncCredentialStatus ( { } ) ;
295+
296+ // Verify that verifyCredential was called again despite no forceFetch
297+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 2 ) ;
298+
299+ // Check that status is now updated to Verified
300+ expect ( statusDocs . length ) . toBe ( 2 ) ;
301+ for ( const statusDoc of statusDocs ) {
302+ expect ( statusDoc . status ) . toBe ( CredentialStatus . Verified ) ;
303+ }
304+ } ) ;
305+
306+ it ( 'should always refetch credentials with Pending status even when cached' , async ( ) => {
307+ // First, manually create a Pending status document
308+ const pendingStatusDoc = {
309+ type : 'CredentialStatus' ,
310+ id : `${ customerCredential . id } #status` ,
311+ createdAt : new Date ( ) . toISOString ( ) ,
312+ updatedAt : new Date ( ) . toISOString ( ) ,
313+ status : CredentialStatus . Pending ,
314+ error : null ,
315+ warning : null ,
316+ } ;
317+ await wallet . updateDocument ( pendingStatusDoc ) ;
318+
319+ // Mock successful verification
320+ jest
321+ . spyOn ( credentialServiceRPC , 'verifyCredential' )
322+ . mockImplementation ( async ( ) => {
323+ return {
324+ verified : true ,
325+ } ;
326+ } ) ;
327+
328+ // Call syncCredentialStatus without forceFetch
329+ // Pending status should trigger refetch
330+ const statusDocs = await provider . syncCredentialStatus ( {
331+ credentialIds : [ customerCredential . id ] ,
332+ } ) ;
333+
334+ // Verify that verifyCredential was called for the Pending credential
335+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 1 ) ;
336+
337+ // Check that status is now updated to Verified
338+ expect ( statusDocs . length ) . toBe ( 1 ) ;
339+ expect ( statusDocs [ 0 ] . status ) . toBe ( CredentialStatus . Verified ) ;
340+ } ) ;
341+
342+ it ( 'should cache Revoked status and not refetch within 24 hours' , async ( ) => {
343+ // First, create credentials with Revoked status
344+ jest
345+ . spyOn ( credentialServiceRPC , 'verifyCredential' )
346+ . mockImplementation ( async ( ) => {
347+ return {
348+ verified : false ,
349+ error : 'Revocation check failed' ,
350+ } ;
351+ } ) ;
352+
353+ await provider . syncCredentialStatus ( { forceFetch : true } ) ;
354+
355+ // Verify initial calls
356+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 2 ) ;
357+
358+ // Check that status is set to Revoked
359+ const revokedStatusDoc = await wallet . getDocumentById (
360+ `${ customerCredential . id } #status` ,
361+ ) ;
362+ expect ( revokedStatusDoc . status ) . toBe ( CredentialStatus . Revoked ) ;
363+
364+ // Clear mocks to track only subsequent calls
365+ jest . clearAllMocks ( ) ;
366+
367+ // Call syncCredentialStatus again WITHOUT forceFetch
368+ // Revoked status should NOT trigger refetch within 24 hours
369+ const statusDocs = await provider . syncCredentialStatus ( { } ) ;
370+
371+ // Verify that verifyCredential was NOT called (cached)
372+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 0 ) ;
373+
374+ // Check that status remains Revoked
375+ expect ( statusDocs . length ) . toBe ( 2 ) ;
376+ for ( const statusDoc of statusDocs ) {
377+ expect ( statusDoc . status ) . toBe ( CredentialStatus . Revoked ) ;
378+ }
379+ } ) ;
380+
381+ it ( 'should cache Verified status and not refetch within 24 hours' , async ( ) => {
382+ // First, create credentials with Verified status
383+ jest
384+ . spyOn ( credentialServiceRPC , 'verifyCredential' )
385+ . mockImplementation ( async ( ) => {
386+ return {
387+ verified : true ,
388+ } ;
389+ } ) ;
390+
391+ await provider . syncCredentialStatus ( { forceFetch : true } ) ;
392+
393+ // Verify initial calls
394+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 2 ) ;
395+
396+ // Check that status is set to Verified
397+ const verifiedStatusDoc = await wallet . getDocumentById (
398+ `${ customerCredential . id } #status` ,
399+ ) ;
400+ expect ( verifiedStatusDoc . status ) . toBe ( CredentialStatus . Verified ) ;
401+
402+ // Clear mocks to track only subsequent calls
403+ jest . clearAllMocks ( ) ;
404+
405+ // Call syncCredentialStatus again WITHOUT forceFetch
406+ // Verified status should NOT trigger refetch within 24 hours
407+ const statusDocs = await provider . syncCredentialStatus ( { } ) ;
408+
409+ // Verify that verifyCredential was NOT called (cached)
410+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 0 ) ;
411+
412+ // Check that status remains Verified
413+ expect ( statusDocs . length ) . toBe ( 2 ) ;
414+ for ( const statusDoc of statusDocs ) {
415+ expect ( statusDoc . status ) . toBe ( CredentialStatus . Verified ) ;
416+ }
417+ } ) ;
418+
419+ it ( 'should refetch credentials without status document' , async ( ) => {
420+ // Remove any existing status documents
421+ await wallet . removeDocument ( `${ customerCredential . id } #status` ) . catch ( ( ) => { } ) ;
422+ await wallet . removeDocument ( `${ biometricsBBSRevocation . id } #status` ) . catch ( ( ) => { } ) ;
423+
424+ // Mock successful verification
425+ jest
426+ . spyOn ( credentialServiceRPC , 'verifyCredential' )
427+ . mockImplementation ( async ( ) => {
428+ return {
429+ verified : true ,
430+ } ;
431+ } ) ;
432+
433+ // Call syncCredentialStatus
434+ // Missing status docs should trigger fetch
435+ const statusDocs = await provider . syncCredentialStatus ( { } ) ;
436+
437+ // Verify that verifyCredential was called for both credentials
438+ expect ( credentialServiceRPC . verifyCredential ) . toHaveBeenCalledTimes ( 2 ) ;
439+
440+ // Check that status documents were created
441+ expect ( statusDocs . length ) . toBe ( 2 ) ;
442+ for ( const statusDoc of statusDocs ) {
443+ expect ( statusDoc . status ) . toBe ( CredentialStatus . Verified ) ;
444+ expect ( statusDoc . type ) . toBe ( 'CredentialStatus' ) ;
445+ }
446+ } ) ;
447+
258448 afterEach ( ( ) => {
259449 ( credentialServiceRPC . verifyCredential as any ) . mockReset ( ) ;
260450 } ) ;
0 commit comments