@sync-in/server 1.9.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +41 -4
  2. package/environment/environment.dist.yaml +15 -5
  3. package/package.json +18 -19
  4. package/server/app.bootstrap.js +1 -1
  5. package/server/app.bootstrap.js.map +1 -1
  6. package/server/app.constants.js +3 -2
  7. package/server/app.constants.js.map +1 -1
  8. package/server/applications/files/constants/cache.js +2 -5
  9. package/server/applications/files/constants/cache.js.map +1 -1
  10. package/server/applications/files/constants/files.js +4 -0
  11. package/server/applications/files/constants/files.js.map +1 -1
  12. package/server/applications/files/constants/operations.js +4 -0
  13. package/server/applications/files/constants/operations.js.map +1 -1
  14. package/server/applications/files/constants/routes.js +1 -26
  15. package/server/applications/files/constants/routes.js.map +1 -1
  16. package/server/applications/files/files.config.js +15 -39
  17. package/server/applications/files/files.config.js.map +1 -1
  18. package/server/applications/files/files.controller.js +4 -4
  19. package/server/applications/files/files.controller.js.map +1 -1
  20. package/server/applications/files/files.module.js +12 -9
  21. package/server/applications/files/files.module.js.map +1 -1
  22. package/server/applications/files/interfaces/file-lock.interface.js.map +1 -1
  23. package/server/applications/files/interfaces/file-props.interface.js.map +1 -1
  24. package/server/applications/files/modules/collabora-online/collabora-online-environment.decorator.js +32 -0
  25. package/server/applications/files/modules/collabora-online/collabora-online-environment.decorator.js.map +1 -0
  26. package/server/applications/files/modules/collabora-online/collabora-online-manager.service.js +280 -0
  27. package/server/applications/files/modules/collabora-online/collabora-online-manager.service.js.map +1 -0
  28. package/server/applications/files/modules/collabora-online/collabora-online-manager.service.spec.js +552 -0
  29. package/server/applications/files/modules/collabora-online/collabora-online-manager.service.spec.js.map +1 -0
  30. package/server/applications/files/modules/collabora-online/collabora-online.config.js +40 -0
  31. package/server/applications/files/modules/collabora-online/collabora-online.config.js.map +1 -0
  32. package/server/applications/files/modules/collabora-online/collabora-online.constants.js +110 -0
  33. package/server/applications/files/modules/collabora-online/collabora-online.constants.js.map +1 -0
  34. package/server/applications/files/modules/collabora-online/collabora-online.controller.js +128 -0
  35. package/server/applications/files/modules/collabora-online/collabora-online.controller.js.map +1 -0
  36. package/server/applications/files/modules/collabora-online/collabora-online.controller.spec.js +47 -0
  37. package/server/applications/files/modules/collabora-online/collabora-online.controller.spec.js.map +1 -0
  38. package/server/applications/files/{interfaces/only-office-config.interface.js → modules/collabora-online/collabora-online.dtos.js} +1 -1
  39. package/server/applications/files/modules/collabora-online/collabora-online.dtos.js.map +1 -0
  40. package/server/applications/files/{guards/files-only-office.guard.js → modules/collabora-online/collabora-online.guard.js} +7 -21
  41. package/server/applications/files/modules/collabora-online/collabora-online.guard.js.map +1 -0
  42. package/server/applications/files/modules/collabora-online/collabora-online.guard.spec.js +86 -0
  43. package/server/applications/files/modules/collabora-online/collabora-online.guard.spec.js.map +1 -0
  44. package/server/applications/files/modules/collabora-online/collabora-online.interface.js +10 -0
  45. package/server/applications/files/modules/collabora-online/collabora-online.interface.js.map +1 -0
  46. package/server/applications/files/modules/collabora-online/collabora-online.module.js +41 -0
  47. package/server/applications/files/modules/collabora-online/collabora-online.module.js.map +1 -0
  48. package/server/applications/files/modules/collabora-online/collabora-online.routes.js +35 -0
  49. package/server/applications/files/modules/collabora-online/collabora-online.routes.js.map +1 -0
  50. package/server/applications/files/modules/collabora-online/collabora-online.strategy.js +59 -0
  51. package/server/applications/files/modules/collabora-online/collabora-online.strategy.js.map +1 -0
  52. package/server/applications/files/modules/collabora-online/collabora-online.utils.js +28 -0
  53. package/server/applications/files/modules/collabora-online/collabora-online.utils.js.map +1 -0
  54. package/server/applications/files/{decorators → modules/only-office}/only-office-environment.decorator.js +5 -5
  55. package/server/applications/files/modules/only-office/only-office-environment.decorator.js.map +1 -0
  56. package/server/applications/files/{services/files-only-office-manager.service.js → modules/only-office/only-office-manager.service.js} +101 -97
  57. package/server/applications/files/modules/only-office/only-office-manager.service.js.map +1 -0
  58. package/server/applications/files/modules/only-office/only-office-manager.service.spec.js +477 -0
  59. package/server/applications/files/modules/only-office/only-office-manager.service.spec.js.map +1 -0
  60. package/server/applications/files/modules/only-office/only-office.config.js +51 -0
  61. package/server/applications/files/modules/only-office/only-office.config.js.map +1 -0
  62. package/server/applications/files/modules/only-office/only-office.constants.js +417 -0
  63. package/server/applications/files/modules/only-office/only-office.constants.js.map +1 -0
  64. package/server/applications/files/{files-only-office.controller.js → modules/only-office/only-office.controller.js} +35 -52
  65. package/server/applications/files/modules/only-office/only-office.controller.js.map +1 -0
  66. package/server/applications/files/{files-only-office.controller.spec.js → modules/only-office/only-office.controller.spec.js} +24 -21
  67. package/server/applications/files/modules/only-office/only-office.controller.spec.js.map +1 -0
  68. package/server/applications/files/modules/only-office/only-office.dtos.js +10 -0
  69. package/server/applications/files/modules/only-office/only-office.dtos.js.map +1 -0
  70. package/server/applications/files/modules/only-office/only-office.guard.js +40 -0
  71. package/server/applications/files/modules/only-office/only-office.guard.js.map +1 -0
  72. package/server/applications/files/{guards/files-only-office.guard.spec.js → modules/only-office/only-office.guard.spec.js} +15 -21
  73. package/server/applications/files/modules/only-office/only-office.guard.spec.js.map +1 -0
  74. package/server/applications/files/modules/only-office/only-office.interface.js +10 -0
  75. package/server/applications/files/modules/only-office/only-office.interface.js.map +1 -0
  76. package/server/applications/files/modules/only-office/only-office.module.js +41 -0
  77. package/server/applications/files/modules/only-office/only-office.module.js.map +1 -0
  78. package/server/applications/files/modules/only-office/only-office.routes.js +45 -0
  79. package/server/applications/files/modules/only-office/only-office.routes.js.map +1 -0
  80. package/server/applications/files/{guards/files-only-office.strategy.js → modules/only-office/only-office.strategy.js} +11 -11
  81. package/server/applications/files/modules/only-office/only-office.strategy.js.map +1 -0
  82. package/server/applications/files/services/files-lock-manager.service.js +25 -33
  83. package/server/applications/files/services/files-lock-manager.service.js.map +1 -1
  84. package/server/applications/files/services/files-manager.service.js +17 -16
  85. package/server/applications/files/services/files-manager.service.js.map +1 -1
  86. package/server/applications/files/services/files-methods.service.js +2 -2
  87. package/server/applications/files/services/files-methods.service.js.map +1 -1
  88. package/server/applications/files/services/files-methods.service.spec.js +5 -5
  89. package/server/applications/files/services/files-methods.service.spec.js.map +1 -1
  90. package/server/applications/files/services/files-recents.service.js +4 -0
  91. package/server/applications/files/services/files-recents.service.js.map +1 -1
  92. package/server/applications/files/services/files-scheduler.service.js +24 -5
  93. package/server/applications/files/services/files-scheduler.service.js.map +1 -1
  94. package/server/applications/files/utils/files.js +10 -2
  95. package/server/applications/files/utils/files.js.map +1 -1
  96. package/server/applications/links/constants/routes.js +5 -0
  97. package/server/applications/links/constants/routes.js.map +1 -1
  98. package/server/applications/links/interfaces/link-space.interface.js.map +1 -1
  99. package/server/applications/links/links.controller.js +25 -5
  100. package/server/applications/links/links.controller.js.map +1 -1
  101. package/server/applications/links/services/links-manager.service.js +43 -21
  102. package/server/applications/links/services/links-manager.service.js.map +1 -1
  103. package/server/applications/links/services/links-manager.service.spec.js +4 -3
  104. package/server/applications/links/services/links-manager.service.spec.js.map +1 -1
  105. package/server/applications/links/services/links-queries.service.js +9 -2
  106. package/server/applications/links/services/links-queries.service.js.map +1 -1
  107. package/server/applications/shares/interfaces/share-link.interface.js.map +1 -1
  108. package/server/applications/shares/services/shares-manager.service.js +3 -0
  109. package/server/applications/shares/services/shares-manager.service.js.map +1 -1
  110. package/server/applications/shares/services/shares-manager.service.spec.js +2 -1
  111. package/server/applications/shares/services/shares-manager.service.spec.js.map +1 -1
  112. package/server/applications/shares/services/shares-queries.service.js +1 -0
  113. package/server/applications/shares/services/shares-queries.service.js.map +1 -1
  114. package/server/applications/spaces/constants/spaces.js +2 -2
  115. package/server/applications/spaces/constants/spaces.js.map +1 -1
  116. package/server/applications/spaces/decorators/space-override-permission.decorator.js +18 -0
  117. package/server/applications/spaces/decorators/space-override-permission.decorator.js.map +1 -0
  118. package/server/applications/spaces/guards/space.guard.js +40 -33
  119. package/server/applications/spaces/guards/space.guard.js.map +1 -1
  120. package/server/applications/spaces/guards/space.guard.spec.js +10 -15
  121. package/server/applications/spaces/guards/space.guard.spec.js.map +1 -1
  122. package/server/applications/spaces/services/spaces-scheduler.service.js +9 -1
  123. package/server/applications/spaces/services/spaces-scheduler.service.js.map +1 -1
  124. package/server/applications/webdav/constants/webdav.js +4 -0
  125. package/server/applications/webdav/constants/webdav.js.map +1 -1
  126. package/server/applications/webdav/guards/webdav-protocol.guard.js +9 -8
  127. package/server/applications/webdav/guards/webdav-protocol.guard.js.map +1 -1
  128. package/server/applications/webdav/guards/webdav-protocol.guard.spec.js +1 -1
  129. package/server/applications/webdav/guards/webdav-protocol.guard.spec.js.map +1 -1
  130. package/server/applications/webdav/interfaces/webdav.interface.js.map +1 -1
  131. package/server/applications/webdav/services/webdav-methods.service.js +40 -17
  132. package/server/applications/webdav/services/webdav-methods.service.js.map +1 -1
  133. package/server/applications/webdav/services/webdav-methods.service.spec.js +2157 -1289
  134. package/server/applications/webdav/services/webdav-methods.service.spec.js.map +1 -1
  135. package/server/applications/webdav/utils/webdav.js +8 -4
  136. package/server/applications/webdav/utils/webdav.js.map +1 -1
  137. package/server/applications/webdav/webdav.controller.js +4 -4
  138. package/server/applications/webdav/webdav.controller.js.map +1 -1
  139. package/server/authentication/guards/auth-token-access.guard.js +8 -3
  140. package/server/authentication/guards/auth-token-access.guard.js.map +1 -1
  141. package/server/authentication/services/auth-methods/auth-method-two-fa.service.js +1 -1
  142. package/server/authentication/services/auth-methods/auth-method-two-fa.service.js.map +1 -1
  143. package/server/authentication/services/auth-methods/auth-method-two-fa.service.spec.js +350 -4
  144. package/server/authentication/services/auth-methods/auth-method-two-fa.service.spec.js.map +1 -1
  145. package/server/configuration/config.environment.js +5 -1
  146. package/server/configuration/config.environment.js.map +1 -1
  147. package/server/configuration/config.interfaces.js.map +1 -1
  148. package/static/3rdpartylicenses.txt +507 -507
  149. package/static/assets/pdfjs/build/pdf.mjs +93 -33
  150. package/static/assets/pdfjs/build/pdf.mjs.map +1 -1
  151. package/static/assets/pdfjs/build/pdf.sandbox.mjs +3 -3
  152. package/static/assets/pdfjs/build/pdf.sandbox.mjs.map +1 -1
  153. package/static/assets/pdfjs/build/pdf.worker.mjs +166 -54
  154. package/static/assets/pdfjs/build/pdf.worker.mjs.map +1 -1
  155. package/static/assets/pdfjs/version +1 -1
  156. package/static/assets/pdfjs/web/images/checkmark.svg +5 -0
  157. package/static/assets/pdfjs/web/images/pages_closeButton.svg +3 -0
  158. package/static/assets/pdfjs/web/images/pages_selected.svg +7 -0
  159. package/static/assets/pdfjs/web/images/pages_viewArrow.svg +3 -0
  160. package/static/assets/pdfjs/web/images/pages_viewButton.svg +3 -0
  161. package/static/assets/pdfjs/web/locale/be/viewer.ftl +0 -2
  162. package/static/assets/pdfjs/web/locale/bs/viewer.ftl +0 -5
  163. package/static/assets/pdfjs/web/locale/cs/viewer.ftl +4 -6
  164. package/static/assets/pdfjs/web/locale/cy/viewer.ftl +0 -2
  165. package/static/assets/pdfjs/web/locale/da/viewer.ftl +0 -2
  166. package/static/assets/pdfjs/web/locale/de/viewer.ftl +0 -2
  167. package/static/assets/pdfjs/web/locale/dsb/viewer.ftl +0 -2
  168. package/static/assets/pdfjs/web/locale/el/viewer.ftl +0 -2
  169. package/static/assets/pdfjs/web/locale/en-CA/viewer.ftl +6 -2
  170. package/static/assets/pdfjs/web/locale/en-GB/viewer.ftl +0 -2
  171. package/static/assets/pdfjs/web/locale/en-US/viewer.ftl +82 -17
  172. package/static/assets/pdfjs/web/locale/eo/viewer.ftl +0 -2
  173. package/static/assets/pdfjs/web/locale/es-AR/viewer.ftl +0 -2
  174. package/static/assets/pdfjs/web/locale/es-CL/viewer.ftl +0 -2
  175. package/static/assets/pdfjs/web/locale/es-ES/viewer.ftl +0 -2
  176. package/static/assets/pdfjs/web/locale/es-MX/viewer.ftl +0 -2
  177. package/static/assets/pdfjs/web/locale/eu/viewer.ftl +0 -2
  178. package/static/assets/pdfjs/web/locale/fi/viewer.ftl +0 -2
  179. package/static/assets/pdfjs/web/locale/fr/viewer.ftl +0 -2
  180. package/static/assets/pdfjs/web/locale/fur/viewer.ftl +0 -5
  181. package/static/assets/pdfjs/web/locale/fy-NL/viewer.ftl +3 -5
  182. package/static/assets/pdfjs/web/locale/gn/viewer.ftl +0 -2
  183. package/static/assets/pdfjs/web/locale/he/viewer.ftl +0 -2
  184. package/static/assets/pdfjs/web/locale/hr/viewer.ftl +66 -0
  185. package/static/assets/pdfjs/web/locale/hsb/viewer.ftl +0 -2
  186. package/static/assets/pdfjs/web/locale/hu/viewer.ftl +0 -2
  187. package/static/assets/pdfjs/web/locale/hy-AM/viewer.ftl +3 -8
  188. package/static/assets/pdfjs/web/locale/ia/viewer.ftl +0 -2
  189. package/static/assets/pdfjs/web/locale/id/viewer.ftl +0 -5
  190. package/static/assets/pdfjs/web/locale/is/viewer.ftl +0 -5
  191. package/static/assets/pdfjs/web/locale/it/viewer.ftl +0 -2
  192. package/static/assets/pdfjs/web/locale/ja/viewer.ftl +0 -14
  193. package/static/assets/pdfjs/web/locale/ka/viewer.ftl +4 -6
  194. package/static/assets/pdfjs/web/locale/kab/viewer.ftl +0 -5
  195. package/static/assets/pdfjs/web/locale/kk/viewer.ftl +0 -2
  196. package/static/assets/pdfjs/web/locale/ko/viewer.ftl +0 -2
  197. package/static/assets/pdfjs/web/locale/nb-NO/viewer.ftl +1 -3
  198. package/static/assets/pdfjs/web/locale/nl/viewer.ftl +0 -2
  199. package/static/assets/pdfjs/web/locale/nn-NO/viewer.ftl +4 -2
  200. package/static/assets/pdfjs/web/locale/pa-IN/viewer.ftl +0 -2
  201. package/static/assets/pdfjs/web/locale/pl/viewer.ftl +0 -2
  202. package/static/assets/pdfjs/web/locale/pt-BR/viewer.ftl +0 -2
  203. package/static/assets/pdfjs/web/locale/pt-PT/viewer.ftl +35 -0
  204. package/static/assets/pdfjs/web/locale/rm/viewer.ftl +0 -5
  205. package/static/assets/pdfjs/web/locale/ro/viewer.ftl +4 -6
  206. package/static/assets/pdfjs/web/locale/ru/viewer.ftl +3 -5
  207. package/static/assets/pdfjs/web/locale/sk/viewer.ftl +0 -2
  208. package/static/assets/pdfjs/web/locale/sl/viewer.ftl +0 -2
  209. package/static/assets/pdfjs/web/locale/sq/viewer.ftl +0 -2
  210. package/static/assets/pdfjs/web/locale/sv-SE/viewer.ftl +0 -2
  211. package/static/assets/pdfjs/web/locale/tg/viewer.ftl +0 -2
  212. package/static/assets/pdfjs/web/locale/th/viewer.ftl +2 -2
  213. package/static/assets/pdfjs/web/locale/tr/viewer.ftl +0 -2
  214. package/static/assets/pdfjs/web/locale/vi/viewer.ftl +0 -2
  215. package/static/assets/pdfjs/web/locale/zh-CN/viewer.ftl +0 -2
  216. package/static/assets/pdfjs/web/locale/zh-TW/viewer.ftl +0 -2
  217. package/static/assets/pdfjs/web/viewer.css +1778 -835
  218. package/static/assets/pdfjs/web/viewer.html +167 -86
  219. package/static/assets/pdfjs/web/viewer.mjs +1106 -801
  220. package/static/assets/pdfjs/web/viewer.mjs.map +1 -1
  221. package/static/chunk-27V66YJV.js +2 -0
  222. package/static/{chunk-WJYVS27M.js → chunk-27Z3SYRL.js} +1 -1
  223. package/static/{chunk-NFIES7BC.js → chunk-2RWLNKZH.js} +1 -1
  224. package/static/chunk-2YQ4SX3A.js +13 -0
  225. package/static/{chunk-GENTF6JM.js → chunk-3JYMJQYT.js} +1 -1
  226. package/static/chunk-3QTROEHV.js +1 -0
  227. package/static/{chunk-ZPI7RQ2S.js → chunk-3RPUQ22U.js} +1 -1
  228. package/static/{chunk-R6VB3INJ.js → chunk-3WZ6F3LC.js} +1 -1
  229. package/static/chunk-3ZLBVUCX.js +2 -0
  230. package/static/{chunk-5HCVWZMA.js → chunk-45AZ6ZML.js} +1 -1
  231. package/static/chunk-46TJLPJY.js +1 -0
  232. package/static/chunk-4NIYCYRS.js +2 -0
  233. package/static/{chunk-XXYMVRSH.js → chunk-4TPFERL6.js} +1 -1
  234. package/static/{chunk-CAZSNVMS.js → chunk-5O66CLTD.js} +1 -1
  235. package/static/chunk-6OEOADR6.js +1 -0
  236. package/static/chunk-6WMXMIE4.js +1 -0
  237. package/static/{chunk-NK2NMAJI.js → chunk-7VRYTDX4.js} +1 -1
  238. package/static/{chunk-ASBPYTLT.js → chunk-ARS47O5X.js} +1 -1
  239. package/static/chunk-B6HQYQYG.js +1 -0
  240. package/static/chunk-BCN4T5DO.js +2 -0
  241. package/static/{chunk-PKU4IIIR.js → chunk-CCZWPM7Q.js} +1 -1
  242. package/static/{chunk-QUSS6SUC.js → chunk-CMNMPG6Z.js} +1 -1
  243. package/static/{chunk-GDPJRUVU.js → chunk-CSVPAZHK.js} +1 -1
  244. package/static/{chunk-BJARRIS6.js → chunk-D55YR5X7.js} +4 -4
  245. package/static/{chunk-Z6RJZIDG.js → chunk-D5FQ72R4.js} +1 -1
  246. package/static/{chunk-4DF2SQD4.js → chunk-DGCVA6BM.js} +1 -1
  247. package/static/{chunk-TVJQXN73.js → chunk-DVCN3P7Q.js} +1 -1
  248. package/static/chunk-E32J777S.js +5 -0
  249. package/static/{chunk-5NHB7SV3.js → chunk-FIUF2JM4.js} +1 -1
  250. package/static/{chunk-RJOHDAPM.js → chunk-G3PL6YX3.js} +1 -1
  251. package/static/chunk-G7RZN7HN.js +1 -0
  252. package/static/{chunk-DDRGLHOP.js → chunk-GQHXYX6Z.js} +1 -1
  253. package/static/{chunk-5HYSNQR4.js → chunk-GWRAGN3M.js} +1 -1
  254. package/static/{chunk-ZC5ZDCDC.js → chunk-GXWGB7WO.js} +1 -1
  255. package/static/{chunk-25PWAXTJ.js → chunk-HGODIZTV.js} +1 -1
  256. package/static/{chunk-4KXJ6C4N.js → chunk-HZAB6F4Q.js} +1 -1
  257. package/static/chunk-I3FR3A45.js +1 -0
  258. package/static/{chunk-A6J6SOM6.js → chunk-I5SPA4G2.js} +1 -1
  259. package/static/{chunk-TGHBDJZA.js → chunk-IMFO2MI7.js} +1 -1
  260. package/static/{chunk-CURVLK7L.js → chunk-JNTNMIUH.js} +1 -1
  261. package/static/chunk-JRXG43AA.js +2 -0
  262. package/static/{chunk-XAIOGRBO.js → chunk-KAUCN24H.js} +1 -1
  263. package/static/chunk-KDUAB76O.js +1 -0
  264. package/static/chunk-KPOQLDWF.js +1 -0
  265. package/static/{chunk-HE6EDXWI.js → chunk-KWFELZTM.js} +1 -1
  266. package/static/{chunk-2CAAJBRO.js → chunk-L3BIP4AA.js} +1 -1
  267. package/static/{chunk-U75PLYIJ.js → chunk-LGIVVJDD.js} +1 -1
  268. package/static/{chunk-JEVBUJQ4.js → chunk-LNLBIJZD.js} +1 -1
  269. package/static/chunk-LTJNLOX2.js +1 -0
  270. package/static/{chunk-SDR3UG2F.js → chunk-LZUHREOF.js} +1 -1
  271. package/static/{chunk-VO4WVT6K.js → chunk-NIR4YE2E.js} +1 -1
  272. package/static/{chunk-S6YKBWJE.js → chunk-NJJURHX4.js} +1 -1
  273. package/static/chunk-NNZWSNAW.js +1 -0
  274. package/static/chunk-NWKBB7J4.js +1 -0
  275. package/static/chunk-O3YLAEVE.js +3 -0
  276. package/static/chunk-OUHCDDT6.js +1 -0
  277. package/static/{chunk-ZRBLCAOK.js → chunk-PDG7DOEF.js} +1 -1
  278. package/static/chunk-POUWUMC4.js +1 -0
  279. package/static/{chunk-YTBSB2GE.js → chunk-PPJCVBJH.js} +1 -1
  280. package/static/{chunk-K3MOXDU5.js → chunk-PQZLR4P3.js} +1 -1
  281. package/static/chunk-PVYVY3GD.js +1 -0
  282. package/static/chunk-Q5X5TPAG.js +1 -0
  283. package/static/{chunk-LFAQLJZK.js → chunk-QHJT5H4M.js} +1 -1
  284. package/static/{chunk-A7DSX7VP.js → chunk-R4VMWCM5.js} +1 -1
  285. package/static/{chunk-27XEAHMV.js → chunk-R7PLNX75.js} +1 -1
  286. package/static/chunk-RJULB733.js +1 -0
  287. package/static/{chunk-MBFMTBVJ.js → chunk-RNVPQQKT.js} +5 -5
  288. package/static/chunk-RTNEBRKJ.js +1 -0
  289. package/static/{chunk-FXM7XXWA.js → chunk-S3TTWPQA.js} +1 -1
  290. package/static/{chunk-6VJI4X2A.js → chunk-SDJNZULP.js} +1 -1
  291. package/static/chunk-SNOOCDJD.js +1 -0
  292. package/static/chunk-T42BV6TR.js +1 -0
  293. package/static/{chunk-4OV3SAUS.js → chunk-TNCKNU6I.js} +1 -1
  294. package/static/{chunk-2LHHXDD5.js → chunk-ULSPQ3HP.js} +1 -1
  295. package/static/{chunk-4EUHBTWV.js → chunk-UOK3LKSX.js} +1 -1
  296. package/static/{chunk-7NI353LS.js → chunk-VD5JHSDS.js} +1 -1
  297. package/static/{chunk-YXWF2DGF.js → chunk-XBKCQCBI.js} +1 -1
  298. package/static/{chunk-KBWK65KM.js → chunk-XEWLBWFF.js} +1 -1
  299. package/static/{chunk-FLPZB3OX.js → chunk-XTVNHFKX.js} +1 -1
  300. package/static/chunk-ZCSHU3D7.js +1 -0
  301. package/static/{chunk-FRBTL2ER.js → chunk-ZEJLIGAY.js} +1 -1
  302. package/static/{chunk-7H5O4BLV.js → chunk-ZHOE5VEY.js} +1 -1
  303. package/static/chunk-ZOMRIN3G.js +2 -0
  304. package/static/index.html +2 -2
  305. package/static/main-YKDNJ7LK.js +11 -0
  306. package/static/{styles-S5HVK4H5.css → styles-XLLEY5Y3.css} +1 -1
  307. package/server/applications/files/constants/only-office.js +0 -531
  308. package/server/applications/files/constants/only-office.js.map +0 -1
  309. package/server/applications/files/decorators/only-office-environment.decorator.js.map +0 -1
  310. package/server/applications/files/files-only-office.controller.js.map +0 -1
  311. package/server/applications/files/files-only-office.controller.spec.js.map +0 -1
  312. package/server/applications/files/guards/files-only-office.guard.js.map +0 -1
  313. package/server/applications/files/guards/files-only-office.guard.spec.js.map +0 -1
  314. package/server/applications/files/guards/files-only-office.strategy.js.map +0 -1
  315. package/server/applications/files/interfaces/only-office-config.interface.js.map +0 -1
  316. package/server/applications/files/services/files-only-office-manager.service.js.map +0 -1
  317. package/server/applications/files/services/files-only-office-manager.service.spec.js +0 -58
  318. package/server/applications/files/services/files-only-office-manager.service.spec.js.map +0 -1
  319. package/static/chunk-2XY4PMI5.js +0 -1
  320. package/static/chunk-33WFRCUP.js +0 -1
  321. package/static/chunk-3LVFDMTN.js +0 -1
  322. package/static/chunk-42L6C5MT.js +0 -1
  323. package/static/chunk-5WCQBTXW.js +0 -1
  324. package/static/chunk-A7R246NW.js +0 -1
  325. package/static/chunk-BSB4VROD.js +0 -2
  326. package/static/chunk-DHFQIFOF.js +0 -1
  327. package/static/chunk-DRHPEERW.js +0 -2
  328. package/static/chunk-FCGTI42I.js +0 -1
  329. package/static/chunk-H4RLHI3Y.js +0 -1
  330. package/static/chunk-ITVA26X2.js +0 -2
  331. package/static/chunk-IUJ4IK26.js +0 -1
  332. package/static/chunk-L3PDWJZ3.js +0 -3
  333. package/static/chunk-LBXOAKBD.js +0 -1
  334. package/static/chunk-LZKI5P5T.js +0 -1
  335. package/static/chunk-MYM43ENO.js +0 -1
  336. package/static/chunk-MZBO5PAR.js +0 -1
  337. package/static/chunk-NAH4V2R6.js +0 -2
  338. package/static/chunk-O7UXVNR2.js +0 -1
  339. package/static/chunk-PCFH5HCI.js +0 -2
  340. package/static/chunk-SRBOO7AO.js +0 -1
  341. package/static/chunk-UUX3M6DC.js +0 -1
  342. package/static/chunk-VJ2HWQRJ.js +0 -5
  343. package/static/chunk-VZPCXSRG.js +0 -2
  344. package/static/chunk-W72JYHOH.js +0 -1
  345. package/static/chunk-XHQEF2IX.js +0 -1
  346. package/static/chunk-XKEBQNQJ.js +0 -1
  347. package/static/chunk-ZERBTNFW.js +0 -13
  348. package/static/main-FE6GWZXU.js +0 -11
  349. /package/static/assets/pdfjs/web/images/{toolbarButton-sidebarToggle.svg → toolbarButton-viewsManagerToggle.svg} +0 -0
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "__esModule", {
8
8
  });
9
9
  const _common = require("@nestjs/common");
10
10
  const _testing = require("@nestjs/testing");
11
+ const _fileerror = require("../../files/models/file-error");
12
+ const _filelockerror = require("../../files/models/file-lock-error");
11
13
  const _fileslockmanagerservice = require("../../files/services/files-lock-manager.service");
12
14
  const _filesmanagerservice = require("../../files/services/files-manager.service");
13
15
  const _files = require("../../files/utils/files");
@@ -59,10 +61,13 @@ function _interop_require_wildcard(obj, nodeInterop) {
59
61
  }
60
62
  return newObj;
61
63
  }
64
+ // Mock external dependencies
62
65
  jest.mock('../../files/utils/files', ()=>({
63
- isPathExists: jest.fn(),
66
+ isPathExists: jest.fn().mockReturnValue(false),
67
+ isPathIsDir: jest.fn(),
68
+ fileName: jest.fn().mockReturnValue('fileName'),
64
69
  dirName: jest.fn(),
65
- genEtag: jest.fn().mockReturnValue('W/"etag"')
70
+ genEtag: jest.fn().mockReturnValue('W/"etag-123"')
66
71
  }));
67
72
  jest.mock('../../spaces/utils/permissions', ()=>({
68
73
  haveSpaceEnvPermissions: jest.fn()
@@ -77,12 +82,13 @@ jest.mock('../../spaces/utils/paths', ()=>{
77
82
  jest.mock('../decorators/if-header.decorator', ()=>({
78
83
  IfHeaderDecorator: ()=>(_target, _key, _desc)=>undefined
79
84
  }));
80
- describe(_webdavmethodsservice.WebDAVMethods.name, ()=>{
85
+ describe('WebDAVMethods', ()=>{
81
86
  let service;
82
87
  let filesManager;
83
88
  let filesLockManager;
84
89
  let webDAVSpaces;
85
- const makeRes = ()=>{
90
+ // Helper to create a mocked response object
91
+ const createMockResponse = ()=>{
86
92
  const res = {
87
93
  statusCode: undefined,
88
94
  body: undefined,
@@ -107,56 +113,64 @@ describe(_webdavmethodsservice.WebDAVMethods.name, ()=>{
107
113
  };
108
114
  return res;
109
115
  };
110
- const baseReq = (overrides = {})=>({
116
+ // Helper to create a base request object
117
+ const createBaseRequest = (overrides = {})=>({
111
118
  method: 'GET',
112
119
  user: {
113
120
  id: 1,
114
- login: 'user-1'
121
+ login: 'test-user',
122
+ fullName: 'Test User',
123
+ email: 'test-user@sync-in.com'
115
124
  },
116
125
  dav: {
117
- url: '/webdav/url',
126
+ url: '/webdav/test/file.txt',
118
127
  depth: '0',
119
128
  httpVersion: 'HTTP/1.1',
120
- body: '<lockrequest/>',
129
+ body: '<lockinfo/>',
121
130
  lock: {
122
131
  timeout: 60,
123
132
  lockscope: 'exclusive',
124
- owner: 'user-1',
125
- token: 'opaquetoken:abc'
126
- }
133
+ owner: 'test-user',
134
+ token: 'opaquelocktoken:abc123'
135
+ },
136
+ ifHeaders: []
127
137
  },
128
138
  space: {
129
- id: 10,
130
- alias: 'spaceA',
131
- url: '/spaces/spaceA/file.txt',
132
- realPath: '/real/path/file.txt',
139
+ id: 1,
140
+ alias: 'test-space',
141
+ url: '/webdav/test/file.txt',
142
+ realPath: '/real/path/to/file.txt',
133
143
  inSharesList: false,
134
144
  dbFile: {
135
- path: 'file.txt'
145
+ path: 'file.txt',
146
+ spaceId: 1,
147
+ inTrash: false
136
148
  }
137
149
  },
138
150
  ...overrides
139
151
  });
140
- beforeAll(async ()=>{
152
+ beforeEach(async ()=>{
153
+ // Initialize mocks
141
154
  filesManager = {
142
155
  sendFileFromSpace: jest.fn(),
143
- mkFile: jest.fn().mockResolvedValue(undefined),
156
+ mkFile: jest.fn(),
144
157
  saveStream: jest.fn(),
145
- delete: jest.fn().mockResolvedValue(undefined),
146
- touch: jest.fn().mockResolvedValue(undefined),
147
- mkDir: jest.fn().mockResolvedValue(undefined),
148
- copyMove: jest.fn().mockResolvedValue(undefined)
158
+ delete: jest.fn(),
159
+ touch: jest.fn(),
160
+ mkDir: jest.fn(),
161
+ copyMove: jest.fn()
149
162
  };
150
163
  filesLockManager = {
151
164
  create: jest.fn(),
152
165
  isLockedWithToken: jest.fn(),
153
- removeLock: jest.fn().mockResolvedValue(undefined),
166
+ removeLock: jest.fn(),
154
167
  browseLocks: jest.fn(),
155
168
  browseParentChildLocks: jest.fn(),
156
- checkConflicts: jest.fn().mockResolvedValue(undefined),
169
+ checkConflicts: jest.fn(),
157
170
  getLocksByPath: jest.fn(),
158
171
  getLockByToken: jest.fn(),
159
- refreshLockTimeout: jest.fn().mockResolvedValue(undefined)
172
+ refreshLockTimeout: jest.fn(),
173
+ genDAVToken: jest.fn().mockReturnValue('opaquelocktoken:new-token')
160
174
  };
161
175
  webDAVSpaces = {
162
176
  propfind: jest.fn(),
@@ -183,1436 +197,2290 @@ describe(_webdavmethodsservice.WebDAVMethods.name, ()=>{
183
197
  'fatal'
184
198
  ]);
185
199
  service = module.get(_webdavmethodsservice.WebDAVMethods);
186
- });
187
- beforeEach(()=>{
200
+ // Reset global mocks
188
201
  jest.clearAllMocks();
189
- _files.isPathExists.mockReset().mockResolvedValue(true);
190
- _files.dirName.mockReturnValue('/real/path');
202
+ _files.isPathExists.mockResolvedValue(true);
203
+ _files.dirName.mockReturnValue('/real/path/to');
191
204
  _permissions.haveSpaceEnvPermissions.mockReturnValue(true);
192
205
  });
193
206
  afterEach(()=>{
194
207
  jest.restoreAllMocks();
195
208
  });
196
- it('should be defined', ()=>{
197
- expect(service).toBeDefined();
209
+ describe('Service initialization', ()=>{
210
+ it('should be defined', ()=>{
211
+ expect(service).toBeDefined();
212
+ expect(service).toBeInstanceOf(_webdavmethodsservice.WebDAVMethods);
213
+ });
198
214
  });
199
215
  describe('headOrGet', ()=>{
200
- it('streams the file when repository is FILES and not in shares list', async ()=>{
201
- const req = baseReq();
202
- const res = makeRes();
203
- const streamable = {
204
- stream: 'ok'
205
- };
206
- const send = {
207
- checks: jest.fn().mockResolvedValue(undefined),
208
- stream: jest.fn().mockResolvedValue(streamable)
209
- };
210
- filesManager.sendFileFromSpace.mockReturnValue(send);
211
- const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
212
- expect(filesManager.sendFileFromSpace).toHaveBeenCalledWith(req.space);
213
- expect(send.checks).toHaveBeenCalledTimes(1);
214
- expect(send.stream).toHaveBeenCalledWith(req, res);
215
- expect(result).toBe(streamable);
216
- });
217
- it('returns 403 when repository is not allowed', async ()=>{
218
- const req = baseReq({
219
- space: {
220
- ...baseReq().space,
221
- inSharesList: true
222
- }
216
+ describe('Success cases', ()=>{
217
+ it('should stream file when repository is FILES and not in shares list', async ()=>{
218
+ const req = createBaseRequest();
219
+ const res = createMockResponse();
220
+ const streamable = {
221
+ stream: 'file-content'
222
+ };
223
+ const sendFile = {
224
+ checks: jest.fn().mockResolvedValue(undefined),
225
+ stream: jest.fn().mockResolvedValue(streamable)
226
+ };
227
+ filesManager.sendFileFromSpace.mockReturnValue(sendFile);
228
+ const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
229
+ expect(filesManager.sendFileFromSpace).toHaveBeenCalledWith(req.space);
230
+ expect(sendFile.checks).toHaveBeenCalledTimes(1);
231
+ expect(sendFile.stream).toHaveBeenCalledWith(req, res);
232
+ expect(result).toBe(streamable);
223
233
  });
224
- const res = makeRes();
225
- // repository not FILES or inSharesList true => forbidden
226
- await service.headOrGet(req, res, 'OTHER');
227
- expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
228
- expect(res.body).toBe('Not allowed on this resource');
229
234
  });
230
- it('handles error thrown by sendFile.checks via handleError', async ()=>{
231
- const req = baseReq();
232
- const res = makeRes();
233
- const send = {
234
- checks: jest.fn().mockRejectedValue(new Error('boom')),
235
- stream: jest.fn()
236
- };
237
- filesManager.sendFileFromSpace.mockReturnValue(send);
238
- const handleSpy = jest.spyOn(service, 'handleError').mockReturnValue('handled');
239
- const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
240
- expect(handleSpy).toHaveBeenCalled();
241
- expect(result).toBe('handled');
235
+ describe('Error cases', ()=>{
236
+ it('should return 403 when repository is not FILES', async ()=>{
237
+ const req = createBaseRequest();
238
+ const res = createMockResponse();
239
+ await service.headOrGet(req, res, 'OTHER_REPO');
240
+ expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
241
+ expect(res.body).toBe('Not allowed on this resource');
242
+ });
243
+ it('should return 403 when resource is in shares list', async ()=>{
244
+ const req = createBaseRequest({
245
+ space: {
246
+ ...createBaseRequest().space,
247
+ inSharesList: true
248
+ }
249
+ });
250
+ const res = createMockResponse();
251
+ await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
252
+ expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
253
+ expect(res.body).toBe('Not allowed on this resource');
254
+ });
255
+ it('should handle errors from sendFile.checks', async ()=>{
256
+ const req = createBaseRequest();
257
+ const res = createMockResponse();
258
+ const error = new Error('File check failed');
259
+ const sendFile = {
260
+ checks: jest.fn().mockRejectedValue(error),
261
+ stream: jest.fn()
262
+ };
263
+ filesManager.sendFileFromSpace.mockReturnValue(sendFile);
264
+ jest.spyOn(service, 'handleError').mockReturnValue('error-handled');
265
+ const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
266
+ expect(result).toBe('error-handled');
267
+ });
268
+ it('should handle errors from sendFile.stream', async ()=>{
269
+ const req = createBaseRequest();
270
+ const res = createMockResponse();
271
+ const error = new Error('Stream failed');
272
+ const sendFile = {
273
+ checks: jest.fn().mockResolvedValue(undefined),
274
+ stream: jest.fn().mockRejectedValue(error)
275
+ };
276
+ filesManager.sendFileFromSpace.mockReturnValue(sendFile);
277
+ jest.spyOn(service, 'handleError').mockReturnValue('error-handled');
278
+ const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
279
+ expect(result).toBe('error-handled');
280
+ });
242
281
  });
243
282
  });
244
283
  describe('lock', ()=>{
245
- it('when body is empty: returns 400 if resource does not exist (lock refresh)', async ()=>{
246
- ;
247
- _files.isPathExists.mockResolvedValue(false);
248
- const req = baseReq({
249
- dav: {
250
- ...baseReq().dav,
251
- body: undefined
252
- }
284
+ describe('Lock refresh (without body)', ()=>{
285
+ it('should return 400 if resource does not exist for lock refresh', async ()=>{
286
+ ;
287
+ _files.isPathExists.mockResolvedValue(false);
288
+ const req = createBaseRequest({
289
+ dav: {
290
+ ...createBaseRequest().dav,
291
+ body: undefined
292
+ }
293
+ });
294
+ const res = createMockResponse();
295
+ await service.lock(req, res);
296
+ expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
297
+ expect(res.body).toBe('Lock refresh must specify an existing resource');
298
+ });
299
+ it('should delegate to lockRefresh when resource exists and no body', async ()=>{
300
+ ;
301
+ _files.isPathExists.mockResolvedValue(true);
302
+ const req = createBaseRequest({
303
+ dav: {
304
+ ...createBaseRequest().dav,
305
+ body: undefined
306
+ }
307
+ });
308
+ const res = createMockResponse();
309
+ const lockRefreshSpy = jest.spyOn(service, 'lockRefresh').mockResolvedValue('refresh-ok');
310
+ const result = await service.lock(req, res);
311
+ expect(lockRefreshSpy).toHaveBeenCalledWith(req, res, req.space.dbFile.path);
312
+ expect(result).toBe('refresh-ok');
253
313
  });
254
- const res = makeRes();
255
- await service.lock(req, res);
256
- expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
257
- expect(res.body).toBe('Lock refresh must specify an existing resource');
258
314
  });
259
- it('when body is empty: delegates to lockRefresh if resource exists', async ()=>{
260
- ;
261
- _files.isPathExists.mockResolvedValue(true);
262
- const req = baseReq({
263
- dav: {
264
- ...baseReq().dav,
265
- body: undefined
266
- }
315
+ describe('Lock creation on existing resource', ()=>{
316
+ it('should create lock successfully and return 200', async ()=>{
317
+ ;
318
+ _files.isPathExists.mockResolvedValue(true);
319
+ const req = createBaseRequest();
320
+ const res = createMockResponse();
321
+ filesLockManager.create.mockImplementation(async (_user, _dbFile, _app, _depth, options, _timeout)=>{
322
+ return [
323
+ true,
324
+ {
325
+ owner: {
326
+ fullName: 'LockOwner',
327
+ email: 'lock-owner@sync-in.com'
328
+ },
329
+ dbFilePath: _dbFile?.path,
330
+ options: {
331
+ lockRoot: options.lockRoot,
332
+ lockToken: options.lockToken,
333
+ lockScope: options.lockScope,
334
+ lockInfo: options.lockInfo
335
+ }
336
+ }
337
+ ];
338
+ });
339
+ await service.lock(req, res);
340
+ expect(filesLockManager.create).toHaveBeenCalledTimes(1);
341
+ expect(res.statusCode).toBe(_common.HttpStatus.OK);
342
+ expect(res.contentType).toBe('application/xml; charset=utf-8');
343
+ expect(res.headers['lock-token']).toContain('opaquelocktoken:new-token');
344
+ expect(res.body).toBeDefined();
345
+ expect(typeof res.body).toBe('string');
267
346
  });
268
- const res = makeRes();
269
- const lockRefreshSpy = jest.spyOn(service, 'lockRefresh').mockResolvedValue('ok');
270
- const result = await service.lock(req, res);
271
- expect(lockRefreshSpy).toHaveBeenCalledWith(req, res, req.space.dbFile.path);
272
- expect(result).toBe('ok');
273
347
  });
274
- it('returns 403 when creating new lock on non-existing resource without permission', async ()=>{
275
- ;
276
- _files.isPathExists.mockResolvedValueOnce(false) // resource does not exist
277
- ;
278
- _permissions.haveSpaceEnvPermissions.mockReturnValue(false);
279
- const req = baseReq();
280
- const res = makeRes();
281
- await service.lock(req, res);
282
- expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
283
- expect(res.body).toBe('You are not allowed to do this action');
284
- expect(filesLockManager.create).not.toHaveBeenCalled();
285
- });
286
- it('returns 409 when parent does not exist for new lock', async ()=>{
287
- ;
288
- _files.isPathExists.mockResolvedValueOnce(false) // resource does not exist
289
- .mockResolvedValueOnce(false) // parent does not exist
290
- ;
291
- _permissions.haveSpaceEnvPermissions.mockReturnValue(true);
292
- _files.dirName.mockReturnValue('/real/path/parent-missing');
293
- const req = baseReq();
294
- const res = makeRes();
295
- await service.lock(req, res);
296
- expect(res.statusCode).toBe(_common.HttpStatus.CONFLICT);
297
- expect(res.body).toBe('Parent must exists');
298
- expect(filesLockManager.create).not.toHaveBeenCalled();
299
- });
300
- it('creates lock on existing resource and returns 200 with lock-token header', async ()=>{
301
- ;
302
- _files.isPathExists.mockResolvedValue(true); // resource exists
303
- const req = baseReq();
304
- const res = makeRes();
305
- filesLockManager.create.mockImplementation(async (_user, _dbFile, _depth, davLock, _timeout)=>{
306
- davLock.locktoken = 'opaquetoken:1';
307
- return [
308
- true,
309
- {
310
- dbFilePath: _dbFile?.path,
311
- davLock: {
312
- lockroot: davLock.lockroot,
313
- locktoken: davLock.locktoken,
314
- lockscope: davLock.lockscope,
315
- owner: davLock.owner,
316
- depth: _depth,
317
- timeout: _timeout
348
+ describe('Lock creation on non-existent resource', ()=>{
349
+ it('should return 403 when user lacks ADD permission', async ()=>{
350
+ ;
351
+ _files.isPathExists.mockResolvedValue(false);
352
+ _permissions.haveSpaceEnvPermissions.mockReturnValue(false);
353
+ const req = createBaseRequest();
354
+ const res = createMockResponse();
355
+ await service.lock(req, res);
356
+ expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
357
+ expect(res.body).toBe('You are not allowed to do this action');
358
+ expect(filesLockManager.create).not.toHaveBeenCalled();
359
+ });
360
+ it('should return 409 when parent directory does not exist', async ()=>{
361
+ ;
362
+ _files.isPathExists.mockResolvedValueOnce(false) // resource
363
+ .mockResolvedValueOnce(false) // parent
364
+ ;
365
+ _permissions.haveSpaceEnvPermissions.mockReturnValue(true);
366
+ _files.dirName.mockReturnValue('/real/path/missing');
367
+ const req = createBaseRequest();
368
+ const res = createMockResponse();
369
+ await service.lock(req, res);
370
+ expect(res.statusCode).toBe(_common.HttpStatus.CONFLICT);
371
+ expect(res.body).toBe('Parent must exists');
372
+ expect(filesLockManager.create).not.toHaveBeenCalled();
373
+ });
374
+ it('should create empty file and lock, return 201', async ()=>{
375
+ ;
376
+ _files.isPathExists.mockResolvedValueOnce(false) // resource
377
+ .mockResolvedValueOnce(true); // parent exists
378
+ const req = createBaseRequest();
379
+ const res = createMockResponse();
380
+ filesLockManager.create.mockImplementation(async (_user, _dbFile, _app, _depth, options)=>{
381
+ return [
382
+ true,
383
+ {
384
+ owner: {
385
+ fullName: 'LockOwner',
386
+ email: 'lock-owner@sync-in.com'
387
+ },
388
+ dbFilePath: _dbFile?.path,
389
+ options: {
390
+ lockRoot: options.lockRoot,
391
+ lockToken: options.lockToken,
392
+ lockScope: options.lockScope,
393
+ lockInfo: options.lockInfo
394
+ }
318
395
  }
319
- }
320
- ];
396
+ ];
397
+ });
398
+ await service.lock(req, res);
399
+ expect(filesManager.mkFile).toHaveBeenCalledWith(req.user, req.space, false, false, false);
400
+ expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
401
+ expect(res.headers['lock-token']).toContain('opaquelocktoken:new-token');
321
402
  });
322
- await service.lock(req, res);
323
- expect(filesLockManager.create).toHaveBeenCalledTimes(1);
324
- expect(res.headers['lock-token']).toBe('<opaquetoken:1>');
325
- expect(res.statusCode).toBe(_common.HttpStatus.OK);
326
- expect(res.contentType).toBe('application/xml; charset=utf-8');
327
- expect(res.body).toBeDefined();
328
403
  });
329
- it('creates lock on unmapped URL (resource missing), creates empty file and returns 201', async ()=>{
330
- ;
331
- _files.isPathExists.mockResolvedValueOnce(false) // resource missing
332
- .mockResolvedValueOnce(true); // parent exists
333
- const req = baseReq();
334
- const res = makeRes();
335
- filesLockManager.create.mockImplementation(async (_user, _dbFile, _depth, davLock, _timeout)=>{
336
- davLock.locktoken = 'opaquetoken:new';
337
- return [
338
- true,
404
+ describe('Lock conflict', ()=>{
405
+ it('should return 423 when lock conflict occurs', async ()=>{
406
+ ;
407
+ _files.isPathExists.mockResolvedValue(true);
408
+ const req = createBaseRequest();
409
+ const res = createMockResponse();
410
+ filesLockManager.create.mockResolvedValue([
411
+ false,
339
412
  {
340
- dbFilePath: _dbFile?.path,
341
- davLock: {
342
- lockroot: davLock.lockroot,
343
- locktoken: davLock.locktoken,
344
- lockscope: davLock.lockscope,
345
- owner: davLock.owner,
346
- depth: _depth,
347
- timeout: _timeout
413
+ owner: {
414
+ fullName: 'LockOwner',
415
+ email: 'lock-owner@sync-in.com'
416
+ },
417
+ dbFilePath: 'file.txt',
418
+ options: {
419
+ lockRoot: '/webdav/locked/resource'
348
420
  }
349
421
  }
350
- ];
422
+ ]);
423
+ await service.lock(req, res);
424
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
425
+ expect(res.contentType).toBe('application/xml; charset=utf-8');
351
426
  });
352
- await service.lock(req, res);
353
- expect(filesManager.mkFile).toHaveBeenCalledTimes(1);
354
- expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
355
- expect(res.headers['lock-token']).toBe('<opaquetoken:new>');
356
- });
357
- it('returns 423 when a lock conflict occurs', async ()=>{
358
- ;
359
- _files.isPathExists.mockResolvedValue(true);
360
- const req = baseReq();
361
- const res = makeRes();
362
- filesLockManager.create.mockResolvedValue([
363
- false,
364
- {
365
- davLock: {
366
- lockroot: '/locked'
367
- },
368
- dbFilePath: 'file.txt'
369
- }
370
- ]);
371
- await service.lock(req, res);
372
- // DAV_ERROR_RES should have set 423 on the response
373
- expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
374
427
  });
375
428
  });
376
429
  describe('unlock', ()=>{
377
- it('returns 404 when resource does not exist', async ()=>{
378
- ;
379
- _files.isPathExists.mockResolvedValue(false);
380
- const req = baseReq();
381
- const res = makeRes();
382
- await service.unlock(req, res);
383
- expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
384
- expect(res.body).toBe(req.dav.url);
385
- });
386
- it('returns 409 when lock token does not exist or does not match URL', async ()=>{
387
- ;
388
- _files.isPathExists.mockResolvedValue(true);
389
- filesLockManager.isLockedWithToken.mockResolvedValue(null);
390
- const req = baseReq();
391
- const res = makeRes();
392
- await service.unlock(req, res);
393
- expect(filesLockManager.isLockedWithToken).toHaveBeenCalledWith(req.dav.lock.token, req.space.dbFile.path);
394
- expect(res.statusCode).toBe(_common.HttpStatus.CONFLICT);
395
- });
396
- it('returns 403 when the lock owner is a different user', async ()=>{
397
- ;
398
- _files.isPathExists.mockResolvedValue(true);
399
- filesLockManager.isLockedWithToken.mockResolvedValue({
400
- owner: {
401
- id: 2
402
- },
403
- key: 'k1'
404
- });
405
- const req = baseReq();
406
- const res = makeRes();
407
- await service.unlock(req, res);
408
- expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
409
- expect(res.body).toBe('Token was created by another user');
410
- expect(filesLockManager.removeLock).not.toHaveBeenCalled();
430
+ describe('Success cases', ()=>{
431
+ it('should unlock resource and return 204', async ()=>{
432
+ ;
433
+ _files.isPathExists.mockResolvedValue(true);
434
+ filesLockManager.isLockedWithToken.mockResolvedValue({
435
+ owner: {
436
+ id: 1,
437
+ login: 'test-user'
438
+ },
439
+ key: 'lock-key-123'
440
+ });
441
+ const req = createBaseRequest();
442
+ const res = createMockResponse();
443
+ await service.unlock(req, res);
444
+ expect(filesLockManager.removeLock).toHaveBeenCalledWith('lock-key-123');
445
+ expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
446
+ });
411
447
  });
412
- it('removes lock and returns 204 when owner matches', async ()=>{
413
- ;
414
- _files.isPathExists.mockResolvedValue(true);
415
- filesLockManager.isLockedWithToken.mockResolvedValue({
416
- owner: {
417
- id: 1
418
- },
419
- key: 'k2'
448
+ describe('Error cases', ()=>{
449
+ it('should return 404 when resource does not exist', async ()=>{
450
+ ;
451
+ _files.isPathExists.mockResolvedValue(false);
452
+ const req = createBaseRequest();
453
+ const res = createMockResponse();
454
+ await service.unlock(req, res);
455
+ expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
456
+ expect(res.body).toBe(req.dav.url);
457
+ });
458
+ it('should return 409 when lock token does not exist', async ()=>{
459
+ ;
460
+ _files.isPathExists.mockResolvedValue(true);
461
+ filesLockManager.isLockedWithToken.mockResolvedValue(null);
462
+ const req = createBaseRequest();
463
+ const res = createMockResponse();
464
+ await service.unlock(req, res);
465
+ expect(filesLockManager.isLockedWithToken).toHaveBeenCalledWith(req.dav.lock.token, req.space.dbFile.path);
466
+ expect(res.statusCode).toBe(_common.HttpStatus.CONFLICT);
467
+ });
468
+ it('should return 403 when lock owner is different user', async ()=>{
469
+ ;
470
+ _files.isPathExists.mockResolvedValue(true);
471
+ filesLockManager.isLockedWithToken.mockResolvedValue({
472
+ owner: {
473
+ id: 999,
474
+ login: 'other-user'
475
+ },
476
+ key: 'lock-key-456'
477
+ });
478
+ const req = createBaseRequest();
479
+ const res = createMockResponse();
480
+ await service.unlock(req, res);
481
+ expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
482
+ expect(res.body).toBe('Token was created by another user');
483
+ expect(filesLockManager.removeLock).not.toHaveBeenCalled();
420
484
  });
421
- const req = baseReq();
422
- const res = makeRes();
423
- await service.unlock(req, res);
424
- expect(filesLockManager.removeLock).toHaveBeenCalledWith('k2');
425
- expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
426
485
  });
427
486
  });
428
487
  describe('propfind', ()=>{
429
- it('returns 404 when repository is FILES and path does not exist', async ()=>{
430
- ;
431
- _files.isPathExists.mockResolvedValue(false);
432
- const req = baseReq({
433
- dav: {
434
- ...baseReq().dav,
435
- propfindMode: 'prop'
436
- }
437
- });
438
- const res = makeRes();
439
- const result = await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
440
- expect(result).toBe(res);
441
- expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
442
- expect(res.body).toBe(req.dav.url);
443
- });
444
- it('returns multistatus with only property names when PROPNAME mode', async ()=>{
445
- ;
446
- _files.isPathExists.mockResolvedValue(true);
447
- const req = baseReq({
448
- dav: {
449
- ...baseReq().dav,
450
- propfindMode: 'propname',
451
- httpVersion: 'HTTP/1.1'
452
- }
453
- });
454
- const res = makeRes();
455
- const handler = service['webDAVHandler'];
456
- jest.spyOn(handler, 'propfind').mockImplementation(async function*() {
457
- yield {
458
- href: '/a',
459
- name: 'file.txt'
460
- };
461
- });
462
- const result = await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
463
- expect(result).toBe(res);
464
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
465
- expect(res.contentType).toContain('application/xml');
466
- expect(typeof res.body).toBe('string');
467
- expect(res.body).toContain('/a');
468
- });
469
- it('collects lock discovery based on depth', async ()=>{
470
- ;
471
- _files.isPathExists.mockResolvedValue(true);
472
- const req0 = baseReq({
473
- dav: {
474
- ...baseReq().dav,
475
- body: {
476
- propfind: {
477
- prop: {
478
- [_webdav.STANDARD_PROPS[0]]: ''
488
+ describe('Base cases', ()=>{
489
+ it('should return 404 when resource does not exist in FILES repository', async ()=>{
490
+ ;
491
+ _files.isPathExists.mockResolvedValue(false);
492
+ const req = createBaseRequest({
493
+ dav: {
494
+ ...createBaseRequest().dav,
495
+ propfindMode: 'prop'
496
+ }
497
+ });
498
+ const res = createMockResponse();
499
+ const result = await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
500
+ expect(result).toBe(res);
501
+ expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
502
+ expect(res.body).toBe(req.dav.url);
503
+ });
504
+ it('should return multistatus with property names in PROPNAME mode', async ()=>{
505
+ ;
506
+ _files.isPathExists.mockResolvedValue(true);
507
+ const req = createBaseRequest({
508
+ dav: {
509
+ ...createBaseRequest().dav,
510
+ propfindMode: 'propname'
511
+ }
512
+ });
513
+ const res = createMockResponse();
514
+ webDAVSpaces.propfind.mockImplementation(async function*() {
515
+ yield {
516
+ href: '/webdav/test/file.txt',
517
+ name: 'file.txt'
518
+ };
519
+ });
520
+ await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
521
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
522
+ expect(res.contentType).toContain('application/xml');
523
+ expect(typeof res.body).toBe('string');
524
+ expect(res.body).toContain('/webdav/test/file.txt');
525
+ });
526
+ it('should return multistatus with property values in PROP mode', async ()=>{
527
+ ;
528
+ _files.isPathExists.mockResolvedValue(true);
529
+ const req = createBaseRequest({
530
+ dav: {
531
+ ...createBaseRequest().dav,
532
+ propfindMode: 'prop',
533
+ body: {
534
+ propfind: {
535
+ prop: {
536
+ [_webdav.STANDARD_PROPS[0]]: ''
537
+ }
479
538
  }
480
539
  }
481
- },
482
- propfindMode: 'prop',
483
- httpVersion: 'HTTP/1.1',
484
- depth: '0'
485
- }
540
+ }
541
+ });
542
+ const res = createMockResponse();
543
+ webDAVSpaces.propfind.mockImplementation(async function*() {
544
+ yield {
545
+ href: '/webdav/test/file.txt',
546
+ name: 'file.txt',
547
+ getlastmodified: 'Mon, 01 Jan 2024 00:00:00 GMT'
548
+ };
549
+ });
550
+ await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
551
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
552
+ expect(res.contentType).toContain('application/xml');
553
+ expect(typeof res.body).toBe('string');
486
554
  });
487
- const reqInf = baseReq({
488
- dav: {
489
- ...baseReq().dav,
490
- body: {
491
- propfind: {
492
- prop: {
493
- [_webdav.STANDARD_PROPS[0]]: ''
555
+ });
556
+ describe('Lock discovery', ()=>{
557
+ it('should collect locks with depth 0', async ()=>{
558
+ ;
559
+ _files.isPathExists.mockResolvedValue(true);
560
+ const req = createBaseRequest({
561
+ dav: {
562
+ ...createBaseRequest().dav,
563
+ propfindMode: 'prop',
564
+ depth: _webdav.DEPTH.RESOURCE,
565
+ body: {
566
+ propfind: {
567
+ prop: {
568
+ [_webdav.LOCK_DISCOVERY_PROP]: ''
569
+ }
494
570
  }
495
571
  }
496
- },
497
- propfindMode: 'prop',
498
- httpVersion: 'HTTP/1.1',
499
- depth: 'infinity'
500
- }
501
- });
502
- const res0 = makeRes();
503
- const resInf = makeRes();
504
- const handler = service['webDAVHandler'];
505
- jest.spyOn(handler, 'propfind').mockImplementation(async function*() {
506
- yield {
507
- href: '/a',
508
- name: 'file.txt',
509
- getlastmodified: 'x'
510
- };
511
- });
512
- filesLockManager.browseLocks.mockResolvedValue({
513
- 'file.txt': {
514
- davLock: {
515
- lockroot: '/dav/url'
516
572
  }
517
- }
518
- });
519
- await service.propfind(req0, res0, _spaces.SPACE_REPOSITORY.FILES);
520
- expect(filesLockManager.browseLocks).toHaveBeenCalledTimes(1);
521
- filesLockManager.browseParentChildLocks.mockResolvedValue({
522
- 'file.txt': {
523
- davLock: {
524
- lockroot: '/dav/url'
573
+ });
574
+ const res = createMockResponse();
575
+ webDAVSpaces.propfind.mockImplementation(async function*() {
576
+ yield {
577
+ href: '/webdav/test/file.txt',
578
+ name: 'file.txt'
579
+ };
580
+ });
581
+ filesLockManager.browseLocks.mockResolvedValue({
582
+ 'file.txt': {
583
+ owner: {
584
+ fullName: 'LockOwner',
585
+ email: 'lock-owner@sync-in.com'
586
+ },
587
+ options: {
588
+ lockRoot: '/webdav/test/file.txt'
589
+ }
525
590
  }
526
- }
527
- });
528
- await service.propfind(reqInf, resInf, _spaces.SPACE_REPOSITORY.FILES);
529
- expect(filesLockManager.browseParentChildLocks).toHaveBeenCalledTimes(1);
530
- });
531
- it('includes lockdiscovery when requested', async ()=>{
532
- ;
533
- _files.isPathExists.mockResolvedValue(true);
534
- const req = baseReq({
535
- dav: {
536
- ...baseReq().dav,
537
- propfindMode: 'prop',
538
- httpVersion: 'HTTP/1.1',
539
- body: {
540
- propfind: {
541
- prop: {
542
- lockdiscovery: ''
591
+ });
592
+ await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
593
+ expect(filesLockManager.browseLocks).toHaveBeenCalledWith(req.space.dbFile);
594
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
595
+ });
596
+ it('should collect parent and child locks with depth infinity', async ()=>{
597
+ ;
598
+ _files.isPathExists.mockResolvedValue(true);
599
+ const req = createBaseRequest({
600
+ dav: {
601
+ ...createBaseRequest().dav,
602
+ propfindMode: 'prop',
603
+ depth: 'infinity',
604
+ body: {
605
+ propfind: {
606
+ prop: {
607
+ [_webdav.LOCK_DISCOVERY_PROP]: ''
608
+ }
543
609
  }
544
610
  }
545
611
  }
546
- }
547
- });
548
- const res = makeRes();
549
- const handler = service['webDAVHandler'];
550
- jest.spyOn(handler, 'propfind').mockImplementation(async function*() {
551
- yield {
552
- href: '/a',
553
- name: 'file.txt'
554
- };
555
- });
556
- filesLockManager.browseLocks.mockResolvedValue({
557
- 'file.txt': {
558
- davLock: {
559
- lockroot: '/dav/url'
612
+ });
613
+ const res = createMockResponse();
614
+ webDAVSpaces.propfind.mockImplementation(async function*() {
615
+ yield {
616
+ href: '/webdav/test/file.txt',
617
+ name: 'file.txt'
618
+ };
619
+ });
620
+ filesLockManager.browseParentChildLocks.mockResolvedValue({
621
+ 'file.txt': {
622
+ owner: {
623
+ fullName: 'LockOwner',
624
+ email: 'lock-owner@sync-in.com'
625
+ },
626
+ options: {
627
+ lockRoot: '/webdav/test/file.txt'
628
+ }
560
629
  }
561
- }
630
+ });
631
+ await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
632
+ expect(filesLockManager.browseParentChildLocks).toHaveBeenCalledWith(req.space.dbFile);
633
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
634
+ });
635
+ it('should not collect locks for PROPNAME mode', async ()=>{
636
+ ;
637
+ _files.isPathExists.mockResolvedValue(true);
638
+ const req = createBaseRequest({
639
+ dav: {
640
+ ...createBaseRequest().dav,
641
+ propfindMode: _webdav.PROPSTAT.PROPNAME
642
+ }
643
+ });
644
+ const res = createMockResponse();
645
+ webDAVSpaces.propfind.mockImplementation(async function*() {
646
+ yield {
647
+ href: '/webdav/test',
648
+ name: 'test'
649
+ };
650
+ });
651
+ await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
652
+ expect(filesLockManager.browseLocks).not.toHaveBeenCalled();
653
+ expect(filesLockManager.browseParentChildLocks).not.toHaveBeenCalled();
654
+ });
655
+ it('should not collect locks for shares list', async ()=>{
656
+ ;
657
+ _files.isPathExists.mockResolvedValue(true);
658
+ const req = createBaseRequest({
659
+ space: {
660
+ ...createBaseRequest().space,
661
+ inSharesList: true
662
+ },
663
+ dav: {
664
+ ...createBaseRequest().dav,
665
+ propfindMode: 'prop',
666
+ body: {
667
+ propfind: {
668
+ prop: {
669
+ [_webdav.LOCK_DISCOVERY_PROP]: ''
670
+ }
671
+ }
672
+ }
673
+ }
674
+ });
675
+ const res = createMockResponse();
676
+ webDAVSpaces.propfind.mockImplementation(async function*() {
677
+ yield {
678
+ href: '/webdav/shares',
679
+ name: 'shares'
680
+ };
681
+ });
682
+ await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
683
+ expect(filesLockManager.browseLocks).not.toHaveBeenCalled();
562
684
  });
563
- await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
564
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
565
- expect(typeof res.body).toBe('string');
566
- expect(res.body).toContain('/dav/url');
567
685
  });
568
686
  });
569
687
  describe('put', ()=>{
570
- it.each([
571
- {
572
- existed: true,
573
- expected: _common.HttpStatus.NO_CONTENT,
574
- checkEtag: true
575
- },
576
- {
577
- existed: false,
578
- expected: _common.HttpStatus.CREATED,
579
- checkEtag: false
580
- }
581
- ])('returns correct status for PUT when existed=%s', async ({ existed, expected, checkEtag })=>{
582
- filesManager.saveStream.mockResolvedValue(existed);
583
- const req = baseReq({
584
- method: 'PUT'
585
- });
586
- const res = makeRes();
587
- const result = await service.put(req, res);
588
- if (checkEtag) {
688
+ describe('Success cases', ()=>{
689
+ it('should return 204 when updating existing file', async ()=>{
690
+ filesManager.saveStream.mockResolvedValue(true); // file existed
691
+ const req = createBaseRequest({
692
+ method: 'PUT'
693
+ });
694
+ const res = createMockResponse();
695
+ const result = await service.put(req, res);
696
+ expect(filesManager.saveStream).toHaveBeenCalledWith(req.user, req.space, req, expect.objectContaining({
697
+ dav: expect.any(Object)
698
+ }));
699
+ expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
589
700
  expect(res.headers['etag']).toBeDefined();
590
701
  expect(result).toBe(res);
591
- }
592
- expect(res.statusCode).toBe(expected);
702
+ });
703
+ it('should return 201 when creating new file', async ()=>{
704
+ filesManager.saveStream.mockResolvedValue(false); // file didn't exist
705
+ const req = createBaseRequest({
706
+ method: 'PUT'
707
+ });
708
+ const res = createMockResponse();
709
+ const result = await service.put(req, res);
710
+ expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
711
+ expect(res.headers['etag']).toBeDefined();
712
+ expect(result).toBe(res);
713
+ });
714
+ it('should extract and pass lock tokens from if-headers', async ()=>{
715
+ filesManager.saveStream.mockResolvedValue(true);
716
+ const req = createBaseRequest({
717
+ method: 'PUT',
718
+ dav: {
719
+ ...createBaseRequest().dav,
720
+ ifHeaders: [
721
+ {
722
+ token: {
723
+ value: 'opaquelocktoken:xyz',
724
+ mustMatch: true
725
+ }
726
+ }
727
+ ]
728
+ }
729
+ });
730
+ const res = createMockResponse();
731
+ jest.spyOn(_ifheader, 'extractAllTokens').mockReturnValue([
732
+ 'opaquelocktoken:xyz'
733
+ ]);
734
+ await service.put(req, res);
735
+ expect(filesManager.saveStream).toHaveBeenCalledWith(req.user, req.space, req, expect.objectContaining({
736
+ dav: expect.objectContaining({
737
+ lockTokens: [
738
+ 'opaquelocktoken:xyz'
739
+ ]
740
+ })
741
+ }));
742
+ });
593
743
  });
594
- it('delegates errors to handleError', async ()=>{
595
- const req = baseReq({
596
- method: 'PUT'
597
- });
598
- const res = makeRes();
599
- const err = new Error('save failed');
600
- filesManager.saveStream.mockRejectedValue(err);
601
- const spy = jest.spyOn(service, 'handleError').mockReturnValue('handled');
602
- const result = await service.put(req, res);
603
- expect(spy).toHaveBeenCalled();
604
- expect(result).toBe('handled');
744
+ describe('Error handling', ()=>{
745
+ it('should handle lock conflict error', async ()=>{
746
+ const lockError = new _filelockerror.LockConflict({
747
+ dbFilePath: 'file.txt',
748
+ options: {
749
+ lockRoot: '/webdav/locked'
750
+ }
751
+ }, 'Lock conflict');
752
+ filesManager.saveStream.mockRejectedValue(lockError);
753
+ const req = createBaseRequest({
754
+ method: 'PUT'
755
+ });
756
+ const res = createMockResponse();
757
+ await service.put(req, res);
758
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
759
+ });
760
+ it('should handle file error', async ()=>{
761
+ const fileError = new _fileerror.FileError(409, 'File conflict');
762
+ filesManager.saveStream.mockRejectedValue(fileError);
763
+ const req = createBaseRequest({
764
+ method: 'PUT'
765
+ });
766
+ const res = createMockResponse();
767
+ await service.put(req, res);
768
+ expect(res.statusCode).toBe(409);
769
+ expect(res.body).toBe('File conflict');
770
+ });
771
+ it('should throw HttpException for unexpected errors', async ()=>{
772
+ const unexpectedError = new Error('Unexpected error');
773
+ filesManager.saveStream.mockRejectedValue(unexpectedError);
774
+ const req = createBaseRequest({
775
+ method: 'PUT'
776
+ });
777
+ const res = createMockResponse();
778
+ await expect(service.put(req, res)).rejects.toThrow(_common.HttpException);
779
+ });
605
780
  });
606
781
  });
607
782
  describe('delete', ()=>{
608
- it('returns 204 on success', async ()=>{
609
- const req = baseReq({
610
- method: 'DELETE'
611
- });
612
- const res = makeRes();
613
- const result = await service.delete(req, res);
614
- expect(result).toBe(res);
615
- expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
783
+ describe('Success cases', ()=>{
784
+ it('should delete resource and return 204', async ()=>{
785
+ filesManager.delete.mockResolvedValue(undefined);
786
+ const req = createBaseRequest({
787
+ method: 'DELETE'
788
+ });
789
+ const res = createMockResponse();
790
+ const result = await service.delete(req, res);
791
+ expect(filesManager.delete).toHaveBeenCalledWith(req.user, req.space, expect.objectContaining({
792
+ lockTokens: expect.any(Array)
793
+ }));
794
+ expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
795
+ expect(result).toBe(res);
796
+ });
797
+ it('should extract lock tokens from if-headers', async ()=>{
798
+ filesManager.delete.mockResolvedValue(undefined);
799
+ const req = createBaseRequest({
800
+ method: 'DELETE',
801
+ dav: {
802
+ ...createBaseRequest().dav,
803
+ ifHeaders: [
804
+ {
805
+ token: {
806
+ value: 'opaquelocktoken:abc',
807
+ mustMatch: true
808
+ }
809
+ }
810
+ ]
811
+ }
812
+ });
813
+ const res = createMockResponse();
814
+ jest.spyOn(_ifheader, 'extractAllTokens').mockReturnValue([
815
+ 'opaquelocktoken:abc'
816
+ ]);
817
+ await service.delete(req, res);
818
+ expect(filesManager.delete).toHaveBeenCalledWith(req.user, req.space, expect.objectContaining({
819
+ lockTokens: [
820
+ 'opaquelocktoken:abc'
821
+ ]
822
+ }));
823
+ });
616
824
  });
617
- it('delegates errors to handleError', async ()=>{
618
- const req = baseReq({
619
- method: 'DELETE'
620
- });
621
- const res = makeRes();
622
- const err = new Error('delete failed');
623
- filesManager.delete.mockRejectedValue(err);
624
- const spy = jest.spyOn(service, 'handleError').mockReturnValue('handled');
625
- const result = await service.delete(req, res);
626
- expect(spy).toHaveBeenCalled();
627
- expect(result).toBe('handled');
825
+ describe('Error handling', ()=>{
826
+ it('should handle lock conflict', async ()=>{
827
+ const lockError = new _filelockerror.LockConflict({
828
+ dbFilePath: 'file.txt',
829
+ options: {
830
+ lockRoot: '/webdav/locked'
831
+ }
832
+ }, 'Lock conflict');
833
+ filesManager.delete.mockRejectedValue(lockError);
834
+ const req = createBaseRequest({
835
+ method: 'DELETE'
836
+ });
837
+ const res = createMockResponse();
838
+ await service.delete(req, res);
839
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
840
+ });
841
+ it('should handle file errors', async ()=>{
842
+ const fileError = new _fileerror.FileError(404, 'File not found');
843
+ filesManager.delete.mockRejectedValue(fileError);
844
+ const req = createBaseRequest({
845
+ method: 'DELETE'
846
+ });
847
+ const res = createMockResponse();
848
+ await service.delete(req, res);
849
+ expect(res.statusCode).toBe(404);
850
+ expect(res.body).toBe('File not found');
851
+ });
852
+ it('should throw HttpException for unexpected errors', async ()=>{
853
+ const unexpectedError = new Error('Database error');
854
+ filesManager.delete.mockRejectedValue(unexpectedError);
855
+ const req = createBaseRequest({
856
+ method: 'DELETE'
857
+ });
858
+ const res = createMockResponse();
859
+ await expect(service.delete(req, res)).rejects.toThrow(_common.HttpException);
860
+ });
628
861
  });
629
862
  });
630
863
  describe('proppatch', ()=>{
631
- it('returns 404 when target does not exist', async ()=>{
632
- ;
633
- _files.isPathExists.mockResolvedValue(false);
634
- const req = baseReq({
635
- method: 'PROPPATCH',
636
- dav: {
637
- ...baseReq().dav,
638
- url: '/x',
639
- body: {
640
- propertyupdate: {}
864
+ describe('Base cases', ()=>{
865
+ it('should return 404 when resource does not exist', async ()=>{
866
+ ;
867
+ _files.isPathExists.mockResolvedValue(false);
868
+ const req = createBaseRequest({
869
+ method: 'PROPPATCH',
870
+ dav: {
871
+ ...createBaseRequest().dav,
872
+ body: {
873
+ propertyupdate: {
874
+ set: {
875
+ prop: [
876
+ {
877
+ lastmodified: '2024-01-01'
878
+ }
879
+ ]
880
+ }
881
+ }
882
+ }
641
883
  }
642
- }
643
- });
644
- const res = makeRes();
645
- await service.proppatch(req, res);
646
- expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
647
- expect(res.body).toBe('/x');
648
- });
649
- it('returns 400 for unknown action tag', async ()=>{
650
- ;
651
- _files.isPathExists.mockResolvedValue(true);
652
- const req = baseReq({
653
- method: 'PROPPATCH',
654
- dav: {
655
- ...baseReq().dav,
656
- body: {
657
- propertyupdate: {
658
- unknown: {}
884
+ });
885
+ const res = createMockResponse();
886
+ await service.proppatch(req, res);
887
+ expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
888
+ expect(res.body).toBe(req.dav.url);
889
+ });
890
+ it('should return 400 for unknown action tag', async ()=>{
891
+ ;
892
+ _files.isPathExists.mockResolvedValue(true);
893
+ const req = createBaseRequest({
894
+ method: 'PROPPATCH',
895
+ dav: {
896
+ ...createBaseRequest().dav,
897
+ body: {
898
+ propertyupdate: {
899
+ invalidaction: {}
900
+ }
659
901
  }
660
902
  }
661
- }
662
- });
663
- const res = makeRes();
664
- await service.proppatch(req, res);
665
- expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
666
- expect(res.body).toContain('Unknown tag');
667
- });
668
- it('returns 400 when missing prop tag', async ()=>{
669
- ;
670
- _files.isPathExists.mockResolvedValue(true);
671
- const req = baseReq({
672
- method: 'PROPPATCH',
673
- dav: {
674
- ...baseReq().dav,
675
- body: {
676
- propertyupdate: {
677
- set: {
678
- foo: 'bar'
903
+ });
904
+ const res = createMockResponse();
905
+ await service.proppatch(req, res);
906
+ expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
907
+ expect(res.body).toContain('Unknown tag');
908
+ });
909
+ it('should return 400 when missing prop tag', async ()=>{
910
+ ;
911
+ _files.isPathExists.mockResolvedValue(true);
912
+ const req = createBaseRequest({
913
+ method: 'PROPPATCH',
914
+ dav: {
915
+ ...createBaseRequest().dav,
916
+ body: {
917
+ propertyupdate: {
918
+ set: {
919
+ notprop: {}
920
+ }
679
921
  }
680
922
  }
681
923
  }
682
- }
924
+ });
925
+ const res = createMockResponse();
926
+ await service.proppatch(req, res);
927
+ expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
928
+ expect(res.body).toContain('Unknown tag');
683
929
  });
684
- const res = makeRes();
685
- await service.proppatch(req, res);
686
- expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
687
- expect(res.body).toContain('Unknown tag');
688
930
  });
689
- it('returns 207 with errors when unsupported props are provided', async ()=>{
690
- ;
691
- _files.isPathExists.mockResolvedValue(true);
692
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
693
- const req = baseReq({
694
- method: 'PROPPATCH',
695
- dav: {
696
- ...baseReq().dav,
697
- httpVersion: 'HTTP/1.1',
698
- body: {
699
- propertyupdate: {
700
- set: {
701
- prop: [
702
- {
703
- randomProp: 'x'
704
- }
705
- ]
931
+ describe('SET action', ()=>{
932
+ it('should successfully modify lastmodified property', async ()=>{
933
+ ;
934
+ _files.isPathExists.mockResolvedValue(true);
935
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
936
+ filesManager.touch.mockResolvedValue(undefined);
937
+ const req = createBaseRequest({
938
+ method: 'PROPPATCH',
939
+ dav: {
940
+ ...createBaseRequest().dav,
941
+ httpVersion: 'HTTP/1.1',
942
+ body: {
943
+ propertyupdate: {
944
+ set: {
945
+ prop: [
946
+ {
947
+ lastmodified: '2024-01-01'
948
+ }
949
+ ]
950
+ }
706
951
  }
707
952
  }
708
953
  }
709
- }
710
- });
711
- const res = makeRes();
712
- await service.proppatch(req, res);
713
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
714
- expect(res.contentType).toContain('application/xml');
715
- expect(typeof res.body).toBe('string');
716
- expect(res.body).toContain('randomProp');
717
- expect(res.body).toContain('403');
718
- });
719
- it('applies modified props and still returns 207; failed dependency if one fails', async ()=>{
720
- ;
721
- _files.isPathExists.mockResolvedValue(true);
722
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
723
- filesManager.touch.mockResolvedValueOnce(undefined);
724
- const req = baseReq({
725
- method: 'PROPPATCH',
726
- dav: {
727
- ...baseReq().dav,
728
- httpVersion: 'HTTP/1.1',
729
- body: {
730
- propertyupdate: {
731
- set: {
732
- prop: [
733
- {
734
- lastmodified: '2024-01-01'
735
- },
736
- {
737
- ['Win32CreationTime']: 'keep'
738
- }
739
- ]
954
+ });
955
+ const res = createMockResponse();
956
+ await service.proppatch(req, res);
957
+ expect(filesManager.touch).toHaveBeenCalled();
958
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
959
+ expect(res.contentType).toContain('application/xml');
960
+ expect(typeof res.body).toBe('string');
961
+ expect(res.body).toContain('lastmodified');
962
+ expect(res.body).toContain('200');
963
+ });
964
+ it('should return 207 with 403 for unsupported properties', async ()=>{
965
+ ;
966
+ _files.isPathExists.mockResolvedValue(true);
967
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
968
+ const req = createBaseRequest({
969
+ method: 'PROPPATCH',
970
+ dav: {
971
+ ...createBaseRequest().dav,
972
+ httpVersion: 'HTTP/1.1',
973
+ body: {
974
+ propertyupdate: {
975
+ set: {
976
+ prop: [
977
+ {
978
+ unsupportedProp: 'value'
979
+ }
980
+ ]
981
+ }
740
982
  }
741
983
  }
742
984
  }
743
- }
744
- });
745
- const res = makeRes();
746
- await service.proppatch(req, res);
747
- expect(filesManager.touch).toHaveBeenCalled();
748
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
749
- expect(typeof res.body).toBe('string');
750
- expect(res.body).toContain('lastmodified');
751
- expect(res.body).toContain('200');
752
- });
753
- it('delegates lock conflict to handleError when checkConflicts throws', async ()=>{
754
- ;
755
- _files.isPathExists.mockResolvedValue(true);
756
- const req = baseReq({
757
- method: 'PROPPATCH',
758
- dav: {
759
- ...baseReq().dav,
760
- body: {
761
- propertyupdate: {
762
- set: {
763
- prop: [
764
- {
765
- lastmodified: 'x'
766
- }
767
- ]
985
+ });
986
+ const res = createMockResponse();
987
+ await service.proppatch(req, res);
988
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
989
+ expect(res.body).toContain('unsupportedProp');
990
+ expect(res.body).toContain('403');
991
+ });
992
+ it('should handle Win32 properties correctly', async ()=>{
993
+ ;
994
+ _files.isPathExists.mockResolvedValue(true);
995
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
996
+ const req = createBaseRequest({
997
+ method: 'PROPPATCH',
998
+ dav: {
999
+ ...createBaseRequest().dav,
1000
+ httpVersion: 'HTTP/1.1',
1001
+ body: {
1002
+ propertyupdate: {
1003
+ set: {
1004
+ prop: [
1005
+ {
1006
+ Win32CreationTime: '2024-01-01'
1007
+ }
1008
+ ]
1009
+ }
768
1010
  }
769
1011
  }
770
1012
  }
771
- }
772
- });
773
- const res = makeRes();
774
- const err = new Error('conflict');
775
- filesLockManager.checkConflicts.mockRejectedValue(err);
776
- const spy = jest.spyOn(service, 'handleError').mockReturnValue('handled');
777
- const result = await service.proppatch(req, res);
778
- expect(spy).toHaveBeenCalled();
779
- expect(result).toBe('handled');
780
- });
781
- it('normalizes array of propertyupdate items containing {prop: ...}', async ()=>{
782
- ;
783
- _files.isPathExists.mockResolvedValue(true);
784
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
785
- filesManager.touch.mockResolvedValueOnce(undefined);
786
- const req = baseReq({
787
- method: 'PROPPATCH',
788
- dav: {
789
- ...baseReq().dav,
790
- httpVersion: 'HTTP/1.1',
791
- body: {
792
- propertyupdate: {
793
- set: [
794
- {
795
- prop: {
796
- lastmodified: '2024-03-01'
797
- }
798
- },
799
- {
800
- prop: {
801
- ['Win32CreationTime']: 'ignore'
802
- }
1013
+ });
1014
+ const res = createMockResponse();
1015
+ await service.proppatch(req, res);
1016
+ expect(filesManager.touch).not.toHaveBeenCalled();
1017
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
1018
+ expect(res.body).toContain('Win32CreationTime');
1019
+ expect(res.body).toContain('200');
1020
+ });
1021
+ it('should return 424 failed dependency when touch fails', async ()=>{
1022
+ ;
1023
+ _files.isPathExists.mockResolvedValue(true);
1024
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
1025
+ filesManager.touch.mockRejectedValue(new Error('Touch failed'));
1026
+ const req = createBaseRequest({
1027
+ method: 'PROPPATCH',
1028
+ dav: {
1029
+ ...createBaseRequest().dav,
1030
+ httpVersion: 'HTTP/1.1',
1031
+ body: {
1032
+ propertyupdate: {
1033
+ set: {
1034
+ prop: [
1035
+ {
1036
+ lastmodified: '2024-01-01'
1037
+ }
1038
+ ]
803
1039
  }
804
- ]
1040
+ }
805
1041
  }
806
1042
  }
807
- }
808
- });
809
- const res = makeRes();
810
- await service.proppatch(req, res);
811
- expect(filesManager.touch).toHaveBeenCalled();
812
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
813
- expect(res.contentType).toContain('application/xml');
814
- expect(typeof res.body).toBe('string');
815
- expect(res.body).toContain('lastmodified');
816
- expect(res.body).toContain('200');
817
- });
818
- it('wraps single prop object into an array for processing', async ()=>{
819
- ;
820
- _files.isPathExists.mockResolvedValue(true);
821
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
822
- const req = baseReq({
823
- method: 'PROPPATCH',
824
- dav: {
825
- ...baseReq().dav,
826
- httpVersion: 'HTTP/1.1',
827
- body: {
828
- propertyupdate: {
829
- set: {
830
- prop: {
831
- lastmodified: '2024-03-02'
1043
+ });
1044
+ const res = createMockResponse();
1045
+ await service.proppatch(req, res);
1046
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
1047
+ expect(res.body).toContain('424');
1048
+ });
1049
+ it('should mark supported props as 424 when unsupported prop fails', async ()=>{
1050
+ ;
1051
+ _files.isPathExists.mockResolvedValue(true);
1052
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
1053
+ const req = createBaseRequest({
1054
+ method: 'PROPPATCH',
1055
+ dav: {
1056
+ ...createBaseRequest().dav,
1057
+ httpVersion: 'HTTP/1.1',
1058
+ body: {
1059
+ propertyupdate: {
1060
+ set: {
1061
+ prop: [
1062
+ {
1063
+ unsupportedProp: 'fail'
1064
+ },
1065
+ {
1066
+ Win32CreationTime: 'ok'
1067
+ }
1068
+ ]
832
1069
  }
833
1070
  }
834
1071
  }
835
1072
  }
836
- }
1073
+ });
1074
+ const res = createMockResponse();
1075
+ await service.proppatch(req, res);
1076
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
1077
+ expect(res.body).toContain('unsupportedProp');
1078
+ expect(res.body).toContain('403');
1079
+ expect(res.body).toContain('Win32CreationTime');
1080
+ expect(res.body).toContain('424');
837
1081
  });
838
- const res = makeRes();
839
- await service.proppatch(req, res);
840
- expect(filesManager.touch).toHaveBeenCalled();
841
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
842
- expect(typeof res.body).toBe('string');
843
- expect(res.body).toContain('lastmodified');
844
- expect(res.body).toContain('200');
845
1082
  });
846
- it('handles REMOVE action on supported property and returns 207', async ()=>{
847
- ;
848
- _files.isPathExists.mockResolvedValue(true);
849
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
850
- const req = baseReq({
851
- method: 'PROPPATCH',
852
- dav: {
853
- ...baseReq().dav,
854
- httpVersion: 'HTTP/1.1',
855
- body: {
856
- propertyupdate: {
857
- remove: {
858
- prop: [
859
- {
860
- ['Win32CreationTime']: ''
861
- }
862
- ]
1083
+ describe('REMOVE action', ()=>{
1084
+ it('should handle REMOVE action on supported property', async ()=>{
1085
+ ;
1086
+ _files.isPathExists.mockResolvedValue(true);
1087
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
1088
+ const req = createBaseRequest({
1089
+ method: 'PROPPATCH',
1090
+ dav: {
1091
+ ...createBaseRequest().dav,
1092
+ httpVersion: 'HTTP/1.1',
1093
+ body: {
1094
+ propertyupdate: {
1095
+ remove: {
1096
+ prop: [
1097
+ {
1098
+ Win32CreationTime: ''
1099
+ }
1100
+ ]
1101
+ }
863
1102
  }
864
1103
  }
865
1104
  }
866
- }
1105
+ });
1106
+ const res = createMockResponse();
1107
+ await service.proppatch(req, res);
1108
+ expect(filesManager.touch).not.toHaveBeenCalled();
1109
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
1110
+ expect(res.body).toContain('Win32CreationTime');
1111
+ expect(res.body).toContain('200');
867
1112
  });
868
- const res = makeRes();
869
- await service.proppatch(req, res);
870
- expect(filesManager.touch).not.toHaveBeenCalled();
871
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
872
- expect(res.contentType).toContain('application/xml');
873
- expect(typeof res.body).toBe('string');
874
- expect(res.body).toContain('Win32CreationTime');
875
- expect(res.body).toContain('200');
876
1113
  });
877
- it('returns 207 with 424 Failed Dependency when touching lastmodified fails', async ()=>{
878
- ;
879
- _files.isPathExists.mockResolvedValue(true);
880
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
881
- filesManager.touch.mockRejectedValueOnce(new Error('touch failed'));
882
- const req = baseReq({
883
- method: 'PROPPATCH',
884
- dav: {
885
- ...baseReq().dav,
886
- httpVersion: 'HTTP/1.1',
887
- body: {
888
- propertyupdate: {
889
- set: {
890
- prop: [
1114
+ describe('Data normalization', ()=>{
1115
+ it('should normalize array of propertyupdate items', async ()=>{
1116
+ ;
1117
+ _files.isPathExists.mockResolvedValue(true);
1118
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
1119
+ filesManager.touch.mockResolvedValue(undefined);
1120
+ const req = createBaseRequest({
1121
+ method: 'PROPPATCH',
1122
+ dav: {
1123
+ ...createBaseRequest().dav,
1124
+ httpVersion: 'HTTP/1.1',
1125
+ body: {
1126
+ propertyupdate: {
1127
+ set: [
891
1128
  {
892
- lastmodified: '2024-01-01'
1129
+ prop: {
1130
+ lastmodified: '2024-01-01'
1131
+ }
893
1132
  },
894
1133
  {
895
- ['Win32CreationTime']: 'ok'
1134
+ prop: {
1135
+ Win32CreationTime: 'ok'
1136
+ }
896
1137
  }
897
1138
  ]
898
1139
  }
899
1140
  }
900
1141
  }
901
- }
1142
+ });
1143
+ const res = createMockResponse();
1144
+ await service.proppatch(req, res);
1145
+ expect(filesManager.touch).toHaveBeenCalled();
1146
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
1147
+ expect(res.body).toContain('lastmodified');
1148
+ });
1149
+ it('should wrap single prop object into array', async ()=>{
1150
+ ;
1151
+ _files.isPathExists.mockResolvedValue(true);
1152
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
1153
+ filesManager.touch.mockResolvedValue(undefined);
1154
+ const req = createBaseRequest({
1155
+ method: 'PROPPATCH',
1156
+ dav: {
1157
+ ...createBaseRequest().dav,
1158
+ httpVersion: 'HTTP/1.1',
1159
+ body: {
1160
+ propertyupdate: {
1161
+ set: {
1162
+ prop: {
1163
+ lastmodified: '2024-01-01'
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+ });
1170
+ const res = createMockResponse();
1171
+ await service.proppatch(req, res);
1172
+ expect(filesManager.touch).toHaveBeenCalled();
1173
+ expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
902
1174
  });
903
- const res = makeRes();
904
- await service.proppatch(req, res);
905
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
906
- expect(typeof res.body).toBe('string');
907
- expect(res.body).toContain('424');
908
- expect(res.body).toContain('lastmodified');
909
1175
  });
910
- it('returns 207 with 403 for unsupported prop and 424 for supported prop as failed dependency', async ()=>{
911
- // Préparation : la ressource existe et pas de conflit de lock
912
- ;
913
- _files.isPathExists.mockResolvedValue(true);
914
- filesLockManager.checkConflicts.mockResolvedValue(undefined);
915
- // On envoie à la fois une prop non supportée ('randomProp') et une prop supportée ('Win32CreationTime')
916
- const req = baseReq({
917
- method: 'PROPPATCH',
918
- dav: {
919
- ...baseReq().dav,
920
- httpVersion: 'HTTP/1.1',
921
- body: {
922
- propertyupdate: {
923
- set: {
924
- prop: [
925
- {
926
- randomProp: 'x'
927
- },
928
- {
929
- Win32CreationTime: 'keep'
930
- }
931
- ]
1176
+ describe('Lock handling', ()=>{
1177
+ it('should check lock conflicts before applying changes', async ()=>{
1178
+ ;
1179
+ _files.isPathExists.mockResolvedValue(true);
1180
+ filesLockManager.checkConflicts.mockResolvedValue(undefined);
1181
+ const req = createBaseRequest({
1182
+ method: 'PROPPATCH',
1183
+ dav: {
1184
+ ...createBaseRequest().dav,
1185
+ body: {
1186
+ propertyupdate: {
1187
+ set: {
1188
+ prop: [
1189
+ {
1190
+ Win32CreationTime: 'ok'
1191
+ }
1192
+ ]
1193
+ }
932
1194
  }
933
1195
  }
934
1196
  }
935
- }
1197
+ });
1198
+ const res = createMockResponse();
1199
+ await service.proppatch(req, res);
1200
+ expect(filesLockManager.checkConflicts).toHaveBeenCalledWith(req.space.dbFile, req.dav.depth, expect.objectContaining({
1201
+ userId: req.user.id,
1202
+ lockTokens: expect.any(Array)
1203
+ }));
1204
+ });
1205
+ it('should handle lock conflict error', async ()=>{
1206
+ ;
1207
+ _files.isPathExists.mockResolvedValue(true);
1208
+ const lockError = new _filelockerror.LockConflict({
1209
+ dbFilePath: 'file.txt',
1210
+ options: {
1211
+ lockRoot: '/webdav/locked'
1212
+ }
1213
+ }, 'Lock conflict');
1214
+ filesLockManager.checkConflicts.mockRejectedValue(lockError);
1215
+ const req = createBaseRequest({
1216
+ method: 'PROPPATCH',
1217
+ dav: {
1218
+ ...createBaseRequest().dav,
1219
+ body: {
1220
+ propertyupdate: {
1221
+ set: {
1222
+ prop: [
1223
+ {
1224
+ lastmodified: '2024-01-01'
1225
+ }
1226
+ ]
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+ });
1232
+ const res = createMockResponse();
1233
+ await service.proppatch(req, res);
1234
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
936
1235
  });
937
- const res = makeRes();
938
- // Exécution
939
- await service.proppatch(req, res);
940
- // Assertions : multistatus, xml, et contenu
941
- expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
942
- expect(res.contentType).toContain('application/xml');
943
- const xml = res.body;
944
- // On doit trouver le nom de la prop non supportée avec status 403
945
- expect(xml).toContain('randomProp');
946
- expect(xml).toContain('403');
947
- // On doit trouver le nom de la prop supportée avec status 424 (failed dependency)
948
- expect(xml).toContain('Win32CreationTime');
949
- expect(xml).toContain('424');
950
1236
  });
951
1237
  });
952
1238
  describe('mkcol', ()=>{
953
- it('returns 201 when directory created', async ()=>{
954
- const req = baseReq({
955
- method: 'MKCOL'
956
- });
957
- const res = makeRes();
958
- await service.mkcol(req, res);
959
- expect(filesManager.mkDir).toHaveBeenCalled();
960
- expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
1239
+ describe('Success cases', ()=>{
1240
+ it('should create directory and return 201', async ()=>{
1241
+ filesManager.mkDir.mockResolvedValue(undefined);
1242
+ const req = createBaseRequest({
1243
+ method: 'MKCOL'
1244
+ });
1245
+ const res = createMockResponse();
1246
+ await service.mkcol(req, res);
1247
+ expect(filesManager.mkDir).toHaveBeenCalledWith(req.user, req.space, false, expect.objectContaining({
1248
+ depth: req.dav.depth,
1249
+ lockTokens: expect.any(Array)
1250
+ }));
1251
+ expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
1252
+ });
961
1253
  });
962
- it('delegates errors to handleError', async ()=>{
963
- const req = baseReq({
964
- method: 'MKCOL'
965
- });
966
- const res = makeRes();
967
- filesManager.mkDir.mockRejectedValue(new Error('mkdir failed'));
968
- const spy = jest.spyOn(service, 'handleError').mockReturnValue('handled');
969
- const result = await service.mkcol(req, res);
970
- expect(spy).toHaveBeenCalled();
971
- expect(result).toBe('handled');
1254
+ describe('Error handling', ()=>{
1255
+ it('should handle lock conflict', async ()=>{
1256
+ const lockError = new _filelockerror.LockConflict({
1257
+ dbFilePath: 'dir',
1258
+ options: {
1259
+ lockRoot: '/webdav/locked'
1260
+ }
1261
+ }, 'Lock conflict');
1262
+ filesManager.mkDir.mockRejectedValue(lockError);
1263
+ const req = createBaseRequest({
1264
+ method: 'MKCOL'
1265
+ });
1266
+ const res = createMockResponse();
1267
+ await service.mkcol(req, res);
1268
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
1269
+ });
1270
+ it('should handle file errors', async ()=>{
1271
+ const fileError = new _fileerror.FileError(409, 'Directory already exists');
1272
+ filesManager.mkDir.mockRejectedValue(fileError);
1273
+ const req = createBaseRequest({
1274
+ method: 'MKCOL'
1275
+ });
1276
+ const res = createMockResponse();
1277
+ await service.mkcol(req, res);
1278
+ expect(res.statusCode).toBe(409);
1279
+ expect(res.body).toBe('Directory already exists');
1280
+ });
1281
+ it('should throw HttpException for unexpected errors', async ()=>{
1282
+ const unexpectedError = new Error('Filesystem error');
1283
+ filesManager.mkDir.mockRejectedValue(unexpectedError);
1284
+ const req = createBaseRequest({
1285
+ method: 'MKCOL'
1286
+ });
1287
+ const res = createMockResponse();
1288
+ await expect(service.mkcol(req, res)).rejects.toThrow(_common.HttpException);
1289
+ });
972
1290
  });
973
1291
  });
974
1292
  describe('copyMove', ()=>{
975
- it('returns 404 when destination space not found', async ()=>{
976
- const req = baseReq({
977
- method: 'MOVE',
978
- dav: {
979
- ...baseReq().dav,
980
- copyMove: {
981
- destination: '/unknown',
982
- isMove: false,
983
- overwrite: false
1293
+ describe('Base cases', ()=>{
1294
+ it('should return 404 when destination space not found', async ()=>{
1295
+ webDAVSpaces.spaceEnv.mockResolvedValue(null);
1296
+ const req = createBaseRequest({
1297
+ method: 'MOVE',
1298
+ dav: {
1299
+ ...createBaseRequest().dav,
1300
+ copyMove: {
1301
+ destination: '/webdav/unknown',
1302
+ isMove: true,
1303
+ overwrite: false
1304
+ }
984
1305
  }
985
- }
1306
+ });
1307
+ const res = createMockResponse();
1308
+ await service.copyMove(req, res);
1309
+ expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
1310
+ expect(res.body).toBe('/webdav/unknown');
986
1311
  });
987
- const res = makeRes();
988
- const handler = service['webDAVHandler'];
989
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue(null);
990
- await service.copyMove(req, res);
991
- expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
992
- expect(res.body).toBe('/unknown');
993
1312
  });
994
- it('aborts when evaluateIfHeaders fails', async ()=>{
995
- const req = baseReq({
996
- method: 'COPY',
997
- dav: {
998
- ...baseReq().dav,
999
- copyMove: {
1000
- destination: '/dst',
1001
- isMove: false,
1002
- overwrite: true
1003
- },
1004
- ifHeaders: [
1005
- {
1006
- path: '/dst',
1007
- token: {
1008
- value: 'bad',
1009
- mustMatch: true
1010
- }
1313
+ describe('COPY operation', ()=>{
1314
+ it('should copy file and return 201 when destination does not exist', async ()=>{
1315
+ const dstSpace = {
1316
+ ...createBaseRequest().space,
1317
+ url: '/webdav/test/copy.txt',
1318
+ realPath: '/real/path/to/copy.txt',
1319
+ dbFile: {
1320
+ path: 'copy.txt',
1321
+ spaceId: 1,
1322
+ inTrash: false
1323
+ }
1324
+ };
1325
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1326
+ _files.isPathExists.mockResolvedValue(false);
1327
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1328
+ filesManager.copyMove.mockResolvedValue(undefined);
1329
+ const req = createBaseRequest({
1330
+ method: 'COPY',
1331
+ dav: {
1332
+ ...createBaseRequest().dav,
1333
+ copyMove: {
1334
+ destination: '/webdav/test/copy.txt',
1335
+ isMove: false,
1336
+ overwrite: false
1011
1337
  }
1012
- ]
1013
- }
1014
- });
1015
- const res = makeRes();
1016
- const handler = service['webDAVHandler'];
1017
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue({
1018
- ...req.space,
1019
- url: '/dst',
1020
- realPath: '/real/dst',
1021
- dbFile: {
1022
- path: 'dst'
1023
- }
1024
- });
1025
- const spy = jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(false);
1026
- const result = await service.copyMove(req, res);
1027
- expect(spy).toHaveBeenCalled();
1028
- expect(result).toBeUndefined();
1029
- });
1030
- it('aborts with 412 when destination If-Header haveLock mismatches', async ()=>{
1031
- const handler = service['webDAVHandler'];
1032
- const dstSpace = {
1033
- ...baseReq().space,
1034
- url: '/dst',
1035
- realPath: '/real/dst',
1036
- dbFile: {
1037
- path: 'dst'
1038
- }
1039
- };
1040
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue(dstSpace);
1041
- _files.isPathExists.mockResolvedValue(true);
1042
- filesLockManager.getLocksByPath.mockResolvedValue([
1043
- {}
1044
- ]); // there is a lock, but mustMatch=false -> mismatch
1045
- const req = baseReq({
1046
- method: 'COPY',
1047
- dav: {
1048
- ...baseReq().dav,
1049
- copyMove: {
1050
- destination: '/dst',
1051
- isMove: false,
1052
- overwrite: true
1053
- },
1054
- ifHeaders: [
1055
- {
1056
- path: '/dst',
1057
- haveLock: {
1058
- mustMatch: false
1059
- }
1338
+ }
1339
+ });
1340
+ const res = createMockResponse();
1341
+ await service.copyMove(req, res);
1342
+ expect(filesManager.copyMove).toHaveBeenCalledWith(req.user, req.space, dstSpace, false, false, false, expect.objectContaining({
1343
+ depth: req.dav.depth,
1344
+ lockTokens: expect.any(Array)
1345
+ }));
1346
+ expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
1347
+ });
1348
+ it('should copy file and return 204 when destination exists', async ()=>{
1349
+ const dstSpace = {
1350
+ ...createBaseRequest().space,
1351
+ url: '/webdav/test/existing.txt',
1352
+ realPath: '/real/path/to/existing.txt',
1353
+ dbFile: {
1354
+ path: 'existing.txt',
1355
+ spaceId: 1,
1356
+ inTrash: false
1357
+ }
1358
+ };
1359
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1360
+ _files.isPathExists.mockResolvedValue(true);
1361
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1362
+ filesManager.copyMove.mockResolvedValue(undefined);
1363
+ const req = createBaseRequest({
1364
+ method: 'COPY',
1365
+ dav: {
1366
+ ...createBaseRequest().dav,
1367
+ copyMove: {
1368
+ destination: '/webdav/test/existing.txt',
1369
+ isMove: false,
1370
+ overwrite: true
1060
1371
  }
1061
- ]
1062
- }
1372
+ }
1373
+ });
1374
+ const res = createMockResponse();
1375
+ await service.copyMove(req, res);
1376
+ expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
1063
1377
  });
1064
- const res = makeRes();
1065
- const result = await service.copyMove(req, res);
1066
- expect(result).toBeUndefined();
1067
- expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1068
- expect(res.body).toBe('If header condition failed');
1069
1378
  });
1070
- it('returns 204 when destination existed; 201 when not', async ()=>{
1071
- const handler = service['webDAVHandler'];
1072
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue({
1073
- ...baseReq().space,
1074
- url: '/dst',
1075
- realPath: '/real/dst',
1076
- dbFile: {
1077
- path: 'dst'
1078
- }
1079
- });
1080
- jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1081
- _files.isPathExists.mockResolvedValueOnce(true);
1082
- const req1 = baseReq({
1083
- method: 'MOVE',
1084
- dav: {
1085
- ...baseReq().dav,
1086
- copyMove: {
1087
- destination: '/dst',
1088
- isMove: true,
1089
- overwrite: true
1379
+ describe('MOVE operation', ()=>{
1380
+ it('should move file and return 201 when destination does not exist', async ()=>{
1381
+ const dstSpace = {
1382
+ ...createBaseRequest().space,
1383
+ url: '/webdav/test/moved.txt',
1384
+ realPath: '/real/path/to/moved.txt',
1385
+ dbFile: {
1386
+ path: 'moved.txt',
1387
+ spaceId: 1,
1388
+ inTrash: false
1090
1389
  }
1091
- }
1092
- });
1093
- const res1 = makeRes();
1094
- await service.copyMove(req1, res1);
1095
- expect(res1.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
1096
- _files.isPathExists.mockResolvedValueOnce(false);
1097
- const req2 = baseReq({
1098
- method: 'COPY',
1099
- dav: {
1100
- ...baseReq().dav,
1101
- copyMove: {
1102
- destination: '/dst',
1103
- isMove: false,
1104
- overwrite: false
1390
+ };
1391
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1392
+ _files.isPathExists.mockResolvedValue(false);
1393
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1394
+ filesManager.copyMove.mockResolvedValue(undefined);
1395
+ const req = createBaseRequest({
1396
+ method: 'MOVE',
1397
+ dav: {
1398
+ ...createBaseRequest().dav,
1399
+ copyMove: {
1400
+ destination: '/webdav/test/moved.txt',
1401
+ isMove: true,
1402
+ overwrite: false
1403
+ }
1105
1404
  }
1106
- }
1107
- });
1108
- const res2 = makeRes();
1109
- await service.copyMove(req2, res2);
1110
- expect(res2.statusCode).toBe(_common.HttpStatus.CREATED);
1111
- });
1112
- it('delegates errors to handleError', async ()=>{
1113
- const handler = service['webDAVHandler'];
1114
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue({
1115
- ...baseReq().space,
1116
- url: '/dst',
1117
- realPath: '/real/dst',
1118
- dbFile: {
1119
- path: 'dst'
1120
- }
1121
- });
1122
- jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1123
- // Chain 1) LockConflict without lockroot (fallback to dbFilePath) then 2) unexpected error
1124
- const { LockConflict } = jest.requireActual('../../files/models/file-lock-error');
1125
- filesManager.copyMove.mockRejectedValueOnce(new LockConflict({
1126
- dbFilePath: 'dst'
1127
- })).mockRejectedValueOnce(new Error('copy failed'));
1128
- const req = baseReq({
1129
- method: 'COPY',
1130
- dav: {
1131
- ...baseReq().dav,
1132
- copyMove: {
1133
- destination: '/dst',
1134
- isMove: false,
1135
- overwrite: true
1405
+ });
1406
+ const res = createMockResponse();
1407
+ await service.copyMove(req, res);
1408
+ expect(filesManager.copyMove).toHaveBeenCalledWith(req.user, req.space, dstSpace, true, false, false, expect.any(Object));
1409
+ expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
1410
+ });
1411
+ it('should move file and return 204 when destination exists', async ()=>{
1412
+ const dstSpace = {
1413
+ ...createBaseRequest().space,
1414
+ url: '/webdav/test/existing.txt',
1415
+ realPath: '/real/path/to/existing.txt',
1416
+ dbFile: {
1417
+ path: 'existing.txt',
1418
+ spaceId: 1,
1419
+ inTrash: false
1136
1420
  }
1137
- }
1138
- });
1139
- // 1) LockConflict => DAV_ERROR_RES(423) using e.lock.dbFilePath
1140
- const res1 = makeRes();
1141
- const logSpy = jest.spyOn(service['logger'], 'error').mockImplementation(()=>undefined);
1142
- const result1 = await service.copyMove(req, res1);
1143
- expect(result1).toBe(res1);
1144
- expect(res1.statusCode).toBe(_common.HttpStatus.LOCKED);
1145
- // 2) Unexpected error => HttpException 500 and log contains " -> /dst"
1146
- const res2 = makeRes();
1147
- try {
1148
- await service.copyMove(req, res2);
1149
- expect(true).toBe(false); // should not reach
1150
- } catch (e) {
1151
- expect(e).toBeInstanceOf(_common.HttpException);
1152
- expect(e.getStatus()).toBe(_common.HttpStatus.INTERNAL_SERVER_ERROR);
1153
- }
1154
- // Verify the log message includes an arrow to the destination (toUrl)
1155
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(' -> /dst'));
1156
- });
1157
- it('returns early when evaluateIfHeaders returns false (explicit spy)', async ()=>{
1158
- const req = baseReq({
1159
- method: 'COPY',
1160
- dav: {
1161
- ...baseReq().dav,
1162
- copyMove: {
1163
- destination: '/dst',
1164
- isMove: false,
1165
- overwrite: true
1166
- },
1167
- ifHeaders: [
1168
- {
1169
- path: '/dst'
1421
+ };
1422
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1423
+ _files.isPathExists.mockResolvedValue(true);
1424
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1425
+ filesManager.copyMove.mockResolvedValue(undefined);
1426
+ const req = createBaseRequest({
1427
+ method: 'MOVE',
1428
+ dav: {
1429
+ ...createBaseRequest().dav,
1430
+ copyMove: {
1431
+ destination: '/webdav/test/existing.txt',
1432
+ isMove: true,
1433
+ overwrite: true
1170
1434
  }
1171
- ]
1172
- }
1173
- });
1174
- const res = makeRes();
1175
- const handler = service['webDAVHandler'];
1176
- // Ensure destination space resolves so we reach the evaluateIfHeaders call
1177
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue({
1178
- ...req.space,
1179
- url: '/dst',
1180
- realPath: '/real/dst',
1181
- dbFile: {
1182
- path: 'dst'
1183
- }
1435
+ }
1436
+ });
1437
+ const res = createMockResponse();
1438
+ await service.copyMove(req, res);
1439
+ expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
1184
1440
  });
1185
- _files.isPathExists.mockResolvedValue(true);
1186
- // Decorator is no-op (mocked), so copyMove calls evaluateIfHeaders once for destination: force it to return false
1187
- const spy = jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(false);
1188
- const result = await service.copyMove(req, res);
1189
- expect(spy).toHaveBeenCalledTimes(1);
1190
- expect(result).toBeUndefined();
1191
- expect(filesManager.copyMove).not.toHaveBeenCalled();
1192
1441
  });
1193
- });
1194
- describe('evaluateIfHeaders', ()=>{
1195
- it('returns true when no headers', async ()=>{
1196
- const req = baseReq({
1197
- dav: {
1198
- ...baseReq().dav,
1199
- ifHeaders: undefined
1200
- }
1442
+ describe('If-Headers on destination', ()=>{
1443
+ it('should return early when evaluateIfHeaders fails for destination', async ()=>{
1444
+ const dstSpace = {
1445
+ ...createBaseRequest().space,
1446
+ url: '/webdav/test/dest.txt',
1447
+ realPath: '/real/path/to/dest.txt',
1448
+ dbFile: {
1449
+ path: 'dest.txt',
1450
+ spaceId: 1,
1451
+ inTrash: false
1452
+ }
1453
+ };
1454
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1455
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(false);
1456
+ const req = createBaseRequest({
1457
+ method: 'COPY',
1458
+ dav: {
1459
+ ...createBaseRequest().dav,
1460
+ copyMove: {
1461
+ destination: '/webdav/test/dest.txt',
1462
+ isMove: false,
1463
+ overwrite: true
1464
+ },
1465
+ ifHeaders: [
1466
+ {
1467
+ path: '/webdav/test/dest.txt',
1468
+ etag: {
1469
+ value: 'W/"wrong"',
1470
+ mustMatch: true
1471
+ }
1472
+ }
1473
+ ]
1474
+ }
1475
+ });
1476
+ const res = createMockResponse();
1477
+ const result = await service.copyMove(req, res);
1478
+ expect(result).toBeUndefined();
1479
+ expect(filesManager.copyMove).not.toHaveBeenCalled();
1480
+ });
1481
+ it('should return 412 when destination If-Header haveLock mismatches', async ()=>{
1482
+ const dstSpace = {
1483
+ ...createBaseRequest().space,
1484
+ url: '/webdav/test/dest.txt',
1485
+ realPath: '/real/path/to/dest.txt',
1486
+ dbFile: {
1487
+ path: 'dest.txt',
1488
+ spaceId: 1,
1489
+ inTrash: false
1490
+ }
1491
+ };
1492
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1493
+ _files.isPathExists.mockResolvedValue(true);
1494
+ _paths.dbFileFromSpace.mockReturnValue(dstSpace.dbFile);
1495
+ filesLockManager.getLocksByPath.mockResolvedValue([
1496
+ {
1497
+ key: 'lock1'
1498
+ }
1499
+ ]);
1500
+ const req = createBaseRequest({
1501
+ method: 'COPY',
1502
+ dav: {
1503
+ ...createBaseRequest().dav,
1504
+ copyMove: {
1505
+ destination: '/webdav/test/dest.txt',
1506
+ isMove: false,
1507
+ overwrite: true
1508
+ },
1509
+ ifHeaders: [
1510
+ {
1511
+ path: '/webdav/test/dest.txt',
1512
+ haveLock: {
1513
+ mustMatch: false
1514
+ }
1515
+ }
1516
+ ]
1517
+ }
1518
+ });
1519
+ const res = createMockResponse();
1520
+ const result = await service.copyMove(req, res);
1521
+ expect(result).toBeUndefined();
1522
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1201
1523
  });
1202
- const res = makeRes();
1203
- const ok = await service.evaluateIfHeaders(req, res);
1204
- expect(ok).toBe(true);
1205
1524
  });
1206
- const negativeIfHeaderCases = [
1207
- {
1208
- name: 'haveLock mismatch',
1209
- dav: {
1210
- ifHeaders: [
1211
- {
1212
- haveLock: {
1213
- mustMatch: true
1214
- }
1525
+ describe('Error handling', ()=>{
1526
+ it('should handle lock conflict error', async ()=>{
1527
+ const dstSpace = {
1528
+ ...createBaseRequest().space,
1529
+ url: '/webdav/test/dest.txt',
1530
+ realPath: '/real/path/to/dest.txt',
1531
+ dbFile: {
1532
+ path: 'dest.txt',
1533
+ spaceId: 1,
1534
+ inTrash: false
1535
+ }
1536
+ };
1537
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1538
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1539
+ const lockError = new _filelockerror.LockConflict({
1540
+ dbFilePath: 'dest.txt',
1541
+ options: {
1542
+ lockRoot: '/webdav/locked'
1543
+ }
1544
+ }, 'Lock conflict');
1545
+ filesManager.copyMove.mockRejectedValue(lockError);
1546
+ const req = createBaseRequest({
1547
+ method: 'COPY',
1548
+ dav: {
1549
+ ...createBaseRequest().dav,
1550
+ copyMove: {
1551
+ destination: '/webdav/test/dest.txt',
1552
+ isMove: false,
1553
+ overwrite: true
1215
1554
  }
1216
- ]
1217
- },
1218
- setup: async ()=>{
1219
- // No lock present => match=false, mustMatch=true => mismatch
1220
- ;
1221
- _paths.dbFileFromSpace.mockReturnValue({
1222
- path: 'file.txt',
1555
+ }
1556
+ });
1557
+ const res = createMockResponse();
1558
+ await service.copyMove(req, res);
1559
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
1560
+ });
1561
+ it('should handle lock conflict without lockRoot (fallback to dbFilePath)', async ()=>{
1562
+ const dstSpace = {
1563
+ ...createBaseRequest().space,
1564
+ url: '/webdav/test/dest.txt',
1565
+ realPath: '/real/path/to/dest.txt',
1566
+ dbFile: {
1567
+ path: 'dest.txt',
1223
1568
  spaceId: 1,
1224
1569
  inTrash: false
1225
- });
1226
- filesLockManager.getLocksByPath.mockResolvedValue([]);
1227
- }
1228
- },
1229
- {
1230
- name: 'haveLock mismatch when locks exist but mustMatch=false',
1231
- dav: {
1232
- ifHeaders: [
1233
- {
1234
- haveLock: {
1235
- mustMatch: false
1236
- }
1570
+ }
1571
+ };
1572
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1573
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1574
+ const lockError = new _filelockerror.LockConflict({
1575
+ dbFilePath: 'dest.txt'
1576
+ }, 'Lock conflict');
1577
+ filesManager.copyMove.mockRejectedValue(lockError);
1578
+ const req = createBaseRequest({
1579
+ method: 'COPY',
1580
+ dav: {
1581
+ ...createBaseRequest().dav,
1582
+ copyMove: {
1583
+ destination: '/webdav/test/dest.txt',
1584
+ isMove: false,
1585
+ overwrite: true
1237
1586
  }
1238
- ]
1239
- },
1240
- setup: async ()=>{
1241
- // Lock present => match=true, mustMatch=false => mismatch
1242
- ;
1243
- _paths.dbFileFromSpace.mockReturnValue({
1244
- path: 'dst',
1587
+ }
1588
+ });
1589
+ const res = createMockResponse();
1590
+ await service.copyMove(req, res);
1591
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
1592
+ });
1593
+ it('should handle file errors', async ()=>{
1594
+ const dstSpace = {
1595
+ ...createBaseRequest().space,
1596
+ url: '/webdav/test/dest.txt',
1597
+ realPath: '/real/path/to/dest.txt',
1598
+ dbFile: {
1599
+ path: 'dest.txt',
1245
1600
  spaceId: 1,
1246
1601
  inTrash: false
1247
- });
1248
- filesLockManager.getLocksByPath.mockResolvedValue([
1249
- {}
1250
- ]);
1251
- }
1252
- },
1253
- {
1254
- name: 'token not found',
1255
- dav: {
1256
- ifHeaders: [
1257
- {
1258
- token: {
1259
- value: 'missing',
1260
- mustMatch: true
1261
- }
1602
+ }
1603
+ };
1604
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1605
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1606
+ const fileError = new _fileerror.FileError(409, 'File conflict');
1607
+ filesManager.copyMove.mockRejectedValue(fileError);
1608
+ const req = createBaseRequest({
1609
+ method: 'MOVE',
1610
+ dav: {
1611
+ ...createBaseRequest().dav,
1612
+ copyMove: {
1613
+ destination: '/webdav/test/dest.txt',
1614
+ isMove: true,
1615
+ overwrite: false
1262
1616
  }
1263
- ]
1264
- },
1265
- setup: async ()=>{
1266
- filesLockManager.getLockByToken.mockResolvedValue(null);
1267
- }
1268
- },
1269
- {
1270
- name: 'etag mismatch',
1271
- dav: {
1272
- ifHeaders: [
1273
- {
1274
- etag: {
1275
- value: 'W/"bad"',
1276
- mustMatch: true
1277
- }
1617
+ }
1618
+ });
1619
+ const res = createMockResponse();
1620
+ await service.copyMove(req, res);
1621
+ expect(res.statusCode).toBe(409);
1622
+ expect(res.body).toBe('File conflict');
1623
+ });
1624
+ it('should throw HttpException for unexpected errors', async ()=>{
1625
+ const dstSpace = {
1626
+ ...createBaseRequest().space,
1627
+ url: '/webdav/test/dest.txt',
1628
+ realPath: '/real/path/to/dest.txt',
1629
+ dbFile: {
1630
+ path: 'dest.txt',
1631
+ spaceId: 1,
1632
+ inTrash: false
1633
+ }
1634
+ };
1635
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1636
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1637
+ const unexpectedError = new Error('Unexpected filesystem error');
1638
+ filesManager.copyMove.mockRejectedValue(unexpectedError);
1639
+ const req = createBaseRequest({
1640
+ method: 'COPY',
1641
+ dav: {
1642
+ ...createBaseRequest().dav,
1643
+ copyMove: {
1644
+ destination: '/webdav/test/dest.txt',
1645
+ isMove: false,
1646
+ overwrite: true
1278
1647
  }
1279
- ]
1280
- },
1281
- setup: async ()=>{
1282
- ;
1283
- _files.isPathExists.mockResolvedValue(true);
1284
- }
1285
- }
1286
- ];
1287
- it.each(negativeIfHeaderCases)('fails with 412 on %s', async ({ dav, setup })=>{
1288
- await setup();
1289
- const req = baseReq({
1290
- dav: {
1291
- ...baseReq().dav,
1292
- ...dav
1648
+ }
1649
+ });
1650
+ const res = createMockResponse();
1651
+ await expect(service.copyMove(req, res)).rejects.toThrow(_common.HttpException);
1652
+ });
1653
+ it('should include destination URL in error log', async ()=>{
1654
+ const dstSpace = {
1655
+ ...createBaseRequest().space,
1656
+ url: '/webdav/test/dest.txt',
1657
+ realPath: '/real/path/to/dest.txt',
1658
+ dbFile: {
1659
+ path: 'dest.txt',
1660
+ spaceId: 1,
1661
+ inTrash: false
1662
+ }
1663
+ };
1664
+ webDAVSpaces.spaceEnv.mockResolvedValue(dstSpace);
1665
+ jest.spyOn(service, 'evaluateIfHeaders').mockResolvedValue(true);
1666
+ const logSpy = jest.spyOn(service['logger'], 'error').mockImplementation(()=>undefined);
1667
+ filesManager.copyMove.mockRejectedValue(new Error('Copy failed'));
1668
+ const req = createBaseRequest({
1669
+ method: 'COPY',
1670
+ dav: {
1671
+ ...createBaseRequest().dav,
1672
+ copyMove: {
1673
+ destination: '/webdav/test/dest.txt',
1674
+ isMove: false,
1675
+ overwrite: true
1676
+ }
1677
+ }
1678
+ });
1679
+ const res = createMockResponse();
1680
+ try {
1681
+ await service.copyMove(req, res);
1682
+ } catch {
1683
+ // Expected to throw
1293
1684
  }
1685
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(' -> /webdav/test/dest.txt'));
1294
1686
  });
1295
- const res = makeRes();
1296
- const ok = await service.evaluateIfHeaders(req, res);
1297
- expect(ok).toBe(false);
1298
- expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1299
1687
  });
1300
- it('returns true when one condition matches', async ()=>{
1301
- ;
1302
- _files.isPathExists.mockResolvedValue(true);
1303
- const req = baseReq({
1304
- dav: {
1305
- ...baseReq().dav,
1306
- ifHeaders: [
1307
- {
1308
- etag: {
1309
- value: 'W/"bad"',
1310
- mustMatch: true
1688
+ });
1689
+ describe('evaluateIfHeaders', ()=>{
1690
+ describe('Base cases', ()=>{
1691
+ it('should return true when no if-headers present', async ()=>{
1692
+ const req = createBaseRequest({
1693
+ dav: {
1694
+ ...createBaseRequest().dav,
1695
+ ifHeaders: undefined
1696
+ }
1697
+ });
1698
+ const res = createMockResponse();
1699
+ const result = await service.evaluateIfHeaders(req, res);
1700
+ expect(result).toBe(true);
1701
+ expect(res.statusCode).toBeUndefined();
1702
+ });
1703
+ it('should return true when at least one condition matches', async ()=>{
1704
+ ;
1705
+ _files.isPathExists.mockResolvedValue(true);
1706
+ const req = createBaseRequest({
1707
+ dav: {
1708
+ ...createBaseRequest().dav,
1709
+ ifHeaders: [
1710
+ {
1711
+ etag: {
1712
+ value: 'W/"wrong-etag"',
1713
+ mustMatch: true
1714
+ }
1715
+ },
1716
+ {
1717
+ etag: {
1718
+ value: 'W/"etag-123"',
1719
+ mustMatch: true
1720
+ }
1311
1721
  }
1312
- },
1313
- {
1314
- etag: {
1315
- value: 'W/"etag"',
1316
- mustMatch: true
1722
+ ]
1723
+ }
1724
+ });
1725
+ const res = createMockResponse();
1726
+ const result = await service.evaluateIfHeaders(req, res);
1727
+ expect(result).toBe(true);
1728
+ expect(res.statusCode).toBeUndefined();
1729
+ });
1730
+ it('should return false when path cannot be resolved', async ()=>{
1731
+ webDAVSpaces.spaceEnv.mockResolvedValue(null);
1732
+ const req = createBaseRequest({
1733
+ dav: {
1734
+ ...createBaseRequest().dav,
1735
+ ifHeaders: [
1736
+ {
1737
+ path: '/webdav/unknown/file.txt'
1317
1738
  }
1318
- } // this should pass
1319
- ]
1320
- }
1739
+ ]
1740
+ }
1741
+ });
1742
+ const res = createMockResponse();
1743
+ const result = await service.evaluateIfHeaders(req, res);
1744
+ expect(result).toBe(false);
1745
+ expect(res.statusCode).toBeUndefined();
1321
1746
  });
1322
- const res = makeRes();
1323
- const ok = await service.evaluateIfHeaders(req, res);
1324
- expect(ok).toBe(true);
1325
- expect(res.statusCode).toBeUndefined();
1326
1747
  });
1327
- it('fails with 412 on token url mismatch', async ()=>{
1328
- const req = baseReq({
1329
- dav: {
1330
- ...baseReq().dav,
1331
- ifHeaders: [
1332
- {
1333
- path: '/dav/other',
1334
- token: {
1335
- value: 'opaquetoken:xyz',
1336
- mustMatch: true
1748
+ describe('haveLock condition', ()=>{
1749
+ it('should return true when haveLock matches (lock exists, mustMatch=true)', async ()=>{
1750
+ ;
1751
+ _paths.dbFileFromSpace.mockReturnValue({
1752
+ path: 'file.txt',
1753
+ spaceId: 1
1754
+ });
1755
+ filesLockManager.getLocksByPath.mockResolvedValue([
1756
+ {
1757
+ key: 'lock1'
1758
+ }
1759
+ ]);
1760
+ const req = createBaseRequest({
1761
+ dav: {
1762
+ ...createBaseRequest().dav,
1763
+ ifHeaders: [
1764
+ {
1765
+ haveLock: {
1766
+ mustMatch: true
1767
+ }
1337
1768
  }
1338
- }
1339
- ]
1340
- }
1341
- });
1342
- const res = makeRes();
1343
- // Space for the explicit path
1344
- const handler = service['webDAVHandler'];
1345
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue({
1346
- ...req.space,
1347
- url: '/dav/other',
1348
- realPath: '/real/other',
1349
- dbFile: {
1350
- path: 'other'
1351
- }
1352
- });
1353
- filesLockManager.getLockByToken.mockResolvedValue({
1354
- davLock: {
1355
- lockroot: '/dav/url'
1356
- }
1357
- }); // not a parent of /dav/other
1358
- const ok = await service.evaluateIfHeaders(req, res);
1359
- expect(ok).toBe(false);
1360
- expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1361
- });
1362
- it('fails with 412 and logs error when haveLock lookup throws', async ()=>{
1363
- const req = baseReq({
1364
- dav: {
1365
- ...baseReq().dav,
1366
- ifHeaders: [
1367
- {
1368
- path: '/dav/url',
1369
- haveLock: {
1370
- mustMatch: true
1769
+ ]
1770
+ }
1771
+ });
1772
+ const res = createMockResponse();
1773
+ const result = await service.evaluateIfHeaders(req, res);
1774
+ expect(result).toBe(true);
1775
+ });
1776
+ it('should return true when haveLock matches (no lock, mustMatch=false)', async ()=>{
1777
+ ;
1778
+ _paths.dbFileFromSpace.mockReturnValue({
1779
+ path: 'file.txt',
1780
+ spaceId: 1
1781
+ });
1782
+ filesLockManager.getLocksByPath.mockResolvedValue([]);
1783
+ const req = createBaseRequest({
1784
+ dav: {
1785
+ ...createBaseRequest().dav,
1786
+ ifHeaders: [
1787
+ {
1788
+ haveLock: {
1789
+ mustMatch: false
1790
+ }
1371
1791
  }
1372
- }
1373
- ]
1374
- }
1792
+ ]
1793
+ }
1794
+ });
1795
+ const res = createMockResponse();
1796
+ const result = await service.evaluateIfHeaders(req, res);
1797
+ expect(result).toBe(true);
1798
+ });
1799
+ it('should return false with 412 when haveLock mismatches (lock exists, mustMatch=false)', async ()=>{
1800
+ ;
1801
+ _paths.dbFileFromSpace.mockReturnValue({
1802
+ path: 'file.txt',
1803
+ spaceId: 1
1804
+ });
1805
+ filesLockManager.getLocksByPath.mockResolvedValue([
1806
+ {
1807
+ key: 'lock1'
1808
+ }
1809
+ ]);
1810
+ const req = createBaseRequest({
1811
+ dav: {
1812
+ ...createBaseRequest().dav,
1813
+ ifHeaders: [
1814
+ {
1815
+ haveLock: {
1816
+ mustMatch: false
1817
+ }
1818
+ }
1819
+ ]
1820
+ }
1821
+ });
1822
+ const res = createMockResponse();
1823
+ const result = await service.evaluateIfHeaders(req, res);
1824
+ expect(result).toBe(false);
1825
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1826
+ });
1827
+ it('should return false with 412 when haveLock mismatches (no lock, mustMatch=true)', async ()=>{
1828
+ ;
1829
+ _paths.dbFileFromSpace.mockReturnValue({
1830
+ path: 'file.txt',
1831
+ spaceId: 1
1832
+ });
1833
+ filesLockManager.getLocksByPath.mockResolvedValue([]);
1834
+ const req = createBaseRequest({
1835
+ dav: {
1836
+ ...createBaseRequest().dav,
1837
+ ifHeaders: [
1838
+ {
1839
+ haveLock: {
1840
+ mustMatch: true
1841
+ }
1842
+ }
1843
+ ]
1844
+ }
1845
+ });
1846
+ const res = createMockResponse();
1847
+ const result = await service.evaluateIfHeaders(req, res);
1848
+ expect(result).toBe(false);
1849
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1850
+ });
1851
+ it('should return false with 412 when haveLock lookup throws error', async ()=>{
1852
+ ;
1853
+ _paths.dbFileFromSpace.mockReturnValue({
1854
+ path: 'file.txt',
1855
+ spaceId: 1
1856
+ });
1857
+ filesLockManager.getLocksByPath.mockRejectedValue(new Error('Database error'));
1858
+ const req = createBaseRequest({
1859
+ dav: {
1860
+ ...createBaseRequest().dav,
1861
+ ifHeaders: [
1862
+ {
1863
+ haveLock: {
1864
+ mustMatch: true
1865
+ }
1866
+ }
1867
+ ]
1868
+ }
1869
+ });
1870
+ const res = createMockResponse();
1871
+ const result = await service.evaluateIfHeaders(req, res);
1872
+ expect(result).toBe(false);
1873
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1874
+ expect(res.body).toBe('If header condition failed');
1375
1875
  });
1376
- const res = makeRes();
1377
- const handler = service['webDAVHandler'];
1378
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue(req.space);
1379
- filesLockManager.getLocksByPath.mockRejectedValue(new Error('boom'));
1380
- const ok = await service.evaluateIfHeaders(req, res);
1381
- expect(ok).toBe(false);
1382
- expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1383
- expect(res.body).toBe('If header condition failed');
1384
1876
  });
1385
- it('returns true when token exists and path matches lockroot', async ()=>{
1386
- const req = baseReq({
1387
- dav: {
1388
- ...baseReq().dav,
1389
- ifHeaders: [
1390
- {
1391
- path: '/dav/url',
1392
- token: {
1393
- value: 'opaquetoken:good',
1394
- mustMatch: true
1877
+ describe('token condition', ()=>{
1878
+ it('should return true when token exists and path matches lockroot', async ()=>{
1879
+ filesLockManager.getLockByToken.mockResolvedValue({
1880
+ options: {
1881
+ lockRoot: '/webdav/test/file.txt'
1882
+ }
1883
+ });
1884
+ const req = createBaseRequest({
1885
+ dav: {
1886
+ ...createBaseRequest().dav,
1887
+ ifHeaders: [
1888
+ {
1889
+ token: {
1890
+ value: 'opaquelocktoken:abc',
1891
+ mustMatch: true
1892
+ }
1395
1893
  }
1396
- }
1397
- ]
1398
- }
1399
- });
1400
- const res = makeRes();
1401
- const handler = service['webDAVHandler'];
1402
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue(req.space);
1403
- filesLockManager.getLockByToken.mockResolvedValue({
1404
- davLock: {
1405
- lockroot: '/dav/url'
1406
- }
1894
+ ]
1895
+ }
1896
+ });
1897
+ const res = createMockResponse();
1898
+ const result = await service.evaluateIfHeaders(req, res);
1899
+ expect(result).toBe(true);
1900
+ });
1901
+ it('should return true when token exists and path is child of lockroot', async ()=>{
1902
+ filesLockManager.getLockByToken.mockResolvedValue({
1903
+ options: {
1904
+ lockRoot: '/webdav/test'
1905
+ }
1906
+ });
1907
+ const req = createBaseRequest({
1908
+ dav: {
1909
+ ...createBaseRequest().dav,
1910
+ url: '/webdav/test/subfolder/file.txt',
1911
+ ifHeaders: [
1912
+ {
1913
+ token: {
1914
+ value: 'opaquelocktoken:abc',
1915
+ mustMatch: true
1916
+ }
1917
+ }
1918
+ ]
1919
+ }
1920
+ });
1921
+ const res = createMockResponse();
1922
+ const result = await service.evaluateIfHeaders(req, res);
1923
+ expect(result).toBe(true);
1924
+ });
1925
+ it('should return false with 412 when token not found', async ()=>{
1926
+ filesLockManager.getLockByToken.mockResolvedValue(null);
1927
+ const req = createBaseRequest({
1928
+ dav: {
1929
+ ...createBaseRequest().dav,
1930
+ ifHeaders: [
1931
+ {
1932
+ token: {
1933
+ value: 'opaquelocktoken:missing',
1934
+ mustMatch: true
1935
+ }
1936
+ }
1937
+ ]
1938
+ }
1939
+ });
1940
+ const res = createMockResponse();
1941
+ const result = await service.evaluateIfHeaders(req, res);
1942
+ expect(result).toBe(false);
1943
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1944
+ });
1945
+ it('should return false with 412 when token exists but path does not match lockroot', async ()=>{
1946
+ filesLockManager.getLockByToken.mockResolvedValue({
1947
+ options: {
1948
+ lockRoot: '/webdav/other/file.txt'
1949
+ }
1950
+ });
1951
+ const req = createBaseRequest({
1952
+ dav: {
1953
+ ...createBaseRequest().dav,
1954
+ url: '/webdav/test/file.txt',
1955
+ ifHeaders: [
1956
+ {
1957
+ token: {
1958
+ value: 'opaquelocktoken:abc',
1959
+ mustMatch: true
1960
+ }
1961
+ }
1962
+ ]
1963
+ }
1964
+ });
1965
+ const res = createMockResponse();
1966
+ const result = await service.evaluateIfHeaders(req, res);
1967
+ expect(result).toBe(false);
1968
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1969
+ });
1970
+ it('should evaluate token condition with explicit path in if-header', async ()=>{
1971
+ const explicitSpace = {
1972
+ ...createBaseRequest().space,
1973
+ url: '/webdav/explicit/path.txt',
1974
+ realPath: '/real/path/to/explicit.txt',
1975
+ dbFile: {
1976
+ path: 'explicit/path.txt',
1977
+ spaceId: 1,
1978
+ inTrash: false
1979
+ }
1980
+ };
1981
+ webDAVSpaces.spaceEnv.mockResolvedValue(explicitSpace);
1982
+ filesLockManager.getLockByToken.mockResolvedValue({
1983
+ options: {
1984
+ lockRoot: '/webdav/explicit'
1985
+ }
1986
+ });
1987
+ const req = createBaseRequest({
1988
+ dav: {
1989
+ ...createBaseRequest().dav,
1990
+ ifHeaders: [
1991
+ {
1992
+ path: '/webdav/explicit/path.txt',
1993
+ token: {
1994
+ value: 'opaquelocktoken:xyz',
1995
+ mustMatch: true
1996
+ }
1997
+ }
1998
+ ]
1999
+ }
2000
+ });
2001
+ const res = createMockResponse();
2002
+ const result = await service.evaluateIfHeaders(req, res);
2003
+ expect(webDAVSpaces.spaceEnv).toHaveBeenCalledWith(req.user, '/webdav/explicit/path.txt');
2004
+ expect(result).toBe(true);
1407
2005
  });
1408
- const ok = await service.evaluateIfHeaders(req, res);
1409
- expect(ok).toBe(true);
1410
- expect(res.statusCode).toBeUndefined();
1411
2006
  });
1412
- it('returns false without setting status when If-Header path cannot be resolved', async ()=>{
1413
- const req = baseReq({
1414
- dav: {
1415
- ...baseReq().dav,
1416
- ifHeaders: [
1417
- {
1418
- path: '/dav/missing'
1419
- }
1420
- ]
1421
- }
2007
+ describe('etag condition', ()=>{
2008
+ it('should return true when etag matches', async ()=>{
2009
+ ;
2010
+ _files.isPathExists.mockResolvedValue(true);
2011
+ _files.genEtag.mockReturnValue('W/"etag-123"');
2012
+ const req = createBaseRequest({
2013
+ dav: {
2014
+ ...createBaseRequest().dav,
2015
+ ifHeaders: [
2016
+ {
2017
+ etag: {
2018
+ value: 'W/"etag-123"',
2019
+ mustMatch: true
2020
+ }
2021
+ }
2022
+ ]
2023
+ }
2024
+ });
2025
+ const res = createMockResponse();
2026
+ const result = await service.evaluateIfHeaders(req, res);
2027
+ expect(result).toBe(true);
2028
+ });
2029
+ it('should return true when etag does not match and mustMatch=false', async ()=>{
2030
+ ;
2031
+ _files.isPathExists.mockResolvedValue(true);
2032
+ _files.genEtag.mockReturnValue('W/"etag-123"');
2033
+ const req = createBaseRequest({
2034
+ dav: {
2035
+ ...createBaseRequest().dav,
2036
+ ifHeaders: [
2037
+ {
2038
+ etag: {
2039
+ value: 'W/"different"',
2040
+ mustMatch: false
2041
+ }
2042
+ }
2043
+ ]
2044
+ }
2045
+ });
2046
+ const res = createMockResponse();
2047
+ const result = await service.evaluateIfHeaders(req, res);
2048
+ expect(result).toBe(true);
2049
+ });
2050
+ it('should return false with 412 when etag mismatches', async ()=>{
2051
+ ;
2052
+ _files.isPathExists.mockResolvedValue(true);
2053
+ _files.genEtag.mockReturnValue('W/"etag-123"');
2054
+ const req = createBaseRequest({
2055
+ dav: {
2056
+ ...createBaseRequest().dav,
2057
+ ifHeaders: [
2058
+ {
2059
+ etag: {
2060
+ value: 'W/"wrong-etag"',
2061
+ mustMatch: true
2062
+ }
2063
+ }
2064
+ ]
2065
+ }
2066
+ });
2067
+ const res = createMockResponse();
2068
+ const result = await service.evaluateIfHeaders(req, res);
2069
+ expect(result).toBe(false);
2070
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
2071
+ });
2072
+ it('should return false with 412 when resource does not exist (null etag)', async ()=>{
2073
+ ;
2074
+ _files.isPathExists.mockResolvedValue(false);
2075
+ const req = createBaseRequest({
2076
+ dav: {
2077
+ ...createBaseRequest().dav,
2078
+ ifHeaders: [
2079
+ {
2080
+ etag: {
2081
+ value: 'W/"etag-123"',
2082
+ mustMatch: true
2083
+ }
2084
+ }
2085
+ ]
2086
+ }
2087
+ });
2088
+ const res = createMockResponse();
2089
+ const result = await service.evaluateIfHeaders(req, res);
2090
+ expect(result).toBe(false);
2091
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
2092
+ });
2093
+ it('should cache etag for multiple conditions on same path', async ()=>{
2094
+ ;
2095
+ _files.isPathExists.mockResolvedValue(true);
2096
+ _files.genEtag.mockReturnValue('W/"etag-123"');
2097
+ const req = createBaseRequest({
2098
+ dav: {
2099
+ ...createBaseRequest().dav,
2100
+ ifHeaders: [
2101
+ {
2102
+ etag: {
2103
+ value: 'W/"wrong1"',
2104
+ mustMatch: true
2105
+ }
2106
+ },
2107
+ {
2108
+ etag: {
2109
+ value: 'W/"etag-123"',
2110
+ mustMatch: true
2111
+ }
2112
+ }
2113
+ ]
2114
+ }
2115
+ });
2116
+ const res = createMockResponse();
2117
+ const result = await service.evaluateIfHeaders(req, res);
2118
+ expect(result).toBe(true);
2119
+ expect(_files.genEtag).toHaveBeenCalledTimes(1); // Cached
1422
2120
  });
1423
- const res = makeRes();
1424
- const handler = service['webDAVHandler'];
1425
- jest.spyOn(handler, 'spaceEnv').mockResolvedValue(null);
1426
- const ok = await service.evaluateIfHeaders(req, res);
1427
- expect(ok).toBe(false);
1428
- expect(res.statusCode).toBeUndefined();
1429
- expect(res.body).toBeUndefined();
1430
2121
  });
1431
- it('fails with 412 on etag when resource does not exist (null etag)', async ()=>{
1432
- ;
1433
- _files.isPathExists.mockResolvedValue(false);
1434
- const req = baseReq({
1435
- dav: {
1436
- ...baseReq().dav,
1437
- ifHeaders: [
1438
- {
1439
- etag: {
1440
- value: 'W/"etag"',
1441
- mustMatch: true
2122
+ describe('Multiple conditions', ()=>{
2123
+ it('should evaluate multiple conditions and return true if any matches', async ()=>{
2124
+ ;
2125
+ _files.isPathExists.mockResolvedValue(true);
2126
+ _paths.dbFileFromSpace.mockReturnValue({
2127
+ path: 'file.txt',
2128
+ spaceId: 1
2129
+ });
2130
+ filesLockManager.getLocksByPath.mockResolvedValue([]);
2131
+ const req = createBaseRequest({
2132
+ dav: {
2133
+ ...createBaseRequest().dav,
2134
+ ifHeaders: [
2135
+ {
2136
+ haveLock: {
2137
+ mustMatch: true
2138
+ }
2139
+ },
2140
+ {
2141
+ haveLock: {
2142
+ mustMatch: false
2143
+ }
2144
+ } // Will succeed (no lock)
2145
+ ]
2146
+ }
2147
+ });
2148
+ const res = createMockResponse();
2149
+ const result = await service.evaluateIfHeaders(req, res);
2150
+ expect(result).toBe(true);
2151
+ });
2152
+ it('should return false with 412 when all conditions fail', async ()=>{
2153
+ ;
2154
+ _files.isPathExists.mockResolvedValue(true);
2155
+ _files.genEtag.mockReturnValue('W/"etag-123"');
2156
+ filesLockManager.getLockByToken.mockResolvedValue(null);
2157
+ const req = createBaseRequest({
2158
+ dav: {
2159
+ ...createBaseRequest().dav,
2160
+ ifHeaders: [
2161
+ {
2162
+ etag: {
2163
+ value: 'W/"wrong1"',
2164
+ mustMatch: true
2165
+ }
2166
+ },
2167
+ {
2168
+ etag: {
2169
+ value: 'W/"wrong2"',
2170
+ mustMatch: true
2171
+ }
2172
+ },
2173
+ {
2174
+ token: {
2175
+ value: 'opaquelocktoken:missing',
2176
+ mustMatch: true
2177
+ }
1442
2178
  }
1443
- }
1444
- ]
1445
- }
2179
+ ]
2180
+ }
2181
+ });
2182
+ const res = createMockResponse();
2183
+ const result = await service.evaluateIfHeaders(req, res);
2184
+ expect(result).toBe(false);
2185
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
2186
+ expect(res.body).toBe('If header condition failed');
1446
2187
  });
1447
- const res = makeRes();
1448
- const ok = await service.evaluateIfHeaders(req, res);
1449
- expect(ok).toBe(false);
1450
- expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1451
2188
  });
1452
2189
  });
1453
- describe('lockRefresh', ()=>{
1454
- const refresh400Cases = [
1455
- {
1456
- name: 'more than one or zero tokens in If header',
1457
- dav: {
1458
- body: undefined,
1459
- ifHeaders: []
1460
- },
1461
- expectMsg: 'Expected a lock token'
1462
- },
1463
- {
1464
- name: 'token extraction fails',
1465
- dav: {
1466
- body: undefined,
1467
- ifHeaders: [
1468
- {
1469
- notAToken: true
1470
- }
1471
- ]
1472
- },
1473
- expectMsg: 'Unable to extract token'
1474
- }
1475
- ];
1476
- it.each(refresh400Cases)('returns 400 when %s', async ({ dav, expectMsg })=>{
1477
- const req = baseReq({
1478
- dav: {
1479
- ...baseReq().dav,
1480
- ...dav
1481
- }
1482
- });
1483
- const res = makeRes();
1484
- await service.lockRefresh(req, res, req.space.dbFile.path);
1485
- expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
1486
- expect(res.body).toContain(expectMsg);
1487
- });
1488
- it('returns 412 when token not found or not matching URL', async ()=>{
1489
- const req = baseReq({
1490
- dav: {
1491
- ...baseReq().dav,
1492
- body: undefined,
1493
- ifHeaders: [
1494
- {
1495
- token: {
1496
- value: 'opaquetoken:missing',
1497
- mustMatch: true
2190
+ describe('lockRefresh (private method)', ()=>{
2191
+ describe('Parameter validation', ()=>{
2192
+ it('should return 400 when no if-headers present', async ()=>{
2193
+ const req = createBaseRequest({
2194
+ dav: {
2195
+ ...createBaseRequest().dav,
2196
+ body: undefined,
2197
+ ifHeaders: []
2198
+ }
2199
+ });
2200
+ const res = createMockResponse();
2201
+ await service.lockRefresh(req, res, 'file.txt');
2202
+ expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
2203
+ expect(res.body).toContain('Expected a lock token');
2204
+ });
2205
+ it('should return 400 when more than one if-header present', async ()=>{
2206
+ const req = createBaseRequest({
2207
+ dav: {
2208
+ ...createBaseRequest().dav,
2209
+ body: undefined,
2210
+ ifHeaders: [
2211
+ {
2212
+ token: {
2213
+ value: 'token1',
2214
+ mustMatch: true
2215
+ }
2216
+ },
2217
+ {
2218
+ token: {
2219
+ value: 'token2',
2220
+ mustMatch: true
2221
+ }
1498
2222
  }
1499
- }
1500
- ]
1501
- }
2223
+ ]
2224
+ }
2225
+ });
2226
+ const res = createMockResponse();
2227
+ await service.lockRefresh(req, res, 'file.txt');
2228
+ expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
2229
+ expect(res.body).toContain('Expected a lock token');
2230
+ });
2231
+ it('should return 400 when token extraction fails', async ()=>{
2232
+ const req = createBaseRequest({
2233
+ dav: {
2234
+ ...createBaseRequest().dav,
2235
+ body: undefined,
2236
+ ifHeaders: [
2237
+ {
2238
+ notAToken: true
2239
+ }
2240
+ ]
2241
+ }
2242
+ });
2243
+ const res = createMockResponse();
2244
+ jest.spyOn(_ifheader, 'extractOneToken').mockImplementation(()=>{
2245
+ throw new Error('No token found');
2246
+ });
2247
+ await service.lockRefresh(req, res, 'file.txt');
2248
+ expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
2249
+ expect(res.body).toContain('Unable to extract token');
1502
2250
  });
1503
- const res = makeRes();
1504
- filesLockManager.isLockedWithToken.mockResolvedValue(null);
1505
- jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquetoken:missing');
1506
- await service.lockRefresh(req, res, req.space.dbFile.path);
1507
- expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
1508
2251
  });
1509
- it('returns 403 when owner mismatch', async ()=>{
1510
- const req = baseReq({
1511
- dav: {
1512
- ...baseReq().dav,
1513
- body: undefined,
1514
- ifHeaders: [
1515
- {
1516
- token: {
1517
- value: 'opaquetoken:abc',
1518
- mustMatch: true
2252
+ describe('Token validation', ()=>{
2253
+ it('should return 412 when token not found or does not match path', async ()=>{
2254
+ const req = createBaseRequest({
2255
+ dav: {
2256
+ ...createBaseRequest().dav,
2257
+ body: undefined,
2258
+ ifHeaders: [
2259
+ {
2260
+ token: {
2261
+ value: 'opaquelocktoken:missing',
2262
+ mustMatch: true
2263
+ }
1519
2264
  }
1520
- }
1521
- ]
1522
- }
1523
- });
1524
- const res = makeRes();
1525
- jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquetoken:abc');
1526
- filesLockManager.isLockedWithToken.mockResolvedValue({
1527
- owner: {
1528
- id: 2
1529
- }
2265
+ ]
2266
+ }
2267
+ });
2268
+ const res = createMockResponse();
2269
+ jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquelocktoken:missing');
2270
+ filesLockManager.isLockedWithToken.mockResolvedValue(null);
2271
+ await service.lockRefresh(req, res, 'file.txt');
2272
+ expect(filesLockManager.isLockedWithToken).toHaveBeenCalledWith('opaquelocktoken:missing', 'file.txt');
2273
+ expect(res.statusCode).toBe(_common.HttpStatus.PRECONDITION_FAILED);
2274
+ });
2275
+ it('should return 403 when lock owner is different user', async ()=>{
2276
+ const req = createBaseRequest({
2277
+ user: {
2278
+ id: 1,
2279
+ login: 'user1'
2280
+ },
2281
+ dav: {
2282
+ ...createBaseRequest().dav,
2283
+ body: undefined,
2284
+ ifHeaders: [
2285
+ {
2286
+ token: {
2287
+ value: 'opaquelocktoken:abc',
2288
+ mustMatch: true
2289
+ }
2290
+ }
2291
+ ]
2292
+ }
2293
+ });
2294
+ const res = createMockResponse();
2295
+ jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquelocktoken:abc');
2296
+ filesLockManager.isLockedWithToken.mockResolvedValue({
2297
+ owner: {
2298
+ id: 999,
2299
+ login: 'other-user'
2300
+ }
2301
+ });
2302
+ await service.lockRefresh(req, res, 'file.txt');
2303
+ expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
2304
+ expect(res.body).toBe('Lock token does not match owner');
1530
2305
  });
1531
- await service.lockRefresh(req, res, baseReq().space.dbFile.path);
1532
- expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
1533
- expect(res.body).toBe('Lock token does not match owner');
1534
2306
  });
1535
- it('returns 200 and XML body on success', async ()=>{
1536
- const req = baseReq({
1537
- dav: {
1538
- ...baseReq().dav,
1539
- body: undefined,
1540
- lock: {
1541
- ...baseReq().dav.lock,
1542
- timeout: 120
2307
+ describe('Successful refresh', ()=>{
2308
+ it('should refresh lock and return 200 with XML body', async ()=>{
2309
+ const req = createBaseRequest({
2310
+ dav: {
2311
+ ...createBaseRequest().dav,
2312
+ body: undefined,
2313
+ lock: {
2314
+ ...createBaseRequest().dav.lock,
2315
+ timeout: 180
2316
+ },
2317
+ ifHeaders: [
2318
+ {
2319
+ token: {
2320
+ value: 'opaquelocktoken:abc',
2321
+ mustMatch: true
2322
+ }
2323
+ }
2324
+ ]
2325
+ }
2326
+ });
2327
+ const res = createMockResponse();
2328
+ const mockLock = {
2329
+ owner: {
2330
+ id: 1,
2331
+ login: 'test-user'
1543
2332
  },
1544
- ifHeaders: [
1545
- {
1546
- token: {
1547
- value: 'opaquetoken:abc',
1548
- mustMatch: true
2333
+ options: {
2334
+ lockRoot: '/webdav/test/file.txt',
2335
+ lockToken: 'opaquelocktoken:abc'
2336
+ }
2337
+ };
2338
+ jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquelocktoken:abc');
2339
+ filesLockManager.isLockedWithToken.mockResolvedValue(mockLock);
2340
+ await service.lockRefresh(req, res, 'file.txt');
2341
+ expect(filesLockManager.refreshLockTimeout).toHaveBeenCalledWith(mockLock, 180);
2342
+ expect(res.statusCode).toBe(_common.HttpStatus.OK);
2343
+ expect(res.contentType).toContain('application/xml');
2344
+ expect(typeof res.body).toBe('string');
2345
+ });
2346
+ it('should use default timeout when not specified', async ()=>{
2347
+ const req = createBaseRequest({
2348
+ dav: {
2349
+ ...createBaseRequest().dav,
2350
+ body: undefined,
2351
+ lock: {
2352
+ ...createBaseRequest().dav.lock,
2353
+ timeout: undefined
2354
+ },
2355
+ ifHeaders: [
2356
+ {
2357
+ token: {
2358
+ value: 'opaquelocktoken:abc',
2359
+ mustMatch: true
2360
+ }
1549
2361
  }
1550
- }
1551
- ]
1552
- }
1553
- });
1554
- const res = makeRes();
1555
- jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquetoken:abc');
1556
- filesLockManager.isLockedWithToken.mockResolvedValue({
1557
- owner: {
1558
- id: 1
1559
- },
1560
- davLock: {
1561
- lockroot: '/dav/url'
1562
- }
2362
+ ]
2363
+ }
2364
+ });
2365
+ const res = createMockResponse();
2366
+ const mockLock = {
2367
+ owner: {
2368
+ id: 1,
2369
+ login: 'test-user'
2370
+ },
2371
+ options: {
2372
+ lockRoot: '/webdav/test/file.txt'
2373
+ }
2374
+ };
2375
+ jest.spyOn(_ifheader, 'extractOneToken').mockReturnValue('opaquelocktoken:abc');
2376
+ filesLockManager.isLockedWithToken.mockResolvedValue(mockLock);
2377
+ await service.lockRefresh(req, res, 'file.txt');
2378
+ expect(filesLockManager.refreshLockTimeout).toHaveBeenCalledWith(mockLock, undefined);
1563
2379
  });
1564
- await service.lockRefresh(req, res, req.space.dbFile.path);
1565
- expect(filesLockManager.refreshLockTimeout).toHaveBeenCalled();
1566
- expect(res.statusCode).toBe(_common.HttpStatus.OK);
1567
- expect(res.contentType).toContain('application/xml');
1568
- expect(typeof res.body).toBe('string');
1569
2380
  });
1570
2381
  });
1571
- describe('handleError integration', ()=>{
1572
- it('maps LockConflict to 423 Locked via DAV_ERROR_RES', async ()=>{
1573
- // simulate LockConflict during PUT
1574
- const { LockConflict } = jest.requireActual('../../files/models/file-lock-error');
1575
- filesManager.saveStream.mockRejectedValue(new LockConflict({
1576
- dbFilePath: 'file.txt',
1577
- davLock: {
1578
- lockroot: '/dav/url'
1579
- }
1580
- }));
1581
- const req = baseReq({
1582
- method: 'PUT'
1583
- });
1584
- const res = makeRes();
1585
- const result = await service.put(req, res);
1586
- expect(result).toBe(res);
1587
- expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
1588
- expect(typeof res.body).toBe('string');
2382
+ describe('handleError (private method)', ()=>{
2383
+ describe('LockConflict errors', ()=>{
2384
+ it('should handle LockConflict with lockRoot', async ()=>{
2385
+ const lockError = new _filelockerror.LockConflict({
2386
+ dbFilePath: 'file.txt',
2387
+ options: {
2388
+ lockRoot: '/webdav/locked/resource'
2389
+ }
2390
+ }, 'Lock conflict');
2391
+ const req = createBaseRequest({
2392
+ method: 'PUT'
2393
+ });
2394
+ const res = createMockResponse();
2395
+ const result = service.handleError(req, res, lockError);
2396
+ expect(result).toBe(res);
2397
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
2398
+ expect(res.contentType).toContain('application/xml');
2399
+ });
2400
+ it('should handle LockConflict without lockRoot (fallback to dbFilePath)', async ()=>{
2401
+ const lockError = new _filelockerror.LockConflict({
2402
+ dbFilePath: 'file.txt'
2403
+ }, 'Lock conflict');
2404
+ const req = createBaseRequest({
2405
+ method: 'DELETE'
2406
+ });
2407
+ const res = createMockResponse();
2408
+ const result = service.handleError(req, res, lockError);
2409
+ expect(result).toBe(res);
2410
+ expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
2411
+ });
1589
2412
  });
1590
- it('maps FileError to its httpCode and message', async ()=>{
1591
- const { FileError } = jest.requireActual('../../files/models/file-error');
1592
- filesManager.delete.mockRejectedValue(new FileError(409, 'conflict happened'));
1593
- const req = baseReq({
1594
- method: 'DELETE'
1595
- });
1596
- const res = makeRes();
1597
- const result = await service.delete(req, res);
1598
- expect(result).toBe(res);
1599
- expect(res.statusCode).toBe(409);
1600
- expect(res.body).toBe('conflict happened');
2413
+ describe('FileError errors', ()=>{
2414
+ it('should handle FileError and return correct status code', async ()=>{
2415
+ const fileError = new _fileerror.FileError(409, 'Conflict: file already exists');
2416
+ const req = createBaseRequest({
2417
+ method: 'MKCOL'
2418
+ });
2419
+ const res = createMockResponse();
2420
+ const result = service.handleError(req, res, fileError);
2421
+ expect(result).toBe(res);
2422
+ expect(res.statusCode).toBe(409);
2423
+ expect(res.body).toBe('Conflict: file already exists');
2424
+ });
2425
+ it('should strip additional error information after comma', async ()=>{
2426
+ const fileError = new _fileerror.FileError(404, 'File not found, /real/path/details');
2427
+ const req = createBaseRequest({
2428
+ method: 'GET'
2429
+ });
2430
+ const res = createMockResponse();
2431
+ const result = service.handleError(req, res, fileError);
2432
+ expect(result).toBe(res);
2433
+ expect(res.statusCode).toBe(404);
2434
+ expect(res.body).toBe('File not found');
2435
+ });
1601
2436
  });
1602
- it('throws 500 HttpException for unexpected errors', async ()=>{
1603
- const req = baseReq({
1604
- method: 'PUT'
2437
+ describe('Unexpected errors', ()=>{
2438
+ it('should throw HttpException for unexpected errors', ()=>{
2439
+ const unexpectedError = new Error('Database connection failed');
2440
+ const req = createBaseRequest({
2441
+ method: 'PUT'
2442
+ });
2443
+ const res = createMockResponse();
2444
+ expect(()=>{
2445
+ ;
2446
+ service.handleError(req, res, unexpectedError);
2447
+ }).toThrow(_common.HttpException);
2448
+ });
2449
+ it('should log error with method and URL', ()=>{
2450
+ const logSpy = jest.spyOn(service['logger'], 'error').mockImplementation(()=>undefined);
2451
+ const req = createBaseRequest({
2452
+ method: 'PUT',
2453
+ dav: {
2454
+ ...createBaseRequest().dav,
2455
+ url: '/webdav/test.txt'
2456
+ }
2457
+ });
2458
+ const res = createMockResponse();
2459
+ const error = new Error('Test error');
2460
+ try {
2461
+ ;
2462
+ service.handleError(req, res, error);
2463
+ } catch {
2464
+ // Expected to throw
2465
+ }
2466
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PUT'));
2467
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('/webdav/test.txt'));
2468
+ });
2469
+ it('should include destination URL in log when provided', ()=>{
2470
+ const logSpy = jest.spyOn(service['logger'], 'error').mockImplementation(()=>undefined);
2471
+ const req = createBaseRequest({
2472
+ method: 'COPY'
2473
+ });
2474
+ const res = createMockResponse();
2475
+ const error = new Error('Copy error');
2476
+ try {
2477
+ ;
2478
+ service.handleError(req, res, error, '/webdav/destination.txt');
2479
+ } catch {
2480
+ // Expected to throw
2481
+ }
2482
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(' -> /webdav/destination.txt'));
1605
2483
  });
1606
- const res = makeRes();
1607
- filesManager.saveStream.mockRejectedValue(new Error('unexpected'));
1608
- try {
1609
- await service.put(req, res);
1610
- // If we reach this line, the test should fail
1611
- expect(true).toBe(false);
1612
- } catch (e) {
1613
- expect(e).toBeInstanceOf(_common.HttpException);
1614
- expect(e.getStatus()).toBe(_common.HttpStatus.INTERNAL_SERVER_ERROR);
1615
- }
1616
2484
  });
1617
2485
  });
1618
2486
  });