ExtensionsHttpService.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import 'reflect-metadata'
  2. import { KeyParamsData } from '@standardnotes/responses'
  3. import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
  4. import { Logger } from 'winston'
  5. import { ContentDecoderInterface } from '../Item/ContentDecoderInterface'
  6. import { Item } from '../Item/Item'
  7. import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
  8. import { ExtensionsHttpService } from './ExtensionsHttpService'
  9. import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
  10. import { AxiosInstance } from 'axios'
  11. import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
  12. describe('ExtensionsHttpService', () => {
  13. let httpClient: AxiosInstance
  14. let primaryItemRepository: ItemRepositoryInterface
  15. let secondaryItemRepository: ItemRepositoryInterface | null
  16. let contentDecoder: ContentDecoderInterface
  17. let domainEventPublisher: DomainEventPublisherInterface
  18. let domainEventFactory: DomainEventFactoryInterface
  19. let item: Item
  20. let authParams: KeyParamsData
  21. let logger: Logger
  22. const createService = () =>
  23. new ExtensionsHttpService(
  24. httpClient,
  25. primaryItemRepository,
  26. secondaryItemRepository,
  27. contentDecoder,
  28. domainEventPublisher,
  29. domainEventFactory,
  30. logger,
  31. )
  32. beforeEach(() => {
  33. httpClient = {} as jest.Mocked<AxiosInstance>
  34. httpClient.request = jest.fn().mockReturnValue({ status: 200, data: { foo: 'bar' } })
  35. item = Item.create(
  36. {
  37. userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
  38. updatedWithSession: null,
  39. content: 'foobar',
  40. contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
  41. encItemKey: null,
  42. authHash: null,
  43. itemsKeyId: null,
  44. duplicateOf: null,
  45. deleted: false,
  46. dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
  47. timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
  48. },
  49. new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
  50. ).getValue()
  51. authParams = {} as jest.Mocked<KeyParamsData>
  52. primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
  53. primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
  54. logger = {} as jest.Mocked<Logger>
  55. logger.error = jest.fn()
  56. domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
  57. domainEventPublisher.publish = jest.fn()
  58. domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
  59. domainEventFactory.createEmailRequestedEvent = jest.fn()
  60. contentDecoder = {} as jest.Mocked<ContentDecoderInterface>
  61. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
  62. })
  63. it('should trigger cloud backup on extensions server', async () => {
  64. await createService().triggerCloudBackupOnExtensionsServer({
  65. userUuid: '1-2-3',
  66. extensionsServerUrl: 'https://extensions-server/extension1',
  67. forceMute: false,
  68. backupFilename: 'test',
  69. authParams,
  70. cloudProvider: 'DROPBOX',
  71. })
  72. expect(httpClient.request).toHaveBeenCalledWith({
  73. data: {
  74. auth_params: authParams,
  75. backup_filename: 'test',
  76. silent: false,
  77. user_uuid: '1-2-3',
  78. },
  79. headers: {
  80. 'Content-Type': 'application/json',
  81. },
  82. method: 'POST',
  83. url: 'https://extensions-server/extension1',
  84. validateStatus: expect.any(Function),
  85. })
  86. })
  87. it('should publish a failed Dropbox backup event if request was not sent successfully', async () => {
  88. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
  89. httpClient.request = jest.fn().mockImplementation(() => {
  90. throw new Error('Could not reach the extensions server')
  91. })
  92. await createService().triggerCloudBackupOnExtensionsServer({
  93. userUuid: '1-2-3',
  94. extensionsServerUrl: 'https://extensions-server/extension1',
  95. forceMute: false,
  96. backupFilename: 'test',
  97. authParams,
  98. cloudProvider: 'DROPBOX',
  99. })
  100. expect(domainEventPublisher.publish).toHaveBeenCalled()
  101. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  102. })
  103. it('should send items to extensions server', async () => {
  104. await createService().sendItemsToExtensionsServer({
  105. userUuid: '1-2-3',
  106. extensionId: '2-3-4',
  107. extensionsServerUrl: 'https://extensions-server/extension1',
  108. forceMute: false,
  109. items: [item],
  110. backupFilename: '',
  111. authParams,
  112. })
  113. expect(httpClient.request).toHaveBeenCalledWith({
  114. data: {
  115. auth_params: authParams,
  116. backup_filename: '',
  117. items: [item],
  118. silent: false,
  119. user_uuid: '1-2-3',
  120. },
  121. headers: {
  122. 'Content-Type': 'application/json',
  123. },
  124. method: 'POST',
  125. url: 'https://extensions-server/extension1',
  126. validateStatus: expect.any(Function),
  127. })
  128. })
  129. it('should send items proxy backup file name only to extensions server', async () => {
  130. await createService().sendItemsToExtensionsServer({
  131. userUuid: '1-2-3',
  132. extensionId: '2-3-4',
  133. extensionsServerUrl: 'https://extensions-server/extension1',
  134. forceMute: false,
  135. backupFilename: 'backup-file',
  136. authParams,
  137. })
  138. expect(httpClient.request).toHaveBeenCalledWith({
  139. data: {
  140. auth_params: authParams,
  141. backup_filename: 'backup-file',
  142. silent: false,
  143. user_uuid: '1-2-3',
  144. },
  145. headers: {
  146. 'Content-Type': 'application/json',
  147. },
  148. method: 'POST',
  149. url: 'https://extensions-server/extension1',
  150. validateStatus: expect.any(Function),
  151. })
  152. })
  153. it('should publish a failed Dropbox backup event if request was not sent successfully', async () => {
  154. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
  155. httpClient.request = jest.fn().mockImplementation(() => {
  156. throw new Error('Could not reach the extensions server')
  157. })
  158. await createService().sendItemsToExtensionsServer({
  159. userUuid: '1-2-3',
  160. extensionId: '2-3-4',
  161. extensionsServerUrl: 'https://extensions-server/extension1',
  162. forceMute: false,
  163. items: [item],
  164. backupFilename: 'backup-file',
  165. authParams,
  166. })
  167. expect(domainEventPublisher.publish).toHaveBeenCalled()
  168. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  169. })
  170. it('should publish a failed backup event if the extension is in the secondary repository', async () => {
  171. primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
  172. secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
  173. secondaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
  174. httpClient.request = jest.fn().mockImplementation(() => {
  175. throw new Error('Could not reach the extensions server')
  176. })
  177. await createService().sendItemsToExtensionsServer({
  178. userUuid: '1-2-3',
  179. extensionId: '2-3-4',
  180. extensionsServerUrl: '',
  181. forceMute: false,
  182. items: [item],
  183. backupFilename: 'backup-file',
  184. authParams,
  185. })
  186. expect(domainEventPublisher.publish).toHaveBeenCalled()
  187. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  188. secondaryItemRepository = null
  189. })
  190. it('should publish a failed Dropbox backup event if request was sent and extensions server responded not ok', async () => {
  191. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
  192. httpClient.request = jest.fn().mockReturnValue({ status: 400, data: { error: 'foo-bar' } })
  193. await createService().sendItemsToExtensionsServer({
  194. userUuid: '1-2-3',
  195. extensionId: '2-3-4',
  196. extensionsServerUrl: 'https://extensions-server/extension1',
  197. forceMute: false,
  198. items: [item],
  199. backupFilename: 'backup-file',
  200. authParams,
  201. })
  202. expect(domainEventPublisher.publish).toHaveBeenCalled()
  203. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  204. })
  205. it('should publish a failed Google Drive backup event if request was not sent successfully', async () => {
  206. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Google Drive' })
  207. httpClient.request = jest.fn().mockImplementation(() => {
  208. throw new Error('Could not reach the extensions server')
  209. })
  210. await createService().sendItemsToExtensionsServer({
  211. userUuid: '1-2-3',
  212. extensionId: '2-3-4',
  213. extensionsServerUrl: 'https://extensions-server/extension1',
  214. forceMute: false,
  215. items: [item],
  216. backupFilename: 'backup-file',
  217. authParams,
  218. })
  219. expect(domainEventPublisher.publish).toHaveBeenCalled()
  220. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  221. })
  222. it('should publish a failed One Drive backup event if request was not sent successfully', async () => {
  223. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'OneDrive' })
  224. httpClient.request = jest.fn().mockImplementation(() => {
  225. throw new Error('Could not reach the extensions server')
  226. })
  227. await createService().sendItemsToExtensionsServer({
  228. userUuid: '1-2-3',
  229. extensionId: '2-3-4',
  230. extensionsServerUrl: 'https://extensions-server/extension1',
  231. forceMute: false,
  232. items: [item],
  233. backupFilename: 'backup-file',
  234. authParams,
  235. })
  236. expect(domainEventPublisher.publish).toHaveBeenCalled()
  237. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  238. })
  239. it('should not publish a failed backup event if emailes are force muted', async () => {
  240. contentDecoder.decode = jest.fn().mockReturnValue({ name: 'OneDrive' })
  241. httpClient.request = jest.fn().mockImplementation(() => {
  242. throw new Error('Could not reach the extensions server')
  243. })
  244. await createService().sendItemsToExtensionsServer({
  245. userUuid: '1-2-3',
  246. extensionId: '2-3-4',
  247. extensionsServerUrl: 'https://extensions-server/extension1',
  248. forceMute: true,
  249. items: [item],
  250. backupFilename: 'backup-file',
  251. authParams,
  252. })
  253. expect(domainEventPublisher.publish).not.toHaveBeenCalled()
  254. })
  255. it('should throw an error if the extension to post to is not found', async () => {
  256. primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
  257. httpClient.request = jest.fn().mockImplementation(() => {
  258. throw new Error('Could not reach the extensions server')
  259. })
  260. let error = null
  261. try {
  262. await createService().sendItemsToExtensionsServer({
  263. userUuid: '1-2-3',
  264. extensionId: '2-3-4',
  265. extensionsServerUrl: 'https://extensions-server/extension1',
  266. forceMute: false,
  267. items: [item],
  268. backupFilename: 'backup-file',
  269. authParams,
  270. })
  271. } catch (e) {
  272. error = e
  273. }
  274. expect(error).not.toBeNull()
  275. })
  276. it('should throw an error if the extension to post to has no content', async () => {
  277. item = {} as jest.Mocked<Item>
  278. primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
  279. httpClient.request = jest.fn().mockImplementation(() => {
  280. throw new Error('Could not reach the extensions server')
  281. })
  282. let error = null
  283. try {
  284. await createService().sendItemsToExtensionsServer({
  285. userUuid: '1-2-3',
  286. extensionId: '2-3-4',
  287. extensionsServerUrl: 'https://extensions-server/extension1',
  288. forceMute: false,
  289. items: [item],
  290. backupFilename: 'backup-file',
  291. authParams,
  292. })
  293. } catch (e) {
  294. error = e
  295. }
  296. expect(error).not.toBeNull()
  297. })
  298. it('should publish a failed Dropbox backup event judging by extension url if request was not sent successfully', async () => {
  299. contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://dbt.com/...' })
  300. httpClient.request = jest.fn().mockImplementation(() => {
  301. throw new Error('Could not reach the extensions server')
  302. })
  303. await createService().sendItemsToExtensionsServer({
  304. userUuid: '1-2-3',
  305. extensionId: '2-3-4',
  306. extensionsServerUrl: 'https://extensions-server/extension1',
  307. forceMute: false,
  308. items: [item],
  309. backupFilename: 'backup-file',
  310. authParams,
  311. })
  312. expect(domainEventPublisher.publish).toHaveBeenCalled()
  313. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  314. })
  315. it('should publish a failed Google Drive backup event judging by extension url if request was not sent successfully', async () => {
  316. contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://gdrive.com/...' })
  317. httpClient.request = jest.fn().mockImplementation(() => {
  318. throw new Error('Could not reach the extensions server')
  319. })
  320. await createService().sendItemsToExtensionsServer({
  321. userUuid: '1-2-3',
  322. extensionId: '2-3-4',
  323. extensionsServerUrl: 'https://extensions-server/extension1',
  324. forceMute: false,
  325. items: [item],
  326. backupFilename: 'backup-file',
  327. authParams,
  328. })
  329. expect(domainEventPublisher.publish).toHaveBeenCalled()
  330. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  331. })
  332. it('should publish a failed One Drive backup event judging by extension url if request was not sent successfully', async () => {
  333. contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://onedrive.com/...' })
  334. httpClient.request = jest.fn().mockImplementation(() => {
  335. throw new Error('Could not reach the extensions server')
  336. })
  337. await createService().sendItemsToExtensionsServer({
  338. userUuid: '1-2-3',
  339. extensionId: '2-3-4',
  340. extensionsServerUrl: 'https://extensions-server/extension1',
  341. forceMute: false,
  342. items: [item],
  343. backupFilename: 'backup-file',
  344. authParams,
  345. })
  346. expect(domainEventPublisher.publish).toHaveBeenCalled()
  347. expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
  348. })
  349. it('should throw an error if cannot deduce extension by judging from the url', async () => {
  350. contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://foobar.com/...' })
  351. httpClient.request = jest.fn().mockImplementation(() => {
  352. throw new Error('Could not reach the extensions server')
  353. })
  354. let error = null
  355. try {
  356. await createService().sendItemsToExtensionsServer({
  357. userUuid: '1-2-3',
  358. extensionId: '2-3-4',
  359. extensionsServerUrl: 'https://extensions-server/extension1',
  360. forceMute: false,
  361. items: [item],
  362. backupFilename: 'backup-file',
  363. authParams,
  364. })
  365. } catch (e) {
  366. error = e
  367. }
  368. expect(error).not.toBeNull()
  369. })
  370. it('should throw an error if there is no extension name or url', async () => {
  371. contentDecoder.decode = jest.fn().mockReturnValue({})
  372. httpClient.request = jest.fn().mockImplementation(() => {
  373. throw new Error('Could not reach the extensions server')
  374. })
  375. let error = null
  376. try {
  377. await createService().sendItemsToExtensionsServer({
  378. userUuid: '1-2-3',
  379. extensionId: '2-3-4',
  380. extensionsServerUrl: 'https://extensions-server/extension1',
  381. forceMute: false,
  382. items: [item],
  383. backupFilename: 'backup-file',
  384. authParams,
  385. })
  386. } catch (e) {
  387. error = e
  388. }
  389. expect(error).not.toBeNull()
  390. })
  391. })