From 1cd02ec4b01bfbd677993405c444ff12181d37a4 Mon Sep 17 00:00:00 2001 From: cvmcosta Date: Fri, 29 Jan 2021 15:32:52 -0300 Subject: [PATCH] feat(NamesAndRoles): Added the capability to remove page limitation --- dist/Provider/Services/NamesAndRoles.js | 14 ++++---- docs/changelog.md | 5 +++ docs/namesandroles.md | 4 +-- package.json | 2 +- src/Provider/Services/NamesAndRoles.js | 12 +++---- test/4-namesandroles.js | 47 ++++++++++++++++++++++++- 6 files changed, 67 insertions(+), 17 deletions(-) diff --git a/dist/Provider/Services/NamesAndRoles.js b/dist/Provider/Services/NamesAndRoles.js index 5886cff..0639027 100644 --- a/dist/Provider/Services/NamesAndRoles.js +++ b/dist/Provider/Services/NamesAndRoles.js @@ -46,7 +46,7 @@ class NamesAndRoles { * @param {Object} options - Request options. * @param {String} [options.role] - Filters based on the User role. * @param {Number} [options.limit] - Sets a maximum number of memberships to be returned per page. - * @param {Number} [options.pages = 1] - Sets a maximum number of pages to be returned. Defaults to 1. + * @param {Number} [options.pages = 1] - Sets a maximum number of pages to be returned. Defaults to 1. If set to false retrieves every available page. * @param {String} [options.url] - Retrieve memberships from a specific URL. Usually retrieved from the `next` link header of a previous request. * @param {Boolean} [options.resourceLinkId = false] - If set to true, retrieves resource Link level memberships. */ @@ -78,6 +78,11 @@ class NamesAndRoles { let next = idtoken.platformContext.namesRoles.context_memberships_url; if (options) { + if (options.pages || options.pages === false) { + provNamesAndRolesServiceDebug('Maximum number of pages retrieved: ' + options.pages); + pages = options.pages; + } + if (options.url) { next = options.url; query = false; @@ -96,11 +101,6 @@ class NamesAndRoles { provNamesAndRolesServiceDebug('Adding rlid parameter with value: ' + idtoken.platformContext.resource.id); query.push(['rlid', idtoken.platformContext.resource.id]); } - - if (options.pages) { - provNamesAndRolesServiceDebug('Maximum number of pages retrieved: ' + options.pages); - pages = options.pages; - } } } @@ -110,7 +110,7 @@ class NamesAndRoles { let curPage = 1; do { - if (curPage > pages) { + if (pages && curPage > pages) { if (next) result.next = next; break; } diff --git a/docs/changelog.md b/docs/changelog.md index ee92054..9bbf081 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,11 @@ ### CHANGELOG +#### V5.6.6 +> 2021-01-29 +> - Added the capability to remove the limit of number of pages returned by the Names and Roles Service. + + #### V5.6.5 > 2021-01-27 > - Fixed bug where Blackboard would not see access token request iat and exp claims. diff --git a/docs/namesandroles.md b/docs/namesandroles.md index b088c0a..60d10b5 100644 --- a/docs/namesandroles.md +++ b/docs/namesandroles.md @@ -106,7 +106,7 @@ The `getMembers()` method allows us to apply filters to the request, and these f Specifies the number of members per page that should be returned per members page **(By default only one members page is returned.)**. - **options.pages** - Specifies the number of pages that should be returned. *Defaults to 1*. + Specifies the number of pages that should be returned. *Defaults to 1*. If set to false retrieves every available page. Ex: @@ -212,7 +212,7 @@ Retrieves members from platform. | options | `Object` | Options object. | | options.role| `String` | Specific role to be returned. | | options.limit | `Number` | Specifies maximum number of members per page. | -| options.pages | `Number` | Specifies maximum number of pages returned. Defaults to 1. | +| options.pages | `Number` | Specifies maximum number of pages returned. Defaults to 1. If set to false retrieves every available page. | | options.url | `String` | Specifies the initial members endpoint, usually retrieved from a previous incomplete request. | | options.resourceLinkId | `Boolean` | If set to true, retrieves resource Link level memberships. | diff --git a/package.json b/package.json index 973336e..fb369a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ltijs", - "version": "5.6.5", + "version": "5.6.6", "description": "Easily turn your web application into a LTI 1.3 Learning Tool.", "main": "index.js", "engineStrict": true, diff --git a/src/Provider/Services/NamesAndRoles.js b/src/Provider/Services/NamesAndRoles.js index ad1747b..4bf7e6a 100644 --- a/src/Provider/Services/NamesAndRoles.js +++ b/src/Provider/Services/NamesAndRoles.js @@ -23,7 +23,7 @@ class NamesAndRoles { * @param {Object} options - Request options. * @param {String} [options.role] - Filters based on the User role. * @param {Number} [options.limit] - Sets a maximum number of memberships to be returned per page. - * @param {Number} [options.pages = 1] - Sets a maximum number of pages to be returned. Defaults to 1. + * @param {Number} [options.pages = 1] - Sets a maximum number of pages to be returned. Defaults to 1. If set to false retrieves every available page. * @param {String} [options.url] - Retrieve memberships from a specific URL. Usually retrieved from the `next` link header of a previous request. * @param {Boolean} [options.resourceLinkId = false] - If set to true, retrieves resource Link level memberships. */ @@ -50,6 +50,10 @@ class NamesAndRoles { let next = idtoken.platformContext.namesRoles.context_memberships_url if (options) { + if (options.pages || options.pages === false) { + provNamesAndRolesServiceDebug('Maximum number of pages retrieved: ' + options.pages) + pages = options.pages + } if (options.url) { next = options.url query = false @@ -66,10 +70,6 @@ class NamesAndRoles { provNamesAndRolesServiceDebug('Adding rlid parameter with value: ' + idtoken.platformContext.resource.id) query.push(['rlid', idtoken.platformContext.resource.id]) } - if (options.pages) { - provNamesAndRolesServiceDebug('Maximum number of pages retrieved: ' + options.pages) - pages = options.pages - } } } @@ -81,7 +81,7 @@ class NamesAndRoles { let curPage = 1 do { - if (curPage > pages) { + if (pages && curPage > pages) { if (next) result.next = next break } diff --git a/test/4-namesandroles.js b/test/4-namesandroles.js index 7ac08e3..e0751d8 100644 --- a/test/4-namesandroles.js +++ b/test/4-namesandroles.js @@ -128,7 +128,7 @@ describe('Testing Names and Roles Service', function () { lti.onConnect(async (token, req, res) => { try { - return res.send(await lti.NamesAndRoles.getMembers(token, { role: 'Learner', limit: 2, pages: 1 })) + return res.send(await lti.NamesAndRoles.getMembers(token, { role: 'Learner', limit: 2 })) } catch (err) { res.sendStatus(500) } @@ -184,6 +184,51 @@ describe('Testing Names and Roles Service', function () { expect(result.members[3]).to.deep.include(membersResult.members[1]) }) }) + + it('NamesAndRoles.getMembers() expected to return valid member list and include multiple pages when "pages" parameter is set to false', async () => { + const token = JSON.parse(JSON.stringify(tokenValid)) + token.nonce = encodeURIComponent([...Array(25)].map(_ => (Math.random() * 36 | 0).toString(36)).join``) + + const payload = signToken(token, '123456') + const state = encodeURIComponent([...Array(25)].map(_ => (Math.random() * 36 | 0).toString(36)).join``) + const url = await lti.appRoute() + + nock('http://localhost/moodle').get('/keyset').reply(200, { + keys: [ + { kty: 'RSA', e: 'AQAB', kid: '123456', n: 'VrJSr-xli8NfuAdk_Wem5BARmmW4BpJvXBx3MbFY_0grH9Cd7OxBwVYSwI4P4yhL27upa1_FCRwLi3raOPSJOkHEDvFwtyYZMvdYcpDYTv6JRVqbgEyZtHa-vjL1wBqqW75yPDRoyZdnA8MWrfyRUOak53ZVWHRKgBnP53oXm7M' } + ] + }) + + nock('http://localhost/moodle').post('/AccessTokenUrl').reply(200, { + access_token: 'dkj4985kjaIAJDJ89kl8rkn5', + token_type: 'bearer', + expires_in: 3600, + scope: 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly' + }) + + nock('http://localhost/moodle').get('/members?role=Learner&limit=2').reply(200, membersResult, { link: '; rel="differences",; rel="next",; rel="first",; rel="last"' }) + + nock('http://localhost/moodle').get('/api/lti/courses/1/names_and_roles?page=2&per_page=1').reply(200, membersResult) + + lti.onConnect(async (token, req, res) => { + try { + return res.send(await lti.NamesAndRoles.getMembers(token, { role: 'Learner', limit: 2, pages: false })) + } catch (err) { + console.log(err) + res.sendStatus(500) + } + }) + + return chai.request(lti.app).post(url).type('json').send({ id_token: payload, state: state }).set('Cookie', ['state' + state + '=s%3Ahttp%3A%2F%2Flocalhost%2Fmoodle.fsJogjTuxtbJwvJcuG4esveQAlih67sfEltuwRM6MX0; Path=/; HttpOnly;', 'ltiaHR0cDovL2xvY2FsaG9zdC9tb29kbGVDbGllbnRJZDEy=s%3A2.ZezwPKtv3Uibp4A%2F6cN0UzbIQlhA%2BTAKvbtN%2FvgGaCI; Path=/; HttpOnly; SameSite=None']).then(res => { + expect(res).to.have.status(200) + + const result = JSON.parse(res.text) + expect(result.differences).to.eql('http://localhost/moodle/api/lti/courses/1/names_and_roles?since=623698163') + expect(result.members[0]).to.deep.include(membersResult.members[0]) + expect(result.members[3]).to.deep.include(membersResult.members[1]) + }) + }) + it('NamesAndRoles.getMembers() expected to return valid member list when using custom url', async () => { const token = JSON.parse(JSON.stringify(tokenValid)) token.nonce = encodeURIComponent([...Array(25)].map(_ => (Math.random() * 36 | 0).toString(36)).join``)