UpdateExistingItem.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
  2. import { Timer, TimerInterface } from '@standardnotes/time'
  3. import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
  4. import { Item } from '../../../Item/Item'
  5. import { ItemHash } from '../../../Item/ItemHash'
  6. import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
  7. import { UpdateExistingItem } from './UpdateExistingItem'
  8. import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
  9. import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
  10. import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
  11. describe('UpdateExistingItem', () => {
  12. let itemRepository: ItemRepositoryInterface
  13. let timer: TimerInterface
  14. let domainEventPublisher: DomainEventPublisherInterface
  15. let domainEventFactory: DomainEventFactoryInterface
  16. let itemHash1: ItemHash
  17. let item1: Item
  18. const createUseCase = () => new UpdateExistingItem(itemRepository, timer, domainEventPublisher, domainEventFactory, 5)
  19. beforeEach(() => {
  20. const timeHelper = new Timer()
  21. item1 = Item.create(
  22. {
  23. userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
  24. updatedWithSession: null,
  25. content: 'foobar',
  26. contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
  27. encItemKey: null,
  28. authHash: null,
  29. itemsKeyId: null,
  30. duplicateOf: null,
  31. deleted: false,
  32. dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
  33. timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
  34. },
  35. new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
  36. ).getValue()
  37. itemHash1 = ItemHash.create({
  38. uuid: '1-2-3',
  39. user_uuid: '00000000-0000-0000-0000-000000000000',
  40. key_system_identifier: null,
  41. shared_vault_uuid: null,
  42. content: 'asdqwe1',
  43. content_type: ContentType.TYPES.Note,
  44. duplicate_of: null,
  45. enc_item_key: 'qweqwe1',
  46. auth_hash: 'auth_hash',
  47. items_key_id: 'asdasd1',
  48. created_at: timeHelper.formatDate(
  49. timeHelper.convertMicrosecondsToDate(item1.props.timestamps.createdAt),
  50. 'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
  51. ),
  52. updated_at: timeHelper.formatDate(
  53. new Date(timeHelper.convertMicrosecondsToMilliseconds(item1.props.timestamps.updatedAt) + 1),
  54. 'YYYY-MM-DDTHH:mm:ss.SSS[Z]',
  55. ),
  56. }).getValue()
  57. itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
  58. itemRepository.save = jest.fn()
  59. timer = {} as jest.Mocked<TimerInterface>
  60. timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
  61. timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date(123456789))
  62. timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(123456789)
  63. timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(123456789)
  64. timer.convertStringDateToDate = jest.fn().mockReturnValue(new Date(123456789))
  65. domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
  66. domainEventPublisher.publish = jest.fn()
  67. domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
  68. domainEventFactory.createDuplicateItemSyncedEvent = jest
  69. .fn()
  70. .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
  71. domainEventFactory.createItemRevisionCreationRequested = jest
  72. .fn()
  73. .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
  74. })
  75. it('should update item', async () => {
  76. const useCase = createUseCase()
  77. const result = await useCase.execute({
  78. existingItem: item1,
  79. itemHash: itemHash1,
  80. sessionUuid: '00000000-0000-0000-0000-000000000000',
  81. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  82. })
  83. expect(result.isFailed()).toBeFalsy()
  84. expect(itemRepository.save).toHaveBeenCalled()
  85. })
  86. it('should return error if session uuid is invalid', async () => {
  87. const useCase = createUseCase()
  88. const result = await useCase.execute({
  89. existingItem: item1,
  90. itemHash: itemHash1,
  91. sessionUuid: 'invalid-uuid',
  92. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  93. })
  94. expect(result.isFailed()).toBeTruthy()
  95. })
  96. it('should return error if content type is invalid', async () => {
  97. const useCase = createUseCase()
  98. const result = await useCase.execute({
  99. existingItem: item1,
  100. itemHash: ItemHash.create({
  101. ...itemHash1.props,
  102. content_type: 'invalid',
  103. }).getValue(),
  104. sessionUuid: '00000000-0000-0000-0000-000000000000',
  105. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  106. })
  107. expect(result.isFailed()).toBeTruthy()
  108. })
  109. it('should mark item as deleted if item hash is deleted', async () => {
  110. const useCase = createUseCase()
  111. const result = await useCase.execute({
  112. existingItem: item1,
  113. itemHash: ItemHash.create({
  114. ...itemHash1.props,
  115. deleted: true,
  116. }).getValue(),
  117. sessionUuid: '00000000-0000-0000-0000-000000000000',
  118. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  119. })
  120. expect(result.isFailed()).toBeFalsy()
  121. expect(itemRepository.save).toHaveBeenCalled()
  122. expect(item1.props.deleted).toBeTruthy()
  123. expect(item1.props.content).toBeNull()
  124. expect(item1.props.encItemKey).toBeNull()
  125. expect(item1.props.authHash).toBeNull()
  126. expect(item1.props.itemsKeyId).toBeNull()
  127. expect(item1.props.duplicateOf).toBeNull()
  128. })
  129. it('should mark item as duplicate if item hash has duplicate_of', async () => {
  130. const useCase = createUseCase()
  131. const result = await useCase.execute({
  132. existingItem: item1,
  133. itemHash: ItemHash.create({
  134. ...itemHash1.props,
  135. duplicate_of: '00000000-0000-0000-0000-000000000001',
  136. }).getValue(),
  137. sessionUuid: '00000000-0000-0000-0000-000000000000',
  138. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  139. })
  140. expect(result.isFailed()).toBeFalsy()
  141. expect(itemRepository.save).toHaveBeenCalled()
  142. expect(item1.props.duplicateOf?.value).toBe('00000000-0000-0000-0000-000000000001')
  143. })
  144. it('shuld return error if duplicate uuid is invalid', async () => {
  145. const useCase = createUseCase()
  146. const result = await useCase.execute({
  147. existingItem: item1,
  148. itemHash: ItemHash.create({
  149. ...itemHash1.props,
  150. duplicate_of: 'invalid-uuid',
  151. }).getValue(),
  152. sessionUuid: '00000000-0000-0000-0000-000000000000',
  153. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  154. })
  155. expect(result.isFailed()).toBeTruthy()
  156. })
  157. it('should update item with update timestamps', async () => {
  158. const useCase = createUseCase()
  159. const result = await useCase.execute({
  160. existingItem: item1,
  161. itemHash: ItemHash.create({
  162. ...itemHash1.props,
  163. updated_at_timestamp: 123,
  164. created_at_timestamp: 123,
  165. }).getValue(),
  166. sessionUuid: '00000000-0000-0000-0000-000000000000',
  167. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  168. })
  169. expect(result.isFailed()).toBeFalsy()
  170. expect(itemRepository.save).toHaveBeenCalled()
  171. })
  172. it('should return error if created at time is not give in any form', async () => {
  173. const useCase = createUseCase()
  174. const result = await useCase.execute({
  175. existingItem: item1,
  176. itemHash: ItemHash.create({
  177. ...itemHash1.props,
  178. created_at: undefined,
  179. created_at_timestamp: undefined,
  180. }).getValue(),
  181. sessionUuid: '00000000-0000-0000-0000-000000000000',
  182. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  183. })
  184. expect(result.isFailed()).toBeTruthy()
  185. })
  186. it('should return error if dates could not be created from timestamps', async () => {
  187. const mock = jest.spyOn(Dates, 'create')
  188. mock.mockImplementation(() => {
  189. return Result.fail('Oops')
  190. })
  191. const useCase = createUseCase()
  192. const result = await useCase.execute({
  193. existingItem: item1,
  194. itemHash: ItemHash.create({
  195. ...itemHash1.props,
  196. created_at_timestamp: 123,
  197. updated_at_timestamp: 123,
  198. }).getValue(),
  199. sessionUuid: '00000000-0000-0000-0000-000000000000',
  200. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  201. })
  202. expect(result.isFailed()).toBeTruthy()
  203. mock.mockRestore()
  204. })
  205. it('should return error if timestamps could not be created from timestamps', async () => {
  206. const mock = jest.spyOn(Timestamps, 'create')
  207. mock.mockImplementation(() => {
  208. return Result.fail('Oops')
  209. })
  210. const useCase = createUseCase()
  211. const result = await useCase.execute({
  212. existingItem: item1,
  213. itemHash: ItemHash.create({
  214. ...itemHash1.props,
  215. created_at_timestamp: 123,
  216. updated_at_timestamp: 123,
  217. }).getValue(),
  218. sessionUuid: '00000000-0000-0000-0000-000000000000',
  219. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  220. })
  221. expect(result.isFailed()).toBeTruthy()
  222. mock.mockRestore()
  223. })
  224. it('should return error if performing user uuid is invalid', async () => {
  225. const useCase = createUseCase()
  226. const result = await useCase.execute({
  227. existingItem: item1,
  228. itemHash: itemHash1,
  229. sessionUuid: '00000000-0000-0000-0000-000000000000',
  230. performingUserUuid: 'invalid-uuid',
  231. })
  232. expect(result.isFailed()).toBeTruthy()
  233. })
  234. describe('when item is associated to a shared vault', () => {
  235. it('should add a shared vault association if item hash represents a shared vault item and the existing item is not already associated to the shared vault', async () => {
  236. const useCase = createUseCase()
  237. const itemHash = ItemHash.create({
  238. ...itemHash1.props,
  239. shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
  240. }).getValue()
  241. const result = await useCase.execute({
  242. existingItem: item1,
  243. itemHash,
  244. sessionUuid: '00000000-0000-0000-0000-000000000000',
  245. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  246. })
  247. expect(result.isFailed()).toBeFalsy()
  248. expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
  249. expect(item1.props.sharedVaultAssociation?.props.sharedVaultUuid.value).toBe(
  250. '00000000-0000-0000-0000-000000000000',
  251. )
  252. })
  253. it('should not add a shared vault association if item hash represents a shared vault item and the existing item is already associated to the shared vault', async () => {
  254. const useCase = createUseCase()
  255. const itemHash = ItemHash.create({
  256. ...itemHash1.props,
  257. shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
  258. }).getValue()
  259. item1.props.sharedVaultAssociation = SharedVaultAssociation.create({
  260. itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
  261. sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
  262. lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
  263. timestamps: Timestamps.create(123, 123).getValue(),
  264. }).getValue()
  265. const idBefore = item1.props.sharedVaultAssociation?.id.toString()
  266. const result = await useCase.execute({
  267. existingItem: item1,
  268. itemHash,
  269. sessionUuid: '00000000-0000-0000-0000-000000000000',
  270. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  271. })
  272. expect(result.isFailed()).toBeFalsy()
  273. expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
  274. expect(item1.props.sharedVaultAssociation.id.toString()).toEqual(idBefore)
  275. })
  276. it('should return error if shared vault association could not be created', async () => {
  277. const useCase = createUseCase()
  278. const itemHash = ItemHash.create({
  279. ...itemHash1.props,
  280. shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
  281. }).getValue()
  282. const mock = jest.spyOn(SharedVaultAssociation, 'create')
  283. mock.mockImplementation(() => {
  284. return Result.fail('Oops')
  285. })
  286. const result = await useCase.execute({
  287. existingItem: item1,
  288. itemHash,
  289. sessionUuid: '00000000-0000-0000-0000-000000000000',
  290. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  291. })
  292. expect(result.isFailed()).toBeTruthy()
  293. mock.mockRestore()
  294. })
  295. })
  296. describe('when item is associated to a key system', () => {
  297. it('should add a key system association if item hash has a dedicated key system and the existing item is not already associated to the key system', async () => {
  298. const useCase = createUseCase()
  299. const itemHash = ItemHash.create({
  300. ...itemHash1.props,
  301. key_system_identifier: '00000000-0000-0000-0000-000000000000',
  302. }).getValue()
  303. const result = await useCase.execute({
  304. existingItem: item1,
  305. itemHash,
  306. sessionUuid: '00000000-0000-0000-0000-000000000000',
  307. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  308. })
  309. expect(result.isFailed()).toBeFalsy()
  310. expect(item1.props.keySystemAssociation).not.toBeUndefined()
  311. expect(item1.props.keySystemAssociation?.props.keySystemIdentifier).toBe('00000000-0000-0000-0000-000000000000')
  312. })
  313. it('should not add a key system association if item hash has a dedicated key system and the existing item is already associated to the key system', async () => {
  314. const useCase = createUseCase()
  315. const itemHash = ItemHash.create({
  316. ...itemHash1.props,
  317. key_system_identifier: '00000000-0000-0000-0000-000000000000',
  318. }).getValue()
  319. item1.props.keySystemAssociation = KeySystemAssociation.create({
  320. itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
  321. keySystemIdentifier: '00000000-0000-0000-0000-000000000000',
  322. timestamps: Timestamps.create(123, 123).getValue(),
  323. }).getValue()
  324. const idBefore = item1.props.keySystemAssociation?.id.toString()
  325. const result = await useCase.execute({
  326. existingItem: item1,
  327. itemHash,
  328. sessionUuid: '00000000-0000-0000-0000-000000000000',
  329. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  330. })
  331. expect(result.isFailed()).toBeFalsy()
  332. expect(item1.props.keySystemAssociation).not.toBeUndefined()
  333. expect(item1.props.keySystemAssociation.id.toString()).toEqual(idBefore)
  334. })
  335. it('should return error if key system identifier is invalid', async () => {
  336. const useCase = createUseCase()
  337. const itemHash = ItemHash.create({
  338. ...itemHash1.props,
  339. key_system_identifier: 123 as unknown as string,
  340. }).getValue()
  341. const result = await useCase.execute({
  342. existingItem: item1,
  343. itemHash,
  344. sessionUuid: '00000000-0000-0000-0000-000000000000',
  345. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  346. })
  347. expect(result.isFailed()).toBeTruthy()
  348. })
  349. it('should return error if key system association could not be created', async () => {
  350. const useCase = createUseCase()
  351. const itemHash = ItemHash.create({
  352. ...itemHash1.props,
  353. key_system_identifier: '00000000-0000-0000-0000-000000000000',
  354. }).getValue()
  355. const mock = jest.spyOn(KeySystemAssociation, 'create')
  356. mock.mockImplementation(() => {
  357. return Result.fail('Oops')
  358. })
  359. const result = await useCase.execute({
  360. existingItem: item1,
  361. itemHash,
  362. sessionUuid: '00000000-0000-0000-0000-000000000000',
  363. performingUserUuid: '00000000-0000-0000-0000-000000000000',
  364. })
  365. expect(result.isFailed()).toBeTruthy()
  366. mock.mockRestore()
  367. })
  368. })
  369. })