@@ -7,17 +7,21 @@ import { showError } from '@nextcloud/dialogs'
77import { getUploader } from '@nextcloud/upload'
88import { createPinia , setActivePinia } from 'pinia'
99import { afterEach , beforeEach , describe , expect , test , vi } from 'vitest'
10+ import { getTalkConfig } from '../../services/CapabilitiesManager.ts'
1011import { getDavClient } from '../../services/DavClient.ts'
11- import { shareFile } from '../../services/filesSharingServices.ts'
12+ import { postAttachment , probeAttachmentFolder , shareFile } from '../../services/filesSharingServices.ts'
1213import { findUniquePath } from '../../utils/fileUpload.ts'
1314import { useActorStore } from '../actor.ts'
1415import { useSettingsStore } from '../settings.ts'
1516import { useUploadStore } from '../upload.ts'
1617
18+ // conversationGetter must be defined before vi.mock so the factory can close over it.
19+ // Vitest evaluates the factory lazily (after module-level init), so this works.
20+ const conversationGetter = vi . fn ( ) . mockReturnValue ( null )
1721const vuexStoreDispatch = vi . fn ( )
1822vi . mock ( 'vuex' , ( ) => ( {
1923 useStore : vi . fn ( ( ) => ( {
20- getters : { } ,
24+ getters : { conversation : conversationGetter } ,
2125 dispatch : vuexStoreDispatch ,
2226 } ) ) ,
2327} ) )
@@ -33,8 +37,17 @@ vi.mock('../../utils/fileUpload.ts', async () => {
3337 }
3438} )
3539vi . mock ( '../../services/filesSharingServices.ts' , ( ) => ( {
40+ postAttachment : vi . fn ( ) ,
41+ probeAttachmentFolder : vi . fn ( ) ,
3642 shareFile : vi . fn ( ) ,
3743} ) )
44+ vi . mock ( '../../services/CapabilitiesManager.ts' , async ( importOriginal ) => {
45+ const actual = await importOriginal ( )
46+ return {
47+ ...actual ,
48+ getTalkConfig : vi . fn ( ) . mockReturnValue ( true ) ,
49+ }
50+ } )
3851
3952describe ( 'fileUploadStore' , ( ) => {
4053 let actorStore
@@ -63,13 +76,16 @@ describe('fileUploadStore', () => {
6376 describe ( 'uploading' , ( ) => {
6477 const uploadMock = vi . fn ( )
6578 const client = {
79+ createDirectory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
6680 exists : vi . fn ( ) ,
6781 }
6882
6983 beforeEach ( ( ) => {
7084 getDavClient . mockReturnValue ( client )
7185 getUploader . mockReturnValue ( { upload : uploadMock } )
7286 console . error = vi . fn ( )
87+ // Default: ONE_TO_ONE room — no conversation folder used
88+ conversationGetter . mockReturnValue ( { type : 1 , displayName : 'Direct message' } )
7389 } )
7490
7591 afterEach ( ( ) => {
@@ -369,6 +385,109 @@ describe('fileUploadStore', () => {
369385 expect ( uploadStore . currentUploadId ) . not . toBeDefined ( )
370386 } )
371387
388+ describe ( 'conversation folder (group/public rooms)' , ( ) => {
389+ const TOKEN = 'XXTOKENXX'
390+ const DRAFT_PATH = 'Talk/My Room-XXTOKENXX/Draft'
391+
392+ beforeEach ( ( ) => {
393+ uploadMock . mockResolvedValue ( )
394+ postAttachment . mockResolvedValue ( )
395+ probeAttachmentFolder . mockResolvedValue ( { folder : DRAFT_PATH , renames : [ ] } )
396+ } )
397+
398+ test ( 'probes the attachment folder and posts via postAttachment for a group room' , async ( ) => {
399+ conversationGetter . mockReturnValue ( { type : 2 , displayName : 'My Room' } )
400+
401+ const file = {
402+ name : 'pngimage.png' ,
403+ type : 'image/png' ,
404+ size : 123 ,
405+ lastModified : Date . UTC ( 2021 , 3 , 27 , 15 , 30 , 0 ) ,
406+ }
407+
408+ uploadStore . initialiseUpload ( { uploadId : 'upload-id1' , token : TOKEN , files : [ file ] } )
409+
410+ await uploadStore . uploadFiles ( { token : TOKEN , uploadId : 'upload-id1' , options : { silent : false } } )
411+
412+ // Probe endpoint is called with the desired file names;
413+ // folder creation and the TYPE_ROOM share happen server-side.
414+ expect ( probeAttachmentFolder ) . toHaveBeenCalledTimes ( 1 )
415+ expect ( probeAttachmentFolder ) . toHaveBeenCalledWith ( {
416+ token : TOKEN ,
417+ fileNames : [ file . name ] ,
418+ } )
419+
420+ // No client-side MKCOL and no PROPFIND round-trip.
421+ expect ( client . createDirectory ) . not . toHaveBeenCalled ( )
422+ expect ( findUniquePath ) . not . toHaveBeenCalled ( )
423+
424+ // File is uploaded under a temp name inside the Draft folder.
425+ expect ( uploadMock ) . toHaveBeenCalledTimes ( 1 )
426+ const uploadedPath = uploadMock . mock . calls [ 0 ] [ 0 ]
427+ expect ( uploadedPath ) . toMatch ( new RegExp ( '^/' + DRAFT_PATH + '/upload-id1-.*-' + file . name + '$' ) )
428+
429+ // File is posted via the Talk attachment endpoint with the
430+ // original name for rename-on-conflict on the backend.
431+ expect ( postAttachment ) . toHaveBeenCalledTimes ( 1 )
432+ expect ( postAttachment ) . toHaveBeenCalledWith ( expect . objectContaining ( {
433+ token : TOKEN ,
434+ fileName : file . name ,
435+ filePath : expect . stringMatching ( new RegExp ( '^' + DRAFT_PATH + '/upload-id1-.*-' + file . name + '$' ) ) ,
436+ } ) )
437+ expect ( shareFile ) . not . toHaveBeenCalled ( )
438+ } )
439+
440+ test ( 'probes with all file names and posts each file in a multi-file upload' , async ( ) => {
441+ conversationGetter . mockReturnValue ( { type : 2 , displayName : 'Room' } )
442+
443+ const file1 = { name : 'photo.jpg' , type : 'image/jpeg' , size : 100 , lastModified : 0 }
444+ const file2 = { name : 'doc.pdf' , type : 'application/pdf' , size : 200 , lastModified : 0 }
445+
446+ uploadStore . initialiseUpload ( { uploadId : 'upload-id1' , token : TOKEN , files : [ file1 , file2 ] } )
447+
448+ await uploadStore . uploadFiles ( { token : TOKEN , uploadId : 'upload-id1' , options : null } )
449+
450+ expect ( probeAttachmentFolder ) . toHaveBeenCalledWith ( {
451+ token : TOKEN ,
452+ fileNames : [ 'photo.jpg' , 'doc.pdf' ] ,
453+ } )
454+ expect ( findUniquePath ) . not . toHaveBeenCalled ( )
455+ expect ( postAttachment ) . toHaveBeenCalledTimes ( 2 )
456+ expect ( postAttachment ) . toHaveBeenCalledWith ( expect . objectContaining ( { fileName : 'photo.jpg' } ) )
457+ expect ( postAttachment ) . toHaveBeenCalledWith ( expect . objectContaining ( { fileName : 'doc.pdf' } ) )
458+ } )
459+
460+ test ( 'falls back to shareFile when the probe endpoint fails' , async ( ) => {
461+ conversationGetter . mockReturnValue ( { type : 2 , displayName : 'My Room' } )
462+ probeAttachmentFolder . mockRejectedValueOnce ( new Error ( 'boom' ) )
463+ findUniquePath . mockResolvedValueOnce ( { path : '/Talk/photo.jpg' , name : 'photo.jpg' } )
464+
465+ const file = { name : 'photo.jpg' , type : 'image/jpeg' , size : 100 , lastModified : 0 }
466+ uploadStore . initialiseUpload ( { uploadId : 'upload-id1' , token : TOKEN , files : [ file ] } )
467+
468+ await uploadStore . uploadFiles ( { token : TOKEN , uploadId : 'upload-id1' , options : null } )
469+
470+ expect ( probeAttachmentFolder ) . toHaveBeenCalledTimes ( 1 )
471+ expect ( postAttachment ) . not . toHaveBeenCalled ( )
472+ expect ( shareFile ) . toHaveBeenCalledTimes ( 1 )
473+ } )
474+
475+ test ( 'falls back to shareFile when conversation-subfolders capability is false' , async ( ) => {
476+ getTalkConfig . mockReturnValueOnce ( false )
477+ conversationGetter . mockReturnValue ( { type : 2 , displayName : 'My Room' } )
478+ findUniquePath . mockResolvedValueOnce ( { path : '/Talk/photo.jpg' , name : 'photo.jpg' } )
479+
480+ const file = { name : 'photo.jpg' , type : 'image/jpeg' , size : 100 , lastModified : 0 }
481+ uploadStore . initialiseUpload ( { uploadId : 'upload-id1' , token : TOKEN , files : [ file ] } )
482+
483+ await uploadStore . uploadFiles ( { token : TOKEN , uploadId : 'upload-id1' , options : null } )
484+
485+ expect ( probeAttachmentFolder ) . not . toHaveBeenCalled ( )
486+ expect ( postAttachment ) . not . toHaveBeenCalled ( )
487+ expect ( shareFile ) . toHaveBeenCalledTimes ( 1 )
488+ } )
489+ } )
490+
372491 test ( 'autorenames files using timestamps when requested' , ( ) => {
373492 const files = [
374493 {
0 commit comments