From 884464ef843a1fd18b5afa65426c8ada1f1334c5 Mon Sep 17 00:00:00 2001 From: Tim Hostetler Date: Fri, 23 Mar 2018 14:44:48 -0400 Subject: [PATCH 1/5] Make "Record Not Known" button a panel --- .../list_of_things/templates/item-template.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/js/widgets/list_of_things/templates/item-template.html b/src/js/widgets/list_of_things/templates/item-template.html index e616ab9e9..b950b6420 100644 --- a/src/js/widgets/list_of_things/templates/item-template.html +++ b/src/js/widgets/list_of_things/templates/item-template.html @@ -211,10 +211,12 @@

{{{title}}}

Claim in ORCID {{else}} - +
+
+ orcid logo inactive + Record not known to ADS +
+
{{/if}} From 790f7d74d4858f189271f066bf5d259eee4676cd Mon Sep 17 00:00:00 2001 From: Tim Hostetler Date: Mon, 26 Mar 2018 17:38:01 -0400 Subject: [PATCH 2/5] Fix FileSaver Issue --- grunt/copy.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/grunt/copy.js b/grunt/copy.js index 21693b6b4..f331d5238 100644 --- a/grunt/copy.js +++ b/grunt/copy.js @@ -18,6 +18,15 @@ module.exports = function (grunt) { return dest + src.replace('FileSaver.min', 'index'); } }, + { + cwd: 'node_modules/file-saver', + src: 'FileSaver.min.js', + dest: 'src/libs/file-saver/', + expand: true, + rename: function (dest, src) { + return dest + src.replace('FileSaver.min', 'index'); + } + }, { src: 'bower_components/lodash/dist/*', dest: 'src/libs/lodash/', From 6f404b40408a1f5a45cc09434248effe15053ec8 Mon Sep 17 00:00:00 2001 From: Tim Hostetler Date: Mon, 26 Mar 2018 17:43:12 -0400 Subject: [PATCH 3/5] Add Orcid Record Reconciler This checks to see if there are multiple sources for the records. If there are, then it tries to pick the best one from the bunch. --- src/js/modules/orcid/extension.js | 3 +- src/js/modules/orcid/orcid_api.js | 81 ++++++++++++++++++++++++++++++- src/js/modules/orcid/profile.js | 31 +++++++++++- src/js/modules/orcid/work.js | 19 +++++++- 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/js/modules/orcid/extension.js b/src/js/modules/orcid/extension.js index e6bc8f507..3a6959f86 100644 --- a/src/js/modules/orcid/extension.js +++ b/src/js/modules/orcid/extension.js @@ -171,6 +171,7 @@ define([ self.trigger('orcid-update-finished'); } }); + recInfo.fail(function (data) { counter -= 1; @@ -450,7 +451,7 @@ define([ model.set({ orcid: self._getOrcidInfo(recInfo), - 'source_name': work.getSourceName() + 'source_name': work.sources.join('; ') }); self.trigger('orcidAction:' + action, model); diff --git a/src/js/modules/orcid/orcid_api.js b/src/js/modules/orcid/orcid_api.js index c087983c3..b2602b784 100644 --- a/src/js/modules/orcid/orcid_api.js +++ b/src/js/modules/orcid/orcid_api.js @@ -372,7 +372,7 @@ define([ request.done(function (profile) { _.forEach(cache, function (promise) { - promise.resolve(new Profile(profile)); + promise.resolve(self._reconcileProfileWorks(profile)); }); }); @@ -384,6 +384,85 @@ define([ }); }, + /** + * Reconcile the works contained in the incoming profile. + * Since it's possible for an ORCiD record to contain multiple sources, + * we have to figure out the best one to pick. + * + * The user can selected a "preferred" source, but since we can only match + * on items that have enough information (bibcode, doi, etc), we have to search + * through them all to find the best one. + * + * @param {object} rawProfile - the incoming profile + * @returns {Profile} - the new profile (with reconciled works) + */ + _reconcileProfileWorks: function (rawProfile) { + /* + 1. Source is ADS + 2. Has Bibcode + 3. Has DOI + 4. Other + */ + var self = this; + var profile = new Profile(rawProfile); + var works = _.map(profile.getWorksDeep(), function (work, idx) { + var w; + + // only operate on arrays > 1 + if (work.length > 1) { + var workWithBibcode, workWithDoi; + _.forEach(work, function (item) { + + // check if the source is ADS + var isADS = self.isSourcedByADS(item); + + // grab an array of external ids ['bibcode', 'doi', '...'] + var exIds = item.getExternalIdType(); + var hasBibcode = exIds.indexOf('bibcode') > -1; + var hasDoi = exIds.indexOf('doi') > -1; + + // if it's sourced by ADS, use that one and break out of loop + if (isADS) { + w = item; + return false; + } + + // grab the first one that has a bibcode + if (hasBibcode && !workWithBibcode) { + workWithBibcode = item; + } + + // grab the first one that has a doi + if (hasDoi && !workWithDoi) { + workWithDoi = item; + } + }); + + // w will be defined if we found an ADS-sourced work + // otherwise, set the work accordingly below + if (!w && workWithBibcode) { + w = workWithBibcode; + } else if (!w && workWithDoi) { + w = workWithDoi; + } else if (!w) { + w = work[0]; + } + + // set the work's list of sources based on the full list from orcid + w.sources = _.map(work, function (_w) { + return _w.getSourceName(); + }); + } + + // take the first work if we haven't found an array to process + return w ? w : work[0]; + }); + + // set the new works + profile.setWorks(works); + return profile; + }, + /** * Retrieves user profile * Must have scope: /orcid-profile/read-limited diff --git a/src/js/modules/orcid/profile.js b/src/js/modules/orcid/profile.js index a0aeee68d..3ab98aac3 100644 --- a/src/js/modules/orcid/profile.js +++ b/src/js/modules/orcid/profile.js @@ -20,6 +20,7 @@ define([ */ var Profile = function Profile(profile) { this._root = profile; + this.works = []; /** * search profile for value at specified path @@ -39,12 +40,35 @@ define([ /** * Gets all the work summaries from the profile + * Shallow (only grabs the first entry) * * @returns {Work[]} - the array of Work summaries */ this.getWorks = function () { + return this.works; + }; + + /** + * Set the profile works + * + * @param {*} works + */ + this.setWorks = function (works) { + this.works = works; + return this; + }; + + /** + * Pull all arrays of works from the profile + * grabs all works, not just the first + * + * @returns {[]Work[]} - the array of arrays of Work summaries + */ + this.getWorksDeep = function () { return _.map(this.getWorkSummaries(), function (w) { - return new Work(w); + return _.map(w['work-summary'], function (subWork) { + return new Work(subWork); + }); }); }; @@ -100,6 +124,11 @@ define([ } return obj; }, this); + + // to maintain old behavior, make sure works is filled when the profile is created + this.works = _.map(this.getWorkSummaries(), function (w) { + return new Work(w); + }); }; return Profile; diff --git a/src/js/modules/orcid/work.js b/src/js/modules/orcid/work.js index 446338638..6fd44141e 100644 --- a/src/js/modules/orcid/work.js +++ b/src/js/modules/orcid/work.js @@ -52,10 +52,25 @@ define([ * @param work * @constructor */ - var Work = function Work(work) { + var Work = function Work(work, options) { work = work || {}; + + // find the inner summary as the root this._root = (work['work-summary']) ? work['work-summary'][0] : work; + var sources = ''; + Object.defineProperty(this, 'sources', { + set: function (data) { + sources = data; + }, + get: function () { + if (sources.length > 0) { + return sources; + } + return [this.getSourceName()]; + } + }); + /** * Get the value at path * @@ -109,7 +124,7 @@ define([ title: [this.getTitle()], formattedDate: this.getFormattedPubDate(), abstract: this.getShortDescription(), - source_name: this.getSourceName(), + source_name: this.sources.join('; '), pub: this.getJournalTitle(), _work: this }); From b85b30c50a1b592a7512f17edb73cfadd1f71e83 Mon Sep 17 00:00:00 2001 From: Tim Hostetler Date: Mon, 26 Mar 2018 17:46:06 -0400 Subject: [PATCH 4/5] Add Check For Children Records Checks to see if we matched an alternate bibcode for an orcid record, if so, it marks it as a child and the view is updated to reflect the "merging" of the two records --- src/js/modules/orcid/extension.js | 72 +++++++++++++++++------------- src/js/modules/orcid/orcid_api.js | 74 ++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/src/js/modules/orcid/extension.js b/src/js/modules/orcid/extension.js index 3a6959f86..d81169eed 100644 --- a/src/js/modules/orcid/extension.js +++ b/src/js/modules/orcid/extension.js @@ -165,6 +165,18 @@ define([ if (model) { model.set('orcid', actions); } + + if (rInfo.children) { + _.forEach(rInfo.children, function (putcode) { + var childModel = _.find(self.hiddenCollection.models, function (m) { + return m.get('_work').getPutCode() === putcode; + }); + + if (childModel) { + self.removeModel(childModel); + } + }); + } } if (counter === 0) { @@ -207,10 +219,6 @@ define([ } }); - if (counter === 0) { - self.trigger('orcid-update-finished', docs); - } - return docs; }; @@ -248,7 +256,7 @@ define([ if (exIds.bibcode === wIds.bibcode || doi) { m.set({ - 'source_name': w.getSourceName(), + 'source_name': w.sources.join('; '), '_work': w }); } @@ -424,6 +432,34 @@ define([ return $dd.promise(); }; + WidgetClass.prototype.removeModel = function (model) { + var idx = model.resultsIndex; + this.hiddenCollection.remove(model); + var models = this.hiddenCollection.models; + _.forEach(_.rest(models, idx), function (m) { + m.set('resultsIndex', m.get('resultsIndex') - 1); + m.set('indexToShow', m.get('indexToShow') - 1); + }); + + var showRange = this.model.get('showRange'); + var range = _.range(showRange[0], showRange[1] + 1); + var visible = []; + _.forEach(range, function (i) { + if (models[i] && models[i].set) { + models[i].set('visible', true); + models[i].resultsIndex = i; + models[i].set('resultsIndex', i); + models[i].set('indexToShow', i + 1); + visible.push(models[i]); + } + }); + this.hiddenCollection.reset(models); + this.collection.reset(visible); + + // reset the total number of papers + this.model.set('totalPapers', models.length); + }; + WidgetClass.prototype.onAllInternalEvents = function(ev, arg1, arg2) { if (ev === 'childview:OrcidAction') { var self = this; @@ -497,31 +533,7 @@ define([ // Remove entry from collection after delete if (self.orcidWidget) { - var idx = model.resultsIndex; - self.hiddenCollection.remove(model); - var models = self.hiddenCollection.models; - _.forEach(_.rest(models, idx), function (m) { - m.set('resultsIndex', m.get('resultsIndex') - 1); - m.set('indexToShow', m.get('indexToShow') - 1); - }); - - var showRange = self.model.get('showRange'); - var range = _.range(showRange[0], showRange[1] + 1); - var visible = []; - _.forEach(range, function (i) { - if (models[i] && models[i].set) { - models[i].set('visible', true); - models[i].resultsIndex = i; - models[i].set('resultsIndex', i); - models[i].set('indexToShow', i + 1); - visible.push(models[i]); - } - }); - self.hiddenCollection.reset(models); - self.collection.reset(visible); - - // reset the total number of papers - self.model.set('totalPapers', models.length); + self.removeModel(model); } else { // reset orcid actions model.set('orcid', self._getOrcidInfo({})); diff --git a/src/js/modules/orcid/orcid_api.js b/src/js/modules/orcid/orcid_api.js index b2602b784..df09b64c0 100644 --- a/src/js/modules/orcid/orcid_api.js +++ b/src/js/modules/orcid/orcid_api.js @@ -722,12 +722,27 @@ define([ }); // on fail, reject the promises - prom.fail(function () { - self.addCache = _.reduce(self.addCache, function (res, entry) { - entry.promise.state() === 'pending' ? - entry.promise.reject() : res.push(entry); - return res; - }, []); + // this should receive a list of ids which we can finish up with + prom.fail(function (ids) { + var args = arguments; + _.forEach(ids, function (id) { + + // find the cache entry + var idx = _.findIndex(self.addCache, { id: id }); + if (idx >= 0) { + + // grab reference to promise + var promise = self.addCache[idx].promise; + + // remove entry from cache + self.addCache.splice(idx, idx + 1); + + // if it is still pending, reject it now + if (promise.state() === 'pending') { + promise.reject.apply(promise, args); + } + } + }); }); }, @@ -789,9 +804,9 @@ define([ }, {}); $dd.resolve(obj); - }, function () { + }, function (xhr) { self.setDirty(); - $dd.reject.apply($dd, arguments); + $dd.reject.apply($dd, [xhr.cacheIds].concat(arguments)); }); return $dd.promise(); @@ -1085,8 +1100,7 @@ define([ * unset (-1) so that it won't be counted as an orcid record. * */ - var querySuccess = function () { - var ids = _.flatten(arguments); + var querySuccess = function (ids) { // Update each orcid record with identifier info gained from ADS _.each(db, function (v, key) { @@ -1100,6 +1114,7 @@ define([ } }); + self._combineDatabaseWorks(db); finishUpdate(db); }; @@ -1126,6 +1141,41 @@ define([ return self.dbUpdatePromise.promise(); }, + /** + * Looks at the identifier of the work and attempts to + * detect if a bibcode has a child within the other entries + * of the database. + * + * @param {object} db - the database object + * @returns {object} db - the update database object + */ + _combineDatabaseWorks: function (db) { + + // loop through each entry of the database + _.forEach(db, function (data, identifier) { + + // we can only do this for entries with data and bibcodes + if (_.isUndefined(data) || _.isUndefined(data.bibcode)) { + return true; + } + + // remove 'identifier:' from front of key + var key = identifier.split(':')[1]; + + // add an children property to the current (parent entry) + _.forEach(db, function (entry, subKey) { + + // excluding our parent, see if the key matches the bibcode + if (entry.bibcode === key && subKey !== identifier) { + data.children = data.children || []; + data.children.push(entry.putcode); + } + }); + }); + + return db; + }, + /** * Creates a metadata object based on the work that is passed in that * helps with understanding the record's relationship with ADS. Figures @@ -1181,6 +1231,10 @@ define([ } out.putcode = rec.putcode; out.bibcode = rec.bibcode; + + if (rec.children) { + out.children = rec.children; + } } }; From 88d5aece3bafea5e00b782d8da111aa5fcf599b0 Mon Sep 17 00:00:00 2001 From: Tim Hostetler Date: Mon, 26 Mar 2018 17:35:11 -0400 Subject: [PATCH 5/5] Add Tests --- src/js/modules/orcid/profile.js | 2 +- test/mocha/js/modules/orcid/orcid_api.spec.js | 1275 +++++++++-------- 2 files changed, 696 insertions(+), 581 deletions(-) diff --git a/src/js/modules/orcid/profile.js b/src/js/modules/orcid/profile.js index 3ab98aac3..32270dc5f 100644 --- a/src/js/modules/orcid/profile.js +++ b/src/js/modules/orcid/profile.js @@ -19,7 +19,7 @@ define([ * @constructor */ var Profile = function Profile(profile) { - this._root = profile; + this._root = profile || {}; this.works = []; /** diff --git a/test/mocha/js/modules/orcid/orcid_api.spec.js b/test/mocha/js/modules/orcid/orcid_api.spec.js index 266623441..7709b2c5e 100644 --- a/test/mocha/js/modules/orcid/orcid_api.spec.js +++ b/test/mocha/js/modules/orcid/orcid_api.spec.js @@ -28,695 +28,810 @@ define([ Work, Profile ) { - sinon.test(function () { - var createOrcidServer = function (orcidApi, minsub) { - var server = sinon.fakeServer.create(); + var createOrcidServer = function (orcidApi, minsub) { + var server = sinon.fakeServer.create(); - server.respondWith('GET', /\/orcid-works\/(.*)/, function (xhr, putcodes) { - xhr.respond(200, { - 'Content-Type': 'application/json' - }, JSON.stringify(helpers.getMock('work'))); - }); + server.respondWith('GET', /\/orcid-works\/(.*)/, function (xhr, putcodes) { + xhr.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify(helpers.getMock('work'))); + }); + + server.respondWith('POST', /\/orcid-works/, function (xhr) { + xhr.respond(201, { + 'Content-Type': 'application/json' + }, xhr.requestBody); + }); + + server.respondWith('PUT', /\/orcid-works\/(.*)/, function (xhr, putcodes) { + xhr.respond(200, { + 'Content-Type': 'application/json' + }, xhr.requestBody); + }); + + server.respondWith('DELETE', /\/orcid-works\/.*/, function (xhr) { + xhr.respond(204); + }); + + server.respondWith('GET', /\/orcid-profile/, function (xhr) { + xhr.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify(helpers.getMock('profile'))); + }); + + var sendRequest = function (url, options, data) { + var $dd = $.Deferred(); - server.respondWith('POST', /\/orcid-works/, function (xhr) { - xhr.respond(201, { + $.ajax(url, _.extend(options, { + data: JSON.stringify(data), + dataType: 'json', + headers: { 'Content-Type': 'application/json' - }, xhr.requestBody); + } + })) + .done(function () { + options && options.done && options.done.apply(this, arguments); + $dd.resolve.apply($dd, arguments); + }) + .fail(function () { + options && options.fail && options.fail.apply(this, arguments); + $dd.reject.apply($dd, arguments); }); - server.respondWith('PUT', /\/orcid-works\/(.*)/, function (xhr, putcodes) { + server.respond(); + return $dd.promise(); + }; + + sinon.stub(orcidApi, 'createRequest', function (url, options, data) { + var parts = url.split('/'); + if (parts.length === 5) { + url = '/' + _.last(parts); + } else { + url = '/' + parts.slice(-2).join('/'); + } + return sendRequest(url, options, data); + }); + + if (minsub) { + server.respondWith('GET', /\/exchangeOAuthCode/, [200, { + 'Content-Type': 'application/json' + }, JSON.stringify(helpers.getMock('oAuth'))]); + + server.respondWith('GET', /search\/query/, function (xhr) { xhr.respond(200, { 'Content-Type': 'application/json' - }, xhr.requestBody); + }, JSON.stringify(helpers.getMock('adsResponse'))); }); - server.respondWith('DELETE', /\/orcid-works\/.*/, function (xhr) { - xhr.respond(204); + sinon.stub(minsub, 'request', function (apiRequest) { + var url = apiRequest.get('target'); + var options = apiRequest.get('options'); + var data = options.data; + return sendRequest(url, options, data); }); + } - server.respondWith('GET', /\/orcid-profile/, function (xhr) { - xhr.respond(200, { - 'Content-Type': 'application/json' - }, JSON.stringify(helpers.getMock('profile'))); + return server; + }; + + var getMinSub = function () { + var minsub = new MinimalPubsub({ verbose: false }); + minsub.beehive.addObject('DynamicConfig', { + orcidClientId: 'APP-P5ANJTQRRTMA6GXZ', + orcidApiEndpoint: 'https://api.orcid.org', + orcidRedirectUrlBase: 'http://localhost:8000' + }); + return minsub; + }; + + var getOrcidApi = function (beehive) { + var oModule = new OrcidModule(); + oModule.activate(beehive); + var oApi = beehive.getService('OrcidApi'); + oApi.saveAccessData({ + "access_token":"4274a0f1-36a1-4152-9a6b-4246f166bafe", + "orcid":"0000-0001-8178-9506" + }); + return oApi; + }; + + describe("Orcid API service (orcid_api.spec.js)", function () { + describe("OAuth", function() { + beforeEach(function (done) { + var minsub = new (MinimalPubsub.extend({ + request: function (apiRequest) { + if (apiRequest.get('target').indexOf('/exchangeOAuthCode') > -1) { + expect(apiRequest.get('query').get('code')).to.eql(['secret']); + return { + "access_token":"4274a0f1-36a1-4152-9a6b-4246f166bafe", + "token_type":"bearer", + "expires_in":3599, + "scope":"/orcid-works/create /orcid-profile/read-limited /orcid-works/update", + "orcid":"0000-0001-8178-9506", + "name":"Roman Chyla"}; + } + else if (apiRequest.get('target').indexOf('test-query') > -1) { + var opts = apiRequest.get('options'); + expect(opts.headers["Orcid-Authorization"]).to.eql('Bearer 4274a0f1-36a1-4152-9a6b-4246f166bafe'); + expect(opts.data).to.eql('{"data":{"foo":"bar"}}'); + return {success: true}; + } + } + }))({verbose: false}); + minsub.beehive.addObject('DynamicConfig', { + orcidClientId: 'APP-P5ANJTQRRTMA6GXZ', + orcidApiEndpoint: 'https://api.orcid.org', + orcidRedirectUrlBase: 'http://localhost:8000' + }); + this.minsub = minsub; + this.beehive = minsub.beehive; + done(); }); - var sendRequest = function (url, options, data) { - var $dd = $.Deferred(); + it("has methods to extract access code", function() { + var oApi = getOrcidApi(this.minsub.beehive); + // it receives window.location.search + expect(oApi.getUrlParameter('code', '?foo=bar&code=H1trXI')).to.eql('H1trXI'); + expect(oApi.hasExchangeCode('?foo=bar&code=H1trXI')).to.eql(true); + expect(oApi.getExchangeCode('?foo=bar&code=H1trXI')).to.eql('H1trXI'); + }); + + it("can exchange code for access_token (auth data)", function(done) { + var oApi = getOrcidApi(this.minsub.beehive); + var r = oApi.getAccessData('secret'); + expect(_.isUndefined(r.done)).to.eql(false); + //this.server.respond(); + r.done(function(res) { + expect(res).to.eql({ + "access_token":"4274a0f1-36a1-4152-9a6b-4246f166bafe", + "token_type":"bearer", + "expires_in":3599, + "scope":"/orcid-works/create /orcid-profile/read-limited /orcid-works/update", + "orcid":"0000-0001-8178-9506", + "name":"Roman Chyla"}); + + oApi.saveAccessData(res); + + expect(oApi.authData).to.eql(res); + + // the expires was added + expect(oApi.authData.expires).to.be.gt(new Date().getTime()); + + // now request uses access_token + var req = oApi.sendData('test-query', {data: {foo: 'bar'}}); + req.done(function(res) { + expect(res).to.eql({success: true}); + done(); + }); - $.ajax(url, _.extend(options, { - data: JSON.stringify(data), - dataType: 'json', - headers: { - 'Content-Type': 'application/json' - } - })) - .done(function () { - options && options.done && options.done.apply(this, arguments); - $dd.resolve.apply($dd, arguments); }) - .fail(function () { - options && options.fail && options.fail.apply(this, arguments); - $dd.reject.apply($dd, arguments); - }); + }); - server.respond(); - return $dd.promise(); - }; + it("should handle ORCID sign in", function(){ - sinon.stub(orcidApi, 'createRequest', function (url, options, data) { - var parts = url.split('/'); - if (parts.length === 5) { - url = '/' + _.last(parts); - } else { - url = '/' + parts.slice(-2).join('/'); - } - return sendRequest(url, options, data); - }); + var oApi = getOrcidApi(this.minsub.beehive); - if (minsub) { - server.respondWith('GET', /\/exchangeOAuthCode/, [200, { - 'Content-Type': 'application/json' - }, JSON.stringify(helpers.getMock('oAuth'))]); + this.beehive.getService("PubSub").publish = sinon.spy(); - server.respondWith('GET', /search\/query/, function (xhr) { - xhr.respond(200, { - 'Content-Type': 'application/json' - }, JSON.stringify(helpers.getMock('adsResponse'))); - }); + oApi.signIn(); - sinon.stub(minsub, 'request', function (apiRequest) { - var url = apiRequest.get('target'); - var options = apiRequest.get('options'); - var data = options.data; - return sendRequest(url, options, data); - }); - } + expect(JSON.stringify(this.beehive.getService("PubSub").publish.args[0])).to.eql('[{},"[App]-Exit",{"type":"orcid","url":"undefined?scope=/orcid-profile/read-limited%20/orcid-works/create%20/orcid-works/update&response_type=code&access_type=offline&show_login=true&client_id=APP-P5ANJTQRRTMA6GXZ&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2F%23%2Fuser%2Forcid"}]'); + expect(JSON.stringify(this.beehive.getService("PubSub").publish.args[1])).to.eql('[{},"[PubSub]-Orcid-Announcement","login"]') + }); + + it("signOut forgets authentication details", function() { + var oApi = getOrcidApi(this.minsub.beehive); + expect(oApi.hasAccess()).to.be.eql(false, 'needs to have authData defined'); + oApi.authData = {foo: 'bar'}; + expect(oApi.hasAccess()).to.be.eql(false, 'should need to have expires'); + oApi.signOut(); + expect(oApi.hasAccess()).to.be.eql(false, 'sign out cleared authdata'); + + oApi.authData = {expires: new Date().getTime() + 100}; + expect(oApi.hasAccess()).to.be.eql(true, 'authData was not expired'); + oApi.authData = {expires: new Date().getTime() - 100}; + expect(oApi.hasAccess()).to.be.eql(false, 'authData was expired'); + }); + + }); - return server; + var isPromise = function (val) { + var con = val.constructor === $.Deferred().constructor; + var props = val.done && !val.resolve; + return con && props; }; - var getMinSub = function () { - var minsub = new MinimalPubsub({ verbose: false }); - minsub.beehive.addObject('DynamicConfig', { - orcidClientId: 'APP-P5ANJTQRRTMA6GXZ', - orcidApiEndpoint: 'https://api.orcid.org', - orcidRedirectUrlBase: 'http://localhost:8000' + describe('addWork', function () { + var sb, server, oApi; + beforeEach(function () { + sb = sinon.sandbox.create(); + var minsub = getMinSub(); + oApi = getOrcidApi(minsub.beehive); + server = createOrcidServer(oApi, minsub); + }); + afterEach(function () { + sb.restore(); + server && server.restore && server.restore(); }); - return minsub; - }; - var getOrcidApi = function (beehive) { - var oModule = new OrcidModule(); - oModule.activate(beehive); - var oApi = beehive.getService('OrcidApi'); - oApi.saveAccessData({ - "access_token":"4274a0f1-36a1-4152-9a6b-4246f166bafe", - "orcid":"0000-0001-8178-9506" + it('handles bad input', function (done) { + expect(oApi.addWork.bind(oApi)).to.throw(TypeError); + expect(oApi.addWork.bind(oApi, null)).to.throw(TypeError); + expect(oApi.addWork.bind(oApi, [])).to.throw(TypeError); + done(); }); - return oApi; - }; - describe("Orcid API service (orcid_api.spec.js)", function () { - describe("OAuth", function() { - beforeEach(function (done) { - var minsub = new (MinimalPubsub.extend({ - request: function (apiRequest) { - if (apiRequest.get('target').indexOf('/exchangeOAuthCode') > -1) { - expect(apiRequest.get('query').get('code')).to.eql(['secret']); - return { - "access_token":"4274a0f1-36a1-4152-9a6b-4246f166bafe", - "token_type":"bearer", - "expires_in":3599, - "scope":"/orcid-works/create /orcid-profile/read-limited /orcid-works/update", - "orcid":"0000-0001-8178-9506", - "name":"Roman Chyla"}; - } - else if (apiRequest.get('target').indexOf('test-query') > -1) { - var opts = apiRequest.get('options'); - expect(opts.headers["Orcid-Authorization"]).to.eql('Bearer 4274a0f1-36a1-4152-9a6b-4246f166bafe'); - expect(opts.data).to.eql('{"data":{"foo":"bar"}}'); - return {success: true}; - } - } - }))({verbose: false}); - minsub.beehive.addObject('DynamicConfig', { - orcidClientId: 'APP-P5ANJTQRRTMA6GXZ', - orcidApiEndpoint: 'https://api.orcid.org', - orcidRedirectUrlBase: 'http://localhost:8000' - }); - this.minsub = minsub; - this.beehive = minsub.beehive; - done(); - }); + it('updates cache', function (done) { + oApi.addWork({ test: 'test' }); + expect(oApi.addCache.length).to.eql(1); + expect(oApi.addCache[0].work).to.eql({ test: 'test' }); + done(); + }); - it("has methods to extract access code", function() { - var oApi = getOrcidApi(this.minsub.beehive); - // it receives window.location.search - expect(oApi.getUrlParameter('code', '?foo=bar&code=H1trXI')).to.eql('H1trXI'); - expect(oApi.hasExchangeCode('?foo=bar&code=H1trXI')).to.eql(true); - expect(oApi.getExchangeCode('?foo=bar&code=H1trXI')).to.eql('H1trXI'); - }); + it('calls _addWork', function (done) { + sb.spy(oApi, '_addWork'); + oApi.addWork({ test: 'test' }); + expect(oApi._addWork.callCount).to.eql(1); + done(); + }); - it("can exchange code for access_token (auth data)", function(done) { - var oApi = getOrcidApi(this.minsub.beehive); - var r = oApi.getAccessData('secret'); - expect(_.isUndefined(r.done)).to.eql(false); - //this.server.respond(); - r.done(function(res) { - expect(res).to.eql({ - "access_token":"4274a0f1-36a1-4152-9a6b-4246f166bafe", - "token_type":"bearer", - "expires_in":3599, - "scope":"/orcid-works/create /orcid-profile/read-limited /orcid-works/update", - "orcid":"0000-0001-8178-9506", - "name":"Roman Chyla"}); - - oApi.saveAccessData(res); - - expect(oApi.authData).to.eql(res); - - // the expires was added - expect(oApi.authData.expires).to.be.gt(new Date().getTime()); - - // now request uses access_token - var req = oApi.sendData('test-query', {data: {foo: 'bar'}}); - req.done(function(res) { - expect(res).to.eql({success: true}); - done(); - }); - - }) - }); + it('returns a promise', function (done) { + var ret = oApi.addWork({ test: 'test' }); + expect(isPromise(ret)).to.eql(true); + done(); + }); - it("should handle ORCID sign in", function(){ + it('returned promise equals one in cache', function (done) { + var ret = oApi.addWork({ test: 'test' }); + expect(isPromise(ret)).to.eql(true); + expect(oApi.addCache[0].promise.promise()).to.eql(ret); + done(); + }); + }); - var oApi = getOrcidApi(this.minsub.beehive); + describe('_addWork', function () { + var sb, server, oApi; + beforeEach(function () { + sb = sinon.sandbox.create(); + var minsub = getMinSub(); + oApi = getOrcidApi(minsub.beehive); + server = createOrcidServer(oApi, minsub); + oApi._addWork = _.debounce(OrcidApi.prototype._addWork, 10); + }); + afterEach(function () { + sb.restore(); + server && server.restore && server.restore(); + }); - this.beehive.getService("PubSub").publish = sinon.spy(); + it('calls _addWorks', function (done) { + sb.spy(oApi, '_addWorks'); + oApi._addWork(); + _.delay(function () { + expect(oApi._addWorks.callCount).to.eql(1); + done(); + }, 15); + }); - oApi.signIn(); + it('is debounced, firing only after idle period', function (done) { + sb.spy(oApi, '_addWorks'); + oApi._addWork(); + oApi._addWork(); + oApi._addWork(); + oApi._addWork(); + _.delay(function () { + expect(oApi._addWorks.callCount).to.eql(1); + done(); + }, 15); + }); - expect(JSON.stringify(this.beehive.getService("PubSub").publish.args[0])).to.eql('[{},"[App]-Exit",{"type":"orcid","url":"undefined?scope=/orcid-profile/read-limited%20/orcid-works/create%20/orcid-works/update&response_type=code&access_type=offline&show_login=true&client_id=APP-P5ANJTQRRTMA6GXZ&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2F%23%2Fuser%2Forcid"}]'); - expect(JSON.stringify(this.beehive.getService("PubSub").publish.args[1])).to.eql('[{},"[PubSub]-Orcid-Announcement","login"]') - }); + it('recovers from orcid conflict', function (done) { - it("signOut forgets authentication details", function() { - var oApi = getOrcidApi(this.minsub.beehive); - expect(oApi.hasAccess()).to.be.eql(false, 'needs to have authData defined'); - oApi.authData = {foo: 'bar'}; - expect(oApi.hasAccess()).to.be.eql(false, 'should need to have expires'); - oApi.signOut(); - expect(oApi.hasAccess()).to.be.eql(false, 'sign out cleared authdata'); - - oApi.authData = {expires: new Date().getTime() + 100}; - expect(oApi.hasAccess()).to.be.eql(true, 'authData was not expired'); - oApi.authData = {expires: new Date().getTime() - 100}; - expect(oApi.hasAccess()).to.be.eql(false, 'authData was expired'); + // returns an orcid conflict error msg + sb.stub(oApi, '_addWorks', function () { + var val = { work: { error: { 'response-code': 409 }}}; + return $.Deferred().resolve(val).promise(); }); + var $dd = $.Deferred(); + oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); + oApi._addWork(); + $dd.done(function (work) { + + // should get an object back with *old* work + expect(_.isPlainObject(work)).to.eql(true); + expect(work).to.eql({ test: 'old' }); + done(); + }); }); - var isPromise = function (val) { - var con = val.constructor === $.Deferred().constructor; - var props = val.done && !val.resolve; - return con && props; - }; + it('rejects promise on orcid error (non-conflict)', function (done) { - describe('addWork', function () { - var sb, server, oApi; - beforeEach(function () { - sb = sinon.sandbox.create(); - var minsub = getMinSub(); - oApi = getOrcidApi(minsub.beehive); - server = createOrcidServer(oApi, minsub); - }); - afterEach(function () { - sb.restore(); - server && server.restore && server.restore(); + // returns an orcid generic error msg + sb.stub(oApi, '_addWorks', function () { + var val = { work: { error: {}}}; + return $.Deferred().resolve(val).promise(); }); - it('handles bad input', function (done) { - expect(oApi.addWork.bind(oApi)).to.throw(TypeError); - expect(oApi.addWork.bind(oApi, null)).to.throw(TypeError); - expect(oApi.addWork.bind(oApi, [])).to.throw(TypeError); + var $dd = $.Deferred(); + oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); + oApi._addWork(); + $dd.done(function () { expect(false); }); + $dd.fail(function () { + expect(true); done(); }); + }); - it('updates cache', function (done) { - oApi.addWork({ test: 'test' }); - expect(oApi.addCache.length).to.eql(1); - expect(oApi.addCache[0].work).to.eql({ test: 'test' }); - done(); - }); + it('returns a Work record', function (done) { - it('calls _addWork', function (done) { - sb.spy(oApi, '_addWork'); - oApi.addWork({ test: 'test' }); - expect(oApi._addWork.callCount).to.eql(1); - done(); + // returns a *new* orcid object + sb.stub(oApi, '_addWorks', function () { + var val = { 0: { work: { test: 'new' }}}; + return $.Deferred().resolve(val).promise(); }); - it('returns a promise', function (done) { - var ret = oApi.addWork({ test: 'test' }); - expect(isPromise(ret)).to.eql(true); + var $dd = $.Deferred(); + oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); + oApi._addWork(); + $dd.done(function (orcidWork) { + + // should get a Work object back with *new* work + expect(orcidWork instanceof Work).to.eql(true); + expect(orcidWork._root).to.eql({ test: 'new' }); done(); }); + }); - it('returned promise equals one in cache', function (done) { - var ret = oApi.addWork({ test: 'test' }); - expect(isPromise(ret)).to.eql(true); - expect(oApi.addCache[0].promise.promise()).to.eql(ret); + it('removes cache items as they are used', function (done) { + oApi.addCache = [ + { id: 0, work: {}, promise: $.Deferred() }, + { id: 1, work: {}, promise: $.Deferred() }, + { id: 2, work: {}, promise: $.Deferred() } + ]; + oApi._addWork(); + _.delay(function () { + expect(oApi.addCache.length).to.eql(0); done(); - }); + }, 15); }); - describe('_addWork', function () { - var sb, server, oApi; - beforeEach(function () { - sb = sinon.sandbox.create(); - var minsub = getMinSub(); - oApi = getOrcidApi(minsub.beehive); - server = createOrcidServer(oApi, minsub); - oApi._addWork = _.debounce(OrcidApi.prototype._addWork, 10); - }); - afterEach(function () { - sb.restore(); - server && server.restore && server.restore(); - }); + it('rejects cached promises upon request failure', function (done) { - it('calls _addWorks', function (done) { - sb.spy(oApi, '_addWorks'); - oApi._addWork(); - _.delay(function () { - expect(oApi._addWorks.callCount).to.eql(1); - done(); - }, 15); + // the request fails... + sb.stub(oApi, '_addWorks', function () { + var val = [0]; + return $.Deferred().reject(val).promise(); }); - it('is debounced, firing only after idle period', function (done) { - sb.spy(oApi, '_addWorks'); - oApi._addWork(); - oApi._addWork(); - oApi._addWork(); - oApi._addWork(); - _.delay(function () { - expect(oApi._addWorks.callCount).to.eql(1); - done(); - }, 15); + var $dd = $.Deferred(); + oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); + oApi._addWork(); + $dd.fail(function (ids) { + expect(ids).to.eql([0]); + expect(oApi.addCache.length).to.eql(0); + done(); }); + }); + }); - it('recovers from orcid conflict', function (done) { + describe('_addWorks', function () { + var sb, server, oApi; - // returns an orcid conflict error msg - sb.stub(oApi, '_addWorks', function () { - var val = { work: { error: { 'response-code': 409 }}}; - return $.Deferred().resolve(val).promise(); - }); + beforeEach(function () { + sb = sinon.sandbox.create(); + var minsub = getMinSub(); + oApi = getOrcidApi(minsub.beehive); + server = createOrcidServer(oApi, minsub); + oApi._addWork = _.debounce(OrcidApi.prototype._addWork, 10); + }); + afterEach(function () { + sb.restore(); + }); - var $dd = $.Deferred(); - oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); - oApi._addWork(); - $dd.done(function (work) { + it('handles bad input', function () { + expect(oApi._addWorks.bind(oApi)).to.throw(TypeError); + expect(oApi._addWorks.bind(oApi, null, [])).to.throw(TypeError); + expect(oApi._addWorks.bind(oApi, [], null)).to.throw(TypeError); + expect(oApi._addWorks.bind(oApi, null, null)).to.throw(TypeError); + expect(oApi._addWorks.bind(oApi, {}, {})).to.throw(TypeError); + expect(oApi._addWorks.bind(oApi, {}, null)).to.throw(TypeError); + expect(oApi._addWorks.bind(oApi, null, {})).to.throw(TypeError); + }); - // should get an object back with *old* work - expect(_.isPlainObject(work)).to.eql(true); - expect(work).to.eql({ test: 'old' }); - done(); - }); + it('correctly chunks input', function (done) { + oApi.maxAddChunkSize = 10; + var works = _.map(_.range(0, 100), function (i) { + return { title: 'test', i: i }; }); + var ids = _.range(100, 200); + var prom = oApi._addWorks(works, ids); + prom.done(function () { + expect(oApi.createRequest.callCount).to.eql(10, '10 requests'); + done(); + }); + }); - it('rejects promise on orcid error (non-conflict)', function (done) { - - // returns an orcid generic error msg - sb.stub(oApi, '_addWorks', function () { - var val = { work: { error: {}}}; - return $.Deferred().resolve(val).promise(); - }); + it('request on success, resolves w/ id-indexed works', function (done) { + var works = _.map(_.range(0, 200), function (i) { + return { title: 'test', i: i }; + }); + var ids = _.range(100, 300); + var prom = oApi._addWorks(works, ids); + prom.done(function (res) { - var $dd = $.Deferred(); - oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); - oApi._addWork(); - $dd.done(function () { expect(false); }); - $dd.fail(function () { - expect(true); - done(); + // check the response is right + _.forEach(ids, function (n, i) { + expect(res[n].work).to.eql(works[i], 'work matches'); }); + done(); }); + }); - it('returns a Work record', function (done) { - - // returns a *new* orcid object - sb.stub(oApi, '_addWorks', function () { - var val = { 0: { work: { test: 'new' }}}; - return $.Deferred().resolve(val).promise(); - }); + it.skip('request on fail, resolves promise w/ array of ids', function (done) { - var $dd = $.Deferred(); - oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); - oApi._addWork(); - $dd.done(function (orcidWork) { + server.respondWith('POST', /\/orcid-works/, function (xhr) { + xhr.respond(404, { 'Content-Type': 'application/json' }, xhr.requestBody); + }); - // should get a Work object back with *new* work - expect(orcidWork instanceof Work).to.eql(true); - expect(orcidWork._root).to.eql({ test: 'new' }); - done(); - }); + var ids = _.range(0, 300); + var works = _.map(ids, function (i) { + return { title: 'test', i: i }; }); + var prom = oApi._addWorks(works, ids); + prom.fail(function (res) { + expect(_.isArray(res)).to.eql(true); - it('removes cache items as they are used', function (done) { - oApi.addCache = [ - { id: 0, work: {}, promise: $.Deferred() }, - { id: 1, work: {}, promise: $.Deferred() }, - { id: 2, work: {}, promise: $.Deferred() } - ]; - oApi._addWork(); - _.delay(function () { - expect(oApi.addCache.length).to.eql(0); - done(); - }, 15); + // jquery fails on first failure + expect(res).to.deep.equal(ids.slice(0, 100)); + done(); + }); + prom.done(function () { + done(new Error()); }); + prom.always(function () { + done(); + }); + }); - it('rejects cached promises upon request failure', function (done) { + it('returns a promise', function () { + var prom = oApi._addWorks([], []); + expect(isPromise(prom)).to.eql(true); + }); + }); - // the request fails... - sb.stub(oApi, '_addWorks', function () { - var val = [0]; - return $.Deferred().reject(val).promise(); - }); + describe("Orcid Actions", function() { - var $dd = $.Deferred(); - oApi.addCache.push({ id: 0, work: { test: 'old' }, promise: $dd }); - oApi._addWork(); - $dd.fail(function (ids) { - expect(ids).to.eql([0]); - expect(oApi.addCache.length).to.eql(0); - done(); - }); - }); + beforeEach(function () { + this.minsub = getMinSub(); + this.sb = sinon.sandbox.create(); }); - describe('_addWorks', function () { - var sb, server, oApi; - beforeEach(function () { - sb = sinon.sandbox.create(); - var minsub = getMinSub(); - oApi = getOrcidApi(minsub.beehive); - server = createOrcidServer(oApi, minsub); - oApi._addWork = _.debounce(OrcidApi.prototype._addWork, 10); - }); - afterEach(function () { - sb.restore(); - }); + afterEach(function () { + this.sb.restore(); + }); - it('handles bad input', function (done) { - expect(oApi._addWorks.bind(oApi)).to.throw(TypeError); - expect(oApi._addWorks.bind(oApi, null, [])).to.throw(TypeError); - expect(oApi._addWorks.bind(oApi, [], null)).to.throw(TypeError); - expect(oApi._addWorks.bind(oApi, null, null)).to.throw(TypeError); - expect(oApi._addWorks.bind(oApi, {}, {})).to.throw(TypeError); - expect(oApi._addWorks.bind(oApi, {}, null)).to.throw(TypeError); - expect(oApi._addWorks.bind(oApi, null, {})).to.throw(TypeError); - done(); - }); + it('should be GenericModule', function (done) { + expect(new OrcidApi() instanceof GenericModule).to.equal(true, 'is GenericModule'); + expect(new OrcidApi() instanceof OrcidApi).to.equal(true, 'is OrcidApi'); + done(); + }); - it('correctly chunks input', function (done) { - oApi.maxAddChunkSize = 10; - var works = _.map(_.range(0, 100), function (i) { - return { title: 'test', i: i }; - }); - var ids = _.range(100, 200); - var prom = oApi._addWorks(works, ids); - prom.done(function () { - expect(oApi.createRequest.callCount).to.eql(10, '10 requests'); - done(); + it("exports hardened interface", function() { + var oApi = getOrcidApi(this.minsub.beehive); + var hardened = oApi.getHardenedInstance(); + var check = function (props) { + _.forEach(props, function (p) { + expect(_.has(hardened, p)).to.equal(true, 'Hardened Interface has property ' + p); }); - }); + }; + + check([ + 'hasAccess', + 'getUserProfile', + 'signIn', + 'signOut', + 'getADSUserData', + 'setADSUserData', + 'getRecordInfo', + 'addWork', + 'deleteWork', + 'updateWork', + 'getWork', + 'getWorks' + ]); + }); - it('request on success, resolves w/ id-indexed works', function (done) { - var works = _.map(_.range(0, 200), function (i) { - return { title: 'test', i: i }; - }); - var ids = _.range(100, 300); - var prom = oApi._addWorks(works, ids); - prom.done(function (res) { - - // check the response is right - _.forEach(ids, function (n, i) { - expect(res[n].work).to.eql(works[i], 'work matches'); - }); + //TODO: throwing sinon error, extend this + it('getUserProfile', function (done) { + var oApi = getOrcidApi(this.minsub.beehive); + createOrcidServer(oApi, this.minsub); + oApi.getUserProfile() + .done(function (profile) { + expect(profile instanceof Profile).to.equal(true, 'returns instance of Profile'); done(); }); - }); - - it('request on fail, resolves promise w/ array of ids', function (done) { + }); - server.respondWith('POST', /\/orcid-works/, function (xhr) { - xhr.respond(404, { - 'Content-Type': 'application/json' - }, xhr.requestBody); + it('getWork', function (done) { + var oApi = getOrcidApi(this.minsub.beehive); + createOrcidServer(oApi, this.minsub); + oApi.getWork(99999) + .done(function (work) { + expect(work instanceof Work).to.equal(true, 'returns instance of Work'); + done(); }); + }); - var works = _.map(_.range(0, 200), function (i) { - return { title: 'test', i: i }; + it('deleteWork', function (done) { + var oApi = getOrcidApi(this.minsub.beehive); + createOrcidServer(oApi, this.minsub); + oApi.deleteWork(99999) + .done(function (data, status, xhr) { + expect(xhr.status).to.equal(204, 'get 204 status code'); + expect(data).to.equal(undefined, 'got no content'); + done(); }); - var ids = _.range(100, 300); - var prom = oApi._addWorks(works, ids); - prom.fail(function (res) { - expect(_.isArray(res)).to.eql(true); + }); - // jquery fails on first failure - expect(res).to.eql(ids.splice(0, 100)); + it('updateWork', function (done) { + var oApi = getOrcidApi(this.minsub.beehive); + createOrcidServer(oApi, this.minsub); + var orcidWork = new Work(helpers.getMock('work')).getAsOrcid(); + oApi.updateWork(orcidWork) + .done(function (work) { + expect(JSON.stringify(work)).to.equal(JSON.stringify(orcidWork), + 'get our updated work back'); done(); }); - }); - - it('returns a promise', function (done) { - var prom = oApi._addWorks([], []); - expect(isPromise(prom)).to.eql(true); - done(); - }); }); - describe("Orcid Actions", function() { - - beforeEach(function () { - this.minsub = getMinSub(); - }); - - it('should be GenericModule', function (done) { - expect(new OrcidApi() instanceof GenericModule).to.equal(true, 'is GenericModule'); - expect(new OrcidApi() instanceof OrcidApi).to.equal(true, 'is OrcidApi'); - done(); - }); + it('getRecordInfo', function () { + var oApi = getOrcidApi(this.minsub.beehive); + sinon.stub(oApi, 'needsUpdate', _.constant(false)); + + var db = { + "identifier:foo": { + "sourcedByADS": true, + "putcode": 889362, + "idx": 0 + }, + "identifier:bar": { + "sourcedByADS": false, + "putcode": 878658, + "idx": 1 + }, + "identifier:boo": { + "sourcedByADS": true, + "putcode": 880867, + "idx": 1 + }, + "identifier:baz": { + "sourcedByADS": false, + "putcode": 880867, + "idx": -1 + } + }; - it("exports hardened interface", function() { - var oApi = getOrcidApi(this.minsub.beehive); - var hardened = oApi.getHardenedInstance(); - var check = function (props) { - _.forEach(props, function (p) { - expect(_.has(hardened, p)).to.equal(true, 'Hardened Interface has property ' + p); - }); - }; - - check([ - 'hasAccess', - 'getUserProfile', - 'signIn', - 'signOut', - 'getADSUserData', - 'setADSUserData', - 'getRecordInfo', - 'addWork', - 'deleteWork', - 'updateWork', - 'getWork', - 'getWorks' - ]); - }); + var check = function (rInfo, val, exp) { + expect(rInfo[val]).to.equal(exp, val + ' : ' + exp); + }; - //TODO: throwing sinon error, extend this - it('getUserProfile', function (done) { - var oApi = getOrcidApi(this.minsub.beehive); - createOrcidServer(oApi, this.minsub); - oApi.getUserProfile() - .done(function (profile) { - expect(profile instanceof Profile).to.equal(true, 'returns instance of Profile'); - done(); - }); + oApi.db = db; + oApi.getRecordInfo({ bibcode: 'foo' }).done(function (rInfo) { + check(rInfo, 'isCreatedByADS', true); + check(rInfo, 'isCreatedByOthers', false); + check(rInfo, 'isKnownToADS', true); }); - it('getWork', function (done) { - var oApi = getOrcidApi(this.minsub.beehive); - createOrcidServer(oApi, this.minsub); - oApi.getWork(99999) - .done(function (work) { - expect(work instanceof Work).to.equal(true, 'returns instance of Work'); - done(); - }); + oApi.getRecordInfo({ bibcode: 'bar' }).done(function (rInfo) { + check(rInfo, 'isCreatedByADS', false); + check(rInfo, 'isCreatedByOthers', true); + check(rInfo, 'isKnownToADS', true); }); - it('deleteWork', function (done) { - var oApi = getOrcidApi(this.minsub.beehive); - createOrcidServer(oApi, this.minsub); - oApi.deleteWork(99999) - .done(function (data, status, xhr) { - expect(xhr.status).to.equal(204, 'get 204 status code'); - expect(data).to.equal(undefined, 'got no content'); - done(); - }); + oApi.getRecordInfo({ doi: 'boo' }).done(function (rInfo) { + check(rInfo, 'isCreatedByADS', true); + check(rInfo, 'isCreatedByOthers', false); + check(rInfo, 'isKnownToADS', true); }); - it('updateWork', function (done) { - var oApi = getOrcidApi(this.minsub.beehive); - createOrcidServer(oApi, this.minsub); - var orcidWork = new Work(helpers.getMock('work')).getAsOrcid(); - oApi.updateWork(orcidWork) - .done(function (work) { - expect(JSON.stringify(work)).to.equal(JSON.stringify(orcidWork), - 'get our updated work back'); - done(); - }); + oApi.getRecordInfo({ doi: 'baz' }).done(function (rInfo) { + check(rInfo, 'isCreatedByADS', false); + check(rInfo, 'isCreatedByOthers', true); + check(rInfo, 'isKnownToADS', false); }); + }); - it('getRecordInfo', function () { - var oApi = getOrcidApi(this.minsub.beehive); - sinon.stub(oApi, 'needsUpdate', _.constant(false)); - - var db = { - "identifier:foo": { - "sourcedByADS": true, - "putcode": 889362, - "idx": 0 - }, - "identifier:bar": { - "sourcedByADS": false, - "putcode": 878658, - "idx": 1 - }, - "identifier:boo": { - "sourcedByADS": true, - "putcode": 880867, - "idx": 1 - }, - "identifier:baz": { - "sourcedByADS": false, - "putcode": 880867, - "idx": -1 - } - }; - - var check = function (rInfo, val, exp) { - expect(rInfo[val]).to.equal(exp, val + ' : ' + exp); - }; - - oApi.db = db; - oApi.getRecordInfo({ bibcode: 'foo' }).done(function (rInfo) { - check(rInfo, 'isCreatedByADS', true); - check(rInfo, 'isCreatedByOthers', false); - check(rInfo, 'isKnownToADS', true); - }); - - oApi.getRecordInfo({ bibcode: 'bar' }).done(function (rInfo) { - check(rInfo, 'isCreatedByADS', false); - check(rInfo, 'isCreatedByOthers', true); - check(rInfo, 'isKnownToADS', true); - }); - - oApi.getRecordInfo({ doi: 'boo' }).done(function (rInfo) { - check(rInfo, 'isCreatedByADS', true); - check(rInfo, 'isCreatedByOthers', false); - check(rInfo, 'isKnownToADS', true); - }); + it('detects children entries', function () { + var oApi = getOrcidApi(this.minsub.beehive); + var createMockDB = function (entries) { + return _.reduce(entries, function (out, e) { + out['identifier:' + e[0]] = { + bibcode: e[1], + putcode: e[2] + }; + return out; + }, {}); + }; + + var mockDb = createMockDB([ + ['A', '', '1'], + ['B', 'A', '2'], // child of A + ['C', '', '3'], + ['D', 'A', '4'], // child of A + ['E', 'B', '5'], // child of B + ['F', 'B', '6'], // child of B + ['G', '', '7'], + ['H', 'G', '8'] // child of G + ]); + + oApi._combineDatabaseWorks(mockDb); + + expect(['2', '4']).to.deep.equal(mockDb['identifier:A'].children); + expect(['5', '6']).to.deep.equal(mockDb['identifier:B'].children); + expect(['8']).to.deep.equal(mockDb['identifier:G'].children); + expect(undefined).to.eql(mockDb['identifier:H'].children); + }); - oApi.getRecordInfo({ doi: 'baz' }).done(function (rInfo) { - check(rInfo, 'isCreatedByADS', false); - check(rInfo, 'isCreatedByOthers', true); - check(rInfo, 'isKnownToADS', false); + it('reconcile profile works', function () { + var oApi = getOrcidApi(this.minsub.beehive); + createOrcidServer(oApi, this.minsub); + this.sb.stub(oApi, 'isSourcedByADS', function (work) { + return work.getSourceClientIdPath() === 'ADS'; + }); + var createMockProfile = function (groups) { + var out = { 'activities-summary': { 'works': {}}} + out['activities-summary']['works']['group'] = groups.map(function (entries) { + return { + 'work-summary': entries.map(function (entry) { + return { + 'title': { title: { value: entry.title } }, + 'source': { 'source-client-id': { path: entry.path }, 'source-name': { value: entry.title } }, + 'external-ids': { 'external-id': [ { 'external-id-type': entry.type } ]} + }; + }) + }; }); + return out; + } + var mockProfile = createMockProfile([ + [ + { title: 'A', path: '', type: 'other' }, // <-- since there are no better choices + { title: 'B', path: '', type: 'other' }, + { title: 'C', path: '', type: 'other' } + ], + [ + { title: 'A', path: '', type: 'other' }, + { title: 'B', path: '', type: 'other' }, + { title: 'C', path: '', type: 'doi' } // <-- has a doi + ], + [ + { title: 'A', path: '', type: 'other' }, + { title: 'B', path: '', type: 'bibcode' }, // <-- has a bibcode + { title: 'C', path: '', type: 'other' } + ], + [ + { title: 'A', path: '', type: 'other' }, + { title: 'B', path: 'ADS', type: 'other' }, // <-- source is ADS + { title: 'C', path: '', type: 'other' } + ], + [ + { title: 'A', path: '', type: 'other' }, + { title: 'B', path: '', type: 'doi' }, + { title: 'C', path: '', type: 'bibcode' } // <-- bibcode beats doi + ], + [ + { title: 'A', path: '', type: 'doi' }, + { title: 'B', path: '', type: 'bibcode' }, // should take the first bibcode + { title: 'C', path: '', type: 'bibcode' } + ], + [ + { title: 'A', path: '', type: 'bibcode' }, + { title: 'B', path: '', type: 'doi' }, + { title: 'C', path: 'ADS', type: '' } // <-- ADS beats anything + ] + ]); + + var profile = oApi._reconcileProfileWorks(mockProfile); + + // profile should be type Profile + expect(profile).to.be.instanceOf(Profile); + + var expected = ['A', 'C', 'B', 'B', 'C', 'B', 'C']; + var actual = profile.works.map(function (w) { + return w.getSourceName(); + }); + + // check that we get the right works in the final profile + expect(expected).to.deep.equal(actual); + + // for good measure, check the source names, to make sure an array is there + _.forEach(profile.works, function (w) { + expect(['A', 'B', 'C']).to.deep.equal(w.sources); }); + }); - it.skip("has methods to query status of a record", function(done) { - var oApi = getOrcidApi(this.minsub.beehive); - createOrcidServer(oApi, this.minsub); - sinon.spy(oApi, 'updateDatabase'); - sinon.stub(oApi, '_checkIdsInADS', function (query) { - return $.Deferred().resolve({ - "bibcode:2018cnsns..56..296s": "2018cnsns..56..296s", - "doi:10.1016/j.cnsns.2017.08.013": "2018cnsns..56..296s", - "bibcode:2018cnsns..56..270q": "2018cnsns..56..270q", - "doi:10.1016/j.cnsns.2017.08.014": "2018cnsns..56..270q" - }).promise(); - }); + it.skip("has methods to query status of a record", function(done) { + var oApi = getOrcidApi(this.minsub.beehive); + createOrcidServer(oApi, this.minsub); + sinon.spy(oApi, 'updateDatabase'); + sinon.stub(oApi, '_checkIdsInADS', function (query) { + return $.Deferred().resolve({ + "bibcode:2018cnsns..56..296s": "2018cnsns..56..296s", + "doi:10.1016/j.cnsns.2017.08.013": "2018cnsns..56..296s", + "bibcode:2018cnsns..56..270q": "2018cnsns..56..270q", + "doi:10.1016/j.cnsns.2017.08.014": "2018cnsns..56..270q" + }).promise(); + }); + + // for testing purpose, force split into many queries + oApi.maxQuerySize = 2; + + oApi.getRecordInfo({bibcode: '2018cnsns..56..296s'}) + .done(function (recInfo) { + + expect(oApi._checkIdsInADS.called).to.eql(true); + expect(oApi._checkIdsInADS.calledTwice).to.eql(true); + expect(oApi._checkIdsInADS.args[0][0].get('q')).eql(["alternate_bibcode:(\"bibcode-foo\" OR \"test-bibcode\")"]); + expect(oApi._checkIdsInADS.args[1][0].get('q')).eql(["bibcode:(\"bibcode-foo\" OR \"test-bibcode\")"]); + + + expect(recInfo.isCreatedByADS).to.eql(true); + expect(recInfo.isCreatedByOthers).to.eql(false); + + // this one should return immediately + oApi.getRecordInfo({bibcode: 'bibcode-foo'}) + .done(function(recInfo) { + expect(recInfo.isCreatedByADS).to.eql(false); + expect(recInfo.isCreatedByOthers).to.eql(true); + expect(recInfo.isKnownToAds).to.eql(true); + }); + + oApi.getRecordInfo({doi: '10.1126/science.276.5309.88'}) // doi of bibcode-foo + .done(function(recInfo) { + expect(recInfo.isCreatedByADS).to.eql(false); + expect(recInfo.isCreatedByOthers).to.eql(true); + expect(recInfo.isKnownToAds).to.eql(true); + }); + + oApi.getRecordInfo({doi: '10.1103/physrevlett.84.3823'}) // test-bibcode + .done(function(recInfo) { + expect(recInfo.isCreatedByADS).to.eql(true); + expect(recInfo.isCreatedByOthers).to.eql(false); + expect(recInfo.isKnownToAds).to.eql(true); + }); + + oApi.getRecordInfo({bibcode: '1997Sci...276...88V'}) // alternate bibcode of bibcode-foo + .done(function(recInfo) { + expect(recInfo.isCreatedByADS).to.eql(false); + expect(recInfo.isCreatedByOthers).to.eql(true); + expect(recInfo.isKnownToAds).to.eql(true); + }); + + // found by one of the queries, but could not be mapped to bibcode + // this should not normally be happening, but i've added the logic + // to accomodate it - just in case... + oApi.getRecordInfo({bibcode: '2015CeMDA.tmp....1D'}) + .done(function(recInfo) { + expect(recInfo.isCreatedByADS).to.eql(false); + expect(recInfo.isCreatedByOthers).to.eql(true); + expect(recInfo.isKnownToAds).to.eql(false); + }); + + // non-ADS record + oApi.getRecordInfo({bibcode: 'sfasdfsdfsdfsdfsdf'}) + .done(function(recInfo) { + expect(recInfo.isCreatedByADS).to.eql(false); + expect(recInfo.isCreatedByOthers).to.eql(false); + expect(recInfo.isKnownToAds).to.eql(false); + }); + + oApi._checkIdsInADS.restore(); + oApi.updateDatabase.restore(); - // for testing purpose, force split into many queries - oApi.maxQuerySize = 2; - - oApi.getRecordInfo({bibcode: '2018cnsns..56..296s'}) - .done(function (recInfo) { - - expect(oApi._checkIdsInADS.called).to.eql(true); - expect(oApi._checkIdsInADS.calledTwice).to.eql(true); - expect(oApi._checkIdsInADS.args[0][0].get('q')).eql(["alternate_bibcode:(\"bibcode-foo\" OR \"test-bibcode\")"]); - expect(oApi._checkIdsInADS.args[1][0].get('q')).eql(["bibcode:(\"bibcode-foo\" OR \"test-bibcode\")"]); - - - expect(recInfo.isCreatedByADS).to.eql(true); - expect(recInfo.isCreatedByOthers).to.eql(false); - - // this one should return immediately - oApi.getRecordInfo({bibcode: 'bibcode-foo'}) - .done(function(recInfo) { - expect(recInfo.isCreatedByADS).to.eql(false); - expect(recInfo.isCreatedByOthers).to.eql(true); - expect(recInfo.isKnownToAds).to.eql(true); - }); - - oApi.getRecordInfo({doi: '10.1126/science.276.5309.88'}) // doi of bibcode-foo - .done(function(recInfo) { - expect(recInfo.isCreatedByADS).to.eql(false); - expect(recInfo.isCreatedByOthers).to.eql(true); - expect(recInfo.isKnownToAds).to.eql(true); - }); - - oApi.getRecordInfo({doi: '10.1103/physrevlett.84.3823'}) // test-bibcode - .done(function(recInfo) { - expect(recInfo.isCreatedByADS).to.eql(true); - expect(recInfo.isCreatedByOthers).to.eql(false); - expect(recInfo.isKnownToAds).to.eql(true); - }); - - oApi.getRecordInfo({bibcode: '1997Sci...276...88V'}) // alternate bibcode of bibcode-foo - .done(function(recInfo) { - expect(recInfo.isCreatedByADS).to.eql(false); - expect(recInfo.isCreatedByOthers).to.eql(true); - expect(recInfo.isKnownToAds).to.eql(true); - }); - - // found by one of the queries, but could not be mapped to bibcode - // this should not normally be happening, but i've added the logic - // to accomodate it - just in case... - oApi.getRecordInfo({bibcode: '2015CeMDA.tmp....1D'}) - .done(function(recInfo) { - expect(recInfo.isCreatedByADS).to.eql(false); - expect(recInfo.isCreatedByOthers).to.eql(true); - expect(recInfo.isKnownToAds).to.eql(false); - }); - - // non-ADS record - oApi.getRecordInfo({bibcode: 'sfasdfsdfsdfsdfsdf'}) - .done(function(recInfo) { - expect(recInfo.isCreatedByADS).to.eql(false); - expect(recInfo.isCreatedByOthers).to.eql(false); - expect(recInfo.isKnownToAds).to.eql(false); - }); - - oApi._checkIdsInADS.restore(); - oApi.updateDatabase.restore(); - - done(); - }); + done(); + }); - }); }); }); });