Repository: warpdesign/react-explorer
Pull request #131 (2020-04-21 13:52:12)

Update fs local tests


Patch:
diff --git a/package-lock.json b/package-lock.json
index 2d008a3..fcdec8c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -939,6 +939,15 @@
         "@types/node": "*"
       }
     },
+    "@types/mock-fs": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.10.0.tgz",
+      "integrity": "sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/node": {
       "version": "10.11.3",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.3.tgz",
@@ -8987,6 +8996,11 @@
       "resolved": "https://registry.npmjs.org/mobx-react-devtools/-/mobx-react-devtools-4.2.15.tgz",
       "integrity": "sha1-iBwDj7g9tN/9HnK7r1N00msv3rs="
     },
+    "mock-fs": {
+      "version": "github:warpdesign/mock-fs#1d13c92cfea7bd68c8a273d3aeeb0752c06def46",
+      "from": "github:warpdesign/mock-fs",
+      "dev": true
+    },
     "module-details-from-path": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
diff --git a/package.json b/package.json
index 412497d..3004ba5 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
         "@types/hoist-non-react-statics": "^3.3.1",
         "@types/i18next": "^12.1.0",
         "@types/jest": "^24.0.18",
+        "@types/mock-fs": "^4.10.0",
         "@types/react-i18next": "^8.1.0",
         "@types/react-virtualized": "^9.21.4",
         "@types/webpack-env": "^1.14.0",
@@ -76,6 +77,7 @@
         "husky": "^3.0.9",
         "jest": "^24.9.0",
         "local-web-server": "^3.0.7",
+        "mock-fs": "github:warpdesign/mock-fs",
         "native-ext-loader": "^2.3.0",
         "pm2": "^4.1.2",
         "source-map-loader": "^0.2.4",
diff --git a/src/services/Fs.ts b/src/services/Fs.ts
index b6be5c5..09e3135 100644
--- a/src/services/Fs.ts
+++ b/src/services/Fs.ts
@@ -70,7 +70,7 @@ export interface Fs {
 }
 
 export const Extensions: { [index: string]: RegExp } = {
-    'exe': /\.([\.]*(exe|bat|com|msi|mui|cmd))+$/,
+    'exe': /\.([\.]*(exe|bat|com|msi|mui|cmd|sh))+$/,
     'img': /\.([\.]*(png|jpeg|jpg|gif|pcx|tiff|raw|webp|svg|heif|bmp|ilbm|iff|lbm|ppm|pgw|pbm|pnm|psd))+$/,
     'arc': /\.([\.]*(zip|tar|rar|7zip|7z|dmg|shar|ar|bz2|lz|gz|tgz|lha|lzh|lzx|sz|xz|z|s7z|ace|apk|arp|arj|cab|car|cfs|cso|dar|iso|ice|jar|pak|sea|sfx|sit|sitx|lzma|war|xar|zoo|zipx|img|adf|dms|dmz))+$/,
     'snd': /\.([\.]*(mp3|wav|mp2|ogg|aac|aiff|mod|flac|m4a|mpc|oga|opus|ra|rm|vox|wma|8svx))+$/,
@@ -78,9 +78,9 @@ export const Extensions: { [index: string]: RegExp } = {
     'cod': /\.([\.]*(json|js|cpp|c|cxx|java|rb|s|tsx|ts|jsx|lua|as|coffee|ps1|py|r|rexx|spt|sptd|go|rs|sh|bash|vbs|cljs))+$/,
     'doc': /\.([\.]*(log|last|css|htm|html|rtf|doc|pdf|docx|txt|md|1st|asc|epub|xhtml|xml|amigaguide|info))+$/
 };
-const ExeMaskAll = 0o0001;
-const ExeMaskGroup = 0o0010;
-const ExeMaskUser = 0o0100;
+export const ExeMaskAll = 0o0001;
+export const ExeMaskGroup = 0o0010;
+export const ExeMaskUser = 0o0100;
 
 export type FileType = 'exe' | 'img' | 'arc' | 'snd' | 'vid' | 'doc' | 'cod' | '';
 
@@ -92,14 +92,14 @@ export function MakeId(stats: any): FileID {
 }
 
 function isModeExe(mode: number, gid: number, uid: number): Boolean {
-    if (isWin) {
+    if (isWin || mode === -1) {
         return false;
     }
 
     const isGroup = gid ? process.getgid && gid === process.getgid() : false;
     const isUser = uid ? process.getuid && uid === process.getuid() : false;
 
-    return !!((mode !== -1 && (mode & ExeMaskAll)) || ((mode & ExeMaskUser) && isGroup) || ((mode & ExeMaskGroup) && isUser));
+    return !!((mode & ExeMaskAll) || ((mode & ExeMaskUser) && isGroup) || ((mode & ExeMaskGroup) && isUser));
 }
 
 export function filetype(mode: number, gid: number, uid: number, extension: string): FileType {
diff --git a/src/services/plugins/FsLocal.ts b/src/services/plugins/FsLocal.ts
index d93f751..7a76c93 100644
--- a/src/services/plugins/FsLocal.ts
+++ b/src/services/plugins/FsLocal.ts
@@ -87,8 +87,6 @@ export class LocalApi implements FsApi {
         return new Promise((resolve, reject) => {
             const unixPath = path.join(source, dirName).replace(/\\/g, "/");
             try {
-                // console.log('mkdir', unixPath);
-
                 mkdir(unixPath, (err: any) => {
                     if (err) {
                         reject(err);
@@ -113,7 +111,7 @@ export class LocalApi implements FsApi {
                 });
                 resolve(deleted.length);
             } catch (err) {
-                console.log("error delete", err);
+                // console.log("error delete", err);
                 reject(err);
             }
         });
@@ -129,7 +127,6 @@ export class LocalApi implements FsApi {
         const newPath = path.join(source, newName);
 
         if (!newName.match(invalidFileChars)) {
-            console.log("valid !", oldPath, newPath);
             return new Promise((resolve, reject) => {
                 // since node's fs.rename will overwrite the destination
                 // path if it exists, first check that file doesn't exist
@@ -153,7 +150,15 @@ export class LocalApi implements FsApi {
                             }
                         });
                     }
-                });
+                })
+                .catch((err) => {
+                    reject({
+                        code: err.code,
+                        message: err.message,
+                        newName: newName,
+                        oldName: file.fullname
+                    });
+                })
             });
         } else {
             // reject promise with previous name in case of invalid chars
@@ -166,16 +171,13 @@ export class LocalApi implements FsApi {
     }
 
     async makeSymlink(targetPath: string, path: string, transferId = -1): Promise<boolean> {
-        console.log('creating symlink', targetPath, path);
         return new Promise<boolean>((resolve, reject) => fs.symlink(targetPath, path, (err) => {
             if (err) {
-                console.log('error creating symlink', err);
                 reject(err);
             } else {
-                console.log('link created');
                 resolve(true);
             }
-        }))
+        }));
     }
 
     async isDir(path: string, transferId = -1): Promise<boolean> {
@@ -229,7 +231,7 @@ export class LocalApi implements FsApi {
 
             return file;
         } catch (err) {
-            return err;
+            throw err;
         }
     }
 
@@ -248,33 +250,35 @@ export class LocalApi implements FsApi {
     }
 
     async list(dir: string, transferId = -1): Promise<File[]> {
-        const pathExists = await this.isDir(dir);
-
-        if (pathExists) {
+        try {
+            await this.isDir(dir);
             return new Promise<File[]>((resolve, reject) => {
                 fs.readdir(dir, (err, items) => {
                     if (err) {
                         reject(err);
                     } else {
                         const dirPath = path.resolve(dir);
-
+    
                         const files: File[] = [];
-
+    
                         for (var i = 0; i < items.length; i++) {
                             const file = LocalApi.fileFromPath(
                                 path.join(dirPath, items[i])
                             );
                             files.push(file);
                         }
-
+    
                         this.onList(dirPath);
-
+    
                         resolve(files);
                     }
                 });
             });
-        } else {
-            return Promise.reject("Path does not exist");
+        } catch(err) {
+            throw({
+                code: err.code,
+                message: `Could not access path: ${dir}`
+            });
         }
     }
 
@@ -351,8 +355,8 @@ export class LocalApi implements FsApi {
     }
 
     off() {
-        console.log("off", this.path);
-        console.log("stopWatchingPath", this.path);
+        // console.log("off", this.path);
+        // console.log("stopWatchingPath", this.path);
         LocalWatch.stopWatchingPath(this.path, this.onFsChange);
     }
 


Test patch:
diff --git a/e2e/cypress/integration/filetable.spec.ts b/e2e/cypress/integration/filetable.spec.ts
index a3d3c1f..16e7f85 100644
--- a/e2e/cypress/integration/filetable.spec.ts
+++ b/e2e/cypress/integration/filetable.spec.ts
@@ -480,6 +480,7 @@ describe("filetable", () => {
                 .trigger("keydown", { keyCode: KEYS.Enter })
                 .find("[data-cy-file]:first")
                 .find(".file-label")
+                .focus()
                 .type("bar")
                 .blur()
                 .should("contain", "folder2")
diff --git a/src/services/__tests__/Fs.test.ts b/src/services/__tests__/Fs.test.ts
new file mode 100644
index 0000000..9b9850a
--- /dev/null
+++ b/src/services/__tests__/Fs.test.ts
@@ -0,0 +1,72 @@
+import {
+    describeUnix,
+} from "../../utils/test/helpers";
+
+import { MakeId, ExeMaskAll, ExeMaskGroup, ExeMaskUser, filetype } from '../Fs';
+
+describe('makeId', () => {
+    it('should return FileID from stats', () => {
+        const stats = {
+            ino: 123,
+            dev: 456,
+            fullname: 'foo'
+        };
+
+        expect(MakeId(stats)).toEqual({
+            ino: stats.ino,
+            dev: stats.dev
+        });
+    });
+});
+
+describeUnix('filetype mode detection', () => {
+    it('should return exe if user is owner of +x file', () => {
+        const gid = process.getgid();
+        const uid = process.getuid();
+        const mode = ExeMaskUser;
+
+        const type = filetype(mode, gid, uid, '');
+        expect(type).toBe('exe');
+    });
+
+    it('should return exe if user is in group of +x file', () => {
+        const gid = process.getgid();
+        const uid = process.getuid();
+        const mode = ExeMaskGroup;
+
+        const type = filetype(mode, gid, uid, '');
+        expect(type).toBe('exe');
+    });
+
+    it('should return exe file is +x for all', () => {
+        const mode = ExeMaskAll;
+
+        const type = filetype(mode, 0, 0, '');
+        expect(type).toBe('exe');
+    });
+
+    it('should not return exe if mode === -1', () => {
+        const gid = process.getgid();
+        const uid = process.getuid();
+        const type = filetype(-1, gid, uid, '');
+
+        expect(type).not.toEqual('exe');
+    });
+});
+
+describe('filetype extension detection', () => {
+    it('should return exe if extensions === ".bar.exe"', () => {
+        const extension = '.bar.exe';
+        expect(filetype(-1, 0, 0, extension)).toBe('exe');
+    });
+
+    it('should return exe if extension === ".exe"', () => {
+        const extension = '.exe';
+        expect(filetype(-1, 0, 0, extension)).toBe('exe');
+    });
+
+    it('should return no filetype if extension === ""', () => {
+        const extension = '';
+        expect(filetype(-1, 0, 0, extension)).toBe('');
+    });
+});
diff --git a/src/services/plugins/__tests__/FsLocal.test.ts b/src/services/plugins/__tests__/FsLocal.test.ts
index 4bfcfe4..255a31f 100644
--- a/src/services/plugins/__tests__/FsLocal.test.ts
+++ b/src/services/plugins/__tests__/FsLocal.test.ts
@@ -1,219 +1,610 @@
 import { FsLocal } from "../FsLocal";
+import * as fs from 'fs';
 
 import {
     describeUnix,
+    describeWin,
     getPath,
-    prepareTmpTestFiles
 } from "../../../utils/test/helpers";
 
+import mock from 'mock-fs';
+import TmpDir from './fixtures/tmpDir'
+
 let localAPI: any;
 
 const HOMEDIR = getPath("home");
 const TESTS_DIR = getPath("tests_dir");
 
-describeUnix("isDirectoryNameValid: unix", function() {
-    beforeAll(() => {
-        localAPI = new FsLocal.API("/", () => {});
+// needed to avoid crashes when console.log
+// is called and mock() has been called
+// see #
+console.log('');
+
+describe('FsLocal', () => {
+    describeUnix("isDirectoryNameValid", function() {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+        });
+    
+        it("unix: dir name cannot start with  ../", function() {
+            const isValid = localAPI.isDirectoryNameValid("../");
+    
+            expect(isValid).toBe(false);
+        });
+    
+        it("unix: dir name cannot start with  ./", function() {
+            const isValid = localAPI.isDirectoryNameValid("./");
+    
+            expect(isValid).toBe(false);
+        });
+    
+        it("unix: dir name cannot start with  ../.", function() {
+            const isValid = localAPI.isDirectoryNameValid("../.");
+    
+            expect(isValid).toBe(false);
+        });
     });
-
-    it("unix: dir name cannot start with  ../", function() {
-        const isValid = localAPI.isDirectoryNameValid("../");
-
-        expect(isValid).toBe(false);
+    
+    // @todo add Windows specific tests for isDirectoryNameValid
+    describe("resolve", function() {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+        });
+    
+        it("~ resolves to homedir", function() {
+            const dir = localAPI.resolve("~");
+    
+            expect(dir).toBe(HOMEDIR);
+        });
     });
-
-    it("unix: dir name cannot start with  ./", function() {
-        const isValid = localAPI.isDirectoryNameValid("./");
-
-        expect(isValid).toBe(false);
+    
+    describe("cd", function() {
+        const filePath = `${TESTS_DIR}/file`
+
+        beforeAll(() => {
+            mock(TmpDir);
+            localAPI = new FsLocal.API("/", () => {});
+        });
+
+        afterAll(() => mock.restore());
+    
+        it("should throw ENOTDIR if is not a directory", async function() {
+            expect.assertions(1);
+    
+            try {
+                await localAPI.cd(filePath);
+            } catch (err) {
+                expect(err.code).toBe("ENOTDIR");
+            }
+        });
+    
+        it("should throw if does not exist", async function() {
+            expect.assertions(1);
+    
+            try {
+                await localAPI.cd(`${filePath}__`);
+            } catch (err) {
+                expect(err.code).toBe("ENOENT");
+            }
+        });
+    
+        it("should return resolved path if exists and is a dir", function() {
+            const dir = localAPI.resolve(TESTS_DIR);
+    
+            expect(dir).toBe(TESTS_DIR);
+        });
+    
+        it("should return resolved homedir path for ~", function() {
+            expect.assertions(1);
+    
+            const dir = localAPI.resolve("~");
+    
+            expect(dir).toBe(HOMEDIR);
+        });
     });
-
-    it("unix: dir name cannot start with  ../.", function() {
-        const isValid = localAPI.isDirectoryNameValid("../.");
-
-        expect(isValid).toBe(false);
+    
+    describe("size", function() {
+        const SIZE_PATH = `${TESTS_DIR}/sizeTest`;
+    
+        beforeAll(() => {
+            mock(TmpDir);
+            localAPI = new FsLocal.API("/", () => {});
+        });
+
+        afterAll(() => mock.restore());
+    
+        it("should return 0 if no files", async () => {
+            expect.assertions(1);
+    
+            const size = await localAPI.size(__dirname, []);
+            expect(size).toBe(0);
+        });
+    
+        it("should return correct size for 14 bytes file", async () => {
+            expect.assertions(1);
+    
+            const size = await localAPI.size(SIZE_PATH, ["14bytes"]);
+            expect(size).toBe(14);
+        });
+    
+        it("should return correct size for several 14 + 1024 files", async () => {
+            expect.assertions(1);
+    
+            const size = await localAPI.size(SIZE_PATH, ["14bytes", "1024bytes"]);
+            expect(size).toBe(1038);
+        });
+    
+        it("should throw ENOENT if file does not exist", async () => {
+            expect.assertions(1);
+    
+            try {
+                await localAPI.size(SIZE_PATH, ["15bytes"]);
+            } catch (err) {
+                expect(err.code).toBe("ENOENT");
+            }
+        });
     });
-});
-
-// @todo add Windows specific tests for isDirectoryNameValid
-describe("resolve", function() {
-    beforeAll(() => {
-        localAPI = new FsLocal.API("/", () => {});
-    });
-
-    it("~ resolves to homedir", function() {
-        const dir = localAPI.resolve("~");
-
-        expect(dir).toBe(HOMEDIR);
-    });
-});
-
-describe("cd", function() {
-    beforeAll(() => {
-        localAPI = new FsLocal.API("/", () => {});
-    });
-
-    it("should throw if is not a directory", async function() {
-        expect.assertions(1);
-
-        try {
-            await localAPI.cd(__filename);
-        } catch (err) {
-            expect(err.code).toBe("ENOTDIR");
-        }
-    });
-
-    it("should throw if is does not exist", async function() {
-        expect.assertions(1);
-
-        try {
-            await localAPI.cd(__filename + "__");
-        } catch (err) {
-            expect(err.code).toBe("ENOENT");
-        }
-    });
-
-    it("should return resolved path if exists and is a dir", function() {
-        const dir = localAPI.resolve(__dirname);
-
-        expect(dir).toBe(__dirname);
-    });
-
-    it("should return resolved homedir path for ~", function() {
-        expect.assertions(1);
-
-        const dir = localAPI.resolve("~");
-
-        expect(dir).toBe(HOMEDIR);
+    
+    describe("makedir", function() {
+        const MAKEDIR_PATH = `${TESTS_DIR}/makedirTest`;
+    
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            mock(TmpDir);
+        });
+    
+        afterAll(() => mock.restore());
+
+        it("should return success if folder already exists", async () => {
+            expect.assertions(1);
+    
+            const resolved = await localAPI.makedir(MAKEDIR_PATH, "");
+            expect(resolved).toBe(MAKEDIR_PATH);
+        });
+
+        // won't work with mock-fs unfortunately
+        // it.only("should throw EACCES if source does not exist", async () => {
+        //     expect.assertions(1);
+
+        //     try {
+        //         await localAPI.makedir("/azerty", "foo");
+        //     } catch (err) {
+        //         // Catalina now has RO system FS
+        //         expect(err.code).toMatch(/EACCES|EROFS/);
+        //     }
+        // });
+    
+        it("should throw EACCES if source is not readable", async () => {
+            expect.assertions(1);
+    
+            try {
+                await localAPI.makedir(`${MAKEDIR_PATH}/denied`, "foo");
+            } catch (err) {
+                expect(err.code).toBe("EACCES");
+            }
+        });
+    
+        it("should create a single folder", async () => {
+            expect.assertions(1);
+    
+            const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir1");
+    
+            expect(resolved).toBe(`${MAKEDIR_PATH}/dir1`);
+        });
+    
+        it("should create several folders", async () => {
+            expect.assertions(1);
+    
+            const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir2/dir3/dir4");
+    
+            expect(resolved).toBe(`${MAKEDIR_PATH}/dir2/dir3/dir4`);
+        });
     });
-});
-
-describe("size", function() {
-    const SIZE_PATH = `${TESTS_DIR}/sizeTest`;
-
-    beforeAll(() => {
-        localAPI = new FsLocal.API("/", () => {});
-        return prepareTmpTestFiles();
+    
+    describe("makeSymLink", () => {
+        const filePath = `${TESTS_DIR}/file`
+
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            mock(TmpDir);
+        });
+
+        afterAll(() => mock.restore());
+
+        it('should create valid softlink', async () => {
+            expect.assertions(1);
+
+            const res = await localAPI.makeSymlink(filePath, '/link1');
+            expect(res).toBe(true);
+        });
+
+        it('should create non valid softlink', async () => {
+            expect.assertions(1);
+
+            const res = await localAPI.makeSymlink('/not_here', '/link2');
+            expect(res).toBe(true);
+        });
+
+        it('should throw EENOENT if link could not be created because link path does not exist', async () => {
+            expect.assertions(1);
+
+            try {
+                await localAPI.makeSymlink(filePath, '/non/valid');
+            } catch(err) {
+                expect(err.code).toBe('ENOENT');
+            }
+        });
+
+        it('should throw EACCES if access was denied when creating link', async () => {
+            expect.assertions(1);
+
+            try {
+                await localAPI.makeSymlink(filePath, `${TESTS_DIR}/makedirTest/denied/link`);
+            } catch(err) {
+                expect(err.code).toBe('EACCES');
+            }
+        });
+
+        it('should throw EEXIST if file with that name already exists', async () => {
+            expect.assertions(1);
+
+            try {
+                const res = await localAPI.makeSymlink(filePath, `${TESTS_DIR}/file`);
+            } catch(err) {
+                expect(err.code).toBe('EEXIST');
+            }
+        });
     });
 
-    it("should return 0 if no files", async () => {
-        expect.assertions(1);
-
-        const size = await localAPI.size(__dirname, []);
-        expect(size).toBe(0);
+    describe('rename', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            mock(TmpDir);
+        });
+
+        afterAll(() => mock.restore());
+        
+        it('should throw EEXIST if destination already exists', async () => {
+            expect.assertions(1);
+
+            try {
+                await localAPI.rename(`${TESTS_DIR}/sizeTest`, { fullname: '14bytes' }, '1024bytes');
+            } catch(err) {
+                expect(err.code).toBe('EEXIST');
+            }
+        });
+
+        it('should throw ENOENT if source does not exist', async () => {
+            expect.assertions(1);
+
+            try {
+                await localAPI.rename(`${TESTS_DIR}/sizeTest`, { fullname: 'nothing_here' }, 'new_file');
+            } catch(err) {
+                expect(err.code).toBe('ENOENT');
+            }
+        });
+
+        it('should throw EACCES if user cannot access destination folder', async () => {
+            expect.assertions(1);
+
+            try {
+                await localAPI.rename(`${TESTS_DIR}/deleteTest/folder_denied`, { fullname: 'file2' }, 'new_file');
+            } catch(err) {
+                expect(err.code).toBe('EACCES');
+            }
+        });
+
+        it('should allow renaming a protected file', async () => {
+            expect.assertions(1);
+
+            const res = await localAPI.rename(`${TESTS_DIR}/deleteTest`, { fullname: 'file_denied' }, 'new_file');
+            expect(res).toBe('new_file');
+        });
+
+        it('should rename a file', async () => {
+            expect.assertions(1);
+
+            const res = await localAPI.rename(`${TESTS_DIR}`, { fullname: 'file' }, 'new_file');
+            expect(res).toBe('new_file');
+        });
     });
 
-    it("should return correct size for 14 bytes file", async () => {
-        expect.assertions(1);
-
-        const size = await localAPI.size(SIZE_PATH, ["14bytes"]);
-        expect(size).toBe(14);
+    describe('isDir', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            mock(TmpDir);
+        });
+
+        afterAll(() => mock.restore());
+        
+        it('should return true if file is a directory', async () => {
+            expect.assertions(1);
+            const res = await localAPI.isDir(TESTS_DIR);
+            expect(res).toBe(true);
+        });
+
+        it('should return true if symlink is a directory', async () => {
+            expect.assertions(1);
+            const res = await localAPI.isDir(`${TESTS_DIR}/linkToSizeTest`);
+            expect(res).toBe(true);
+        });
+
+        it('should throw ENOENT if file cannot be accessed', async () => {
+            expect.assertions(1);
+
+            try {
+                const res = await localAPI.isDir(`${TESTS_DIR}/deleteTest/fileDenied`);
+                expect(res).toBe(false);
+            } catch (err) {
+                expect(err.code).toBe('ENOENT');
+            }
+        });
+
+        it('should throw ENOENT if file does not exist', async () => {
+            expect.assertions(1);
+            try {
+                await localAPI.isDir(`${TESTS_DIR}/not_here`);
+            } catch(err) {
+                expect(err.code).toBe('ENOENT');
+            }
+        });
+
+        it('should return false if file is a symlink pointing to a file', async () => {
+            expect.assertions(1);
+            const res = await localAPI.isDir(`${TESTS_DIR}/link`);
+            expect(res).toBe(false);
+        });
+
+        it('should return false if path is a file', async () => {
+            expect.assertions(1);
+            const res = await localAPI.isDir(`${TESTS_DIR}/file`);
+            expect(res).toBe(false);
+        });
     });
 
-    it("should return correct size for several 14 + 1024 files", async () => {
-        expect.assertions(1);
-
-        const size = await localAPI.size(SIZE_PATH, ["14bytes", "1024bytes"]);
-        expect(size).toBe(1038);
+    describe('exists', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            mock(TmpDir);
+        });
+
+        afterAll(() => mock.restore());
+
+        it('should resolve to true if file exists', async () => {
+            expect.assertions(1);
+            const res = await localAPI.exists(`${TESTS_DIR}/file`);
+            expect(res).toBe(true);
+        });
+
+        it('should resolve to false if file does not exists', async () => {
+            expect.assertions(1);
+            const res = await localAPI.exists(`${TESTS_DIR}/nothing`);
+            expect(res).toBe(false);
+        });
     });
 
-    it("should throw ENOENT if file does not exist", async () => {
-        expect.assertions(1);
-
-        try {
-            await localAPI.size(SIZE_PATH, ["15bytes"]);
-        } catch (err) {
-            expect(err.code).toBe("ENOENT");
-        }
+    describe('stat', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            mock(TmpDir);
+        });
+
+        afterAll(() => mock.restore());
+
+        // -   "atime": 2020-04-19T16:54:07.529Z,
+        // -   "atimeMs": 1587315247529,
+        // -   "birthtime": 2020-04-19T16:54:07.529Z,
+        // -   "birthtimeMs": 1587315247529,
+        // -   "blksize": 4096,
+        // -   "blocks": 0,
+        // -   "ctime": 2020-04-19T16:54:07.529Z,
+        // -   "ctimeMs": 1587315247529,
+        //     -   "gid": 1140567832,
+        // -   "mtime": 2020-04-19T16:54:07.529Z,
+        // -   "mtimeMs": 1587315247529,
+        // -   "nlink": 1,
+        // -   "rdev": 0,
+        // -   "size": 0,
+        // -   "uid": 138850138,
+
+        it('should return stat for existing file', async () => {
+            const path = `${TESTS_DIR}/file`;
+            expect.assertions(1);
+
+            const fsStat = fs.statSync(path);
+            const stat = await localAPI.stat(path);
+            expect(stat).toEqual({
+                dir: TESTS_DIR,
+                fullname: 'file',
+                name: 'file',
+                extension: '',
+                cDate: fsStat.ctime,
+                bDate: fsStat.ctime,
+                mDate: fsStat.mtime,
+                length: fsStat.size,
+                isDir: false,
+                isSym: false,
+                id: {
+                    dev: fsStat.dev,
+                    ino: fsStat.ino
+                },
+                mode: fsStat.mode,
+                readonly: false,
+                target: null,
+                type: ''
+            });
+        });
+
+        it('should return link target in target prop if file is a link', async () => {
+            const path = `${TESTS_DIR}/link`;
+            expect.assertions(1);
+
+            const stat = await localAPI.stat(path);
+            expect(stat.target).toBe('file');
+        });
+
+        it('should throw error if file does not exist', async () => {
+            const path = `${TESTS_DIR}/nothing_here`;
+            expect.assertions(1);
+
+            try {
+                await localAPI.stat(path);
+            } catch(err) {
+                expect(err.code).toBe('ENOENT');
+            }
+        });
     });
-});
 
-describe("makedir", function() {
-    const MAKEDIR_PATH = `${TESTS_DIR}/makedirTest`;
-
-    beforeAll(() => {
-        localAPI = new FsLocal.API("/", () => {});
-        return prepareTmpTestFiles();
+    describe('login', () => {
+        it('should resolve to true', async () => {
+            expect.assertions(1);
+            expect(localAPI.login()).resolves.toBe(undefined);
+        });
     });
 
-    it("should return success if folder already exists", async () => {
-        expect.assertions(1);
+    // TODO: cannot be tested with mock-fs for now
+    // describe('onList',() => {
+    //     it('should return ')
+    // });
 
-        const resolved = await localAPI.makedir(MAKEDIR_PATH, "");
-        expect(resolved).toBe(MAKEDIR_PATH);
+    describe('list', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+            jest.spyOn(localAPI, 'onList').mockImplementation(() => {});
+            mock(TmpDir);
+        });
+
+        afterEach(() => jest.clearAllMocks())
+
+        afterAll(() => {
+            mock.restore();
+        });
+
+        it('should throw ENOENT if path does not exist', async () => {
+            expect.assertions(1);
+            try {
+                await localAPI.list('/nothing');
+            } catch(err) {
+                expect(err.code).toBe('ENOENT');
+            }
+        });
+
+        // TODO: should have called onList(dir)
+        it('should return every files found in path if it exists', async () => {
+            expect.assertions(1);
+            
+            const files = await localAPI.list(TESTS_DIR);
+            
+            expect(files.length).toBe(6);
+        });
+
+        it('should throw ENOTDIR if path is not a folder', async () => {
+            expect.assertions(1);
+            try {
+                await localAPI.list(`${TESTS_DIR}/deleteTest/file`);
+            } catch(err) {
+                expect(err.code).toBe('ENOTDIR');
+            }
+        });
+
+        it('should throw EACCES if cannot access folder', async () => {
+            expect.assertions(1);
+            try {
+                await localAPI.list(`${TESTS_DIR}/deleteTest/folder_denied`);
+            } catch(err) {
+                expect(err.code).toBe('EACCES');
+            }
+        });
+
+        it('should call onList with path when listing a valid directory', async () => {
+            expect.assertions(2);
+            
+            await localAPI.list(TESTS_DIR);
+            
+            expect(localAPI.onList).toHaveBeenCalledTimes(1);
+            expect(localAPI.onList).toHaveBeenCalledWith(TESTS_DIR);
+        });
     });
 
-    it("should throw EACCES if source does not exist", async () => {
-        expect.assertions(1);
+    describeUnix('isRoot', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("/", () => {});
+        });
 
-        try {
-            await localAPI.makedir("/azerty", "foo");
-        } catch (err) {
-            // Catalina now has RO system FS
-            expect(err.code).toMatch(/EACCES|EROFS/);
-        }
-    });
+        it('should return false for empty path', () => {
+            expect(localAPI.isRoot('')).toBe(false);
+        });
 
-    it("should throw EACCES if source is not readable", async () => {
-        expect.assertions(1);
+        it('should return true is path !== "/"', () => {
+            expect(localAPI.isRoot('/foo')).toBe(false);
+        });
 
-        try {
-            await localAPI.makedir(`${MAKEDIR_PATH}/denied`, "foo");
-        } catch (err) {
-            expect(err.code).toBe("EACCES");
-        }
+        it('should return true is path === "/"', () => {
+            expect(localAPI.isRoot('/')).toBe(true);
+        });
     });
 
-    it("should create a single folder", async () => {
-        expect.assertions(1);
+    describeWin('isRoot', () => {
+        beforeAll(() => {
+            localAPI = new FsLocal.API("C:\\", () => {});
+        });
 
-        const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir1");
+        it('should return false for empty path', () => {
+            expect(localAPI.isRoot('')).toBe(false);
+        });
 
-        expect(resolved).toBe(`${MAKEDIR_PATH}/dir1`);
-    });
+        it('should return true is path !== "X:\\"', () => {
+            expect(localAPI.isRoot('C:\\Windows')).toBe(false);
+        });
 
-    it("should create several folders", async () => {
-        expect.assertions(1);
+        it('should return true is path === "X:\\"', () => {
+            expect(localAPI.isRoot('C:\\')).toBe(true);
+        });
 
-        const resolved = await localAPI.makedir(MAKEDIR_PATH, "dir2/dir3/dir4");
+        it('should return true if path === "\\\\"', () => {
+            expect(localAPI.isRoot('\\\\')).toBe(true);
+        });
 
-        expect(resolved).toBe(`${MAKEDIR_PATH}/dir2/dir3/dir4`);
+        it('should return false if path === "\\\\foo"', () => {
+            expect(localAPI.isRoot('\\\\foo')).toBe(true);
+        });
     });
-});
-
-describe("delete", function() {
-    const DELETE_PATH = `${TESTS_DIR}/deleteTest`;
-
-    beforeAll(() => {
-        localAPI = new FsLocal.API("/", () => {});
-        return prepareTmpTestFiles();
-    });
-
-    // it('should resolve if file/folder does not exist', async () => {
-    //     expect.assertions(1);
-
-    //     const deleted = await localAPI.delete(`${DELETE_PATH}`, [{
-    //         fullname: 'not_here'
-    //     }]);
-
-    //     expect(deleted).toBe(1);
-    // });
-
-    // it.only('should throw EACCESS if file/folder is write protected', async () => {
-    //     expect.assertions(1);
-
-    //     const deleted = await localAPI.delete(`${DELETE_PATH}`, [{
-    //         fullname: 'file_denied'
-    //     }]);
-
-    //     expect(deleted).toBe(2);
+    // // describe("delete", function() {
+    //     const DELETE_PATH = `${TESTS_DIR}/deleteTest`;
+    
+    //     beforeAll(() => {
+    //         localAPI = new FsLocal.API("/", () => {});
+    //         return prepareTmpTestFiles();
+    //     });
+
+    //     it('should resolve if file/folder does not exist', async () => {
+    //         expect.assertions(1);
+    
+    //         const deleted = await localAPI.delete(`${DELETE_PATH}`, [{
+    //             fullname: 'not_here'
+    //         }]);
+    
+    //         expect(deleted).toBe(1);
+    //     });
+    
+    //     it('should throw EACCESS if file/folder is write protected', async () => {
+    //         expect.assertions(1);
+    //         process.stdout.write('ro');
+    //         process.stdout.write(`++${DELETE_PATH}++`)
+    //         // const deleted = await localAPI.delete(`${DELETE_PATH}`, [{
+    //         //     fullname: 'file_denied'
+    //         // }]);
+    
+    //         // expect(deleted).toBe(2);
+    //         expect(1).toBe(2);
+    //     });
+    
+    //     it("should delete single file and resolve if success", () => {});
+    
+    //     it("should delete empty folder and resolve if success", () => {});
+    
+    //     it("should delete not empty folder and resolve if success", () => {});
+    
+    //     it("should delete not empty folder + files and resolve", () => {});
     // });
-
-    it("should delete single file and resolve if success", () => {});
-
-    it("should delete empty folder and resolve if success", () => {});
-
-    it("should delete not empty folder and resolve if success", () => {});
-
-    it("should delete not empty folder + files and resolve", () => {});
-});
+})
diff --git a/src/services/plugins/__tests__/fixtures/tmpDir.ts b/src/services/plugins/__tests__/fixtures/tmpDir.ts
new file mode 100644
index 0000000..dffb574
--- /dev/null
+++ b/src/services/plugins/__tests__/fixtures/tmpDir.ts
@@ -0,0 +1,50 @@
+import mock from 'mock-fs';
+import { tmpdir } from 'os';
+
+const TEST_FILES_DIR = tmpdir() + '/react-explorer-tests';
+
+const TmpDir = {
+    [TEST_FILES_DIR]: {
+        file: 'aa',
+        link: mock.symlink({
+            path: 'file'
+        }),
+        sizeTest: {
+            '14bytes': Buffer.alloc(14),
+            '1024bytes': Buffer.alloc(1024)
+        },
+        linkToSizeTest: mock.symlink({
+            path: 'sizeTest'
+        }),
+        makedirTest: {
+            denied: mock.directory({
+                mode: 0o000
+            })
+        },
+        deleteTest: {
+            folder_denied: mock.directory({
+                mode: 0o000,
+                items: {
+                    file: ''
+                }
+            }),
+            file_denied: mock.file({
+                content: '',
+                mode: 0o000
+            }),
+            file: Buffer.alloc(1024),
+            single_folder: {
+                file1: '',
+                file2: ''
+            },
+            multiple_folder: {
+                file1: '',
+                sub_folder: {
+                    file_sub: ''
+                }
+            }
+        }
+    }
+};
+
+export default TmpDir;
diff --git a/src/utils/test/helpers.ts b/src/utils/test/helpers.ts
index bcab2ba..776b0ad 100644
--- a/src/utils/test/helpers.ts
+++ b/src/utils/test/helpers.ts
@@ -1,4 +1,4 @@
-import { isWin, isMac } from '../platform';
+import { platform } from 'process';
 import { homedir, tmpdir } from 'os';
 import { mkdirSync, writeFileSync, existsSync } from 'fs';
 import { execSync } from 'child_process';
@@ -7,13 +7,28 @@ import { execSync } from 'child_process';
 declare var describe: any;
 
 // assume Unix in that case: may need some tweaks for some exotic platform
-const isUnix = !isWin;
+const isUnix = platform !== 'win32';
 const TEST_FILES_DIR = tmpdir() + '/react-explorer-tests';
 
-// call this to perform unix specified tests
-export const describeUnix = (...args: any) => isUnix && describe(...args) || describe.skip(...args);
-// call this to perform windows specified tests
-export const describeWin = (...args: any) => isWin && describe(...args) || describe.skip(...args);
+// call this to perform unix specific tests
+export const describeUnix = (...args: any) => {
+    args[0] += ' (Unix)';
+    if (isUnix) {
+        describe(...args);
+    } else {
+        describe.skip(...args);
+    }
+};
+
+// call this to perform windows specific tests
+export const describeWin = (...args: any) => {
+    args[0] += ' (Win)';
+    if (!isUnix) {
+        describe(...args);
+    } else {
+        describe.skip(...args);
+    }
+};
 
 export const getPath = (id: string) => {
     switch (id) {