diff --git a/dashboard/public/js/client.js b/dashboard/public/js/client.js index 442d22e01..e85a8d12c 100644 --- a/dashboard/public/js/client.js +++ b/dashboard/public/js/client.js @@ -2314,7 +2314,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout Client.prototype.updateApplink = function (id, data, callback) { post('/api/v1/applinks/' + id, data, null, function (error, data, status) { if (error) return callback(error); - if (status !== 202) return callback(new ClientError(status, data)); + if (status !== 200) return callback(new ClientError(status, data)); callback(null); }); diff --git a/src/applinks.js b/src/applinks.js index f4bd4f078..e57928c0b 100644 --- a/src/applinks.js +++ b/src/applinks.js @@ -7,7 +7,6 @@ exports = module.exports = { get, update, del, - getIcon }; const assert = require('assert'), @@ -55,6 +54,25 @@ function validateUpstreamUri(upstreamUri) { return null; } +function validateAccessRestriction(accessRestriction) { + assert.strictEqual(typeof accessRestriction, 'object'); + + if (accessRestriction === null) return null; + + if (accessRestriction.users) { + if (!Array.isArray(accessRestriction.users)) return new BoxError(BoxError.BAD_FIELD, 'users array property required'); + if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new BoxError(BoxError.BAD_FIELD, 'All users have to be strings'); + } + + if (accessRestriction.groups) { + if (!Array.isArray(accessRestriction.groups)) return new BoxError(BoxError.BAD_FIELD, 'groups array property required'); + if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new BoxError(BoxError.BAD_FIELD, 'All groups have to be strings'); + } + + // TODO: maybe validate if the users and groups actually exist + return null; +} + async function list() { const results = await database.query(`SELECT ${APPLINKS_FIELDS} FROM applinks ORDER BY upstreamUri`); @@ -67,20 +85,18 @@ async function listByUser(user) { assert.strictEqual(typeof user, 'object'); const result = await list(); - return result.filter((app) => apps.canAccess(app, user)); + return result.filter((link) => apps.canAccess(link, user)); } -async function detectMetaInfo(applink) { - assert.strictEqual(typeof applink, 'object'); +async function detectMetaInfo(upstreamUri) { + assert.strictEqual(typeof upstreamUri, 'string'); - const [error, response] = await safe(superagent.get(applink.upstreamUri).set('User-Agent', 'Mozilla').timeout(10*1000)); + const [error, response] = await safe(superagent.get(upstreamUri).set('User-Agent', 'Mozilla').timeout(10*1000)); if (error || !response.text) { debug('detectMetaInfo: Unable to fetch upstreamUri to detect icon and title', error.statusCode); - return; + return null; } - if (applink.favicon && applink.label) return; - // set redirected URI if any for favicon url const redirectUri = (response.redirects && response.redirects.length) ? response.redirects[0] : null; @@ -89,73 +105,84 @@ async function detectMetaInfo(applink) { // No-op to skip console errors. }); - const [jsdomError, dom] = await safe(jsdom.JSDOM.fromURL(applink.upstreamUri, { virtualConsole })); - if (jsdomError) console.error('detectMetaInfo: jsdomError', jsdomError); - - if (!applink.icon && dom) { - let favicon = ''; - if (dom.window.document.querySelector('link[rel="apple-touch-icon"]')) favicon = dom.window.document.querySelector('link[rel="apple-touch-icon"]').href; - if (!favicon && dom.window.document.querySelector('meta[name="msapplication-TileImage"]')) favicon = dom.window.document.querySelector('meta[name="msapplication-TileImage"]').content; - if (!favicon && dom.window.document.querySelector('link[rel="shortcut icon"]')) favicon = dom.window.document.querySelector('link[rel="shortcut icon"]').href; - if (!favicon && dom.window.document.querySelector('link[rel="icon"]')) { - let iconElements = dom.window.document.querySelectorAll('link[rel="icon"]'); - if (iconElements.length) { - favicon = iconElements[0].href; // choose first one for a start - - // check if we have sizes attributes and then choose the largest one - iconElements = Array.from(iconElements).filter(function (e) { - return e.attributes.getNamedItem('sizes') && e.attributes.getNamedItem('sizes').value; - }).sort(function (a, b) { - return parseInt(b.attributes.getNamedItem('sizes').value.split('x')[0]) - parseInt(a.attributes.getNamedItem('sizes').value.split('x')[0]); - }); - if (iconElements.length) favicon = iconElements[0].href; - } - } - if (!favicon && dom.window.document.querySelector('meta[itemprop="image"]')) favicon = dom.window.document.querySelector('meta[itemprop="image"]').content; - - if (favicon) { - favicon = new URL(favicon, redirectUri || applink.upstreamUri).toString(); - - debug(`detectMetaInfo: found icon: ${favicon}`); - - const [error, response] = await safe(superagent.get(favicon).timeout(10*1000)); - if (error) debug(`Failed to fetch icon ${favicon}: `, error); - else if (response.ok && response.headers['content-type'].indexOf('image') !== -1) applink.icon = response.body || response.text; - else debug(`Failed to fetch icon ${favicon}: statusCode=${response.status}`); - } - - if (!favicon) { - debug(`Unable to find a suitable icon for ${applink.upstreamUri}, try fallback favicon.ico`); - - const [error, response] = await safe(superagent.get(`${applink.upstreamUri}/favicon.ico`).timeout(10*1000)); - if (error) debug(`Failed to fetch icon ${favicon}: `, error); - else if (response.ok && response.headers['content-type'].indexOf('image') !== -1) applink.icon = response.body || response.text; - else debug(`Failed to fetch icon ${favicon}: statusCode=${response.status} content type ${response.headers['content-type']}`); - } + const [jsdomError, dom] = await safe(jsdom.JSDOM.fromURL(upstreamUri, { virtualConsole })); + if (jsdomError || !dom) { + console.error('detectMetaInfo: jsdomError', jsdomError); + return null; } - if (!applink.label) { - if (dom.window.document.querySelector('meta[property="og:title"]')) applink.label = dom.window.document.querySelector('meta[property="og:title"]').content; - else if (dom.window.document.querySelector('meta[property="og:site_name"]')) applink.label = dom.window.document.querySelector('meta[property="og:site_name"]').content; - else if (dom.window.document.title) applink.label = dom.window.document.title; + let icon = null, label = ''; + + // icon detection + let favicon = ''; + if (dom.window.document.querySelector('link[rel="apple-touch-icon"]')) favicon = dom.window.document.querySelector('link[rel="apple-touch-icon"]').href; + if (!favicon && dom.window.document.querySelector('meta[name="msapplication-TileImage"]')) favicon = dom.window.document.querySelector('meta[name="msapplication-TileImage"]').content; + if (!favicon && dom.window.document.querySelector('link[rel="shortcut icon"]')) favicon = dom.window.document.querySelector('link[rel="shortcut icon"]').href; + if (!favicon && dom.window.document.querySelector('link[rel="icon"]')) { + let iconElements = dom.window.document.querySelectorAll('link[rel="icon"]'); + if (iconElements.length) { + favicon = iconElements[0].href; // choose first one for a start + + // check if we have sizes attributes and then choose the largest one + iconElements = Array.from(iconElements).filter(function (e) { + return e.attributes.getNamedItem('sizes') && e.attributes.getNamedItem('sizes').value; + }).sort(function (a, b) { + return parseInt(b.attributes.getNamedItem('sizes').value.split('x')[0]) - parseInt(a.attributes.getNamedItem('sizes').value.split('x')[0]); + }); + if (iconElements.length) favicon = iconElements[0].href; + } } + if (!favicon && dom.window.document.querySelector('meta[itemprop="image"]')) favicon = dom.window.document.querySelector('meta[itemprop="image"]').content; + + if (favicon) { + favicon = new URL(favicon, redirectUri || upstreamUri).toString(); + + debug(`detectMetaInfo: found icon: ${favicon}`); + + const [error, response] = await safe(superagent.get(favicon).timeout(10*1000)); + if (error) debug(`Failed to fetch icon ${favicon}: `, error); + else if (response.ok && response.headers['content-type'].indexOf('image') !== -1) icon = response.body || response.text; + else debug(`Failed to fetch icon ${favicon}: statusCode=${response.status}`); + } + + if (!favicon) { + debug(`Unable to find a suitable icon for ${upstreamUri}, try fallback favicon.ico`); + + const [error, response] = await safe(superagent.get(`${upstreamUri}/favicon.ico`).timeout(10*1000)); + if (error) debug(`Failed to fetch icon ${favicon}: `, error); + else if (response.ok && response.headers['content-type'].indexOf('image') !== -1) icon = response.body || response.text; + else debug(`Failed to fetch icon ${favicon}: statusCode=${response.status} content type ${response.headers['content-type']}`); + } + + // detect label + if (dom.window.document.querySelector('meta[property="og:title"]')) label = dom.window.document.querySelector('meta[property="og:title"]').content; + else if (dom.window.document.querySelector('meta[property="og:site_name"]')) label = dom.window.document.querySelector('meta[property="og:site_name"]').content; + else if (dom.window.document.title) label = dom.window.document.title; + + return { icon, label }; } async function add(applink) { assert.strictEqual(typeof applink, 'object'); - assert.strictEqual(typeof applink.upstreamUri, 'string'); debug(`add: ${applink.upstreamUri}`); let error = validateUpstreamUri(applink.upstreamUri); if (error) throw error; + error = validateAccessRestriction(applink.accessRestriction); + if (error) throw error; + if (applink.icon) { if (!validator.isBase64(applink.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64'); applink.icon = Buffer.from(applink.icon, 'base64'); } - await detectMetaInfo(applink); + if (!applink.icon || !applink.label) { + const meta = await detectMetaInfo(applink.upstreamUri); + if (!applink.label) applink.label = meta?.label; + if (!applink.icon) applink.icon = meta?.icon; + } const data = { id: uuid.v4(), @@ -186,49 +213,54 @@ async function get(applinkId) { return result[0]; } -async function update(applinkId, applink) { - assert.strictEqual(typeof applinkId, 'string'); +async function update(applink, data) { assert.strictEqual(typeof applink, 'object'); - assert.strictEqual(typeof applink.upstreamUri, 'string'); + assert.strictEqual(typeof data, 'object'); - debug(`update: ${applink.upstreamUri}`); - - const error = validateUpstreamUri(applink.upstreamUri); - if (error) throw error; - - if (applink.icon) { - if (!validator.isBase64(applink.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64'); - applink.icon = Buffer.from(applink.icon, 'base64'); - } else if (applink.icon === '') { - // empty string means we autodetect in detectMetaInfo - applink.icon = ''; - } else { - // nothing changed reuse old - const result = await get(applinkId); - applink.icon = result.icon; + let error; + if ('upstreamUri' in data) { + error = validateUpstreamUri(data.upstreamUri); + if (error) throw error; } - await detectMetaInfo(applink); + if ('accessRestriction' in data) { + error = validateAccessRestriction(data.accessRestriction); + if (error) throw error; + } - const query = 'UPDATE applinks SET label=?, icon=?, upstreamUri=?, tagsJson=?, accessRestrictionJson=? WHERE id = ?'; - const args = [ applink.label, applink.icon || null, applink.upstreamUri, applink.tags ? JSON.stringify(applink.tags) : null, applink.accessRestriction ? JSON.stringify(applink.accessRestriction) : null, applinkId ]; + if ('icon' in data && data.icon) { + if (!validator.isBase64(data.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64'); + data.icon = Buffer.from(data.icon, 'base64'); + } - const result = await database.query(query, args); + // we don't track if label/icon in db is user-set or was auto detected + if (data.upstreamUri || data.label === '' || data.icon === '') { + const meta = await detectMetaInfo(data.upstreamUri || applink.upstreamUri); + + if (data.label === '') data.label = meta?.label; + if (data.icon === '') data.icon = meta?.icon; + } + + const args = [], fields = []; + for (const k in data) { + if (k === 'accessRestriction' || k === 'tags') { + fields.push(`${k}Json = ?`); + args.push(JSON.stringify(data[k])); + } else { + fields.push(k + ' = ?'); + args.push(data[k]); + } + } + args.push(applink.id); + + const [updateError, result] = await safe(database.query('UPDATE applinks SET ' + fields.join(', ') + ' WHERE id = ?', args)); + if (updateError) throw updateError; if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Applink not found'); } -async function del(applinkId) { - assert.strictEqual(typeof applinkId, 'string'); +async function del(applink) { + assert.strictEqual(typeof applink, 'object'); - const result = await database.query('DELETE FROM applinks WHERE id = ?', [ applinkId ]); + const result = await database.query('DELETE FROM applinks WHERE id = ?', [ applink.id ]); if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Applink not found'); } - -async function getIcon(applinkId) { - assert.strictEqual(typeof applinkId, 'string'); - - const applink = await get(applinkId); - if (!applink) throw new BoxError(BoxError.NOT_FOUND, 'Applink not found'); - - return applink.icon; -} diff --git a/src/routes/applinks.js b/src/routes/applinks.js index 4ba0d8456..2cbba1d75 100644 --- a/src/routes/applinks.js +++ b/src/routes/applinks.js @@ -6,7 +6,9 @@ exports = module.exports = { get, update, del, - getIcon + getIcon, + + load }; const assert = require('assert'), @@ -16,6 +18,18 @@ const assert = require('assert'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess; +async function load(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + const [error, result] = await safe(applinks.get(req.params.id)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Applink not found')); + + req.resource = result; + + next(); +} + async function listByUser(req, res, next) { assert.strictEqual(typeof req.user, 'object'); @@ -31,50 +45,49 @@ async function listByUser(req, res, next) { async function add(req, res, next) { assert.strictEqual(typeof req.body, 'object'); + // required if (!req.body.upstreamUri || typeof req.body.upstreamUri !== 'string') return next(new HttpError(400, 'upstreamUri must be a non-empty string')); + if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); + if ('label' in req.body && typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string')); if ('tags' in req.body && !Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array with strings')); - if ('accessRestriction' in req.body && typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); + if ('icon' in req.body && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon must be a string')); - const [error] = await safe(applinks.add(req.body)); + const [error, id] = await safe(applinks.add(req.body)); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, {})); + next(new HttpSuccess(201, { id })); } async function get(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); - const [error, result] = await safe(applinks.get(req.params.id)); - if (error) return next(BoxError.toHttpError(error)); - if (!result) return next(new HttpError(404, 'Applink not found')); - // we have a separate route for this - delete result.icon; + delete req.resource.icon; - next(new HttpSuccess(200, result)); + next(new HttpSuccess(200, req.resource)); } async function update(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); assert.strictEqual(typeof req.body, 'object'); - if (!req.body.upstreamUri || typeof req.body.upstreamUri !== 'string') return next(new HttpError(400, 'upstreamUri must be a non-empty string')); + if ('upstreamUri' in req.body && (!req.body.upstreamUri || typeof req.body.upstreamUri !== 'string')) return next(new HttpError(400, 'upstreamUri must be a non-empty string')); + if ('accessRestriction' in req.body && typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); if ('label' in req.body && typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string')); if ('tags' in req.body && !Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array with strings')); - if ('accessRestriction' in req.body && typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); if ('icon' in req.body && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon must be a string')); - const [error] = await safe(applinks.update(req.params.id, req.body)); + const [error] = await safe(applinks.update(req.resource, req.body)); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); + next(new HttpSuccess(200, {})); } async function del(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); - const [error] = await safe(applinks.del(req.params.id)); + const [error] = await safe(applinks.del(req.resource)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(204)); @@ -83,9 +96,5 @@ async function del(req, res, next) { async function getIcon(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); - const [error, icon] = await safe(applinks.getIcon(req.params.id, { original: req.query.original })); - if (error) return next(BoxError.toHttpError(error)); - if (!icon) return next(new HttpError(404, 'no such icon')); - - res.send(icon); + res.send(req.resource.icon); } diff --git a/src/routes/groups.js b/src/routes/groups.js index 98404df00..361d55902 100644 --- a/src/routes/groups.js +++ b/src/routes/groups.js @@ -56,7 +56,7 @@ async function setName(req, res, next) { const [error] = await safe(groups.setName(req.resource, req.body.name, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { })); + next(new HttpSuccess(200, {})); } async function setMembers(req, res, next) { @@ -69,7 +69,7 @@ async function setMembers(req, res, next) { const [error] = await safe(groups.setMembers(req.resource, req.body.userIds, { skipSourceCheck: false }, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { })); + next(new HttpSuccess(200, {})); } async function list(req, res, next) { diff --git a/src/routes/test/applinks-test.js b/src/routes/test/applinks-test.js new file mode 100644 index 000000000..99f63414a --- /dev/null +++ b/src/routes/test/applinks-test.js @@ -0,0 +1,88 @@ +'use strict'; + +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +const applinks = require('../../applinks.js'), + common = require('./common.js'), + expect = require('expect.js'), + superagent = require('superagent'); + +describe('AppLinks API', function () { + const { setup, cleanup, serverUrl, owner } = common; + + before(setup); + after(cleanup); + + let applinkId; + + it('can add applink', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/applinks`) + .query({ access_token: owner.token }) + .send({ label: 'Berlin', tags: ['city'], upstreamUri: 'https://www.berlin.de', accessRestriction: null }); + expect(response.statusCode).to.equal(201); + expect(response.body.id).to.be.ok(); + + applinkId = response.body.id; + }); + + it('cannot get random applink', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/applinks/random`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(404); + }); + + it('cannot get valid applink', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/applinks/${applinkId}`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(200); + expect(response.body.upstreamUri).to.be('https://www.berlin.de'); + }); + + it('can get get icon', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/applinks/${applinkId}/icon`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(200); + expect(response.headers['content-type']).to.be('application/octet-stream'); + }); + + it('can update applink tags', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/applinks/${applinkId}`) + .query({ access_token: owner.token }) + .send({ tags: ['city', 'germany'] }); + expect(response.statusCode).to.equal(200); + + const result = await applinks.get(applinkId); + expect(result.tags).to.eql(['city', 'germany']); + }); + + it('can list applinks', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/applinks`) + .query({ access_token: owner.token }); + expect(response.statusCode).to.equal(200); + expect(response.body.applinks.length).to.equal(1); + expect(response.body.applinks[0].upstreamUri).to.be('https://www.berlin.de'); + }); + + it('cannot del random applink', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/applinks/random`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(404); + }); + + it('can del applink', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/applinks/${applinkId}`) + .query({ access_token: owner.token }) + .ok(() => true); + expect(response.statusCode).to.equal(204); + + const result = await applinks.get(applinkId); + expect(result).to.be(null); + }); +}); diff --git a/src/server.js b/src/server.js index 649b06cc8..088ce4096 100644 --- a/src/server.js +++ b/src/server.js @@ -296,12 +296,12 @@ async function initializeExpressSync() { router.get ('/api/v1/apps/:id/exec/:execId/startws', token, routes.apps.load, authorizeOperator, routes.apps.startExecWebSocket); // app links in dashboard - router.get ('/api/v1/applinks', token, authorizeUser, routes.applinks.listByUser); - router.post('/api/v1/applinks', json, token, authorizeAdmin, routes.applinks.add); - router.get ('/api/v1/applinks/:id', token, authorizeAdmin, routes.applinks.get); - router.post('/api/v1/applinks/:id', json, token, authorizeAdmin, routes.applinks.update); - router.del ('/api/v1/applinks/:id', token, authorizeAdmin, routes.applinks.del); - router.get ('/api/v1/applinks/:id/icon', token, authorizeUser, routes.applinks.getIcon); + router.get ('/api/v1/applinks', token, authorizeUser, routes.applinks.listByUser); + router.post('/api/v1/applinks', json, token, authorizeAdmin, routes.applinks.add); + router.get ('/api/v1/applinks/:id', token, routes.applinks.load, authorizeAdmin, routes.applinks.get); + router.post('/api/v1/applinks/:id', json, token, routes.applinks.load, authorizeAdmin, routes.applinks.update); + router.del ('/api/v1/applinks/:id', token, routes.applinks.load, authorizeAdmin, routes.applinks.del); + router.get ('/api/v1/applinks/:id/icon', token, routes.applinks.load, authorizeUser, routes.applinks.getIcon); // branding routes router.get ('/api/v1/branding/cloudron_name', token, authorizeAdmin, routes.branding.getCloudronName); diff --git a/src/test/applinks-test.js b/src/test/applinks-test.js index 3be22bee4..6a1b775f1 100644 --- a/src/test/applinks-test.js +++ b/src/test/applinks-test.js @@ -17,8 +17,13 @@ describe('Applinks', function () { before(setup); after(cleanup); + function deepCopy(x) { + return JSON.parse(JSON.stringify(x)); + } + const APPLINK_0 = { - upstreamUri: 'https://cloudron.io' + upstreamUri: 'https://cloudron.io', + accessRestriction: null }; const APPLINK_1 = { @@ -30,23 +35,25 @@ describe('Applinks', function () { }; const APPLINK_2 = { - upstreamUri: 'https://google.com' + upstreamUri: 'https://google.com', + accessRestriction: null }; const APPLINK_3 = { - upstreamUri: 'http://example.com' + upstreamUri: 'http://example.com', + accessRestriction: null }; it('can add applink with redirect', async function () { - APPLINK_0.id = await applinks.add(JSON.parse(JSON.stringify(APPLINK_0))); + APPLINK_0.id = await applinks.add(deepCopy(APPLINK_0)); }); it('can add second applink with attributes', async function () { - APPLINK_1.id = await applinks.add(JSON.parse(JSON.stringify(APPLINK_1))); + APPLINK_1.id = await applinks.add(deepCopy(APPLINK_1)); }); it('can add third applink to test google.com favicon', async function () { - APPLINK_2.id = await applinks.add(JSON.parse(JSON.stringify(APPLINK_2))); + APPLINK_2.id = await applinks.add(deepCopy(APPLINK_2)); const result = await applinks.get(APPLINK_2.id); expect(result.upstreamUri).to.eql(APPLINK_2.upstreamUri); // should not have changed @@ -54,7 +61,7 @@ describe('Applinks', function () { }); it('can add fourth applink to test no favicon', async function () { - APPLINK_3.id = await applinks.add(JSON.parse(JSON.stringify(APPLINK_3))); + APPLINK_3.id = await applinks.add(deepCopy(APPLINK_3)); const result = await applinks.get(APPLINK_3.id); expect(result.upstreamUri).to.eql('http://example.com'); @@ -97,10 +104,7 @@ describe('Applinks', function () { }); it('can update applink', async function () { - APPLINK_0.upstreamUri = 'https://duckduckgo.com'; - APPLINK_0.icon = APPLINK_1.icon; - - await applinks.update(APPLINK_0.id, JSON.parse(JSON.stringify(APPLINK_0))); + await applinks.update(APPLINK_0, { upstreamUri: 'https://duckduckgo.com', icon: APPLINK_1.icon }); const result = await applinks.get(APPLINK_0.id); expect(result.upstreamUri).to.equal('https://duckduckgo.com'); @@ -108,18 +112,18 @@ describe('Applinks', function () { }); it('can get applink icon', async function () { - const result = await applinks.getIcon(APPLINK_0.id); - expect(result.toString('base64')).to.eql(APPLINK_1.icon); + const result = await applinks.get(APPLINK_0.id); + expect(result.icon.toString('base64')).to.eql(APPLINK_1.icon); }); it('cannot del applink with wrong id', async function () { - const [error] = await safe(applinks.del('doesnotexist')); + const [error] = await safe(applinks.del({ id: 'doesnotexist' })); expect(error).to.be.a(BoxError); expect(error.reason).to.eql(BoxError.NOT_FOUND); }); it('can del applink', async function () { - await applinks.del(APPLINK_0.id); + await applinks.del(APPLINK_0); const result = await applinks.get(APPLINK_0.id); expect(result).to.be(null);