You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

favoriteAction.spec.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /**
  2. * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
  3. *
  4. * @author John Molakvoæ <skjnldsv@protonmail.com>
  5. *
  6. * @license AGPL-3.0-or-later
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. import { action } from './favoriteAction'
  23. import { expect } from '@jest/globals'
  24. import { File, Permission, View, FileAction } from '@nextcloud/files'
  25. import * as eventBus from '@nextcloud/event-bus'
  26. import * as favoriteAction from './favoriteAction'
  27. import axios from '@nextcloud/axios'
  28. import logger from '../logger'
  29. const view = {
  30. id: 'files',
  31. name: 'Files',
  32. } as View
  33. const favoriteView = {
  34. id: 'favorites',
  35. name: 'Favorites',
  36. } as View
  37. global.window.OC = {
  38. TAG_FAVORITE: '_$!<Favorite>!$_',
  39. }
  40. describe('Favorite action conditions tests', () => {
  41. test('Default values', () => {
  42. const file = new File({
  43. id: 1,
  44. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
  45. owner: 'admin',
  46. mime: 'text/plain',
  47. })
  48. expect(action).toBeInstanceOf(FileAction)
  49. expect(action.id).toBe('favorite')
  50. expect(action.displayName([file], view)).toBe('Add to favorites')
  51. expect(action.iconSvgInline([], view)).toBe('<svg>SvgMock</svg>')
  52. expect(action.default).toBeUndefined()
  53. expect(action.order).toBe(-50)
  54. })
  55. test('Display name is Remove from favorites if already in favorites', () => {
  56. const file = new File({
  57. id: 1,
  58. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
  59. owner: 'admin',
  60. mime: 'text/plain',
  61. attributes: {
  62. favorite: 1,
  63. },
  64. })
  65. expect(action.displayName([file], view)).toBe('Remove from favorites')
  66. })
  67. test('Display name for multiple state files', () => {
  68. const file1 = new File({
  69. id: 1,
  70. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
  71. owner: 'admin',
  72. mime: 'text/plain',
  73. permissions: Permission.ALL,
  74. attributes: {
  75. favorite: 1,
  76. },
  77. })
  78. const file2 = new File({
  79. id: 1,
  80. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
  81. owner: 'admin',
  82. mime: 'text/plain',
  83. permissions: Permission.ALL,
  84. attributes: {
  85. favorite: 0,
  86. },
  87. })
  88. const file3 = new File({
  89. id: 1,
  90. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
  91. owner: 'admin',
  92. mime: 'text/plain',
  93. permissions: Permission.ALL,
  94. attributes: {
  95. favorite: 1,
  96. },
  97. })
  98. expect(action.displayName([file1, file2, file3], view)).toBe('Add to favorites')
  99. expect(action.displayName([file1, file2], view)).toBe('Add to favorites')
  100. expect(action.displayName([file2, file3], view)).toBe('Add to favorites')
  101. expect(action.displayName([file1, file3], view)).toBe('Remove from favorites')
  102. })
  103. })
  104. describe('Favorite action enabled tests', () => {
  105. test('Enabled for dav file', () => {
  106. const file = new File({
  107. id: 1,
  108. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
  109. owner: 'admin',
  110. mime: 'text/plain',
  111. permissions: Permission.ALL,
  112. })
  113. expect(action.enabled).toBeDefined()
  114. expect(action.enabled!([file], view)).toBe(true)
  115. })
  116. test('Disabled for non-dav ressources', () => {
  117. const file = new File({
  118. id: 1,
  119. source: 'https://domain.com/data/foobar.txt',
  120. owner: 'admin',
  121. mime: 'text/plain',
  122. })
  123. expect(action.enabled).toBeDefined()
  124. expect(action.enabled!([file], view)).toBe(false)
  125. })
  126. })
  127. describe('Favorite action execute tests', () => {
  128. afterEach(() => {
  129. jest.spyOn(axios, 'post').mockRestore()
  130. })
  131. test('Favorite triggers tag addition', async () => {
  132. jest.spyOn(axios, 'post')
  133. jest.spyOn(eventBus, 'emit')
  134. const file = new File({
  135. id: 1,
  136. source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
  137. owner: 'admin',
  138. mime: 'text/plain',
  139. })
  140. const exec = await action.exec(file, view, '/')
  141. expect(exec).toBe(true)
  142. // Check POST request
  143. expect(axios.post).toBeCalledTimes(1)
  144. expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/foobar.txt', { tags: ['_$!<Favorite>!$_'] })
  145. // Check node change propagation
  146. expect(file.attributes.favorite).toBe(1)
  147. expect(eventBus.emit).toBeCalledTimes(1)
  148. expect(eventBus.emit).toBeCalledWith('files:favorites:added', file)
  149. })
  150. test('Favorite triggers tag removal', async () => {
  151. jest.spyOn(axios, 'post')
  152. jest.spyOn(eventBus, 'emit')
  153. const file = new File({
  154. id: 1,
  155. source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
  156. owner: 'admin',
  157. mime: 'text/plain',
  158. attributes: {
  159. favorite: 1,
  160. },
  161. })
  162. const exec = await action.exec(file, view, '/')
  163. expect(exec).toBe(true)
  164. // Check POST request
  165. expect(axios.post).toBeCalledTimes(1)
  166. expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/foobar.txt', { tags: [] })
  167. // Check node change propagation
  168. expect(file.attributes.favorite).toBe(0)
  169. expect(eventBus.emit).toBeCalledTimes(1)
  170. expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
  171. })
  172. test('Favorite triggers node removal if favorite view and root dir', async () => {
  173. jest.spyOn(axios, 'post')
  174. jest.spyOn(eventBus, 'emit')
  175. const file = new File({
  176. id: 1,
  177. source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
  178. owner: 'admin',
  179. mime: 'text/plain',
  180. attributes: {
  181. favorite: 1,
  182. },
  183. })
  184. const exec = await action.exec(file, favoriteView, '/')
  185. expect(exec).toBe(true)
  186. // Check POST request
  187. expect(axios.post).toBeCalledTimes(1)
  188. expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/foobar.txt', { tags: [] })
  189. // Check node change propagation
  190. expect(file.attributes.favorite).toBe(0)
  191. expect(eventBus.emit).toBeCalledTimes(2)
  192. expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file)
  193. expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:favorites:removed', file)
  194. })
  195. test('Favorite does NOT triggers node removal if favorite view but NOT root dir', async () => {
  196. jest.spyOn(axios, 'post')
  197. jest.spyOn(eventBus, 'emit')
  198. const file = new File({
  199. id: 1,
  200. source: 'http://localhost/remote.php/dav/files/admin/Foo/Bar/foobar.txt',
  201. root: '/files/admin',
  202. owner: 'admin',
  203. mime: 'text/plain',
  204. attributes: {
  205. favorite: 1,
  206. },
  207. })
  208. const exec = await action.exec(file, favoriteView, '/')
  209. expect(exec).toBe(true)
  210. // Check POST request
  211. expect(axios.post).toBeCalledTimes(1)
  212. expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/Foo/Bar/foobar.txt', { tags: [] })
  213. // Check node change propagation
  214. expect(file.attributes.favorite).toBe(0)
  215. expect(eventBus.emit).toBeCalledTimes(1)
  216. expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
  217. })
  218. test('Favorite fails and show error', async () => {
  219. const error = new Error('Mock error')
  220. jest.spyOn(axios, 'post').mockImplementation(() => { throw new Error('Mock error') })
  221. jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
  222. const file = new File({
  223. id: 1,
  224. source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
  225. owner: 'admin',
  226. mime: 'text/plain',
  227. attributes: {
  228. favorite: 0,
  229. },
  230. })
  231. const exec = await action.exec(file, view, '/')
  232. expect(exec).toBe(false)
  233. // Check POST request
  234. expect(axios.post).toBeCalledTimes(1)
  235. expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/foobar.txt', { tags: ['_$!<Favorite>!$_'] })
  236. // Check node change propagation
  237. expect(logger.error).toBeCalledTimes(1)
  238. expect(logger.error).toBeCalledWith('Error while adding a file to favourites', { error, source: file.source, node: file })
  239. expect(file.attributes.favorite).toBe(0)
  240. expect(eventBus.emit).toBeCalledTimes(0)
  241. })
  242. test('Removing from favorites fails and show error', async () => {
  243. const error = new Error('Mock error')
  244. jest.spyOn(axios, 'post').mockImplementation(() => { throw error })
  245. jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
  246. const file = new File({
  247. id: 1,
  248. source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
  249. owner: 'admin',
  250. mime: 'text/plain',
  251. attributes: {
  252. favorite: 1,
  253. },
  254. })
  255. const exec = await action.exec(file, view, '/')
  256. expect(exec).toBe(false)
  257. // Check POST request
  258. expect(axios.post).toBeCalledTimes(1)
  259. expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/foobar.txt', { tags: [] })
  260. // Check node change propagation
  261. expect(logger.error).toBeCalledTimes(1)
  262. expect(logger.error).toBeCalledWith('Error while removing a file from favourites', { error, source: file.source, node: file })
  263. expect(file.attributes.favorite).toBe(1)
  264. expect(eventBus.emit).toBeCalledTimes(0)
  265. })
  266. })
  267. describe('Favorite action batch execute tests', () => {
  268. test('Favorite action batch execute with mixed files', async () => {
  269. jest.spyOn(favoriteAction, 'favoriteNode')
  270. jest.spyOn(axios, 'post')
  271. const file1 = new File({
  272. id: 1,
  273. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
  274. owner: 'admin',
  275. mime: 'text/plain',
  276. permissions: Permission.ALL,
  277. attributes: {
  278. favorite: 1,
  279. },
  280. })
  281. const file2 = new File({
  282. id: 1,
  283. source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
  284. owner: 'admin',
  285. mime: 'text/plain',
  286. permissions: Permission.ALL,
  287. attributes: {
  288. favorite: 0,
  289. },
  290. })
  291. // Mixed states triggers favorite action
  292. const exec = await action.execBatch!([file1, file2], view, '/')
  293. expect(exec).toStrictEqual([true, true])
  294. expect([file1, file2].every(file => file.attributes.favorite === 1)).toBe(true)
  295. expect(favoriteAction.favoriteNode).toBeCalledTimes(2)
  296. expect(axios.post).toBeCalledTimes(2)
  297. expect(axios.post).toHaveBeenNthCalledWith(1, '/index.php/apps/files/api/v1/files/foo.txt', { tags: ['_$!<Favorite>!$_'] })
  298. expect(axios.post).toHaveBeenNthCalledWith(2, '/index.php/apps/files/api/v1/files/bar.txt', { tags: ['_$!<Favorite>!$_'] })
  299. })
  300. test('Remove from favorite action batch execute with favorites only files', async () => {
  301. jest.spyOn(favoriteAction, 'favoriteNode')
  302. jest.spyOn(axios, 'post')
  303. const file1 = new File({
  304. id: 1,
  305. source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
  306. owner: 'admin',
  307. mime: 'text/plain',
  308. permissions: Permission.ALL,
  309. attributes: {
  310. favorite: 1,
  311. },
  312. })
  313. const file2 = new File({
  314. id: 1,
  315. source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
  316. owner: 'admin',
  317. mime: 'text/plain',
  318. permissions: Permission.ALL,
  319. attributes: {
  320. favorite: 1,
  321. },
  322. })
  323. // Mixed states triggers favorite action
  324. const exec = await action.execBatch!([file1, file2], view, '/')
  325. expect(exec).toStrictEqual([true, true])
  326. expect([file1, file2].every(file => file.attributes.favorite === 0)).toBe(true)
  327. expect(favoriteAction.favoriteNode).toBeCalledTimes(2)
  328. expect(axios.post).toBeCalledTimes(2)
  329. expect(axios.post).toHaveBeenNthCalledWith(1, '/index.php/apps/files/api/v1/files/foo.txt', { tags: [] })
  330. expect(axios.post).toHaveBeenNthCalledWith(2, '/index.php/apps/files/api/v1/files/bar.txt', { tags: [] })
  331. })
  332. })