Merge pull request #13 from raphinesse/cleanup

Major code cleanup
diff --git a/index.js b/index.js
index aac2f38..61181ca 100644
--- a/index.js
+++ b/index.js
@@ -17,24 +17,27 @@
     under the License.
 */
 
-var path = require('path');
 var fs = require('fs');
+var os = require('os');
+var path = require('path');
+
+var Promise = require('q');
+var isUrl = require('is-url');
 var shell = require('shelljs');
+var requireFresh = require('import-fresh');
+var validateIdentifier = require('valid-identifier');
+
+var fetch = require('cordova-fetch');
 var events = require('cordova-common').events;
-var Q = require('q');
 var CordovaError = require('cordova-common').CordovaError;
 var ConfigParser = require('cordova-common').ConfigParser;
-var fetch = require('cordova-fetch');
-var url = require('url');
-var validateIdentifier = require('valid-identifier');
 var CordovaLogger = require('cordova-common').CordovaLogger.get();
 
+const DEFAULT_VERSION = '1.0.0';
+
 // Global configuration paths
-var global_config_path = process.env.CORDOVA_HOME;
-if (!global_config_path) {
-    var HOME = process.env[(process.platform.slice(0, 3) === 'win') ? 'USERPROFILE' : 'HOME'];
-    global_config_path = path.join(HOME, '.cordova');
-}
+var global_config_path = process.env.CORDOVA_HOME || path.join(os.homedir(), '.cordova');
+
 /**
  * Sets up to forward events to another instance, or log console.
  * This will make the create internal events visible outside
@@ -64,7 +67,7 @@
  **/
 // Returns a promise.
 module.exports = function (dir, optionalId, optionalName, cfg, extEvents) {
-    return Q.fcall(function () {
+    return Promise.resolve().then(function () {
         events = setupEvents(extEvents);
         events.emit('verbose', 'Using detached cordova-create');
 
@@ -129,20 +132,10 @@
         cfg.lib.www.url = cfg.lib.www.url || cfg.lib.www.uri;
 
         if (!cfg.lib.www.url) {
-            try {
-                cfg.lib.www.url = require.resolve('cordova-app-hello-world');
-                cfg.lib.www.template = true;
-            } catch (e) {
-                // Falling back on npm@2 path hierarchy
-                // TODO: Remove fallback after cordova-app-hello-world release
-                cfg.lib.www.url = path.join(__dirname, '..', '..', 'node_modules', 'cordova-app-hello-world');
-            }
+            cfg.lib.www.url = require.resolve('cordova-app-hello-world');
+            cfg.lib.www.template = true;
         }
 
-        // TODO (kamrik): extend lazy_load for retrieval without caching to allow net urls for --src.
-        cfg.lib.www.version = cfg.lib.www.version || 'not_versioned';
-        cfg.lib.www.id = cfg.lib.www.id || 'dummy_id';
-
         // Make sure that the source www/ is not a direct ancestor of the
         // target www/, or else we will recursively copy forever. To do this,
         // we make sure that the shortest relative path from source-to-target
@@ -163,36 +156,16 @@
             // Finally, Ready to start!
             events.emit('log', 'Creating a new cordova project.');
 
-            // Strip link and url from cfg to avoid them being persisted to disk via .cordova/config.json.
-            // TODO: apparently underscore has no deep clone.  Replace with lodash or something. For now, abuse JSON.
-            var cfgToPersistToDisk = JSON.parse(JSON.stringify(cfg));
-
-            delete cfgToPersistToDisk.lib.www;
-            if (Object.keys(cfgToPersistToDisk.lib).length === 0) {
-                delete cfgToPersistToDisk.lib;
-            }
-
-            // Update cached version of config.json
-            writeToConfigJson(dir, cfgToPersistToDisk, false);
-        })
-        .then(function () {
-            var isGit;
-            var isNPM;
-            var options;
-
             // If symlink, don't fetch
             if (cfg.lib.www.link) {
-                events.emit('verbose', 'Symlinking assets.');
-                return Q(cfg.lib.www.url);
+                return cfg.lib.www.url;
             }
 
-            events.emit('verbose', 'Copying assets."');
-            isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url);
-            isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url))) && !isGit;
+            var isGit = cfg.lib.www.template && isUrl(cfg.lib.www.url);
+            var isNPM = cfg.lib.www.template && (cfg.lib.www.url.indexOf('@') > -1 || !fs.existsSync(path.resolve(cfg.lib.www.url))) && !isGit;
             // Always use cordova fetch to obtain the npm or git template
             if (isGit || isNPM) {
                 // Saved to .Cordova folder (ToDo: Delete installed template after using)
-                // ToDo: @carynbear properly label errors from fetch as such
                 var tempDest = global_config_path;
                 var target = cfg.lib.www.url;
                 // add latest to npm module if no version is specified
@@ -202,30 +175,25 @@
                 }
                 events.emit('verbose', 'Using cordova-fetch for ' + target);
                 return fetch(target, tempDest, {})
-                    .fail(function (err) {
+                    .catch(function (err) {
                         events.emit('error', '\x1B[1m \x1B[31m Error from Cordova Fetch: ' + err.message);
                         events.emit('error', 'The template you are trying to use is invalid.' +
                         ' Make sure you follow the template guide found here https://cordova.apache.org/docs/en/latest/guide/cli/template.html.' +
                         ' Templates now require a package.json.');
-                        if (options.verbose) {
-                            console.trace();
-                        }
                         throw err;
                     });
             // If assets are not online, resolve as a relative path on local computer
             } else {
-                cfg.lib.www.url = path.resolve(cfg.lib.www.url);
-                return Q(cfg.lib.www.url);
+                return path.resolve(cfg.lib.www.url);
             }
-        }).then(function (input_directory) {
+        })
+        .then(function (input_directory) {
             var import_from_path = input_directory;
 
             // handle when input wants to specify sub-directory (specified in index.js as "dirname" export);
             var isSubDir = false;
             try {
-                // Delete cached require incase one exists
-                delete require.cache[require.resolve(input_directory)];
-                var templatePkg = require(input_directory);
+                var templatePkg = requireFresh(input_directory);
                 if (templatePkg && templatePkg.dirname) {
                     import_from_path = templatePkg.dirname;
                     isSubDir = true;
@@ -240,37 +208,31 @@
                     import_from_path);
             }
 
-            var paths = {};
-
-            // get stock config.xml, used if template does not contain config.xml
-            paths.configXml = path.join(require('cordova-app-hello-world').dirname, 'config.xml');
-
-            // get stock www; used if template does not contain www
-            paths.www = path.join(require('cordova-app-hello-world').dirname, 'www');
-
-            // get stock hooks; used if template does not contain hooks
-            paths.hooks = path.join(require('cordova-app-hello-world').dirname, 'hooks');
-
-            // ToDo: get stock package.json if template does not contain package.json;
             var dirAlreadyExisted = fs.existsSync(dir);
             if (!dirAlreadyExisted) {
                 fs.mkdirSync(dir);
             }
 
             try {
-
                 // Copy files from template to project
-                if (cfg.lib.www.template) { copyTemplateFiles(import_from_path, dir, isSubDir); }
+                if (cfg.lib.www.template) {
+                    events.emit('verbose', 'Copying assets.');
+                    copyTemplateFiles(import_from_path, dir, isSubDir);
+                }
 
                 // If --link, link merges, hooks, www, and config.xml (and/or copy to root)
-                if (cfg.lib.www.link) { linkFromTemplate(import_from_path, dir); }
+                if (cfg.lib.www.link) {
+                    events.emit('verbose', 'Symlinking assets.');
+                    linkFromTemplate(import_from_path, dir);
+                }
 
                 // If following were not copied/linked from template, copy from stock app hello world
-                copyIfNotExists(paths.www, path.join(dir, 'www'));
-                copyIfNotExists(paths.hooks, path.join(dir, 'hooks'));
+                // TODO: get stock package.json if template does not contain package.json;
+                copyIfNotExists(stockAssetPath('www'), path.join(dir, 'www'));
+                copyIfNotExists(stockAssetPath('hooks'), path.join(dir, 'hooks'));
                 var configXmlExists = projectConfig(dir); // moves config to root if in www
-                if (paths.configXml && !configXmlExists) {
-                    shell.cp(paths.configXml, path.join(dir, 'config.xml'));
+                if (!configXmlExists) {
+                    shell.cp(stockAssetPath('config.xml'), path.join(dir, 'config.xml'));
                 }
             } catch (e) {
                 if (!dirAlreadyExisted) {
@@ -285,8 +247,7 @@
             var pkgjsonPath = path.join(dir, 'package.json');
             // Update package.json name and version fields
             if (fs.existsSync(pkgjsonPath)) {
-                delete require.cache[require.resolve(pkgjsonPath)];
-                var pkgjson = require(pkgjsonPath);
+                var pkgjson = requireFresh(pkgjsonPath);
 
                 // Pkjson.displayName should equal config's name.
                 if (cfg.name) {
@@ -295,28 +256,27 @@
                 // Pkjson.name should equal config's id.
                 if (cfg.id) {
                     pkgjson.name = cfg.id.toLowerCase();
-                } else if (!cfg.id) {
+                } else {
                     // Use default name.
                     pkgjson.name = 'helloworld';
                 }
 
-                pkgjson.version = '1.0.0';
+                pkgjson.version = DEFAULT_VERSION;
                 fs.writeFileSync(pkgjsonPath, JSON.stringify(pkgjson, null, 4), 'utf8');
             }
 
             // Create basic project structure.
-            if (!fs.existsSync(path.join(dir, 'platforms'))) { shell.mkdir(path.join(dir, 'platforms')); }
-
-            if (!fs.existsSync(path.join(dir, 'plugins'))) { shell.mkdir(path.join(dir, 'plugins')); }
+            shell.mkdir('-p', path.join(dir, 'platforms'));
+            shell.mkdir('-p', path.join(dir, 'plugins'));
 
             var configPath = path.join(dir, 'config.xml');
             // only update config.xml if not a symlink
             if (!fs.lstatSync(configPath).isSymbolicLink()) {
-                // Write out id and name to config.xml; set version to 1.0.0 (to match package.json default version)
+                // Write out id, name and default version to config.xml
                 var conf = new ConfigParser(configPath);
                 if (cfg.id) conf.setPackageName(cfg.id);
                 if (cfg.name) conf.setName(cfg.name);
-                conf.setVersion('1.0.0');
+                conf.setVersion(DEFAULT_VERSION);
                 conf.write();
             }
         });
@@ -352,8 +312,7 @@
         copyPath = path.resolve(templateDir);
         shell.cp('-R', copyPath, projectDir);
     } else {
-        var templateFiles; // Current file
-        templateFiles = fs.readdirSync(templateDir);
+        var templateFiles = fs.readdirSync(templateDir);
         // Remove directories, and files that are unwanted
         if (!isSubDir) {
             var excludes = ['package.json', 'RELEASENOTES.md', '.git', 'NOTICE', 'LICENSE', 'COPYRIGHT', '.npmignore'];
@@ -370,15 +329,6 @@
 }
 
 /**
- * @param  {String} value
- * @return {Boolean} is the input value a url?
- */
-function isUrl (value) {
-    var u = value && url.parse(value);
-    return !!(u && u.protocol && u.protocol.length > 2); // Account for windows c:/ paths
-}
-
-/**
  * Find config file in project directory or www directory
  * If file is in www directory, move it outside
  * @param  {String} project directory to be searched
@@ -414,33 +364,6 @@
 }
 
 /**
- * Write opts to .cordova/config.json
- *
- * @param  {String} project directory
- * @param  {Object} opts containing the additions to config.json
- * @param  {Boolean} autopersist option
- * @return {JSON Data}
- */
-function writeToConfigJson (project_root, opts, autoPersist) {
-    var json = dotCordovaConfig(project_root);
-    for (var p in opts) {
-        json[p] = opts[p];
-    }
-    if (autoPersist) {
-        var configPath = path.join(project_root, '.cordova', 'config.json');
-        var contents = JSON.stringify(json, null, 4);
-        // Don't write the file for an empty config.
-        if (contents !== '{}' || fs.existsSync(configPath)) {
-            shell.mkdir('-p', path.join(project_root, '.cordova'));
-            fs.writeFileSync(configPath, contents, 'utf-8');
-        }
-        return json;
-    } else {
-        return json;
-    }
-}
-
-/**
  * Removes existing files and symlinks them if they exist.
  * Symlinks folders: www, merges, hooks
  * Symlinks file: config.xml (but only if it exists outside of the www folder)
@@ -483,3 +406,7 @@
         shell.cp(copySrc, projectDir);
     }
 }
+
+function stockAssetPath (p) {
+    return path.join(require('cordova-app-hello-world').dirname, p);
+}
diff --git a/package.json b/package.json
index f89a74b..9ac95d1 100644
--- a/package.json
+++ b/package.json
@@ -28,20 +28,22 @@
     "cordova-app-hello-world": "^3.11.0",
     "cordova-common": "^2.2.0",
     "cordova-fetch": "^1.3.0",
-    "q": "1.0.1",
-    "shelljs": "0.3.0",
+    "import-fresh": "^2.0.0",
+    "is-url": "^1.2.4",
+    "q": "^1.5.1",
+    "shelljs": "^0.8.2",
     "valid-identifier": "0.0.1"
   },
   "devDependencies": {
     "eslint": "^4.2.0",
-    "eslint-config-semistandard": "^11.0.0",
-    "eslint-config-standard": "^10.2.1",
+    "eslint-config-semistandard": "^12.0.1",
+    "eslint-config-standard": "^11.0.0",
     "eslint-plugin-import": "^2.3.0",
-    "eslint-plugin-node": "^5.0.0",
+    "eslint-plugin-node": "^6.0.1",
     "eslint-plugin-promise": "^3.5.0",
     "eslint-plugin-standard": "^3.0.1",
-    "jasmine": "^2.4.1",
-    "semver": "^5.3.0"
+    "jasmine": "^3.1.0",
+    "rewire": "^4.0.1"
   },
   "scripts": {
     "test": "npm run eslint && npm run jasmine",
diff --git a/spec/create.spec.js b/spec/create.spec.js
index 1a79a81..68e924a 100644
--- a/spec/create.spec.js
+++ b/spec/create.spec.js
@@ -17,106 +17,46 @@
     under the License.
 */
 
-var helpers = require('./helpers');
-var path = require('path');
-var shell = require('shelljs');
-var events = require('cordova-common').events;
-var ConfigParser = require('cordova-common').ConfigParser;
-var create = require('../index');
 var fs = require('fs');
-var semver = require('semver');
-var tmpDir = helpers.tmpDir('create_test');
+var path = require('path');
+
+var shell = require('shelljs');
+var requireFresh = require('import-fresh');
+
+var create = require('..');
+var events = require('cordova-common').events;
+var CordovaError = require('cordova-common').CordovaError;
+var ConfigParser = require('cordova-common').ConfigParser;
+const {tmpDir, createWith, createWithMockFetch, expectRejection} = require('./helpers');
+
 var appName = 'TestBase';
 var appId = 'org.testing';
 var project = path.join(tmpDir, appName);
 
-// Global configuration paths
-var global_config_path = process.env.CORDOVA_HOME;
-if (!global_config_path) {
-    var HOME = process.env[(process.platform.slice(0, 3) === 'win') ? 'USERPROFILE' : 'HOME'];
-    global_config_path = path.join(HOME, '.cordova');
-}
-
-var configSubDirPkgJson = {
-    lib: {
-        www: {
-            template: true,
-            url: path.join(__dirname, 'templates', 'withsubdirectory_package_json'),
-            version: ''
-        }
-    }
-};
-
-var configConfigInWww = {
-    lib: {
-        www: {
-            template: true,
-            url: path.join(__dirname, 'templates', 'config_in_www'),
-            version: ''
-        }
-    }
-};
-
-var configGit = {
-    lib: {
-        www: {
-            url: 'https://github.com/apache/cordova-app-hello-world',
-            template: true,
-            version: 'not_versioned'
-        }
-    }
-};
-
-var configNPMold = {
-    lib: {
-        www: {
-            template: true,
-            url: 'phonegap-template-vue-f7-tabs@1.0.0',
-            version: ''
-        }
-    }
-};
-
-var configNPM = {
-    lib: {
-        www: {
-            template: true,
-            url: 'phonegap-template-vue-f7-tabs',
-            version: ''
-        }
-    }
-};
+// Setup and teardown test dirs
+beforeEach(function () {
+    shell.rm('-rf', project);
+    shell.mkdir('-p', tmpDir);
+});
+afterEach(function () {
+    process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
+    shell.rm('-rf', tmpDir);
+});
 
 describe('cordova create checks for valid-identifier', function () {
-    it('should reject reserved words from start of id', function (done) {
-        create('projectPath', 'int.bob', 'appName', {}, events)
-            .fail(function (err) {
-                expect(err.message).toBe('App id contains a reserved word, or is not a valid identifier.');
-            })
-            .fin(done);
-    }, 60000);
+    const error = new CordovaError('is not a valid identifier');
 
-    it('should reject reserved words from end of id', function (done) {
-        create('projectPath', 'bob.class', 'appName', {}, events)
-            .fail(function (err) {
-                expect(err.message).toBe('App id contains a reserved word, or is not a valid identifier.');
-            })
-            .fin(done);
-    }, 60000);
+    it('should reject reserved words from start of id', function () {
+        return expectRejection(create(project, 'int.bob', appName, {}, events), error);
+    });
+
+    it('should reject reserved words from end of id', function () {
+        return expectRejection(create(project, 'bob.class', appName, {}, events), error);
+    });
 });
 
 describe('create end-to-end', function () {
 
-    beforeEach(function () {
-        shell.rm('-rf', project);
-        shell.mkdir('-p', tmpDir);
-    });
-
-    afterEach(function () {
-        process.chdir(path.join(__dirname, '..')); // Needed to rm the dir on Windows.
-        shell.rm('-rf', tmpDir);
-    });
-
     function checkProject () {
         // Check if top level dirs exist.
         var dirs = ['hooks', 'platforms', 'plugins', 'www'];
@@ -184,9 +124,8 @@
         var configXml = new ConfigParser(path.join(project, 'config.xml'));
         expect(configXml.packageName()).toEqual(appId);
         expect(configXml.version()).toEqual('1.0.0');
-        delete require.cache[require.resolve(path.join(project, 'package.json'))];
         // Check that we got package.json (the correct one)
-        var pkjson = require(path.join(project, 'package.json'));
+        var pkjson = requireFresh(path.join(project, 'package.json'));
         // Pkjson.displayName should equal config's name.
         expect(pkjson.displayName).toEqual(appName);
         expect(pkjson.valid).toEqual('true');
@@ -195,173 +134,174 @@
         expect(configXml.description()).toEqual('this is the correct config.xml');
     }
 
-    it('should successfully run without template and use default hello-world app', function (done) {
+    it('should successfully run without template and use default hello-world app', function () {
         // Create a real project with no template
         // use default cordova-app-hello-world app
         return create(project, appId, appName, {}, events)
             .then(checkProject)
             .then(function () {
-                delete require.cache[require.resolve(path.join(project, 'package.json'))];
-                var pkgJson = require(path.join(project, 'package.json'));
+                var pkgJson = requireFresh(path.join(project, 'package.json'));
                 // confirm default hello world app copies over package.json and it matched appId
                 expect(pkgJson.name).toEqual(appId);
-            }).fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
+            });
+    });
 
-    it('should successfully run with Git URL', function (done) {
-        // Create a real project with gitURL as template
-        return create(project, appId, appName, configGit, events)
-            .then(checkProject)
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
+    it('should successfully run with Git URL', function () {
+        // Create a real project with git URL as template
+        var config = {
+            lib: {
+                www: {
+                    url: 'https://github.com/apache/cordova-app-hello-world',
+                    template: true
+                }
+            }
+        };
+        return createWithMockFetch(project, appId, appName, config, events)
+            .then(fetchSpy => {
+                expect(fetchSpy).toHaveBeenCalledTimes(1);
+                expect(fetchSpy.calls.argsFor(0)[0]).toBe(config.lib.www.url);
             })
-            .fin(done);
-    }, 60000);
+            .then(checkProject);
+    });
 
-    it('should successfully run with NPM package and not use old cache of template on second create', function (done) {
-        var templatePkgJsonPath = path.join(global_config_path, 'node_modules', 'phonegap-template-vue-f7-tabs', 'package.json');
+    it('should successfully run with NPM package', function () {
         // Create a real project with npm module as template
-        // tests cache clearing of npm template
-        // uses phonegap-template-vue-f7-tabs
-        return create(project, appId, appName, configNPMold)
-            .then(checkProject)
-            .then(function () {
-                shell.rm('-rf', project);
-                delete require.cache[require.resolve(templatePkgJsonPath)];
-                var pkgJson = require(templatePkgJsonPath);
-                expect(pkgJson.version).toBe('1.0.0');
-                return create(project, appId, appName, configNPM);
-            }).then(function () {
-                delete require.cache[require.resolve(templatePkgJsonPath)];
-                var pkgJson = require(templatePkgJsonPath);
-                expect(semver.gt(pkgJson.version, '1.0.0')).toBeTruthy();
-            }).fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
-
-    it('should successfully run with template not having a package.json at toplevel', function (done) {
-        // Call cordova create with no args, should return help.
         var config = {
             lib: {
                 www: {
                     template: true,
-                    url: path.join(__dirname, 'templates', 'nopackage_json'),
-                    version: ''
+                    url: 'phonegap-template-vue-f7-tabs@1'
                 }
             }
         };
-        // Create a real project
+        return createWithMockFetch(project, appId, appName, config, events)
+            .then(fetchSpy => {
+                expect(fetchSpy).toHaveBeenCalledTimes(1);
+                expect(fetchSpy.calls.argsFor(0)[0]).toBe(config.lib.www.url);
+            })
+            .then(checkProject);
+    });
+
+    it('should successfully run with NPM package and explicitly fetch latest if no version is given', function () {
+        // Create a real project with npm module as template
+        // TODO fetch should be responsible for the cache busting part of this test
+        var config = {
+            lib: {
+                www: {
+                    template: true,
+                    url: 'phonegap-template-vue-f7-tabs'
+                }
+            }
+        };
+        return createWithMockFetch(project, appId, appName, config, events)
+            .then(fetchSpy => {
+                expect(fetchSpy).toHaveBeenCalledTimes(1);
+                expect(fetchSpy.calls.argsFor(0)[0]).toBe(config.lib.www.url + '@latest');
+            })
+            .then(checkProject);
+    });
+
+    it('should successfully run with template not having a package.json at toplevel', function () {
+        var config = {
+            lib: {
+                www: {
+                    template: true,
+                    url: path.join(__dirname, 'templates', 'nopackage_json')
+                }
+            }
+        };
         return create(project, appId, appName, config, events)
             .then(checkProject)
             .then(function () {
                 // Check that we got the right config.xml
                 var configXml = new ConfigParser(path.join(project, 'config.xml'));
                 expect(configXml.description()).toEqual('this is the very correct config.xml');
-            })
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
+            });
+    });
 
-    it('should successfully run with template having package.json and no sub directory', function (done) {
-        // Call cordova create with no args, should return help.
+    it('should successfully run with template having package.json and no sub directory', function () {
         var config = {
             lib: {
                 www: {
                     template: true,
-                    url: path.join(__dirname, 'templates', 'withpackage_json'),
-                    version: ''
+                    url: path.join(__dirname, 'templates', 'withpackage_json')
                 }
             }
         };
-        // Create a real project
         return create(project, appId, appName, config, events)
-            .then(checkProject)
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
+            .then(checkProject);
+    });
 
-    it('should successfully run with template having package.json, and subdirectory, and no package.json in subdirectory', function (done) {
-        // Call cordova create with no args, should return help.
+    it('should successfully run with template having package.json, and subdirectory, and no package.json in subdirectory', function () {
         var config = {
             lib: {
                 www: {
                     template: true,
-                    url: path.join(__dirname, 'templates', 'withsubdirectory'),
-                    version: ''
+                    url: path.join(__dirname, 'templates', 'withsubdirectory')
                 }
             }
         };
-
-        // Create a real project
         return create(project, appId, appName, config, events)
-            .then(checkProject)
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
+            .then(checkProject);
+    });
 
-    it('should successfully run with template having package.json, and subdirectory, and package.json in subdirectory', function (done) {
-        // Call cordova create with no args, should return help.
-        var config = configSubDirPkgJson;
-        return create(project, appId, appName, config, events)
-            .then(checkSubDir)
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
-
-    it('should successfully run config.xml in the www folder and move it outside', function (done) {
-        // Call cordova create with no args, should return help.
-        var config = configConfigInWww;
-        // Create a real project
-        return create(project, appId, appName, config, events)
-            .then(checkConfigXml)
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
-
-    it('should successfully run with www folder as the template', function (done) {
+    it('should successfully run with template having package.json, and subdirectory, and package.json in subdirectory', function () {
         var config = {
             lib: {
                 www: {
                     template: true,
-                    url: path.join(__dirname, 'templates', 'config_in_www', 'www'),
-                    version: ''
+                    url: path.join(__dirname, 'templates', 'withsubdirectory_package_json')
                 }
             }
         };
         return create(project, appId, appName, config, events)
-            .then(checkConfigXml)
-            .fail(function (err) {
-                console.log(err && err.stack);
-                expect(err).toBeUndefined();
-            })
-            .fin(done);
-    }, 60000);
+            .then(checkSubDir);
+    });
+
+    it('should successfully run config.xml in the www folder and move it outside', function () {
+        var config = {
+            lib: {
+                www: {
+                    template: true,
+                    url: path.join(__dirname, 'templates', 'config_in_www')
+                }
+            }
+        };
+        return create(project, appId, appName, config, events)
+            .then(checkConfigXml);
+    });
+
+    it('should successfully run with www folder as the template', function () {
+        var config = {
+            lib: {
+                www: {
+                    template: true,
+                    url: path.join(__dirname, 'templates', 'config_in_www', 'www')
+                }
+            }
+        };
+        return create(project, appId, appName, config, events)
+            .then(checkConfigXml);
+    });
+
+    it('should successfully run with existing, empty destination', function () {
+        shell.mkdir('-p', project);
+        return create(project, appId, appName, {}, events)
+            .then(checkProject);
+    });
 
     describe('when --link-to is provided', function () {
-        it('when passed www folder should not move www/config.xml, only copy and update', function (done) {
+        function allowSymlinkErrorOnWindows (err) {
+            const onWindows = process.platform.slice(0, 3) === 'win';
+            const isSymlinkError = err && String(err.message).startsWith('Symlinks on Windows');
+            if (onWindows && isSymlinkError) {
+                pending(err.message);
+            } else {
+                throw err;
+            }
+        }
+
+        it('when passed www folder should not move www/config.xml, only copy and update', function () {
             function checkSymWWW () {
                 // Check if top level dirs exist.
                 var dirs = ['hooks', 'platforms', 'plugins', 'www'];
@@ -403,28 +343,16 @@
                     www: {
                         template: true,
                         url: path.join(__dirname, 'templates', 'config_in_www', 'www'),
-                        version: '',
                         link: true
                     }
                 }
             };
             return create(project, appId, appName, config, events)
                 .then(checkSymWWW)
-                .fail(function (err) {
-                    if (process.platform.slice(0, 3) === 'win') {
-                        // Allow symlink error if not in admin mode
-                        expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
-                    } else {
-                        if (err) {
-                            console.log(err.stack);
-                        }
-                        expect(err).toBeUndefined();
-                    }
-                })
-                .fin(done);
-        }, 60000);
+                .catch(allowSymlinkErrorOnWindows);
+        });
 
-        it('with subdirectory should not update symlinked project/config.xml', function (done) {
+        it('with subdirectory should not update symlinked project/config.xml', function () {
             function checkSymSubDir () {
                 // Check if top level dirs exist.
                 var dirs = ['hooks', 'platforms', 'plugins', 'www'];
@@ -452,9 +380,8 @@
                 // Check that we got the right config.xml
                 expect(configXml.description()).toEqual('this is the correct config.xml');
 
-                delete require.cache[require.resolve(path.join(project, 'package.json'))];
                 // Check that we got package.json (the correct one) and it was changed
-                var pkjson = require(path.join(project, 'package.json'));
+                var pkjson = requireFresh(path.join(project, 'package.json'));
                 // Pkjson.name should equal config's id.
                 expect(pkjson.name).toEqual(appId.toLowerCase());
                 expect(pkjson.valid).toEqual('true');
@@ -464,28 +391,16 @@
                     www: {
                         template: true,
                         url: path.join(__dirname, 'templates', 'withsubdirectory_package_json'),
-                        version: '',
                         link: true
                     }
                 }
             };
             return create(project, appId, appName, config, events)
                 .then(checkSymSubDir)
-                .fail(function (err) {
-                    if (process.platform.slice(0, 3) === 'win') {
-                        // Allow symlink error if not in admin mode
-                        expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
-                    } else {
-                        if (err) {
-                            console.log(err.stack);
-                        }
-                        expect(err).toBeUndefined();
-                    }
-                })
-                .fin(done);
-        }, 60000);
+                .catch(allowSymlinkErrorOnWindows);
+        });
 
-        it('with no config should create one and update it', function (done) {
+        it('with no config should create one and update it', function () {
             function checkSymNoConfig () {
                 // Check if top level dirs exist.
                 var dirs = ['hooks', 'platforms', 'plugins', 'www'];
@@ -514,26 +429,78 @@
                     www: {
                         template: true,
                         url: path.join(__dirname, 'templates', 'noconfig'),
-                        version: '',
                         link: true
                     }
                 }
             };
             return create(project, appId, appName, config, events)
                 .then(checkSymNoConfig)
-                .fail(function (err) {
-                    if (process.platform.slice(0, 3) === 'win') {
-                        // Allow symlink error if not in admin mode
-                        expect(err.message).toBe('Symlinks on Windows require Administrator privileges');
-                    } else {
-                        if (err) {
-                            console.log(err.stack);
-                        }
-                        expect(err).toBeUndefined();
-                    }
-                })
-                .fin(done);
-        }, 60000);
+                .catch(allowSymlinkErrorOnWindows);
+        });
 
     });
 });
+
+describe('when shit happens', function () {
+    it('should fail when dir is missing', function () {
+        return expectRejection(
+            create(null, appId, appName, {}, events),
+            new CordovaError('Directory not specified')
+        );
+    });
+
+    it('should fail when dir already exists', function () {
+        return expectRejection(
+            create(__dirname, appId, appName, {}, events),
+            new CordovaError('Path already exists and is not empty')
+        );
+    });
+
+    it('should fail when destination is inside template', function () {
+        const config = {
+            lib: {
+                www: {
+                    url: path.join(tmpDir, 'template')
+                }
+            }
+        };
+        const destination = path.join(config.lib.www.url, 'destination');
+        return expectRejection(
+            create(destination, appId, appName, config, events),
+            new CordovaError('inside the template')
+        );
+    });
+
+    it('should fail when fetch fails', function () {
+        const config = {
+            lib: {
+                www: {
+                    template: true,
+                    url: 'http://localhost:123456789/cordova-create'
+                }
+            }
+        };
+        const fetchError = new Error('Fetch fail');
+        const failingFetch = jasmine.createSpy('failingFetch')
+            .and.callFake(() => Promise.reject(fetchError));
+        return expectRejection(
+            createWith({fetch: failingFetch})(project, appId, appName, config),
+            fetchError
+        );
+
+    });
+
+    it('should fail when template does not exist', function () {
+        const config = {
+            lib: {
+                www: {
+                    url: path.join(__dirname, 'doesnotexist')
+                }
+            }
+        };
+        return expectRejection(
+            create(project, appId, appName, config, events),
+            new CordovaError('Could not find directory')
+        );
+    });
+});
diff --git a/spec/helpers.js b/spec/helpers.js
index 174a7ae..daf6722 100644
--- a/spec/helpers.js
+++ b/spec/helpers.js
@@ -17,133 +17,61 @@
     under the License.
 */
 
-var path = require('path'),
-    fs = require('fs'),
-    shell = require('shelljs'),
-    os = require('os'),
-    ConfigParser = require('cordova-common').ConfigParser;
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
 
-// Just use Android everywhere; we're mocking out any calls to the `android` binary.
-module.exports.testPlatform = 'android';
+const rewire = require('rewire');
+const shell = require('shelljs');
 
-function getConfigPath (dir) {
-    // if path ends with 'config.xml', return it
-    if (dir.indexOf('config.xml') == dir.length - 10) {
-        return dir;
-    }
-    // otherwise, add 'config.xml' to the end of it
-    return path.join(dir, 'config.xml');
+// Disable regular console output during tests
+const CordovaLogger = require('cordova-common').CordovaLogger;
+CordovaLogger.get().setLevel(CordovaLogger.ERROR);
+
+// Temporary directory to use for all tests
+const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cordova-create-tests-'));
+
+// Returns a version of create with its local scope rewired
+const create = rewire('..');
+function createWith (rewiring) {
+    return (...args) => create.__with__(rewiring)(() => create(...args));
 }
 
-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;
-};
+// Calls create with mocked fetch to not depend on the outside world
+function createWithMockFetch (dir, id, name, cfg, events) {
+    const mockFetchDest = path.join(tmpDir, 'mockFetchDest');
+    const templateDir = path.dirname(require.resolve('cordova-app-hello-world'));
+    const fetchSpy = jasmine.createSpy('fetchSpy')
+        .and.callFake(() => Promise.resolve(mockFetchDest));
 
-// Returns the platform that should be used for testing on this host platform.
-/*
-var host = os.platform();
-if (host.match(/win/)) {
-    module.exports.testPlatform = 'wp8';
-} else if (host.match(/darwin/)) {
-    module.exports.testPlatform = 'ios';
-} else {
-    module.exports.testPlatform = 'android';
+    shell.cp('-R', templateDir, mockFetchDest);
+    return createWith({fetch: fetchSpy})(dir, id, name, cfg, events)
+        .then(() => fetchSpy);
 }
-*/
 
-module.exports.setEngineSpec = function (appPath, engine, spec) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath);
+// Expect promise to get rejected with a reason matching expectedReason
+function expectRejection (promise, expectedReason) {
+    return promise.then(
+        () => fail('Expected promise to be rejected'),
+        reason => {
+            if (expectedReason instanceof Error) {
+                expect(reason instanceof expectedReason.constructor).toBeTruthy();
+                expect(reason.message).toContain(expectedReason.message);
+            } else if (typeof expectedReason === 'function') {
+                expect(expectedReason(reason)).toBeTruthy();
+            } else if (expectedReason !== undefined) {
+                expect(reason).toBe(expectedReason);
+            } else {
+                expect().nothing();
+            }
+        });
+}
 
-    parser.removeEngine(engine);
-    parser.addEngine(engine, spec);
-    parser.write();
-};
-
-module.exports.getEngineSpec = function (appPath, engine) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath),
-        engines = parser.getEngines();
-
-    for (var i = 0; i < engines.length; i++) {
-        if (engines[i].name === module.exports.testPlatform) {
-            return engines[i].spec;
-        }
-    }
-    return null;
-};
-
-module.exports.removeEngine = function (appPath, engine) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath);
-
-    parser.removeEngine(module.exports.testPlatform);
-    parser.write();
-};
-
-module.exports.setPluginSpec = function (appPath, plugin, spec) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath),
-        p = parser.getPlugin(plugin),
-        variables = [];
-
-    if (p) {
-        parser.removePlugin(p.name);
-        if (p.variables.length && p.variables.length > 0) {
-            variables = p.variables;
-        }
-    }
-
-    parser.addPlugin({ 'name': plugin, 'spec': spec }, variables);
-    parser.write();
-};
-
-module.exports.getPluginSpec = function (appPath, plugin) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath),
-        p = parser.getPlugin(plugin);
-
-    if (p) {
-        return p.spec;
-    }
-    return null;
-};
-
-module.exports.getPluginVariable = function (appPath, plugin, variable) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath),
-        p = parser.getPlugin(plugin);
-
-    if (p && p.variables) {
-        return p.variables[variable];
-    }
-    return null;
-};
-
-module.exports.removePlugin = function (appPath, plugin) {
-    appPath = getConfigPath(appPath);
-    var parser = new ConfigParser(appPath);
-
-    parser.removePlugin(plugin);
-    parser.write();
-};
-
-module.exports.getConfigContent = function (appPath) {
-    var configFile = path.join(appPath, 'config.xml');
-    return fs.readFileSync(configFile, 'utf-8');
-};
-
-module.exports.writeConfigContent = function (appPath, configContent) {
-    var configFile = path.join(appPath, 'config.xml');
-    fs.writeFileSync(configFile, configContent, 'utf-8');
+module.exports = {
+    tmpDir,
+    createWith,
+    createWithMockFetch,
+    expectRejection
 };
 
 // Add the toExist matcher.