@@ -22,10 +22,13 @@ import {
2222 serializeRRule ,
2323 convertHumanReadableFrequencyToMachineReadable ,
2424 convertMachineReadableFrequencyToHumanReadable ,
25- getUnsupportedRRuleReason
25+ getUnsupportedRRuleReason ,
26+ prepareDataForHashing ,
27+ generateCryptoHash
2628} from './utils'
27- import { CoinTransaction , ParsedResultType } from './types'
29+ import { CoinTransaction , ParsedResultType , Settings , HabitsData , CoinsData , WishlistData , UserData } from './types'
2830import { DateTime } from "luxon" ;
31+ import { getDefaultSettings , getDefaultHabitsData , getDefaultCoinsData , getDefaultWishlistData , getDefaultUsersData } from './types' ;
2932import { RRule , Weekday } from 'rrule' ;
3033import { Habit } from '@/lib/types' ;
3134import { INITIAL_DUE } from './constants' ;
@@ -956,3 +959,96 @@ describe('convertMachineReadableFrequencyToHumanReadable', () => {
956959 expect ( humanReadable ) . toBe ( 'invalid' )
957960 } )
958961} )
962+
963+ describe ( 'freshness utilities' , ( ) => {
964+ const mockSettings : Settings = getDefaultSettings ( ) ;
965+ const mockHabits : HabitsData = getDefaultHabitsData ( ) ;
966+ const mockCoins : CoinsData = getDefaultCoinsData ( ) ;
967+ const mockWishlist : WishlistData = getDefaultWishlistData ( ) ;
968+ const mockUsers : UserData = getDefaultUsersData ( ) ;
969+
970+ // Add a user to mockUsers for more realistic testing
971+ mockUsers . users . push ( {
972+ id : 'user-123' ,
973+ username : 'testuser' ,
974+ isAdmin : false ,
975+ } ) ;
976+ mockHabits . habits . push ( {
977+ id : 'habit-123' ,
978+ name : 'Test Habit' ,
979+ description : 'A habit for testing' ,
980+ frequency : 'FREQ=DAILY' ,
981+ coinReward : 10 ,
982+ completions : [ ] ,
983+ userIds : [ 'user-123' ]
984+ } ) ;
985+
986+
987+ describe ( 'prepareDataForHashing' , ( ) => {
988+ test ( 'should produce a consistent string for the same data' , ( ) => {
989+ const data1 = { settings : mockSettings , habits : mockHabits , coins : mockCoins , wishlist : mockWishlist , users : mockUsers } ;
990+ const data2 = { settings : mockSettings , habits : mockHabits , coins : mockCoins , wishlist : mockWishlist , users : mockUsers } ; // Identical data
991+
992+ const string1 = prepareDataForHashing ( data1 . settings , data1 . habits , data1 . coins , data1 . wishlist , data1 . users ) ;
993+ const string2 = prepareDataForHashing ( data2 . settings , data2 . habits , data2 . coins , data2 . wishlist , data2 . users ) ;
994+
995+ expect ( string1 ) . toBe ( string2 ) ;
996+ } ) ;
997+
998+ test ( 'should produce a different string if settings data changes' , ( ) => {
999+ const string1 = prepareDataForHashing ( mockSettings , mockHabits , mockCoins , mockWishlist , mockUsers ) ;
1000+ const modifiedSettings = { ...mockSettings , system : { ...mockSettings . system , timezone : 'America/Chicago' } } ;
1001+ const string2 = prepareDataForHashing ( modifiedSettings , mockHabits , mockCoins , mockWishlist , mockUsers ) ;
1002+ expect ( string1 ) . not . toBe ( string2 ) ;
1003+ } ) ;
1004+
1005+ test ( 'should produce a different string if habits data changes' , ( ) => {
1006+ const string1 = prepareDataForHashing ( mockSettings , mockHabits , mockCoins , mockWishlist , mockUsers ) ;
1007+ const modifiedHabits = { ...mockHabits , habits : [ ...mockHabits . habits , { id : 'new-habit' , name : 'New' , description : '' , frequency : 'FREQ=DAILY' , coinReward : 5 , completions : [ ] } ] } ;
1008+ const string2 = prepareDataForHashing ( mockSettings , modifiedHabits , mockCoins , mockWishlist , mockUsers ) ;
1009+ expect ( string1 ) . not . toBe ( string2 ) ;
1010+ } ) ;
1011+
1012+ test ( 'should handle empty data consistently' , ( ) => {
1013+ const emptySettings = getDefaultSettings ( ) ;
1014+ const emptyHabits = getDefaultHabitsData ( ) ;
1015+ const emptyCoins = getDefaultCoinsData ( ) ;
1016+ const emptyWishlist = getDefaultWishlistData ( ) ;
1017+ const emptyUsers = getDefaultUsersData ( ) ;
1018+
1019+ const string1 = prepareDataForHashing ( emptySettings , emptyHabits , emptyCoins , emptyWishlist , emptyUsers ) ;
1020+ const string2 = prepareDataForHashing ( emptySettings , emptyHabits , emptyCoins , emptyWishlist , emptyUsers ) ;
1021+ expect ( string1 ) . toBe ( string2 ) ;
1022+ expect ( string1 ) . toBeDefined ( ) ;
1023+ } ) ;
1024+ } ) ;
1025+
1026+ describe ( 'generateCryptoHash' , ( ) => {
1027+ test ( 'should generate a SHA-256 hex string' , async ( ) => {
1028+ const dataString = 'test string' ;
1029+ const hash = await generateCryptoHash ( dataString ) ;
1030+ expect ( hash ) . toMatch ( / ^ [ a - f 0 - 9 ] { 64 } $ / ) ; // SHA-256 hex is 64 chars
1031+ } ) ;
1032+
1033+ test ( 'should generate different hashes for different strings' , async ( ) => {
1034+ const hash1 = await generateCryptoHash ( 'test string 1' ) ;
1035+ const hash2 = await generateCryptoHash ( 'test string 2' ) ;
1036+ expect ( hash1 ) . not . toBe ( hash2 ) ;
1037+ } ) ;
1038+
1039+ test ( 'should generate the same hash for the same string' , async ( ) => {
1040+ const hash1 = await generateCryptoHash ( 'consistent string' ) ;
1041+ const hash2 = await generateCryptoHash ( 'consistent string' ) ;
1042+ expect ( hash1 ) . toBe ( hash2 ) ;
1043+ } ) ;
1044+
1045+ // Test with a known SHA-256 value if possible, or ensure crypto.subtle.digest is available
1046+ // For "hello world", SHA-256 is "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
1047+ test ( 'should generate correct hash for a known string' , async ( ) => {
1048+ const knownString = "hello world" ;
1049+ const expectedHash = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" ;
1050+ const actualHash = await generateCryptoHash ( knownString ) ;
1051+ expect ( actualHash ) . toBe ( expectedHash ) ;
1052+ } ) ;
1053+ } ) ;
1054+ } )
0 commit comments