From 7c9884ff22c40d93591b89848da052fa345b47ee Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 24 Aug 2016 22:36:08 +0200 Subject: [PATCH 1/5] Start unit tests using supertest --- bin/web.js | 30 +++++++------- lib/backends/github.js | 33 ++++++++++------ lib/index.js | 32 +++++++++++++-- lib/nuts.js | 90 ++++++++++++++++++++++++++---------------- package.json | 7 +++- test/all.js | 6 +++ test/app.js | 8 ++++ test/download.js | 30 ++++++++++++++ test/platforms.js | 1 - test/win-releases.js | 1 - 10 files changed, 169 insertions(+), 69 deletions(-) create mode 100644 test/all.js create mode 100644 test/app.js create mode 100644 test/download.js diff --git a/bin/web.js b/bin/web.js index 9e7c0aab..a161b2a5 100644 --- a/bin/web.js +++ b/bin/web.js @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + var express = require('express'); var uuid = require('uuid'); var basicAuth = require('basic-auth'); @@ -18,15 +20,15 @@ if (process.env.ANALYTICS_TOKEN) { } var myNuts = nuts.Nuts({ - repository: process.env.GITHUB_REPO, - token: process.env.GITHUB_TOKEN, - endpoint: process.env.GITHUB_ENDPOINT, - username: process.env.GITHUB_USERNAME, - password: process.env.GITHUB_PASSWORD, - timeout: process.env.VERSIONS_TIMEOUT, - cache: process.env.VERSIONS_CACHE, + repository: process.env.GITHUB_REPO, + token: process.env.GITHUB_TOKEN, + endpoint: process.env.GITHUB_ENDPOINT, + username: process.env.GITHUB_USERNAME, + password: process.env.GITHUB_PASSWORD, + timeout: process.env.VERSIONS_TIMEOUT, + cache: process.env.VERSIONS_CACHE, refreshSecret: process.env.GITHUB_SECRET, - proxyAssets: !Boolean(process.env.DONT_PROXY_ASSETS) + proxyAssets: !process.env.DONT_PROXY_ASSETS }); // Control access to API @@ -35,28 +37,28 @@ myNuts.before('api', function(access, next) { function unauthorized() { next(new Error('Invalid username/password for API')); - }; + } var user = basicAuth(access.req); if (!user || !user.name || !user.pass) { return unauthorized(); - }; + } if (user.name === apiAuth.username && user.pass === apiAuth.password) { return next(); } else { return unauthorized(); - }; + } }); // Log download myNuts.before('download', function(download, next) { - console.log('download', download.platform.filename, "for version", download.version.tag, "on channel", download.version.channel, "for", download.platform.type); + console.log('download', download.platform.filename, 'for version', download.version.tag, 'on channel', download.version.channel, 'for', download.platform.type); next(); }); myNuts.after('download', function(download, next) { - console.log('downloaded', download.platform.filename, "for version", download.version.tag, "on channel", download.version.channel, "for", download.platform.type); + console.log('downloaded', download.platform.filename, 'for version', download.version.tag, 'on channel', download.version.channel, 'for', download.platform.type); // Track on segment if enabled if (analytics) { @@ -92,7 +94,7 @@ app.use(myNuts.router); // Error handling app.use(function(req, res, next) { - res.status(404).send("Page not found"); + res.status(404).send('Page not found'); }); app.use(function(err, req, res, next) { var msg = err.message || err; diff --git a/lib/backends/github.js b/lib/backends/github.js index 3eaf155c..dae8209e 100644 --- a/lib/backends/github.js +++ b/lib/backends/github.js @@ -1,14 +1,13 @@ var _ = require('lodash'); var Q = require('q'); var util = require('util'); -var destroy = require('destroy'); var GitHub = require('octocat'); var request = require('request'); -var Buffer = require('buffer').Buffer; var githubWebhook = require('github-webhook-handler'); var Backend = require('./backend'); + function GitHubBackend() { var that = this; Backend.apply(this, arguments); @@ -17,19 +16,15 @@ function GitHubBackend() { proxyAssets: true }); - if ((!this.opts.username || !this.opts.password) && (!this.opts.token)) { - throw new Error('GitHub backend require "username" and "token" options'); - } - this.client = new GitHub({ - token: this.opts.token, + token: this.opts.token, endpoint: this.opts.endpoint, username: this.opts.username, password: this.opts.password }); this.ghrepo = this.client.repo(this.opts.repository); - this.releases = this.memoize(this._releases); + this.releases = this.memoize(this.releases); // GitHub webhook to refresh list of versions this.webhookHandler = githubWebhook({ @@ -45,15 +40,24 @@ function GitHubBackend() { } util.inherits(GitHubBackend, Backend); -// List all releases for this repository -GitHubBackend.prototype._releases = function() { +/** + * List all releases for this repository + * @return {Promise>} + */ +GitHubBackend.prototype.releases = function() { return this.ghrepo.releases() .then(function(page) { return page.all(); }); }; -// Return stream for an asset +/** + * Return stream for an asset + * @param {Asset} asset + * @param {Request} req + * @param {Response} res + * @return {Promise}? + */ GitHubBackend.prototype.serveAsset = function(asset, req, res) { if (!this.opts.proxyAssets) { res.redirect(asset.raw.browser_download_url); @@ -62,7 +66,11 @@ GitHubBackend.prototype.serveAsset = function(asset, req, res) { } }; -// Return stream for an asset +/** + * Return stream for an asset + * @param {Asset} asset + * @return {Promise} + */ GitHubBackend.prototype.getAssetStream = function(asset) { var headers = { 'User-Agent': 'nuts', @@ -88,5 +96,4 @@ GitHubBackend.prototype.getAssetStream = function(asset) { })); }; - module.exports = GitHubBackend; diff --git a/lib/index.js b/lib/index.js index bbe28f1c..1fd69e0e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,35 @@ +var express = require('express'); + var Nuts = require('./nuts'); var platforms = require('./utils/platforms'); var winReleases = require('./utils/win-releases'); +/** + * Create an express application with Nuts binded to it. + * This is mostly used for unit testing. + * + * @param {Object} options + * @return {Express.Application} app + */ +function createApp(options) { + var app = express(); + var nuts = Nuts(options); + + app.use(nuts.router); + + app.use(function(err, req, res, next) { + res.status(err.statusCode || 500); + res.send({ + message: err.message + }); + }); + + return app; +} + module.exports = { - Nuts: Nuts, - platforms: platforms, - winReleases: winReleases + Nuts: Nuts, + platforms: platforms, + winReleases: winReleases, + createApp: createApp }; diff --git a/lib/nuts.js b/lib/nuts.js index 1642158b..113e9aee 100644 --- a/lib/nuts.js +++ b/lib/nuts.js @@ -5,6 +5,7 @@ var urljoin = require('urljoin.js'); var Understudy = require('understudy'); var express = require('express'); var useragent = require('express-useragent'); +var createError = require('http-errors'); var BACKENDS = require('./backends'); var Versions = require('./versions'); @@ -90,13 +91,22 @@ Nuts.prototype._init = function() { return that.backend.init(); }) .then(function() { - if (!that.opts.preFetch) return + if (!that.opts.preFetch) { + return; + } + return that.versions.list(); }); -} +}; -// Perform a hook using promised functions +/** + * Perform a hook using promised functions + * @param {String} name + * @param {Object} arg + * @param {Function} fn + * @return {Promise} + */ Nuts.prototype.performQ = function(name, arg, fn) { var that = this; fn = fn || function() { }; @@ -109,10 +119,17 @@ Nuts.prototype.performQ = function(name, arg, fn) { .then(function() { next(); }, next); - }) + }); }; -// Serve an asset to the response +/** + * Serve an asset to the response + * @param {Request} req + * @param {Response} res + * @param {Version} version + * @param {String} asset + * @return {Promise} + */ Nuts.prototype.serveAsset = function(req, res, version, asset) { var that = this; @@ -123,18 +140,18 @@ Nuts.prototype.serveAsset = function(req, res, version, asset) { version: version, platform: asset }, function() { - return that.backend.serveAsset(asset, req, res) + return that.backend.serveAsset(asset, req, res); }); }); }; // Handler for download routes Nuts.prototype.onDownload = function(req, res, next) { - var that = this; - var channel = req.params.channel; - var platform = req.params.platform; - var tag = req.params.tag || 'latest'; - var filename = req.params.filename; + var that = this; + var channel = req.params.channel; + var platform = req.params.platform; + var tag = req.params.tag || 'latest'; + var filename = req.params.filename; var filetypeWanted = req.query.filetype; // When serving a specific file, platform is not required @@ -147,13 +164,17 @@ Nuts.prototype.onDownload = function(req, res, next) { if (req.useragent.isLinux64) platform = platforms.LINUX_64; } - if (!platform) return next(new Error('No platform specified and impossible to detect one')); + if (!platform) { + return next(createError(400, 'No platform specified and impossible to detect one')); + } } else { platform = null; } // If specific version, don't enforce a channel - if (tag != 'latest') channel = '*'; + if (tag != 'latest') { + channel = '*'; + } this.versions.resolve({ channel: channel, @@ -186,7 +207,10 @@ Nuts.prototype.onDownload = function(req, res, next) { }); } - if (!asset) throw new Error("No download available for platform "+platform+" for version "+version.tag+" ("+(channel || "beta")+")"); + if (!asset) { + throw createError(404, 'No download available for platform ' + platform + + ' for version ' + version.tag + ' (' + (channel || 'beta') + ')'); + } // Call analytic middleware, then serve return that.serveAsset(req, res, version, asset); @@ -214,7 +238,7 @@ Nuts.prototype.onUpdate = function(req, res, next) { var platform = req.params.platform; var channel = req.params.channel || '*'; var tag = req.params.version; - var filetype = req.query.filetype ? req.query.filetype : "zip"; + var filetype = req.query.filetype ? req.query.filetype : 'zip'; Q() .then(function() { @@ -238,13 +262,12 @@ Nuts.prototype.onUpdate = function(req, res, next) { notesSlice = [versions[0]]; } var releaseNotes = notes.merge(notesSlice, { includeTag: false }); - console.error(latest.tag); var gitFilePath = (channel === '*' ? '/../../../' : '/../../../../../'); res.status(200).send({ - "url": urljoin(fullUrl, gitFilePath, '/download/version/'+latest.tag+'/'+platform+'?filetype='+filetype), - "name": latest.tag, - "notes": releaseNotes, - "pub_date": latest.published_at.toISOString() + 'url': urljoin(fullUrl, gitFilePath, '/download/version/'+latest.tag+'/'+platform+'?filetype='+filetype), + 'name': latest.tag, + 'notes': releaseNotes, + 'pub_date': latest.published_at.toISOString() }); }) .fail(next); @@ -274,16 +297,16 @@ Nuts.prototype.onUpdateWin = function(req, res, next) { .then(function(versions) { // Update needed? var latest = _.first(versions); - if (!latest) throw new Error("Version not found"); + if (!latest) throw new Error('Version not found'); // File exists var asset = _.find(latest.platforms, { filename: 'RELEASES' }); - if (!asset) throw new Error("File not found"); + if (!asset) throw new Error('File not found'); - return that.backend.readAsset(asset) - .then(function(content) { + return that.backend.readAsset(asset) + .then(function(content) { var releases = winReleases.parse(content.toString('utf-8')); releases = _.chain(releases) @@ -301,9 +324,9 @@ Nuts.prototype.onUpdateWin = function(req, res, next) { var output = winReleases.generate(releases); res.header('Content-Length', output.length); - res.attachment("RELEASES"); + res.attachment('RELEASES'); res.send(output); - }); + }); }) .fail(next); }; @@ -322,21 +345,19 @@ Nuts.prototype.onServeNotes = function(req, res, next) { }) .then(function(versions) { var latest = _.first(versions); - - if (!latest) throw new Error('No versions matching'); + if (!latest) { + throw new Error('No versions matching'); + } res.format({ - 'text/plain': function(){ - res.send(notes.merge(versions)); - }, 'application/json': function(){ res.send({ - "notes": notes.merge(versions, { includeTag: false }), - "pub_date": latest.published_at.toISOString() + 'notes': notes.merge(versions, { includeTag: false }), + 'pub_date': latest.published_at.toISOString() }); }, 'default': function() { - res.send(releaseNotes); + res.send(notes.merge(versions)); } }); }) @@ -390,5 +411,4 @@ Nuts.prototype.onAPIAccessControl = function(req, res, next) { }, next); }; - module.exports = Nuts; diff --git a/package.json b/package.json index 3d1af7f9..b8a86d1f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "express-useragent": "0.1.9", "feed": "^0.3.0", "github-webhook-handler": "0.5.0", + "http-errors": "^1.5.0", "lodash": "3.7.0", "lru-diskcache": "1.1.1", "octocat": "0.10.2", @@ -27,8 +28,10 @@ "uuid": "2.0.1" }, "devDependencies": { + "expect": "^1.20.2", "mocha": "1.18.2", - "should": "7.0.4" + "should": "7.0.4", + "supertest": "^2.0.0" }, "bugs": { "url": "https://github.com/GitbookIO/nuts/issues" @@ -45,6 +48,6 @@ }, "scripts": { "start": "node bin/web.js", - "test": "mocha --reporter list" + "test": "mocha --reporter spec --timeout 600000 ./test/all.js" } } diff --git a/test/all.js b/test/all.js new file mode 100644 index 00000000..2d8571a9 --- /dev/null +++ b/test/all.js @@ -0,0 +1,6 @@ +require('should'); + +require('./platforms'); +require('./win-releases'); + +require('./download'); diff --git a/test/app.js b/test/app.js new file mode 100644 index 00000000..51fa4987 --- /dev/null +++ b/test/app.js @@ -0,0 +1,8 @@ +var nuts = require('../lib'); + +var app = nuts.createApp({ + repository: 'SamyPesse/nuts-testing', + token: process.env.GITHUB_TOKEN +}); + +module.exports = app; diff --git a/test/download.js b/test/download.js new file mode 100644 index 00000000..dff61f97 --- /dev/null +++ b/test/download.js @@ -0,0 +1,30 @@ +var testRequest = require('supertest'); +var app = require('./app'); + +describe('Download', function() { + var agent; + + before(function() { + agent = testRequest.agent(app); + }); + + describe('Latest version', function() { + + it('should fail if no user-agent to detect platform', function(done) { + agent + .get('/') + .expect(400, done); + }); + + it('should download windows file', function(done) { + agent + .get('/') + .set('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; Touch; rv:11.0) like Gecko') + .expect('Content-Length', '13') + .expect('Content-Disposition', 'attachment; filename=test.exe') + .expect(200, done); + }); + + }); + +}); diff --git a/test/platforms.js b/test/platforms.js index 7aadaecb..446c105c 100644 --- a/test/platforms.js +++ b/test/platforms.js @@ -1,4 +1,3 @@ -require('should'); var platforms = require('../lib/utils/platforms'); describe('Platforms', function() { diff --git a/test/win-releases.js b/test/win-releases.js index e06fac03..70b72da4 100644 --- a/test/win-releases.js +++ b/test/win-releases.js @@ -1,4 +1,3 @@ -require('should'); var winReleases = require('../lib/utils/win-releases'); describe('Windows RELEASES', function() { From fc432e9a0ef92458fb1d1179d835dbd98218fa73 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 24 Aug 2016 22:51:27 +0200 Subject: [PATCH 2/5] Start unit tests for update endpoints --- lib/nuts.js | 8 +++++--- test/all.js | 1 + test/download.js | 8 ++------ test/update.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 test/update.js diff --git a/lib/nuts.js b/lib/nuts.js index 113e9aee..bb795d54 100644 --- a/lib/nuts.js +++ b/lib/nuts.js @@ -242,8 +242,8 @@ Nuts.prototype.onUpdate = function(req, res, next) { Q() .then(function() { - if (!tag) throw new Error('Requires "version" parameter'); - if (!platform) throw new Error('Requires "platform" parameter'); + if (!tag) throw createError(400, 'Requires "version" parameter'); + if (!platform) throw createError(400, 'Requires "platform" parameter'); platform = platforms.detect(platform); @@ -255,7 +255,9 @@ Nuts.prototype.onUpdate = function(req, res, next) { }) .then(function(versions) { var latest = _.first(versions); - if (!latest || latest.tag == tag) return res.status(204).send('No updates'); + if (!latest || latest.tag == tag) { + return res.status(204).send('No updates'); + } var notesSlice = versions.slice(0, -1); if (versions.length === 1) { diff --git a/test/all.js b/test/all.js index 2d8571a9..024bf7d9 100644 --- a/test/all.js +++ b/test/all.js @@ -4,3 +4,4 @@ require('./platforms'); require('./win-releases'); require('./download'); +require('./update'); diff --git a/test/download.js b/test/download.js index dff61f97..20cc8204 100644 --- a/test/download.js +++ b/test/download.js @@ -1,12 +1,8 @@ -var testRequest = require('supertest'); +var request = require('supertest'); var app = require('./app'); describe('Download', function() { - var agent; - - before(function() { - agent = testRequest.agent(app); - }); + var agent = request.agent(app); describe('Latest version', function() { diff --git a/test/update.js b/test/update.js new file mode 100644 index 00000000..58f9cb76 --- /dev/null +++ b/test/update.js @@ -0,0 +1,30 @@ +var request = require('supertest'); +var expect = require('expect'); +var app = require('./app'); + +describe('Update', function() { + var agent = request.agent(app); + + describe('Squirrel.Mac (OS X)', function() { + + it('should return a 204 if using latest version', function(done) { + agent + .get('/update/osx/1.0.0') + .expect(204, done); + }); + + it('should return a 200 with json if using old version', function(done) { + agent + .get('/update/osx/0.9.0') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.0.0'); + expect(res.body.url).toExist(); + expect(res.body.pub_date).toExist(); + }) + .expect(200, done); + }); + + }); + +}); From 7c7ce122442823d9c76eccc1fb55e3f4141d0979 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 24 Aug 2016 23:09:57 +0200 Subject: [PATCH 3/5] Add more test for download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return a 404 when version doesn’t exist for a platform --- lib/nuts.js | 2 +- lib/versions.js | 14 ++++++++------ test/download.js | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/lib/nuts.js b/lib/nuts.js index bb795d54..c018c429 100644 --- a/lib/nuts.js +++ b/lib/nuts.js @@ -7,11 +7,11 @@ var express = require('express'); var useragent = require('express-useragent'); var createError = require('http-errors'); -var BACKENDS = require('./backends'); var Versions = require('./versions'); var notes = require('./utils/notes'); var platforms = require('./utils/platforms'); var winReleases = require('./utils/win-releases'); +var BACKENDS = require('./backends'); var API_METHODS = require('./api'); function getFullUrl(req) { diff --git a/lib/versions.js b/lib/versions.js index 467d96a1..366a6465 100644 --- a/lib/versions.js +++ b/lib/versions.js @@ -1,6 +1,6 @@ var _ = require('lodash'); -var Q = require('q'); var semver = require('semver'); +var createError = require('http-errors'); var platforms = require('./utils/platforms'); @@ -45,7 +45,7 @@ function normalizeVersion(release) { return { tag: normalizeTag(release.tag_name).split('-')[0], channel: extractChannel(release.tag_name), - notes: release.body || "", + notes: release.body || '', published_at: new Date(release.published_at), platforms: releasePlatforms }; @@ -64,7 +64,6 @@ function compareVersions(v1, v2) { function Versions(backend) { this.backend = backend; - } // List versions normalized @@ -93,7 +92,9 @@ Versions.prototype.filter = function(opts) { platform: null, channel: 'stable' }); - if (opts.platform) opts.platform = platforms.detect(opts.platform); + if (opts.platform) { + opts.platform = platforms.detect(opts.platform); + } return this.list() .then(function(versions) { @@ -117,7 +118,9 @@ Versions.prototype.resolve = function(opts) { return this.filter(opts) .then(function(versions) { var version = _.first(versions); - if (!version) throw new Error('Version not found: '+opts.tag); + if (!version) { + throw createError(404, 'Version not found: ' + opts.tag); + } return version; }); @@ -149,5 +152,4 @@ Versions.prototype.channels = function(opts) { }); }; - module.exports = Versions; diff --git a/test/download.js b/test/download.js index 20cc8204..495c008f 100644 --- a/test/download.js +++ b/test/download.js @@ -1,10 +1,14 @@ var request = require('supertest'); var app = require('./app'); +var MAC_USERAGENT = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-us)' + + ' AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19'; +var WIN_USERAGENT = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; Touch; rv:11.0) like Gecko'; + describe('Download', function() { var agent = request.agent(app); - describe('Latest version', function() { + describe('Latest version (/)', function() { it('should fail if no user-agent to detect platform', function(done) { agent @@ -15,12 +19,39 @@ describe('Download', function() { it('should download windows file', function(done) { agent .get('/') - .set('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; Touch; rv:11.0) like Gecko') + .set('User-Agent', WIN_USERAGENT) .expect('Content-Length', '13') .expect('Content-Disposition', 'attachment; filename=test.exe') .expect(200, done); }); + it('should download OS X file as DMG', function(done) { + agent + .get('/') + .set('User-Agent', MAC_USERAGENT) + .expect('Content-Length', '19') + .expect('Content-Disposition', 'attachment; filename=test-osx.dmg') + .expect(200, done); + }); + + }); + + describe('Previous version (/download/version/)', function() { + it('should not have a windows file to download', function(done) { + agent + .get('/download/version/0.9.0') + .set('User-Agent', WIN_USERAGENT) + .expect(404, done); + }); + + it('should download OS X file as DMG', function(done) { + agent + .get('/download/version/0.9.0') + .set('User-Agent', MAC_USERAGENT) + .expect('Content-Length', '19') + .expect('Content-Disposition', 'attachment; filename=test-osx.dmg') + .expect(200, done); + }); }); }); From 6853c6508ba90112288046a50fcc808d6f3ce935 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Thu, 25 Aug 2016 12:06:25 +0200 Subject: [PATCH 4/5] Fix update for channel --- lib/nuts.js | 35 ++++++++++++------- lib/versions.js | 91 ++++++++++++++++++++++++++++++++++++++---------- test/all.js | 3 ++ test/app.js | 8 ----- test/download.js | 2 +- test/testing.js | 14 ++++++++ test/update.js | 82 +++++++++++++++++++++++++++++++++++-------- test/versions.js | 39 +++++++++++++++++++++ 8 files changed, 219 insertions(+), 55 deletions(-) delete mode 100644 test/app.js create mode 100644 test/testing.js create mode 100644 test/versions.js diff --git a/lib/nuts.js b/lib/nuts.js index c018c429..42e9b092 100644 --- a/lib/nuts.js +++ b/lib/nuts.js @@ -235,38 +235,49 @@ Nuts.prototype.onUpdateRedirect = function(req, res, next) { Nuts.prototype.onUpdate = function(req, res, next) { var that = this; var fullUrl = getFullUrl(req); + var platform = req.params.platform; - var channel = req.params.channel || '*'; + var channel = req.params.channel || 'stable'; var tag = req.params.version; var filetype = req.query.filetype ? req.query.filetype : 'zip'; Q() .then(function() { - if (!tag) throw createError(400, 'Requires "version" parameter'); - if (!platform) throw createError(400, 'Requires "platform" parameter'); + if (!tag) { + throw createError(400, 'Requires "version" parameter'); + } + if (!platform) { + throw createError(400, 'Requires "platform" parameter'); + } platform = platforms.detect(platform); return that.versions.filter({ tag: '>='+tag, platform: platform, - channel: channel + channel: channel, + stripChannel: true }); }) .then(function(versions) { - var latest = _.first(versions); + var latest = versions[0]; + + // Already using latest version? if (!latest || latest.tag == tag) { return res.status(204).send('No updates'); } - var notesSlice = versions.slice(0, -1); - if (versions.length === 1) { - notesSlice = [versions[0]]; - } + // Extract release notes from all versions in range + var notesSlice = versions.length === 1? [versions[0]] : versions.slice(0, -1); var releaseNotes = notes.merge(notesSlice, { includeTag: false }); - var gitFilePath = (channel === '*' ? '/../../../' : '/../../../../../'); - res.status(200).send({ - 'url': urljoin(fullUrl, gitFilePath, '/download/version/'+latest.tag+'/'+platform+'?filetype='+filetype), + + // URL for download should be absolute + var gitFilePath = (req.params.channel ? '/../../../../../' : '/../../../' ); + + res.status(200) + .send({ + 'url': urljoin(fullUrl, gitFilePath, + '/download/version/' + latest.tag + '/' + platform + '?filetype=' + filetype), 'name': latest.tag, 'notes': releaseNotes, 'pub_date': latest.published_at.toISOString() diff --git a/lib/versions.js b/lib/versions.js index 366a6465..366c0c66 100644 --- a/lib/versions.js +++ b/lib/versions.js @@ -4,13 +4,21 @@ var createError = require('http-errors'); var platforms = require('./utils/platforms'); -// Normalize tag name +/** + * Normalize a tag name to remove the prefix "v" + * @param {String} tag + * @return {String} tag + */ function normalizeTag(tag) { if (tag[0] == 'v') tag = tag.slice(1); return tag; } -// Extract channel of version +/** + * Extract channel of tag name + * @param {String} tag + * @return {String} channel + */ function extractChannel(tag) { var suffix = tag.split('-')[1]; if (!suffix) return 'stable'; @@ -18,10 +26,25 @@ function extractChannel(tag) { return suffix.split('.')[0]; } -// Normalize a release to a version +/** + * Strip channel from a tag name + * @param {String} tag + * @return {String} tag + */ +function stripChannel(tag) { + return tag.split('-')[0]; +} + +/** + * Normalize a release to a version + * @param {Object} release + * @return {Version} version? + */ function normalizeVersion(release) { // Ignore draft - if (release.draft) return null; + if (release.draft) { + return null; + } var downloadCount = 0; var releasePlatforms = _.chain(release.assets) @@ -43,15 +66,20 @@ function normalizeVersion(release) { .value(); return { - tag: normalizeTag(release.tag_name).split('-')[0], - channel: extractChannel(release.tag_name), - notes: release.body || '', + tag: normalizeTag(release.tag_name), + channel: extractChannel(release.tag_name), + notes: release.body || '', published_at: new Date(release.published_at), - platforms: releasePlatforms + platforms: releasePlatforms }; } -// Compare two version +/** + * Compare two version + * @param {Version} v1 + * @param {Version} v2 + * @return {Number} + */ function compareVersions(v1, v2) { if (semver.gt(v1.tag, v2.tag)) { return -1; @@ -85,13 +113,22 @@ Versions.prototype.get = function(tag) { }); }; -// Filter versions with criterias +/** + * Filter versions with criterias + * @param {String} opts.tag? : tag name filter to satisfy (ex: ">=2.0.0") + * @param {String} opts.platform? : name of the platform supported by the version + * @param {String} opts.channel? : only list version of this channel ("*" for all) + * @param {Boolean} opts.stripChannel : compare tag name withotu the channel + * @return {Promise>} versions + */ Versions.prototype.filter = function(opts) { opts = _.defaults(opts || {}, { - tag: 'latest', - platform: null, - channel: 'stable' + tag: 'latest', + platform: null, + stripChannel: false, + channel: 'stable' }); + if (opts.platform) { opts.platform = platforms.detect(opts.platform); } @@ -101,19 +138,32 @@ Versions.prototype.filter = function(opts) { return _.chain(versions) .filter(function(version) { // Check channel - if (opts.channel != '*' && version.channel != opts.channel) return false; + if (opts.channel !== '*' && version.channel != opts.channel) { + return false; + } // Not available for requested paltform - if (opts.platform && !platforms.satisfies(opts.platform, _.pluck(version.platforms, 'type'))) return false; + if (opts.platform && !platforms.satisfies(opts.platform, _.pluck(version.platforms, 'type'))) { + return false; + } // Check tag satisfies request version - return opts.tag == 'latest' || semver.satisfies(version.tag, opts.tag); + var tagName = version.tag; + if (opts.stripChannel) { + tagName = stripChannel(tagName); + } + + return (opts.tag == 'latest' || semver.satisfies(tagName, opts.tag)); }) .value(); }); }; -// Resolve a platform, by filtering then taking the first result +/** + * Resolve a platform, by filtering then taking the first result + * @param {Object} opts + * @return {Promise} version + */ Versions.prototype.resolve = function(opts) { return this.filter(opts) .then(function(versions) { @@ -126,8 +176,11 @@ Versions.prototype.resolve = function(opts) { }); }; -// List all channels from releases -Versions.prototype.channels = function(opts) { +/** + * List all channels from releases + * @return {Promise} + */ +Versions.prototype.channels = function() { return this.list() .then(function(versions) { var channels = {}; diff --git a/test/all.js b/test/all.js index 024bf7d9..77878c48 100644 --- a/test/all.js +++ b/test/all.js @@ -1,7 +1,10 @@ require('should'); +// Sync tests require('./platforms'); require('./win-releases'); +// Require a backend +require('./versions'); require('./download'); require('./update'); diff --git a/test/app.js b/test/app.js deleted file mode 100644 index 51fa4987..00000000 --- a/test/app.js +++ /dev/null @@ -1,8 +0,0 @@ -var nuts = require('../lib'); - -var app = nuts.createApp({ - repository: 'SamyPesse/nuts-testing', - token: process.env.GITHUB_TOKEN -}); - -module.exports = app; diff --git a/test/download.js b/test/download.js index 495c008f..11c0ed09 100644 --- a/test/download.js +++ b/test/download.js @@ -1,5 +1,5 @@ var request = require('supertest'); -var app = require('./app'); +var app = require('./testing').app; var MAC_USERAGENT = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-us)' + ' AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19'; diff --git a/test/testing.js b/test/testing.js new file mode 100644 index 00000000..f5a0557c --- /dev/null +++ b/test/testing.js @@ -0,0 +1,14 @@ +var nuts = require('../lib'); + +var config = { + repository: 'SamyPesse/nuts-testing', + token: process.env.GITHUB_TOKEN +}; + +var instance = nuts.Nuts(config); +var app = nuts.createApp(config); + +module.exports = { + app: app, + nuts: instance +}; diff --git a/test/update.js b/test/update.js index 58f9cb76..96e3a771 100644 --- a/test/update.js +++ b/test/update.js @@ -1,30 +1,82 @@ var request = require('supertest'); var expect = require('expect'); -var app = require('./app'); + +var app = require('./testing').app; describe('Update', function() { var agent = request.agent(app); describe('Squirrel.Mac (OS X)', function() { - it('should return a 204 if using latest version', function(done) { - agent - .get('/update/osx/1.0.0') - .expect(204, done); + describe('/update/osx/', function() { + it('should return a 204 if using latest version', function(done) { + agent + .get('/update/osx/1.0.0') + .expect(204, done); + }); + + it('should return a 200 with json if using old stable version', function(done) { + agent + .get('/update/osx/0.9.0') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.0.0'); + expect(res.body.url).toExist(); + expect(res.body.pub_date).toExist(); + }) + .expect(200, done); + }); + + it('should return a 200 with json if using old beta version (1)', function(done) { + agent + .get('/update/osx/0.9.1-beta') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.0.0'); + }) + .expect(200, done); + }); + + it('should return a 200 with json if using old beta version (2)', function(done) { + agent + .get('/update/osx/0.9.1-beta.1') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.0.0'); + }) + .expect(200, done); + }); }); - it('should return a 200 with json if using old version', function(done) { - agent - .get('/update/osx/0.9.0') - .expect('Content-Type', /json/) - .expect(function(res) { - expect(res.body.name).toBe('1.0.0'); - expect(res.body.url).toExist(); - expect(res.body.pub_date).toExist(); - }) - .expect(200, done); + describe('/update/channel/beta/osx', function() { + it('should update from 0.9.1-beta to 1.0.1-beta.0', function(done) { + agent + .get('/update/channel/beta/osx/0.9.1-beta') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.0.1-beta.0'); + }) + .expect(200, done); + }); + + it('should not update from 1.0.1-beta.0', function(done) { + agent + .get('/update/channel/beta/osx/1.0.1-beta.0') + .expect(204, done); + }); }); + describe('/update/channel/alpha/osx', function() { + it('should update from 0.9.1-beta to 1.1.0-alpha.0', function(done) { + agent + .get('/update/channel/alpha/osx/0.9.1-beta') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.1.0-alpha.0'); + }) + .expect(200, done); + }); + }); }); }); diff --git a/test/versions.js b/test/versions.js new file mode 100644 index 00000000..c102584f --- /dev/null +++ b/test/versions.js @@ -0,0 +1,39 @@ +var expect = require('expect'); +var versions = require('./testing').nuts.versions; + +describe('Versions', function() { + + describe('.list', function() { + it('should list all versions', function() { + return versions.list() + .then(function(out) { + expect(out.length).toEqual(6); + }); + }); + }); + + describe('.filter', function() { + + it('should filter correctly by tag name', function() { + return versions.filter({ tag: '>=0.9.0' }) + .then(function(out) { + expect(out.length).toEqual(2); + expect(out[0].tag).toEqual('1.0.0'); + expect(out[1].tag).toEqual('0.9.0'); + }); + }); + + it('should filter correctly by tag name (stripChannel)', function() { + return versions.filter({ + tag: '>=0.9.0', + channel: '*', + stripChannel: true + }) + .then(function(out) { + expect(out.length).toEqual(6); + }); + }); + + }); + +}); From cd964b532042be50dc75105b767afa0ce9c87ab3 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Thu, 25 Aug 2016 12:16:56 +0200 Subject: [PATCH 5/5] Add test for alpha -> stable on normal update --- package.json | 2 +- test/update.js | 10 ++++++++++ test/versions.js | 6 +++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b8a86d1f..8e036188 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,6 @@ }, "scripts": { "start": "node bin/web.js", - "test": "mocha --reporter spec --timeout 600000 ./test/all.js" + "test": "mocha --bail --reporter spec --timeout 600000 ./test/all.js" } } diff --git a/test/update.js b/test/update.js index 96e3a771..f8603bed 100644 --- a/test/update.js +++ b/test/update.js @@ -46,6 +46,16 @@ describe('Update', function() { }) .expect(200, done); }); + + it('should return a 200 with json if using old alpha version (1)', function(done) { + agent + .get('/update/osx/0.9.2-alpha.2') + .expect('Content-Type', /json/) + .expect(function(res) { + expect(res.body.name).toBe('1.0.0'); + }) + .expect(200, done); + }); }); describe('/update/channel/beta/osx', function() { diff --git a/test/versions.js b/test/versions.js index c102584f..7f99805f 100644 --- a/test/versions.js +++ b/test/versions.js @@ -7,7 +7,7 @@ describe('Versions', function() { it('should list all versions', function() { return versions.list() .then(function(out) { - expect(out.length).toEqual(6); + expect(out.length).toEqual(7); }); }); }); @@ -25,12 +25,12 @@ describe('Versions', function() { it('should filter correctly by tag name (stripChannel)', function() { return versions.filter({ - tag: '>=0.9.0', + tag: '>=1.0.0', channel: '*', stripChannel: true }) .then(function(out) { - expect(out.length).toEqual(6); + expect(out.length).toEqual(3); }); });