serverSideStorage.spec.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { cloneDeep } from 'lodash'
  2. import {
  3. VERSION,
  4. COMMAND_TRIM_FLAGS,
  5. COMMAND_TRIM_FLAGS_AND_RESET,
  6. _moveItemInArray,
  7. _getRecentData,
  8. _getAllFlags,
  9. _mergeFlags,
  10. _mergePrefs,
  11. _resetFlags,
  12. mutations,
  13. defaultState,
  14. newUserFlags
  15. } from 'src/modules/serverSideStorage.js'
  16. describe('The serverSideStorage module', () => {
  17. describe('mutations', () => {
  18. describe('setServerSideStorage', () => {
  19. const { setServerSideStorage } = mutations
  20. const user = {
  21. created_at: new Date('1999-02-09'),
  22. storage: {}
  23. }
  24. it('should initialize storage if none present', () => {
  25. const state = cloneDeep(defaultState)
  26. setServerSideStorage(state, user)
  27. expect(state.cache._version).to.eql(VERSION)
  28. expect(state.cache._timestamp).to.be.a('number')
  29. expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
  30. expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage)
  31. })
  32. it('should initialize storage with proper flags for new users if none present', () => {
  33. const state = cloneDeep(defaultState)
  34. setServerSideStorage(state, { ...user, created_at: new Date() })
  35. expect(state.cache._version).to.eql(VERSION)
  36. expect(state.cache._timestamp).to.be.a('number')
  37. expect(state.cache.flagStorage).to.eql(newUserFlags)
  38. expect(state.cache.prefsStorage).to.eql(defaultState.prefsStorage)
  39. })
  40. it('should merge flags even if remote timestamp is older', () => {
  41. const state = {
  42. ...cloneDeep(defaultState),
  43. cache: {
  44. _timestamp: Date.now(),
  45. _version: VERSION,
  46. ...cloneDeep(defaultState)
  47. }
  48. }
  49. setServerSideStorage(
  50. state,
  51. {
  52. ...user,
  53. storage: {
  54. _timestamp: 123,
  55. _version: VERSION,
  56. flagStorage: {
  57. ...defaultState.flagStorage,
  58. updateCounter: 1
  59. },
  60. prefsStorage: {
  61. ...defaultState.prefsStorage
  62. }
  63. }
  64. }
  65. )
  66. expect(state.cache.flagStorage).to.eql({
  67. ...defaultState.flagStorage,
  68. updateCounter: 1
  69. })
  70. })
  71. it('should reset local timestamp to remote if contents are the same', () => {
  72. const state = {
  73. ...cloneDeep(defaultState),
  74. cache: null
  75. }
  76. setServerSideStorage(
  77. state,
  78. {
  79. ...user,
  80. storage: {
  81. _timestamp: 123,
  82. _version: VERSION,
  83. flagStorage: {
  84. ...defaultState.flagStorage,
  85. updateCounter: 999
  86. }
  87. }
  88. }
  89. )
  90. expect(state.cache._timestamp).to.eql(123)
  91. expect(state.flagStorage.updateCounter).to.eql(999)
  92. expect(state.cache.flagStorage.updateCounter).to.eql(999)
  93. })
  94. it('should remote version if local missing', () => {
  95. const state = cloneDeep(defaultState)
  96. setServerSideStorage(state, user)
  97. expect(state.cache._version).to.eql(VERSION)
  98. expect(state.cache._timestamp).to.be.a('number')
  99. expect(state.cache.flagStorage).to.eql(defaultState.flagStorage)
  100. })
  101. })
  102. describe('setPreference', () => {
  103. const { setPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations
  104. it('should set preference and update journal log accordingly', () => {
  105. const state = cloneDeep(defaultState)
  106. setPreference(state, { path: 'simple.testing', value: 1 })
  107. expect(state.prefsStorage.simple.testing).to.eql(1)
  108. expect(state.prefsStorage._journal.length).to.eql(1)
  109. expect(state.prefsStorage._journal[0]).to.eql({
  110. path: 'simple.testing',
  111. operation: 'set',
  112. args: [1],
  113. // should have A timestamp, we don't really care what it is
  114. timestamp: state.prefsStorage._journal[0].timestamp
  115. })
  116. })
  117. it('should keep journal to a minimum', () => {
  118. const state = cloneDeep(defaultState)
  119. setPreference(state, { path: 'simple.testing', value: 1 })
  120. setPreference(state, { path: 'simple.testing', value: 2 })
  121. addCollectionPreference(state, { path: 'collections.testing', value: 2 })
  122. removeCollectionPreference(state, { path: 'collections.testing', value: 2 })
  123. updateCache(state, { username: 'test' })
  124. expect(state.prefsStorage.simple.testing).to.eql(2)
  125. expect(state.prefsStorage.collections.testing).to.eql([])
  126. expect(state.prefsStorage._journal.length).to.eql(2)
  127. expect(state.prefsStorage._journal[0]).to.eql({
  128. path: 'simple.testing',
  129. operation: 'set',
  130. args: [2],
  131. // should have A timestamp, we don't really care what it is
  132. timestamp: state.prefsStorage._journal[0].timestamp
  133. })
  134. expect(state.prefsStorage._journal[1]).to.eql({
  135. path: 'collections.testing',
  136. operation: 'removeFromCollection',
  137. args: [2],
  138. // should have A timestamp, we don't really care what it is
  139. timestamp: state.prefsStorage._journal[1].timestamp
  140. })
  141. })
  142. it('should remove duplicate entries from journal', () => {
  143. const state = cloneDeep(defaultState)
  144. setPreference(state, { path: 'simple.testing', value: 1 })
  145. setPreference(state, { path: 'simple.testing', value: 1 })
  146. addCollectionPreference(state, { path: 'collections.testing', value: 2 })
  147. addCollectionPreference(state, { path: 'collections.testing', value: 2 })
  148. updateCache(state, { username: 'test' })
  149. expect(state.prefsStorage.simple.testing).to.eql(1)
  150. expect(state.prefsStorage.collections.testing).to.eql([2])
  151. expect(state.prefsStorage._journal.length).to.eql(2)
  152. })
  153. })
  154. })
  155. describe('helper functions', () => {
  156. describe('_moveItemInArray', () => {
  157. it('should move item according to movement value', () => {
  158. expect(_moveItemInArray([1, 2, 3, 4], 4, -1)).to.eql([1, 2, 4, 3])
  159. expect(_moveItemInArray([1, 2, 3, 4], 1, 2)).to.eql([2, 3, 1, 4])
  160. })
  161. it('should clamp movement to within array', () => {
  162. expect(_moveItemInArray([1, 2, 3, 4], 4, -10)).to.eql([4, 1, 2, 3])
  163. expect(_moveItemInArray([1, 2, 3, 4], 3, 99)).to.eql([1, 2, 4, 3])
  164. })
  165. })
  166. describe('_getRecentData', () => {
  167. it('should handle nulls correctly', () => {
  168. expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true })
  169. })
  170. it('doesn\'t choke on invalid data', () => {
  171. expect(_getRecentData({ a: 1 }, { b: 2 })).to.eql({ recent: null, stale: null, needUpload: true })
  172. })
  173. it('should prefer the valid non-null correctly, needUpload works properly', () => {
  174. const nonNull = { _version: VERSION, _timestamp: 1 }
  175. expect(_getRecentData(nonNull, null)).to.eql({ recent: nonNull, stale: null, needUpload: true })
  176. expect(_getRecentData(null, nonNull)).to.eql({ recent: nonNull, stale: null, needUpload: false })
  177. })
  178. it('should prefer the one with higher timestamp', () => {
  179. const a = { _version: VERSION, _timestamp: 1 }
  180. const b = { _version: VERSION, _timestamp: 2 }
  181. expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
  182. expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
  183. })
  184. it('case where both are same', () => {
  185. const a = { _version: VERSION, _timestamp: 3 }
  186. const b = { _version: VERSION, _timestamp: 3 }
  187. expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
  188. expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
  189. })
  190. })
  191. describe('_getAllFlags', () => {
  192. it('should handle nulls properly', () => {
  193. expect(_getAllFlags(null, null)).to.eql([])
  194. })
  195. it('should output list of keys if passed single object', () => {
  196. expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, null)).to.eql(['a', 'b', 'c'])
  197. })
  198. it('should union keys of both objects', () => {
  199. expect(_getAllFlags({ flagStorage: { a: 1, b: 1, c: 1 } }, { flagStorage: { c: 1, d: 1 } })).to.eql(['a', 'b', 'c', 'd'])
  200. })
  201. })
  202. describe('_mergeFlags', () => {
  203. it('should handle merge two flag sets correctly picking higher numbers', () => {
  204. expect(
  205. _mergeFlags(
  206. { flagStorage: { a: 0, b: 3 } },
  207. { flagStorage: { b: 1, c: 4, d: 9 } },
  208. ['a', 'b', 'c', 'd'])
  209. ).to.eql({ a: 0, b: 3, c: 4, d: 9 })
  210. })
  211. })
  212. describe('_mergePrefs', () => {
  213. it('should prefer recent and apply journal to it', () => {
  214. expect(
  215. _mergePrefs(
  216. // RECENT
  217. {
  218. simple: { a: 1, b: 0, c: true },
  219. _journal: [
  220. { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 },
  221. { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 }
  222. ]
  223. },
  224. // STALE
  225. {
  226. simple: { a: 1, b: 1, c: false },
  227. _journal: [
  228. { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 },
  229. { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 }
  230. ]
  231. }
  232. )
  233. ).to.eql({
  234. simple: { a: 1, b: 1, c: true },
  235. _journal: [
  236. { path: 'simple.a', operation: 'set', args: [1], timestamp: 1 },
  237. { path: 'simple.b', operation: 'set', args: [1], timestamp: 3 },
  238. { path: 'simple.c', operation: 'set', args: [true], timestamp: 4 }
  239. ]
  240. })
  241. })
  242. it('should allow setting falsy values', () => {
  243. expect(
  244. _mergePrefs(
  245. // RECENT
  246. {
  247. simple: { a: 1, b: 0, c: false },
  248. _journal: [
  249. { path: 'simple.b', operation: 'set', args: [0], timestamp: 2 },
  250. { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 }
  251. ]
  252. },
  253. // STALE
  254. {
  255. simple: { a: 0, b: 0, c: true },
  256. _journal: [
  257. { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 },
  258. { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 }
  259. ]
  260. }
  261. )
  262. ).to.eql({
  263. simple: { a: 0, b: 0, c: false },
  264. _journal: [
  265. { path: 'simple.a', operation: 'set', args: [0], timestamp: 1 },
  266. { path: 'simple.b', operation: 'set', args: [0], timestamp: 3 },
  267. { path: 'simple.c', operation: 'set', args: [false], timestamp: 4 }
  268. ]
  269. })
  270. })
  271. it('should work with strings', () => {
  272. expect(
  273. _mergePrefs(
  274. // RECENT
  275. {
  276. simple: { a: 'foo' },
  277. _journal: [
  278. { path: 'simple.a', operation: 'set', args: ['foo'], timestamp: 2 }
  279. ]
  280. },
  281. // STALE
  282. {
  283. simple: { a: 'bar' },
  284. _journal: [
  285. { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 }
  286. ]
  287. }
  288. )
  289. ).to.eql({
  290. simple: { a: 'bar' },
  291. _journal: [
  292. { path: 'simple.a', operation: 'set', args: ['bar'], timestamp: 4 }
  293. ]
  294. })
  295. })
  296. })
  297. describe('_resetFlags', () => {
  298. it('should reset all known flags to 0 when reset flag is set to > 0 and < 9000', () => {
  299. const totalFlags = { a: 0, b: 3, reset: 1 }
  300. expect(_resetFlags(totalFlags)).to.eql({ a: 0, b: 0, reset: 0 })
  301. })
  302. it('should trim all flags to known when reset is set to 1000', () => {
  303. const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS }
  304. expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 3, reset: 0 })
  305. })
  306. it('should trim all flags to known and reset when reset is set to 1001', () => {
  307. const totalFlags = { a: 0, b: 3, c: 33, reset: COMMAND_TRIM_FLAGS_AND_RESET }
  308. expect(_resetFlags(totalFlags, { a: 0, b: 0, reset: 0 })).to.eql({ a: 0, b: 0, reset: 0 })
  309. })
  310. })
  311. })
  312. })