Merge pull request #38 from brodybits/gh-37-patch-release-updates

GH-37 patch release updates for 1.3.1
diff --git a/.travis.yml b/.travis.yml
index 53f19a8..d3d12ad 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,7 @@
   - "4"
   - "6"
   - "8"
+  - "10"
 install:
   - "npm install"
 script:
diff --git a/appveyor.yml b/appveyor.yml
index ffe5194..2215987 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -6,7 +6,8 @@
   - nodejs_version: "4"
   - nodejs_version: "6"
   - nodejs_version: "8"
-  
+  - nodejs_version: "10"
+
 install:
   - ps: Install-Product node $env:nodejs_version
   - npm install
@@ -16,4 +17,7 @@
 test_script:
   - node --version
   - npm --version
+  # Solution proposed in https://stackoverflow.com/questions/49256190/how-to-fix-git-sh-setup-file-not-found-in-windows#comment88245403_49256190
+  # to work around https://github.com/appveyor/ci/issues/2420
+  - set "PATH=%PATH%;C:\Program Files\Git\mingw64\libexec\git-core"
   - npm test
diff --git a/index.js b/index.js
index c2510c9..8bb6326 100644
--- a/index.js
+++ b/index.js
@@ -25,6 +25,7 @@
 var fs = require('fs');
 var CordovaError = require('cordova-common').CordovaError;
 var isUrl = require('is-url');
+var isGitUrl = require('is-git-url');
 var hostedGitInfo = require('hosted-git-info');
 
 /*
@@ -144,7 +145,7 @@
  * get the moduleID of the installed module.
  *
  * @param {String} target    target that was passed into cordova-fetch.
- *                           can be moduleID, moduleID@version or gitURL
+ *                           can be moduleID, moduleID@version, gitURL or relative path (file:relative/path)
  *
  * @return {String} ID       moduleID without version.
  */
@@ -154,7 +155,7 @@
     var gitInfo = hostedGitInfo.fromUrl(target);
     if (gitInfo) {
         target = gitInfo.project;
-    } else if (isUrl(target)) {
+    } else if (isUrl(target) || isGitUrl(target)) {
         // strip away .git and everything that follows
         var strippedTarget = target.split('.git');
         var re = /.*\/(.*)/;
@@ -165,6 +166,11 @@
     }
 
     // If local path exists, try to get plugin id from package.json or set target to final directory
+    if (target.startsWith('file:')) {
+        // If target starts with file: prefix, strip it
+        target = target.substring(5);
+    }
+
     if (fs.existsSync(target)) {
         var pluginId;
         var pkgJsonPath = path.join(target, 'package.json');
diff --git a/package.json b/package.json
index 1082595..5bdc877 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "main": "index.js",
   "repository": {
     "type": "git",
-    "url": "https://github.com/apache/cordova-lib"
+    "url": "https://github.com/apache/cordova-fetch"
   },
   "keywords": [
     "cordova",
@@ -21,26 +21,28 @@
     "email": "dev@cordova.apache.org"
   },
   "dependencies": {
-    "cordova-common": "^2.2.0",
+    "cordova-common": "^2.2.5",
     "dependency-ls": "^1.1.0",
     "hosted-git-info": "^2.5.0",
     "is-url": "^1.2.1",
+    "is-git-url": "^1.0.0",
     "q": "^1.4.1",
     "shelljs": "^0.7.0"
   },
   "devDependencies": {
-    "eslint": "^3.19.0",
+    "eslint": "^4.19.1",
     "eslint-config-semistandard": "^11.0.0",
     "eslint-config-standard": "^10.2.1",
-    "eslint-plugin-import": "^2.8.0",
+    "eslint-plugin-import": "^2.13.0",
     "eslint-plugin-node": "^5.2.1",
-    "eslint-plugin-promise": "^3.6.0",
-    "eslint-plugin-standard": "^3.0.1",
+    "eslint-plugin-promise": "^3.8.0",
+    "eslint-plugin-standard": "^3.1.0",
+    "fs-extra": "^4.0.3",
     "jasmine": "^2.4.1"
   },
   "scripts": {
     "test": "npm run eslint && npm run jasmine",
-    "eslint": "eslint index.js spec/fetch.spec.js",
+    "eslint": "eslint .",
     "jasmine": "jasmine spec/fetch.spec.js spec/fetch-unit.spec.js"
   },
   "engines": {
diff --git a/spec/fetch-unit.spec.js b/spec/fetch-unit.spec.js
index c13f75a..c1c1f70 100644
--- a/spec/fetch-unit.spec.js
+++ b/spec/fetch-unit.spec.js
@@ -32,55 +32,35 @@
         spyOn(fs, 'existsSync').and.returnValue(false);
     });
 
-    it('npm install should be called with production flag (default)', function (done) {
-        var opts = { cwd: 'some/path', production: true, save: true};
-        fetch('platform', 'tmpDir', opts)
+    it('npm install should be called with production flag (default)', function () {
+        var opts = { cwd: 'some/path', production: true, save: true };
+        return fetch('platform', 'tmpDir', opts)
             .then(function (result) {
                 expect(superspawn.spawn).toHaveBeenCalledWith('npm', jasmine.stringMatching(/production/), jasmine.any(Object));
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
+            });
     });
 
-    it('save-exact should be true if passed in', function (done) {
+    it('save-exact should be true if passed in', function () {
         var opts = { cwd: 'some/path', save_exact: true };
-        fetch('platform', 'tmpDir', opts)
+        return fetch('platform', 'tmpDir', opts)
             .then(function (result) {
                 expect(superspawn.spawn).toHaveBeenCalledWith('npm', jasmine.stringMatching(/save-exact/), jasmine.any(Object));
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
+            });
     });
 
-    it('noprod should turn production off', function (done) {
-        var opts = { cwd: 'some/path', production: false};
-        fetch('platform', 'tmpDir', opts)
+    it('noprod should turn production off', function () {
+        var opts = { cwd: 'some/path', production: false };
+        return fetch('platform', 'tmpDir', opts)
             .then(function (result) {
                 expect(superspawn.spawn).not.toHaveBeenCalledWith('npm', jasmine.stringMatching(/production/), jasmine.any(Object));
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
+            });
     });
 
-    it('when save is false, no-save flag should be passed through', function (done) {
-        var opts = { cwd: 'some/path', production: true, save: false};
-        fetch('platform', 'tmpDir', opts)
+    it('when save is false, no-save flag should be passed through', function () {
+        var opts = { cwd: 'some/path', production: true, save: false };
+        return fetch('platform', 'tmpDir', opts)
             .then(function (result) {
                 expect(superspawn.spawn).toHaveBeenCalledWith('npm', jasmine.stringMatching(/--no-save/), jasmine.any(Object));
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
+            });
     });
 });
diff --git a/spec/fetch.spec.js b/spec/fetch.spec.js
index 4d9369c..1e0eccf 100644
--- a/spec/fetch.spec.js
+++ b/spec/fetch.spec.js
@@ -16,355 +16,178 @@
 */
 
 /* eslint-env jasmine */
-var fetch = require('../index.js');
-var uninstall = require('../index.js').uninstall;
-var shell = require('shelljs');
+
+var fetch = require('..');
+var uninstall = fetch.uninstall;
+
 var path = require('path');
-var fs = require('fs');
+var fs = require('fs-extra');
 var helpers = require('./helpers.js');
 
-describe('platform fetch/uninstall tests via npm & git', function () {
+var tmpDir, opts;
 
-    var tmpDir = helpers.tmpDir('plat_fetch');
-    var opts = {};
+beforeEach(function () {
+    opts = {};
+    tmpDir = helpers.tmpDir();
+    process.chdir(tmpDir);
+});
+
+afterEach(function () {
+    process.chdir(__dirname); // Needed to rm the dir on Windows.
+    fs.removeSync(tmpDir);
+});
+
+function fetchAndMatch (target, pkgProps) {
+    pkgProps = pkgProps || { name: target };
+    return fetch(target, tmpDir, opts)
+        .then(expectPackageToMatch(pkgProps));
+}
+
+function expectPackageToMatch (pkgProps) {
+    return result => {
+        expect(result).toBeDefined();
+        expect(fs.existsSync(result)).toBe(true);
+        const pkg = fs.readJsonSync(path.join(result, 'package.json'));
+        expect(pkg).toEqual(jasmine.objectContaining(pkgProps));
+        return result;
+    };
+}
+
+function expectNotToBeInstalled (pkgName) {
+    expect(fs.existsSync(path.join(tmpDir, 'node_modules', pkgName))).toBe(false);
+}
+
+function expectDependenciesToBe (deps) {
+    const rootPJ = fs.readJsonSync(path.join(tmpDir, 'package.json'));
+    expect(rootPJ.dependencies).toEqual(deps);
+}
+
+describe('fetch/uninstall tests via npm & git', function () {
+
+    it('should fetch and uninstall a cordova platform via npm & git', function () {
+        return Promise.resolve()
+            .then(_ => fetchAndMatch('cordova-android'))
+            .then(_ => uninstall('cordova-android', tmpDir, opts))
+            .then(_ => expectNotToBeInstalled('cordova-android'))
+
+            .then(_ => fetchAndMatch('https://github.com/apache/cordova-browser.git', { name: 'cordova-browser' }))
+            .then(_ => uninstall('cordova-browser', tmpDir, opts))
+            .then(_ => expectNotToBeInstalled('cordova-browser'));
+    }, 120000);
+
+    it('should fetch a scoped plugin from npm', function () {
+        return fetchAndMatch('@stevegill/cordova-plugin-device');
+    }, 30000);
+});
+
+describe('fetch/uninstall with --save', function () {
 
     beforeEach(function () {
-        process.chdir(tmpDir);
+        opts = {save: true};
+        // copy package.json from spec directory to tmpDir
+        fs.copySync(path.join(__dirname, 'testpkg.json'), 'package.json');
     });
 
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
+    it('should fetch and uninstall a cordova platform via npm & git tags/branches', function () {
+        return Promise.resolve()
+            // npm tag
+            .then(_ => fetchAndMatch('cordova-android@5.1.1', {
+                name: 'cordova-android',
+                version: '5.1.1'
+            }))
+            .then(_ => expectDependenciesToBe({'cordova-android': '^5.1.1'}))
+            .then(_ => uninstall('cordova-android', tmpDir, opts))
+            .then(_ => expectDependenciesToBe({}))
+            .then(_ => expectNotToBeInstalled('cordova-android'))
+
+            // git tag
+            .then(_ => fetchAndMatch('https://github.com/apache/cordova-ios.git#rel/4.1.1', {
+                name: 'cordova-ios',
+                version: '4.1.1'
+            }))
+            .then(_ => expectDependenciesToBe({'cordova-ios': 'git+https://github.com/apache/cordova-ios.git#rel/4.1.1'}))
+            .then(_ => uninstall('cordova-ios', tmpDir, opts))
+            .then(_ => expectDependenciesToBe({}))
+            .then(_ => expectNotToBeInstalled('cordova-ios'))
+
+            // git branch
+            .then(_ => fetchAndMatch('https://github.com/apache/cordova-android.git#4.1.x', {
+                name: 'cordova-android',
+                version: '4.1.1'
+            }))
+            .then(_ => expectDependenciesToBe({'cordova-android': 'git+https://github.com/apache/cordova-android.git#4.1.x'}))
+            .then(_ => uninstall('cordova-android', tmpDir, opts));
+    }, 150000);
+
+    it('should fetch and uninstall a cordova plugin via git commit sha', function () {
+        const URL = 'https://github.com/apache/cordova-plugin-contacts.git#7db612115755c2be73a98dda76ff4c5fd9d8a575';
+        return Promise.resolve()
+            .then(_ => fetchAndMatch(URL, {
+                name: 'cordova-plugin-contacts',
+                version: '2.0.2-dev'
+            }))
+            .then(_ => expectDependenciesToBe({'cordova-plugin-contacts': `git+${URL}`}))
+            .then(_ => uninstall('cordova-plugin-contacts', tmpDir, opts))
+            .then(_ => expectDependenciesToBe({}))
+            .then(_ => expectNotToBeInstalled('cordova-plugin-contacts'));
+    }, 30000);
+});
+
+describe('fetching already installed packages', function () {
+
+    beforeEach(function () {
+        fs.copySync(path.join(__dirname, 'support'), 'support');
     });
 
-    it('should fetch and uninstall a cordova platform via npm & git', function (done) {
+    it('should return package path for registry packages', function () {
+        return Promise.resolve()
+            .then(_ => fetchAndMatch('cordova-plugin-device'))
+            .then(_ => fetchAndMatch('cordova-plugin-device'));
+    }, 40000);
 
-        fetch('cordova-android', tmpDir, opts)
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-android');
+    it('should return package path if git repo name differs from plugin id', function () {
+        const TARGET = 'https://github.com/AzureAD/azure-activedirectory-library-for-cordova.git';
+        return Promise.resolve()
+            .then(_ => fetchAndMatch(TARGET, { name: 'cordova-plugin-ms-adal' }))
+            .then(_ => fetchAndMatch(TARGET, { name: 'cordova-plugin-ms-adal' }));
+    }, 120000);
 
-                return uninstall('cordova-android', tmpDir, opts);
-            })
-            .then(function () {
-                expect(fs.existsSync(path.join(tmpDir, 'node_modules', 'cordova-android'))).toBe(false);
+    it('should return package path if using a relative path', function () {
+        const TARGET = 'file:support/dummy-local-plugin';
+        return Promise.resolve()
+            .then(_ => fetchAndMatch(TARGET, { name: 'test-plugin' }))
+            .then(_ => fetchAndMatch(TARGET, { name: 'test-plugin' }));
+    }, 120000);
 
-                return fetch('https://github.com/apache/cordova-ios.git', tmpDir, opts);
-            })
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-ios');
-
-                return uninstall('cordova-ios', tmpDir, opts);
-            })
-            .then(function () {
-                expect(fs.existsSync(path.join(tmpDir, 'node_modules', 'cordova-ios'))).toBe(false);
-
-                // return fetch('git+ssh://git@github.com/apache/cordova-browser.git#487d91d1ded96b8e2029f2ee90f12a8b20499f54', tmpDir, opts);
-                // can't test ssh right now as it is requiring ssh password
-                return fetch('https://github.com/apache/cordova-browser.git', tmpDir, opts);
-            })
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-browser');
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
+    it('should return package path for git+http variants', function () {
+        return Promise.resolve()
+            .then(_ => fetchAndMatch('github:apache/cordova-plugin-device', { name: 'cordova-plugin-device' }))
+            .then(_ => fetchAndMatch('https://github.com/apache/cordova-plugin-device', { name: 'cordova-plugin-device' }))
+            .then(_ => fetchAndMatch('git+https://github.com/apache/cordova-plugin-device', { name: 'cordova-plugin-device' }));
     }, 60000);
 });
 
-describe('platform fetch/uninstall test via npm & git tags with --save', function () {
+describe('negative tests', function () {
 
-    var tmpDir = helpers.tmpDir('plat_fetch_save');
-    var opts = {'save': true};
-
-    beforeEach(function () {
-        // copy package.json from spec directory to tmpDir
-        shell.cp('spec/testpkg.json', path.join(tmpDir, 'package.json'));
-        process.chdir(tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
-    it('should fetch and uninstall a cordova platform via npm & git tags/branches', function (done) {
-        fetch('cordova-android@5.1.1', tmpDir, opts)
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-android');
-                expect(pkgJSON.version).toBe('5.1.1');
-
-                var rootPJ = require(path.join(tmpDir, 'package.json'));
-                expect(rootPJ.dependencies['cordova-android']).toBe('^5.1.1');
-
-                return uninstall('cordova-android', tmpDir, opts);
-            })
-            .then(function () {
-                var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'));
-                expect(Object.keys(rootPJ.dependencies).length).toBe(0);
-                expect(fs.existsSync(path.join(tmpDir, 'node_modules', 'cordova-android'))).toBe(false);
-
-                return fetch('https://github.com/apache/cordova-ios.git#rel/4.1.1', tmpDir, opts);
-            })
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-ios');
-                expect(pkgJSON.version).toBe('4.1.1');
-
-                var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'));
-                expect(rootPJ.dependencies['cordova-ios']).toBe('git+https://github.com/apache/cordova-ios.git#rel/4.1.1');
-
-                return uninstall('cordova-ios', tmpDir, opts);
-            })
-            .then(function () {
-                var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'));
-                expect(Object.keys(rootPJ.dependencies).length).toBe(0);
-                expect(fs.existsSync(path.join(tmpDir, 'node_modules', 'cordova-ios'))).toBe(false);
-
-                return fetch('https://github.com/apache/cordova-android.git#4.1.x', tmpDir, opts);
-            })
-            .then(function (result) {
-                var pkgJSON = JSON.parse(fs.readFileSync(path.join(result, 'package.json'), 'utf8'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-android');
-                expect(pkgJSON.version).toBe('4.1.1');
-
-                var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'));
-                expect(rootPJ.dependencies['cordova-android']).toBe('git+https://github.com/apache/cordova-android.git#4.1.x');
-
-                return uninstall('cordova-android', tmpDir, opts);
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 150000);
-});
-
-describe('plugin fetch/uninstall test with --save', function () {
-
-    var tmpDir = helpers.tmpDir('plug_fetch_save');
-    var opts = {'save': true};
-
-    beforeEach(function () {
-        // copy package.json from spec directory to tmpDir
-        shell.cp('spec/testpkg.json', path.join(tmpDir, 'package.json'));
-        process.chdir(tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
-    it('should fetch and uninstall a cordova plugin via git commit sha', function (done) {
-        fetch('https://github.com/apache/cordova-plugin-contacts.git#7db612115755c2be73a98dda76ff4c5fd9d8a575', tmpDir, opts)
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-plugin-contacts');
-                expect(pkgJSON.version).toBe('2.0.2-dev');
-
-                var rootPJ = require(path.join(tmpDir, 'package.json'));
-                expect(rootPJ.dependencies['cordova-plugin-contacts']).toBe('git+https://github.com/apache/cordova-plugin-contacts.git#7db612115755c2be73a98dda76ff4c5fd9d8a575');
-
-                return uninstall('cordova-plugin-contacts', tmpDir, opts);
-            })
-            .then(function () {
-                var rootPJ = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8'));
-                expect(Object.keys(rootPJ.dependencies).length).toBe(0);
-                expect(fs.existsSync(path.join(tmpDir, 'node_modules', 'cordova-plugin-contacts'))).toBe(false);
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 30000);
-});
-
-describe('test trimID method for npm and git', function () {
-
-    var tmpDir;
-    var opts = {};
-
-    beforeEach(function () {
-        tmpDir = helpers.tmpDir('plug_trimID');
-        process.chdir(tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
-    it('should fetch the same cordova plugin twice in a row', function (done) {
-        fetch('cordova-plugin-device', tmpDir, opts)
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-plugin-device');
-
-                return fetch('https://github.com/apache/cordova-plugin-media.git', tmpDir, opts);
-            })
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('cordova-plugin-media');
-
-                // refetch to trigger trimID
-                return fetch('cordova-plugin-device', tmpDir, opts);
-
-            })
-            .then(function (result) {
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(result).toMatch('cordova-plugin-device');
-
-                // refetch to trigger trimID, with shortcode URL
-                return fetch('github:apache/cordova-plugin-media', tmpDir, opts);
-            })
-            .then(function (result) {
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(result).toMatch('cordova-plugin-media');
-
-                // refetch to trigger trimID, this time no .git
-                return fetch('https://github.com/apache/cordova-plugin-media', tmpDir, opts);
-            })
-            .then(function (result) {
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(result).toMatch('cordova-plugin-media');
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 40000);
-
-    it('should fetch same plugin twice in a row if git repo name differ from plugin id', function (done) {
-        fetch('https://github.com/AzureAD/azure-activedirectory-library-for-cordova.git', tmpDir, opts)
-            .then(function (result) {
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(result).toMatch('cordova-plugin-ms-adal');
-                return fetch('https://github.com/AzureAD/azure-activedirectory-library-for-cordova.git', tmpDir, opts);
-            })
-            .then(function (result) {
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(result).toMatch('cordova-plugin-ms-adal');
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 30000);
-});
-
-describe('fetch failure with unknown module', function () {
-
-    var tmpDir = helpers.tmpDir('fetch_fails_npm');
-    var opts = {};
-
-    beforeEach(function () {
-        process.chdir(tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
-    it('should fail fetching a module that does not exist on npm', function (done) {
-        fetch('NOTAMODULE', tmpDir, opts)
+    it('should fail fetching a module that does not exist on npm', function () {
+        return fetch('NOTAMODULE', tmpDir, opts)
             .then(function (result) {
                 console.log('This should fail and it should not be seen');
             })
             .fail(function (err) {
                 expect(err.message.code).toBe(1);
                 expect(err).toBeDefined();
-            })
-            .fin(done);
+            });
     }, 30000);
-});
 
-describe('fetch failure with git subdirectory', function () {
-
-    var tmpDir = helpers.tmpDir('fetch_fails_subdirectory');
-    var opts = {};
-
-    beforeEach(function () {
-        process.chdir(tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
-    it('should fail fetching a giturl which contains a subdirectory', function (done) {
-        fetch('https://github.com/apache/cordova-plugins.git#:keyboard', tmpDir, opts)
+    it('should fail fetching a giturl which contains a subdirectory', function () {
+        return fetch('https://github.com/apache/cordova-plugins.git#:keyboard', tmpDir, opts)
             .then(function (result) {
                 console.log('This should fail and it should not be seen');
             })
             .fail(function (err) {
                 expect(err.message.code).toBe(1);
                 expect(err).toBeDefined();
-            })
-            .fin(done);
-    }, 30000);
-});
-
-describe('scoped plugin fetch/uninstall tests via npm', function () {
-
-    var tmpDir = helpers.tmpDir('scoped_plug_fetch');
-    var opts = {};
-
-    beforeEach(function () {
-        process.chdir(tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
-    it('should fetch a scoped plugin from npm', function (done) {
-        fetch('@stevegill/cordova-plugin-device', tmpDir, opts)
-            .then(function (result) {
-                var pkgJSON = require(path.join(result, 'package.json'));
-                expect(result).toBeDefined();
-                expect(fs.existsSync(result)).toBe(true);
-                expect(pkgJSON.name).toBe('@stevegill/cordova-plugin-device');
-            })
-            .fail(function (err) {
-                console.error(err);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
+            });
     }, 30000);
 });
diff --git a/spec/helpers.js b/spec/helpers.js
index e081a78..f6e7248 100644
--- a/spec/helpers.js
+++ b/spec/helpers.js
@@ -18,18 +18,10 @@
  */
 
 var path = require('path');
-var fs = require('fs');
-var shell = require('shelljs');
+var fs = require('fs-extra');
 var os = require('os');
 
-module.exports.tmpDir = function (subdir) {
-    var dir = path.join(os.tmpdir(), 'e2e-test');
-    if (subdir) {
-        dir = path.join(dir, subdir);
-    }
-    if (fs.existsSync(dir)) {
-        shell.rm('-rf', dir);
-    }
-    shell.mkdir('-p', dir);
-    return dir;
+const tmpDirTemplate = path.join(os.tmpdir(), 'cordova-fetch-tests-');
+module.exports.tmpDir = function () {
+    return fs.mkdtempSync(tmpDirTemplate);
 };
diff --git a/spec/support/dummy-local-plugin/package.json b/spec/support/dummy-local-plugin/package.json
new file mode 100644
index 0000000..77cb898
--- /dev/null
+++ b/spec/support/dummy-local-plugin/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "test-plugin",
+  "version": "1.0.0",
+  "description": "This plugin allows you to stream audio and video in a fullscreen, native player on iOS and Android.",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  }
+}
diff --git a/spec/support/dummy-local-plugin/plugin.xml b/spec/support/dummy-local-plugin/plugin.xml
new file mode 100755
index 0000000..411a497
--- /dev/null
+++ b/spec/support/dummy-local-plugin/plugin.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<plugin
+		xmlns="http://apache.org/cordova/ns/plugins/1.0"
+		xmlns:android="http://schemas.android.com/apk/res/android"
+		id="test-plugin"
+		version="1.0.0">
+
+	<name>test plugin</name>
+
+	<engines>
+		<engine name="cordova" version=">=3.0.0" />
+	</engines>
+
+</plugin>