20029: Refactor replaceFiles operations
[arvados-workbench2.git] / src / services / collection-service / collection-service.test.ts
index 061a45ec0111cfedb84405a699abe87017fbe404..0547a8f292d4679a7c022924ed58048cb7bdd238 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { AxiosInstance } from 'axios';
-import { WebDAV } from 'common/webdav';
-import { ApiActions } from '../api/api-actions';
+import axios, { AxiosInstance } from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { snakeCase } from 'lodash';
+import { CollectionResource, defaultCollectionSelectedFields } from 'models/collection';
 import { AuthService } from '../auth-service/auth-service';
-import { CollectionService } from './collection-service';
+import { CollectionService, emptyCollectionUuid } from './collection-service';
 
 describe('collection-service', () => {
     let collectionService: CollectionService;
-    let serverApi;
+    let serverApi: AxiosInstance;
+    let axiosMock: MockAdapter;
     let webdavClient: any;
     let authService;
     let actions;
 
     beforeEach(() => {
-        serverApi = {} as AxiosInstance;
+        serverApi = axios.create();
+        axiosMock = new MockAdapter(serverApi);
         webdavClient = {
             delete: jest.fn(),
+            upload: jest.fn(),
+            mkdir: jest.fn(),
         } as any;
         authService = {} as AuthService;
-        actions = {} as ApiActions;
+        actions = {
+            progressFn: jest.fn(),
+            errorFn: jest.fn(),
+        } as any;
         collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
         collectionService.update = jest.fn();
     });
 
+    describe('get', () => {
+        it('should make a request with default selected fields', async () => {
+            serverApi.get = jest.fn(() => Promise.resolve(
+                { data: { items: [{}] } }
+            ));
+            const uuid = 'zzzzz-4zz18-0123456789abcde'
+            await collectionService.get(uuid);
+            expect(serverApi.get).toHaveBeenCalledWith(
+                `/collections/${uuid}`, {
+                    params: {
+                        select: JSON.stringify(defaultCollectionSelectedFields.map(snakeCase)),
+                    },
+                }
+            );
+        });
+
+        it('should be able to request specific fields', async () => {
+            serverApi.get = jest.fn(() => Promise.resolve(
+                { data: { items: [{}] } }
+            ));
+            const uuid = 'zzzzz-4zz18-0123456789abcde'
+            await collectionService.get(uuid, undefined, ['manifestText']);
+            expect(serverApi.get).toHaveBeenCalledWith(
+                `/collections/${uuid}`, {
+                    params: {
+                        select: `["manifest_text"]`
+                    },
+                }
+            );
+        });
+    });
+
+    describe('update', () => {
+        it('should call put selecting updated fields + others', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const data: Partial<CollectionResource> = {
+                name: 'foo',
+            };
+            const expected = {
+                collection: {
+                    ...data,
+                    preserve_version: true,
+                },
+                select: ['uuid', 'name', 'version', 'modified_at'],
+            }
+            collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
+            await collectionService.update('uuid', data);
+            expect(serverApi.put).toHaveBeenCalledWith('/collections/uuid', expected);
+        });
+    });
+
+    describe('uploadFiles', () => {
+        it('should skip if no files to upload files', async () => {
+            // given
+            const files: File[] = [];
+            const collectionUUID = '';
+
+            // when
+            await collectionService.uploadFiles(collectionUUID, files);
+
+            // then
+            expect(webdavClient.upload).not.toHaveBeenCalled();
+        });
+
+        it('should upload files', async () => {
+            // given
+            const files: File[] = [{name: 'test-file1'} as File];
+            const collectionUUID = 'zzzzz-4zz18-0123456789abcde';
+
+            // when
+            await collectionService.uploadFiles(collectionUUID, files);
+
+            // then
+            expect(webdavClient.upload).toHaveBeenCalledTimes(1);
+            expect(webdavClient.upload.mock.calls[0][0]).toEqual("c=zzzzz-4zz18-0123456789abcde/test-file1");
+        });
+
+        it('should upload files with custom uplaod target', async () => {
+            // given
+            const files: File[] = [{name: 'test-file1'} as File];
+            const collectionUUID = 'zzzzz-4zz18-0123456789abcde';
+            const customTarget = 'zzzzz-4zz18-0123456789adddd/test-path/'
+
+            // when
+            await collectionService.uploadFiles(collectionUUID, files, undefined, customTarget);
+
+            // then
+            expect(webdavClient.upload).toHaveBeenCalledTimes(1);
+            expect(webdavClient.upload.mock.calls[0][0]).toEqual("c=zzzzz-4zz18-0123456789adddd/test-path/test-file1");
+        });
+    });
+
     describe('deleteFiles', () => {
         it('should remove no files', async () => {
             // given
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
             const filePaths: string[] = [];
-            const collectionUUID = '';
+            const collectionUUID = 'zzzzz-tpzed-5o5tg0l9a57gxxx';
 
             // when
             await collectionService.deleteFiles(collectionUUID, filePaths);
 
             // then
-            expect(webdavClient.delete).not.toHaveBeenCalled();
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${collectionUUID}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {},
+                }
+            );
         });
 
         it('should remove only root files', async () => {
             // given
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
             const filePaths: string[] = ['/root/1', '/root/1/100', '/root/1/100/test.txt', '/root/2', '/root/2/200', '/root/3/300/test.txt'];
-            const collectionUUID = '';
+            const collectionUUID = 'zzzzz-tpzed-5o5tg0l9a57gxxx';
 
             // when
             await collectionService.deleteFiles(collectionUUID, filePaths);
 
             // then
-            expect(webdavClient.delete).toHaveBeenCalledTimes(3);
-            expect(webdavClient.delete).toHaveBeenCalledWith("c=/root/3/300/test.txt");
-            expect(webdavClient.delete).toHaveBeenCalledWith("c=/root/2");
-            expect(webdavClient.delete).toHaveBeenCalledWith("c=/root/1");
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${collectionUUID}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        '/root/3/300/test.txt': '',
+                        '/root/2': '',
+                        '/root/1': '',
+                    },
+                }
+            );
         });
 
         it('should remove files with uuid prefix', async () => {
             // given
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
             const filePaths: string[] = ['/root/1'];
             const collectionUUID = 'zzzzz-tpzed-5o5tg0l9a57gxxx';
 
@@ -63,8 +183,295 @@ describe('collection-service', () => {
             await collectionService.deleteFiles(collectionUUID, filePaths);
 
             // then
-            expect(webdavClient.delete).toHaveBeenCalledTimes(1);
-            expect(webdavClient.delete).toHaveBeenCalledWith("c=zzzzz-tpzed-5o5tg0l9a57gxxx/root/1");
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${collectionUUID}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        '/root/1': '',
+                    },
+                }
+            );
+        });
+
+        it('should batch remove files', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            // given
+            const filePaths: string[] = ['/root/1', '/secondFile', 'barefile.txt'];
+            const collectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+
+            // when
+            await collectionService.deleteFiles(collectionUUID, filePaths);
+
+            // then
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${collectionUUID}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        '/root/1': '',
+                        '/secondFile': '',
+                        '/barefile.txt': '',
+                    },
+                }
+            );
+        });
+    });
+
+    describe('renameFile', () => {
+        it('should rename file', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const collectionUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+            const collectionPdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+            const oldPath = '/old/path';
+            const newPath = '/new/filename';
+
+            await collectionService.renameFile(collectionUuid, collectionPdh, oldPath, newPath);
+
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${collectionUuid}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        [newPath]: `${collectionPdh}${oldPath}`,
+                    },
+                }
+            );
+        });
+    });
+
+    describe('copyFiles', () => {
+        it('should batch copy files', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const filePaths: string[] = ['/root/1', '/secondFile', 'barefile.txt'];
+            const sourcePdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+            const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+            const destinationPath = '/destinationPath';
+
+            // when
+            await collectionService.copyFiles(sourcePdh, filePaths, destinationUuid, destinationPath);
+
+            // then
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${destinationUuid}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        [`${destinationPath}/1`]: `${sourcePdh}/root/1`,
+                        [`${destinationPath}/secondFile`]: `${sourcePdh}/secondFile`,
+                        [`${destinationPath}/barefile.txt`]: `${sourcePdh}/barefile.txt`,
+                    },
+                }
+            );
+        });
+
+        it('should copy files from rooth', async () => {
+            // Test copying from root paths
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const filePaths: string[] = ['/'];
+            const sourcePdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+            const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+            const destinationPath = '/destinationPath';
+
+            await collectionService.copyFiles(sourcePdh, filePaths, destinationUuid, destinationPath);
+
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${destinationUuid}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        [`${destinationPath}`]: `${sourcePdh}/`,
+                    },
+                }
+            );
+        });
+
+        it('should copy files to root path', async () => {
+            // Test copying to root paths
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const filePaths: string[] = ['/'];
+            const sourcePdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+            const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+            const destinationPath = '/';
+
+            await collectionService.copyFiles(sourcePdh, filePaths, destinationUuid, destinationPath);
+
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${destinationUuid}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        "/": `${sourcePdh}/`,
+                    },
+                }
+            );
         });
     });
-});
\ No newline at end of file
+
+    describe('moveFiles', () => {
+        it('should batch move files', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            // given
+            const filePaths: string[] = ['/rootFile', '/secondFile', '/subpath/subfile', 'barefile.txt'];
+            const srcCollectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+            const srcCollectionPdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+            const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+            const destinationPath = '/destinationPath';
+
+            // when
+            await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, destinationUuid, destinationPath);
+
+            // then
+            expect(serverApi.put).toHaveBeenCalledTimes(2);
+            // Verify copy
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${destinationUuid}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        [`${destinationPath}/rootFile`]: `${srcCollectionPdh}/rootFile`,
+                        [`${destinationPath}/secondFile`]: `${srcCollectionPdh}/secondFile`,
+                        [`${destinationPath}/subfile`]: `${srcCollectionPdh}/subpath/subfile`,
+                        [`${destinationPath}/barefile.txt`]: `${srcCollectionPdh}/barefile.txt`,
+                    },
+                }
+            );
+            // Verify delete
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${srcCollectionUUID}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        "/rootFile": "",
+                        "/secondFile": "",
+                        "/subpath/subfile": "",
+                        "/barefile.txt": "",
+                    },
+                }
+            );
+        });
+
+        it('should batch move files within collection', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            // given
+            const filePaths: string[] = ['/one', '/two', '/subpath/subfile', 'barefile.txt'];
+            const srcCollectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+            const srcCollectionPdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+            const destinationPath = '/destinationPath';
+
+            // when
+            await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, srcCollectionUUID, destinationPath);
+
+            // then
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            // Verify copy
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${srcCollectionUUID}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        [`${destinationPath}/one`]: `${srcCollectionPdh}/one`,
+                        ['/one']: '',
+                        [`${destinationPath}/two`]: `${srcCollectionPdh}/two`,
+                        ['/two']: '',
+                        [`${destinationPath}/subfile`]: `${srcCollectionPdh}/subpath/subfile`,
+                        ['/subpath/subfile']: '',
+                        [`${destinationPath}/barefile.txt`]: `${srcCollectionPdh}/barefile.txt`,
+                        ['/barefile.txt']: '',
+                    },
+                }
+            );
+        });
+
+        it('should abort batch move when copy fails', async () => {
+            // Simulate failure to copy
+            serverApi.put = jest.fn(() => Promise.reject({
+                data: {},
+                response: {
+                    "errors": ["error getting snapshot of \"rootFile\" from \"8cd9ce1dfa21c635b620b1bfee7aaa08+180\": file does not exist"]
+                }
+            }));
+            // given
+            const filePaths: string[] = ['/rootFile', '/secondFile', '/subpath/subfile', 'barefile.txt'];
+            const srcCollectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+            const srcCollectionPdh = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+            const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+            const destinationPath = '/destinationPath';
+
+            // when
+            try {
+                await collectionService.moveFiles(srcCollectionUUID, srcCollectionPdh, filePaths, destinationUuid, destinationPath);
+            } catch {}
+
+            // then
+            expect(serverApi.put).toHaveBeenCalledTimes(1);
+            // Verify copy
+            expect(serverApi.put).toHaveBeenCalledWith(
+                `/collections/${destinationUuid}`, {
+                    collection: {
+                        preserve_version: true
+                    },
+                    replace_files: {
+                        [`${destinationPath}/rootFile`]: `${srcCollectionPdh}/rootFile`,
+                        [`${destinationPath}/secondFile`]: `${srcCollectionPdh}/secondFile`,
+                        [`${destinationPath}/subfile`]: `${srcCollectionPdh}/subpath/subfile`,
+                        [`${destinationPath}/barefile.txt`]: `${srcCollectionPdh}/barefile.txt`,
+                    },
+                }
+            );
+        });
+    });
+
+    describe('createDirectory', () => {
+        it('creates empty directory', async () => {
+            // given
+            const directoryNames = [
+                {in: 'newDir', out: 'newDir'},
+                {in: '/fooDir', out: 'fooDir'},
+                {in: '/anotherPath/', out: 'anotherPath'},
+                {in: 'trailingSlash/', out: 'trailingSlash'},
+            ];
+            const collectionUuid = 'zzzzz-tpzed-5o5tg0l9a57gxxx';
+
+            for (var i = 0; i < directoryNames.length; i++) {
+                serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+                // when
+                await collectionService.createDirectory(collectionUuid, directoryNames[i].in);
+                // then
+                expect(serverApi.put).toHaveBeenCalledTimes(1);
+                expect(serverApi.put).toHaveBeenCalledWith(
+                    `/collections/${collectionUuid}`, {
+                        collection: {
+                            preserve_version: true
+                        },
+                        replace_files: {
+                            ["/" + directoryNames[i].out]: emptyCollectionUuid,
+                        },
+                    }
+                );
+            }
+        });
+    });
+
+});