@@ -1344,4 +1344,148 @@ describe("internal adapter test", async () => {
13441344 expect ( ttl ) . toBeLessThanOrEqual ( 300 ) ;
13451345 } ) ;
13461346 } ) ;
1347+
1348+ describe ( "safeJSONParse date revival in secondary storage" , ( ) => {
1349+ /**
1350+ * Simulates a Redis client that auto-parses JSON (e.g. ioredis with
1351+ * certain configurations). The `get` method returns a pre-parsed object
1352+ * where date fields are still ISO 8601 strings, not Date instances.
1353+ */
1354+ function createPreParsedStorage ( ) {
1355+ const dataMap = new Map < string , any > ( ) ;
1356+ const ttlMap = new Map < string , number > ( ) ;
1357+ return {
1358+ dataMap,
1359+ ttlMap,
1360+ storage : {
1361+ set ( key : string , value : string , ttl ?: number ) {
1362+ // Store as pre-parsed object (simulating Redis auto-parse)
1363+ dataMap . set ( key , JSON . parse ( value ) ) ;
1364+ if ( ttl ) ttlMap . set ( key , ttl ) ;
1365+ } ,
1366+ get ( key : string ) {
1367+ return dataMap . get ( key ) ?? null ;
1368+ } ,
1369+ delete ( key : string ) {
1370+ dataMap . delete ( key ) ;
1371+ ttlMap . delete ( key ) ;
1372+ } ,
1373+ } ,
1374+ } ;
1375+ }
1376+
1377+ it ( "should return Date objects from findVerificationValue when storage returns pre-parsed objects" , async ( ) => {
1378+ const { storage } = createPreParsedStorage ( ) ;
1379+
1380+ const opts = {
1381+ database : new DatabaseSync ( ":memory:" ) ,
1382+ secondaryStorage : storage ,
1383+ } satisfies BetterAuthOptions ;
1384+
1385+ ( await getMigrations ( opts ) ) . runMigrations ( ) ;
1386+ const ctx = await init ( opts ) ;
1387+
1388+ await ctx . internalAdapter . createVerificationValue ( {
1389+ identifier : "date-test" ,
1390+ value : "test-value" ,
1391+ expiresAt : new Date ( Date . now ( ) + 60000 ) ,
1392+ } ) ;
1393+
1394+ const found =
1395+ await ctx . internalAdapter . findVerificationValue ( "date-test" ) ;
1396+ expect ( found ) . not . toBeNull ( ) ;
1397+ expect ( found ! . expiresAt ) . toBeInstanceOf ( Date ) ;
1398+ expect ( found ! . createdAt ) . toBeInstanceOf ( Date ) ;
1399+ expect ( found ! . updatedAt ) . toBeInstanceOf ( Date ) ;
1400+ } ) ;
1401+
1402+ it ( "should correctly detect expired verification when storage returns pre-parsed objects" , async ( ) => {
1403+ const { storage } = createPreParsedStorage ( ) ;
1404+
1405+ const opts = {
1406+ database : new DatabaseSync ( ":memory:" ) ,
1407+ secondaryStorage : storage ,
1408+ } satisfies BetterAuthOptions ;
1409+
1410+ await ( await getMigrations ( opts ) ) . runMigrations ( ) ;
1411+ const ctx = await init ( opts ) ;
1412+
1413+ await ctx . internalAdapter . createVerificationValue ( {
1414+ identifier : "expiry-check" ,
1415+ value : "test-value" ,
1416+ expiresAt : new Date ( Date . now ( ) + 60000 ) ,
1417+ } ) ;
1418+
1419+ const found =
1420+ await ctx . internalAdapter . findVerificationValue ( "expiry-check" ) ;
1421+ expect ( found ) . not . toBeNull ( ) ;
1422+ // This comparison would silently fail if expiresAt were a string
1423+ // because string < Date coerces to NaN, making it always false
1424+ expect ( found ! . expiresAt > new Date ( ) ) . toBe ( true ) ;
1425+ expect ( found ! . expiresAt < new Date ( Date . now ( ) + 120000 ) ) . toBe ( true ) ;
1426+ } ) ;
1427+
1428+ it ( "should return Date objects for all date fields across multiple reads" , async ( ) => {
1429+ const { storage } = createPreParsedStorage ( ) ;
1430+
1431+ const opts = {
1432+ database : new DatabaseSync ( ":memory:" ) ,
1433+ secondaryStorage : storage ,
1434+ } satisfies BetterAuthOptions ;
1435+
1436+ await ( await getMigrations ( opts ) ) . runMigrations ( ) ;
1437+ const ctx = await init ( opts ) ;
1438+
1439+ const expiresAt = new Date ( Date . now ( ) + 60000 ) ;
1440+ await ctx . internalAdapter . createVerificationValue ( {
1441+ identifier : "multi-read-test" ,
1442+ value : "test-value" ,
1443+ expiresAt,
1444+ } ) ;
1445+
1446+ // First read: safeJSONParse receives pre-parsed object from storage
1447+ const first =
1448+ await ctx . internalAdapter . findVerificationValue ( "multi-read-test" ) ;
1449+ expect ( first ) . not . toBeNull ( ) ;
1450+ expect ( first ! . expiresAt ) . toBeInstanceOf ( Date ) ;
1451+ expect ( first ! . createdAt ) . toBeInstanceOf ( Date ) ;
1452+ expect ( first ! . updatedAt ) . toBeInstanceOf ( Date ) ;
1453+
1454+ // Second read: verify consistency (the stored object wasn't mutated)
1455+ const second =
1456+ await ctx . internalAdapter . findVerificationValue ( "multi-read-test" ) ;
1457+ expect ( second ) . not . toBeNull ( ) ;
1458+ expect ( second ! . expiresAt ) . toBeInstanceOf ( Date ) ;
1459+ expect ( second ! . expiresAt . getTime ( ) ) . toBe ( first ! . expiresAt . getTime ( ) ) ;
1460+ } ) ;
1461+
1462+ it ( "should preserve non-date string fields when reviving dates" , async ( ) => {
1463+ const { storage } = createPreParsedStorage ( ) ;
1464+
1465+ const opts = {
1466+ database : new DatabaseSync ( ":memory:" ) ,
1467+ secondaryStorage : storage ,
1468+ } satisfies BetterAuthOptions ;
1469+
1470+ await ( await getMigrations ( opts ) ) . runMigrations ( ) ;
1471+ const ctx = await init ( opts ) ;
1472+
1473+ await ctx . internalAdapter . createVerificationValue ( {
1474+ identifier : "string-field-test" ,
1475+ value : "my-token-value-123" ,
1476+ expiresAt : new Date ( Date . now ( ) + 60000 ) ,
1477+ } ) ;
1478+
1479+ const found =
1480+ await ctx . internalAdapter . findVerificationValue ( "string-field-test" ) ;
1481+ expect ( found ) . not . toBeNull ( ) ;
1482+ // Non-date strings must NOT be converted
1483+ expect ( found ! . identifier ) . toBe ( "string-field-test" ) ;
1484+ expect ( typeof found ! . identifier ) . toBe ( "string" ) ;
1485+ expect ( found ! . value ) . toBe ( "my-token-value-123" ) ;
1486+ expect ( typeof found ! . value ) . toBe ( "string" ) ;
1487+ // Date strings MUST be converted
1488+ expect ( found ! . expiresAt ) . toBeInstanceOf ( Date ) ;
1489+ } ) ;
1490+ } ) ;
13471491} ) ;
0 commit comments