options_sync.coffee 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. chai = require 'chai'
  2. should = chai.should()
  3. sinon = require 'sinon'
  4. chai.use require('sinon-chai')
  5. describe 'OptionsSync', ->
  6. OptionsSync = require '../src/options_sync'
  7. Storage = require '../src/storage'
  8. Log = require '../src/log'
  9. Promise = require 'bluebird'
  10. before ->
  11. # Silence storage and sync logging.
  12. sinon.stub(Log, 'log')
  13. after ->
  14. Log.log.restore()
  15. # coffeelint: disable=missing_fat_arrows
  16. hookPostBasic = (func, hook) -> ->
  17. result = func.apply(this, arguments)
  18. hook.apply(this, arguments)
  19. return result
  20. # coffeelint: enable=missing_fat_arrows
  21. hookPost = (args...) ->
  22. if args.length == 2
  23. [func, hook] = args
  24. hostPostBasic(func, hook)
  25. else
  26. [obj, method, hook] = args
  27. obj[method] = hookPostBasic(obj[method], hook)
  28. describe '#merge', ->
  29. sync = new OptionsSync()
  30. it 'should choose the one with newer revision', ->
  31. newVal = {revision: '2'}
  32. oldVal = {revision: '1'}
  33. sync.merge('example', newVal, oldVal).should.equal(newVal)
  34. it 'should use oldVal when sync is disabled in newVal', ->
  35. newVal = {revision: '2', is: 'newVal', syncOptions: 'disabled'}
  36. oldVal = {revision: '1', is: 'oldVal'}
  37. sync.merge('example', newVal, oldVal).should.equal(oldVal)
  38. it 'should use oldVal when sync is disabled in oldVal', ->
  39. newVal = {revision: '2', is: 'newVal'}
  40. oldVal = {revision: '1', is: 'oldVal', syncOptions: 'disabled'}
  41. sync.merge('example', newVal, oldVal).should.equal(oldVal)
  42. it 'should favor oldVal when revisions are equal', ->
  43. newVal = {revision: '1', is: 'newVal'}
  44. oldVal = {revision: '1', is: 'oldVal'}
  45. sync.merge('example', newVal, oldVal).should.equal(oldVal)
  46. it 'should favor oldVal when newVal deeply equals oldVal', ->
  47. newVal = {they: 'are', the: 'same'}
  48. oldVal = {they: 'are', the: 'same'}
  49. sync.merge('example', newVal, oldVal).should.equal(oldVal)
  50. it 'should choose newVal when newVal is different', ->
  51. newVal = {they: 'are', not: 'equal'}
  52. oldVal = {they: 'are', not: 'identical'}
  53. sync.merge('example', newVal, oldVal).should.equal(newVal)
  54. describe '#requestPush', ->
  55. unlimited = new OptionsSync.TokenBucket()
  56. it 'should store pendingChanges', ->
  57. sync = new OptionsSync()
  58. sync.enabled = false
  59. sync.requestPush({a: 1})
  60. sync.pendingChanges().should.eql({a: 1})
  61. it 'should schedule storage write', (done) ->
  62. check = ->
  63. return if storage.set.callCount == 0 or storage.remove.callCount == 0
  64. storage.set.should.have.been.calledOnce.and.calledWith({b: 1})
  65. storage.remove.should.have.been.calledOnce.and.calledWith(['a'])
  66. done()
  67. storage = new Storage()
  68. storage.set({a: 1})
  69. hookPost storage, 'set', check
  70. hookPost storage, 'remove', check
  71. sinon.spy(storage, 'set')
  72. sinon.spy(storage, 'remove')
  73. sync = new OptionsSync(storage, unlimited)
  74. sync.debounce = 0
  75. sync.requestPush({a: undefined, b: 1})
  76. it 'should combine multiple write operations', (done) ->
  77. check = ->
  78. return if storage.set.callCount == 0 or storage.remove.callCount == 0
  79. storage.set.should.have.been.calledOnce.and.calledWith({c: 1, d: 1})
  80. storage.remove.should.have.been.calledOnce.and.calledWith(['a', 'b'])
  81. done()
  82. storage = new Storage()
  83. storage.set({a: 1, b: 1})
  84. hookPost storage, 'set', check
  85. hookPost storage, 'remove', check
  86. sinon.spy(storage, 'set')
  87. sinon.spy(storage, 'remove')
  88. sync = new OptionsSync(storage, unlimited)
  89. sync.debounce = 0
  90. sync.requestPush({a: undefined})
  91. sync.requestPush({b: 2})
  92. sync.requestPush({b: undefined})
  93. sync.requestPush({c: 1})
  94. sync.requestPush({d: 1})
  95. sync.requestPush({e: 1})
  96. sync.requestPush({e: undefined})
  97. it 'should disable syncing for the profiles if quota is exceeded', (done) ->
  98. options = {'+a': {is: 'a', oversized: true}, b: {is: 'b'}}
  99. storage = new Storage()
  100. storage.set = (changes) ->
  101. for key, value of changes
  102. if value.oversized
  103. err = new Storage.QuotaExceededError()
  104. err.perItem = true
  105. return Promise.reject(err)
  106. storage.set.should.have.been.calledTwice
  107. storage.set.should.have.been.calledWith(options)
  108. storage.set.should.have.been.calledWith({b: {is: 'b'}})
  109. options['+a'].syncOptions.should.equal('disabled')
  110. options['+a'].syncError.reason.should.equal('quotaPerItem')
  111. done()
  112. Promise.resolve()
  113. sinon.spy(storage, 'set')
  114. sync = new OptionsSync(storage, unlimited)
  115. sync.debounce = 0
  116. sync.requestPush(options)
  117. describe '#copyTo', ->
  118. it 'should fetch all items from remote storage', (done) ->
  119. remote = new Storage()
  120. remote.set({a: 1, b: 2, c: 3})
  121. storage = new Storage()
  122. hookPost storage, 'set', ->
  123. storage.set.should.have.been.calledOnce.and.calledWith(
  124. {a: 1, b: 2, c: 3}
  125. )
  126. done()
  127. sinon.spy(storage, 'set')
  128. sync = new OptionsSync(remote)
  129. sync.copyTo(storage)
  130. it 'should merge with local as base', (done) ->
  131. check = ->
  132. return if storage.set.callCount == 0 or storage.remove.callCount == 0
  133. storage.set.should.have.been.calledOnce.and.calledWith({b: 2, c: 3})
  134. storage.remove.should.have.been.calledOnce.and.calledWith(['d'])
  135. done()
  136. remote = new Storage()
  137. remote.set({a: 1, b: 2, c: 3, d: undefined})
  138. storage = new Storage()
  139. storage.set({a: 1, b: 0, d: 4})
  140. hookPost storage, 'set', check
  141. hookPost storage, 'remove', check
  142. sinon.spy(storage, 'set')
  143. sinon.spy(storage, 'remove')
  144. sync = new OptionsSync(remote)
  145. sync.copyTo(storage)
  146. describe '#watchAndPull', ->
  147. it 'should pull changes into local when remote changes', (done) ->
  148. check = ->
  149. return if storage.set.callCount == 0 or storage.remove.callCount == 0
  150. remote.watch.should.have.been.calledOnce
  151. storage.set.should.have.been.calledOnce.and.calledWith({b: 2, c: 3})
  152. storage.remove.should.have.been.calledOnce.and.calledWith(['d'])
  153. done()
  154. remote = new Storage()
  155. hookPost remote, 'watch', (_, callback) ->
  156. setTimeout (->
  157. callback({a: 1})
  158. callback({b: 2})
  159. callback({c: 3})
  160. callback({d: undefined})
  161. ), 10
  162. sinon.spy(remote, 'watch')
  163. storage = new Storage()
  164. storage.set({a: 1, b: 0, d: 4})
  165. hookPost storage, 'set', check
  166. hookPost storage, 'remove', check
  167. sinon.spy(storage, 'set')
  168. sinon.spy(storage, 'remove')
  169. sync = new OptionsSync(remote)
  170. sync.pullThrottle = 0
  171. sync.watchAndPull(storage)