diff --git a/packages/github-auth/README.md b/packages/github-auth/README.md index cd3f48a3f0..ff74ec6c4c 100644 --- a/packages/github-auth/README.md +++ b/packages/github-auth/README.md @@ -13,7 +13,7 @@ This README outlines the details of collaborating on this Ember addon. To run the demo app and interact with GitHub, you need to register your app via https://github.com/settings/developers and get a client ID & secret. Set them via the environment variables `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`. This allows this plugin to act as an OAuth2 client that speaks to GitHub on behalf of users who authorize it. -For the present, you also need a user token that the Hub can use to represent itself. You can use a [Personal Access Token](https://github.com/settings/tokens). Set is via the environment variable `GITHUB_TOKEN`. +For the present, you also need a user token that the Hub can use to represent itself. You can use a [Personal Access Token](https://github.com/settings/tokens). Make sure that this token has `repo` and `user` scope, as well as push permissions on the GitHub repos that you configure to use with the github-auth plugin datasource. This allows the plugin to have sufficient privileges to be able to query the repo for collaborator permissions. Set is via the environment variable `GITHUB_TOKEN`. * `ember serve` * Visit your app at [http://localhost:4200](http://localhost:4200). diff --git a/packages/github-auth/cardstack/authenticator.js b/packages/github-auth/cardstack/authenticator.js index 93693cb5cd..d6ebe84d14 100644 --- a/packages/github-auth/cardstack/authenticator.js +++ b/packages/github-auth/cardstack/authenticator.js @@ -73,7 +73,8 @@ module.exports = class { attributes: { name: user.name, email: user.email, - "avatar-url": user.avatar_url + "avatar-url": user.avatar_url, + permissions: user.permissions || [] } } }; diff --git a/packages/github-auth/cardstack/searcher.js b/packages/github-auth/cardstack/searcher.js index 6e79c63736..c5d2f28518 100644 --- a/packages/github-auth/cardstack/searcher.js +++ b/packages/github-auth/cardstack/searcher.js @@ -1,19 +1,33 @@ const request = require('./lib/request'); +const { get, groupBy } = require('lodash'); + +const githubPermissions = { + admin: ['read', 'write', 'admin'], + write: ['read', 'write'], + read: ['read'] +}; module.exports = class GitHubSearcher { static create(...args) { return new this(...args); } - constructor({ token, dataSource }) { + constructor(opts) { + let { token, dataSource, permissions } = opts; this.token = token; this.dataSource = dataSource; + this.permissions = permissions; + this.cacheMaxAge = opts['cache-max-age']; } async get(session, branch, type, id, next) { + let result = await await next(); + if (result) { + return result; + } + if (type === 'github-users') { return this._getUser(id); } - return next(); } async search(session, branch, query, next) { @@ -33,6 +47,64 @@ module.exports = class GitHubSearcher { } }; let response = await request(options); - return await this.dataSource.rewriteExternalUser(response.body); + let userData = response.body; + if (!userData) { return; } + + userData.permissions = await this._getPermissions(userData.login); + let user = await this.dataSource.rewriteExternalUser(userData); + + let maxAge = this.cacheMaxAge; + if (maxAge == null) { + let cacheControl = get(response, 'response.headers.cache-control') || + get(response, 'response.headers.Cache-Control'); + if (cacheControl) { + let match = /max-age=(\d+)/.exec(cacheControl); + if (match && match.length > 1) { + maxAge = parseInt(match[1], 10); + } + } + } + + if (maxAge) { + user.meta = user.meta || {}; + user.meta['cardstack-cache-control'] = { 'max-age': maxAge }; + } + + return user; + } + + async _getPermissions(username) { + if (!username || !this.permissions || !this.permissions.length) { return; } + + let permissions = []; + let repos = groupBy(this.permissions, 'repo'); + + for (let repo of Object.keys(repos)) { + let options = { + hostname: 'api.github.com', + port: 443, + path: `/repos/${repo}/collaborators/${username}/permission`, + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': '@cardstack/github-auth', + Authorization: `token ${this.token}` + } + }; + let response = await request(options); + let userPermission = get(response, 'body.permission'); + if (!userPermission) { continue; } + + let userExpandedPermissions = githubPermissions[userPermission]; + if (!userExpandedPermissions) { continue; } + + for (let repoPermission of repos[repo]) { + if (userExpandedPermissions.includes(repoPermission.permission)) { + permissions.push(repoPermission); + } + } + } + + return permissions; } }; diff --git a/packages/github-auth/cardstack/static-model.js b/packages/github-auth/cardstack/static-model.js index d656f4541e..f604be1a50 100644 --- a/packages/github-auth/cardstack/static-model.js +++ b/packages/github-auth/cardstack/static-model.js @@ -20,7 +20,8 @@ module.exports = function({ dataSource, provideUserSchema }) { 'fields': { data: [ { type: 'fields', id: 'name' }, { type: 'fields', id: 'email' }, - { type: 'fields', id: 'avatar-url' } + { type: 'fields', id: 'avatar-url' }, + { type: 'fields', id: 'permissions' } ] } } }, @@ -45,5 +46,12 @@ module.exports = function({ dataSource, provideUserSchema }) { 'field-type': '@cardstack/core-types::string' } }, + { + type: 'fields', + id: 'permissions', + attributes: { + 'field-type': '@cardstack/core-types::object' + } + }, ]; }; diff --git a/packages/github-auth/node-tests/authenticator-test.js b/packages/github-auth/node-tests/authenticator-test.js index 88e350052a..ac2313c398 100644 --- a/packages/github-auth/node-tests/authenticator-test.js +++ b/packages/github-auth/node-tests/authenticator-test.js @@ -9,7 +9,8 @@ const JSONAPIFactory = require('@cardstack/test-support/jsonapi-factory'); const { validAccessToken, githubUser, - githubUsersResponse + githubUsersResponse, + githubReadPermissions } = require('./fixtures/github-responses'); describe('github-auth/authenticator', function() { @@ -25,12 +26,21 @@ describe('github-auth/authenticator', function() { mayLogin: true }); + factory.addResource('grants') + .withRelated('who', [{ type: 'groups', id: 'everyone' }]) + .withRelated('types', [{ type: 'content-types', id: 'github-users' }]) + .withAttributes({ + 'may-read-resource': true, + 'may-read-fields': true + }); + factory.addResource('data-sources', 'github').withAttributes({ sourceType: '@cardstack/github-auth', params: { 'client-id': 'mock-github-client-id', 'client-secret': 'mock-github-client-secret', - token: 'mock-github-token' + token: 'mock-github-token', + permissions: [{ repo: 'cardstack/repo1', permission: 'read' }] } }); @@ -44,36 +54,40 @@ describe('github-auth/authenticator', function() { await destroyDefaultEnvironment(env); } - before(setup); - after(teardown); + beforeEach(setup); + afterEach(teardown); + + it('returns github-users document for an authenticated github session', async function() { + let { login:id } = githubUser; - it('returns token for an authenticated github session', async function() { nock('https://github.com') .post('/login/oauth/access_token') .reply(200, validAccessToken); nock('https://api.github.com') .get('/user') - .reply(function() { - return [ 200, githubUser, { - 'Cache-Control': 'private, max-age=60, s-maxage=60' - }]; - }); + .reply(200, githubUser); nock('https://api.github.com') - .get('/users/habdelra') - .reply(function() { - return [ 200, githubUsersResponse, { - 'Cache-Control': 'private, max-age=60, s-maxage=60' - }]; - }); + .get(`/users/${id}`) + .reply(200, githubUsersResponse); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo1/collaborators/${id}/permission`) + .reply(200, githubReadPermissions); let response = await request.post('/auth/github').send({ authorizationCode: 'authToken' }); expect(response).hasStatus(200); expect(response.body).has.deep.property('data.type', 'github-users'); - expect(response.body).has.deep.property('data.id', 'habdelra'); + expect(response.body).has.deep.property('data.id', id); expect(response.body).has.deep.property('data.meta.source', 'github'); + expect(response.body.data.attributes).to.deep.equal({ + "name": "Hassan Abdel-Rahman", + "email": "hassan@cardstack.com", + "avatar-url": "https://avatars2.githubusercontent.com/u/61075?v=4", + "permissions": [{ "repo": "cardstack/repo1", "permission": "read" }] + }); expect(response.body.data.meta.token).is.ok; expect(response.body.data.meta.validUntil).is.ok; }); diff --git a/packages/github-auth/node-tests/fixtures/github-responses.js b/packages/github-auth/node-tests/fixtures/github-responses.js index a6d147daa9..08085d1014 100644 --- a/packages/github-auth/node-tests/fixtures/github-responses.js +++ b/packages/github-auth/node-tests/fixtures/github-responses.js @@ -93,5 +93,97 @@ module.exports = { "collaborators": 0, "private_repos": 0 } - } + }, + githubAdminPermissions: { + "permission": "admin", + "user": { + "login": "habdelra", + "id": 61076, + "node_id": "MDQ6VXNlcjYxMDc1", + "avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/habdelra", + "html_url": "https://github.com/habdelra", + "followers_url": "https://api.github.com/users/habdelra/followers", + "following_url": "https://api.github.com/users/habdelra/following{/other_user}", + "gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}", + "starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/habdelra/subscriptions", + "organizations_url": "https://api.github.com/users/habdelra/orgs", + "repos_url": "https://api.github.com/users/habdelra/repos", + "events_url": "https://api.github.com/users/habdelra/events{/privacy}", + "received_events_url": "https://api.github.com/users/habdelra/received_events", + "type": "User", + "site_admin": false + } + }, + githubWritePermissions: { + "permission": "write", + "user": { + "login": "habdelra", + "id": 61076, + "node_id": "MDQ6VXNlcjYxMDc1", + "avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/habdelra", + "html_url": "https://github.com/habdelra", + "followers_url": "https://api.github.com/users/habdelra/followers", + "following_url": "https://api.github.com/users/habdelra/following{/other_user}", + "gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}", + "starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/habdelra/subscriptions", + "organizations_url": "https://api.github.com/users/habdelra/orgs", + "repos_url": "https://api.github.com/users/habdelra/repos", + "events_url": "https://api.github.com/users/habdelra/events{/privacy}", + "received_events_url": "https://api.github.com/users/habdelra/received_events", + "type": "User", + "site_admin": false + } + }, + githubReadPermissions: { + "permission": "read", + "user": { + "login": "habdelra", + "id": 61076, + "node_id": "MDQ6VXNlcjYxMDc1", + "avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/habdelra", + "html_url": "https://github.com/habdelra", + "followers_url": "https://api.github.com/users/habdelra/followers", + "following_url": "https://api.github.com/users/habdelra/following{/other_user}", + "gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}", + "starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/habdelra/subscriptions", + "organizations_url": "https://api.github.com/users/habdelra/orgs", + "repos_url": "https://api.github.com/users/habdelra/repos", + "events_url": "https://api.github.com/users/habdelra/events{/privacy}", + "received_events_url": "https://api.github.com/users/habdelra/received_events", + "type": "User", + "site_admin": false + } + }, + githubNoPermissions: { + "permission": "none", + "user": { + "login": "habdelra", + "id": 61076, + "node_id": "MDQ6VXNlcjYxMDc1", + "avatar_url": "https://avatars2.githubusercontent.com/u/61075?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/habdelra", + "html_url": "https://github.com/habdelra", + "followers_url": "https://api.github.com/users/habdelra/followers", + "following_url": "https://api.github.com/users/habdelra/following{/other_user}", + "gists_url": "https://api.github.com/users/habdelra/gists{/gist_id}", + "starred_url": "https://api.github.com/users/habdelra/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/habdelra/subscriptions", + "organizations_url": "https://api.github.com/users/habdelra/orgs", + "repos_url": "https://api.github.com/users/habdelra/repos", + "events_url": "https://api.github.com/users/habdelra/events{/privacy}", + "received_events_url": "https://api.github.com/users/habdelra/received_events", + "type": "User", + "site_admin": false + } + }, }; diff --git a/packages/github-auth/node-tests/searcher-test.js b/packages/github-auth/node-tests/searcher-test.js index ee6636e76d..3f25d1f328 100644 --- a/packages/github-auth/node-tests/searcher-test.js +++ b/packages/github-auth/node-tests/searcher-test.js @@ -5,52 +5,399 @@ const { } = require('@cardstack/test-support/env'); const JSONAPIFactory = require('@cardstack/test-support/jsonapi-factory'); const { - githubUsersResponse + githubUsersResponse, + githubAdminPermissions, + githubWritePermissions, + githubReadPermissions, + githubNoPermissions } = require('./fixtures/github-responses'); describe('github-auth/searcher', function() { let env, searchers; - async function setup() { - let factory = new JSONAPIFactory(); - - factory.addResource('data-sources', 'github').withAttributes({ - sourceType: '@cardstack/github-auth', - params: { - 'client-id': 'mock-github-client-id', - 'client-secret': 'mock-github-client-secret', - token: 'mock-github-token' - } - }); - - env = await createDefaultEnvironment(`${__dirname}/github-authenticator`, factory.getModels()); - searchers = env.lookup('hub:searchers'); + async function alterExpiration(branch, type, id, interval) { + let client = env.lookup(`plugin-client:${require.resolve('@cardstack/pgsearch/client')}`); + let result = await client.query('update documents set expires = expires + $1 where branch=$2 and type=$3 and id=$4', [interval, branch, type, id]); + if (result.rowCount !== 1) { + throw new Error(`test was unable to alter expiration`); + } } async function teardown() { await destroyDefaultEnvironment(env); } - before(setup); - after(teardown); + describe('github no permissions configured', function() { + async function setup() { + let factory = new JSONAPIFactory(); + + factory.addResource('data-sources', 'github').withAttributes({ + sourceType: '@cardstack/github-auth', + params: { + 'client-id': 'mock-github-client-id', + 'client-secret': 'mock-github-client-secret', + token: 'mock-github-token', + } + }); + + env = await createDefaultEnvironment(`${__dirname}/github-authenticator`, factory.getModels()); + searchers = env.lookup('hub:searchers'); + } + + beforeEach(setup); + afterEach(teardown); + + it('does not set permissions for user when no permissions configured', async function() { + let userMock = Object.assign({}, githubUsersResponse); + let { login:id } = userMock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(200, userMock); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user.data.attributes.permissions.length).to.equal(0); + }); + }); + + describe('github user permissions', function() { + async function setup() { + let factory = new JSONAPIFactory(); - it('can get github user', async function() { - const { login:id } = githubUsersResponse; + factory.addResource('data-sources', 'github').withAttributes({ + sourceType: '@cardstack/github-auth', + params: { + 'client-id': 'mock-github-client-id', + 'client-secret': 'mock-github-client-secret', + token: 'mock-github-token', + permissions: [ + { repo: 'cardstack/repo1', permission: 'read' }, + { repo: 'cardstack/repo1', permission: 'write' }, + { repo: 'cardstack/repo1', permission: 'admin' }, - nock('https://api.github.com') - .get(`/users/${id}`) - .reply(function() { - return [ 200, githubUsersResponse, { - 'Cache-Control': 'private, max-age=60, s-maxage=60' - }]; + { repo: 'cardstack/repo2', permission: 'read' }, + + { repo: 'cardstack/repo3', permission: 'write' }, + ] + } }); - let user = await searchers.get(env.session, 'master', 'github-users', id); + env = await createDefaultEnvironment(`${__dirname}/github-authenticator`, factory.getModels()); + searchers = env.lookup('hub:searchers'); + } + + beforeEach(setup); + afterEach(teardown); + + it('does not set permissions for user with no access to repo', async function() { + let userMock = Object.assign({}, githubUsersResponse); + let permissionMock = Object.assign({}, githubNoPermissions); + let { login:id } = userMock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(200, userMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo1/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo2/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo3/collaborators/${id}/permission`) + .reply(200, permissionMock); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user.data.attributes.permissions.length).to.equal(0); + }); + + it('can set permissions for user with read access on repo', async function() { + let userMock = Object.assign({}, githubUsersResponse); + let permissionMock = Object.assign({}, githubReadPermissions); + let { login:id } = userMock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(200, userMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo1/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo2/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo3/collaborators/${id}/permission`) + .reply(200, permissionMock); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo1', permission: 'read' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo1', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo1', permission: 'admin' }); + + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo2', permission: 'read' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo2', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo2', permission: 'admin' }); + + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'read' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'admin' }); + }); + + it('can set permissions for user with write access on repo', async function() { + let userMock = Object.assign({}, githubUsersResponse); + let permissionMock = Object.assign({}, githubWritePermissions); + let { login:id } = userMock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(200, userMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo1/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo2/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo3/collaborators/${id}/permission`) + .reply(200, permissionMock); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo1', permission: 'read' }); + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo1', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo1', permission: 'admin' }); + + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo2', permission: 'read' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo2', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo2', permission: 'admin' }); + + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'read' }); + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo3', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'admin' }); + }); + + it('can set permissions for user with admin access on repo', async function() { + let userMock = Object.assign({}, githubUsersResponse); + let permissionMock = Object.assign({}, githubAdminPermissions); + let { login:id } = userMock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(200, userMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo1/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo2/collaborators/${id}/permission`) + .reply(200, permissionMock); + + nock('https://api.github.com') + .get(`/repos/cardstack/repo3/collaborators/${id}/permission`) + .reply(200, permissionMock); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo1', permission: 'read' }); + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo1', permission: 'write' }); + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo1', permission: 'admin' }); + + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo2', permission: 'read' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo2', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo2', permission: 'admin' }); - expect(user).has.deep.property('data.id', id); - expect(user).has.deep.property('data.type', 'github-users'); - expect(user).has.deep.property('data.attributes.name', githubUsersResponse.name); - expect(user).has.deep.property('data.attributes.email', githubUsersResponse.email); - expect(user).has.deep.property('data.attributes.avatar-url', githubUsersResponse.avatar_url); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'read' }); + expect(user.data.attributes.permissions).to.include({ repo: 'cardstack/repo3', permission: 'write' }); + expect(user.data.attributes.permissions).to.not.include({ repo: 'cardstack/repo3', permission: 'admin' }); + }); + }); + + describe('default cache control', function() { + async function setup() { + let factory = new JSONAPIFactory(); + + factory.addResource('data-sources', 'github').withAttributes({ + sourceType: '@cardstack/github-auth', + params: { + 'client-id': 'mock-github-client-id', + 'client-secret': 'mock-github-client-secret', + token: 'mock-github-token' + } + }); + + env = await createDefaultEnvironment(`${__dirname}/github-authenticator`, factory.getModels()); + searchers = env.lookup('hub:searchers'); + } + + beforeEach(setup); + afterEach(teardown); + + it('can get github user', async function() { + let mock = Object.assign({}, githubUsersResponse); + let { login:id } = mock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(function() { + return [ 200, mock, { + 'cache-control': 'private, max-age=60, s-maxage=60' + }]; + }); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + + expect(user).has.deep.property('data.id', id); + expect(user).has.deep.property('data.type', 'github-users'); + expect(user).has.deep.property('data.attributes.name', githubUsersResponse.name); + expect(user).has.deep.property('data.attributes.email', githubUsersResponse.email); + expect(user).has.deep.property('data.attributes.avatar-url', githubUsersResponse.avatar_url); + }); + + it('can cache github users', async function() { + let mock = Object.assign({}, githubUsersResponse); + let { login:id } = mock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .reply(function() { + return [ 200, mock, { + 'cache-control': 'private, max-age=60, s-maxage=60' + }]; + }); + + await searchers.get(env.session, 'master', 'github-users', id); + mock.name = "Van Gogh"; + + let user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user).has.deep.property('data.attributes.name', "Hassan Abdel-Rahman"); + }); + + it('can invalidate cached github users using github provided cache-control', async function() { + let mock = Object.assign({}, githubUsersResponse); + let { login:id } = mock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .times(2) + .reply(function() { + return [ 200, mock, { + 'cache-control': 'private, max-age=60, s-maxage=60' + }]; + }); + + await searchers.get(env.session, 'master', 'github-users', id); + mock.name = "Van Gogh"; + + await alterExpiration('master', 'github-users', id, '-30 seconds'); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user).has.deep.property('data.attributes.name', "Hassan Abdel-Rahman"); + + await alterExpiration('master', 'github-users', id, '-31 seconds'); + + user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user).has.deep.property('data.attributes.name', "Van Gogh"); + }); + }); + + describe('custom cache control', function() { + async function setup() { + let factory = new JSONAPIFactory(); + + factory.addResource('data-sources', 'github').withAttributes({ + sourceType: '@cardstack/github-auth', + params: { + 'client-id': 'mock-github-client-id', + 'client-secret': 'mock-github-client-secret', + token: 'mock-github-token', + 'cache-max-age': 300 + } + }); + + env = await createDefaultEnvironment(`${__dirname}/github-authenticator`, factory.getModels()); + searchers = env.lookup('hub:searchers'); + } + + beforeEach(setup); + afterEach(teardown); + + it('can invalidate cached github users using custom cache-control', async function() { + let mock = Object.assign({}, githubUsersResponse); + let { login:id } = mock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .times(2) + .reply(function() { + return [ 200, mock, { + 'cache-control': 'private, max-age=60, s-maxage=60' + }]; + }); + + await searchers.get(env.session, 'master', 'github-users', id); + mock.name = "Van Gogh"; + + await alterExpiration('master', 'github-users', id, '-61 seconds'); + + let user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user).has.deep.property('data.attributes.name', "Hassan Abdel-Rahman"); + + await alterExpiration('master', 'github-users', id, '-240 seconds'); + user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user).has.deep.property('data.attributes.name', "Van Gogh"); + }); + }); + + describe('no caching', function() { + async function setup() { + let factory = new JSONAPIFactory(); + + factory.addResource('data-sources', 'github').withAttributes({ + sourceType: '@cardstack/github-auth', + params: { + 'client-id': 'mock-github-client-id', + 'client-secret': 'mock-github-client-secret', + token: 'mock-github-token', + 'cache-max-age': 0 + } + }); + + env = await createDefaultEnvironment(`${__dirname}/github-authenticator`, factory.getModels()); + searchers = env.lookup('hub:searchers'); + } + + beforeEach(setup); + afterEach(teardown); + + it('can not cache github users', async function() { + let mock = Object.assign({}, githubUsersResponse); + let { login:id } = mock; + + nock('https://api.github.com') + .get(`/users/${id}`) + .times(2) + .reply(function() { + return [ 200, mock, { + 'cache-control': 'private, max-age=60, s-maxage=60' + }]; + }); + + await searchers.get(env.session, 'master', 'github-users', id); + mock.name = "Van Gogh"; + + let user = await searchers.get(env.session, 'master', 'github-users', id); + expect(user).has.deep.property('data.attributes.name', "Van Gogh"); + }); }); }); diff --git a/packages/github-auth/tests/dummy/cardstack/data-sources/github.js b/packages/github-auth/tests/dummy/cardstack/data-sources/github.js index 33e876df6a..47dabc9436 100644 --- a/packages/github-auth/tests/dummy/cardstack/data-sources/github.js +++ b/packages/github-auth/tests/dummy/cardstack/data-sources/github.js @@ -7,7 +7,12 @@ module.exports = [ params: { 'client-id': process.env.GITHUB_CLIENT_ID, 'client-secret': process.env.GITHUB_CLIENT_SECRET, - token: process.env.GITHUB_TOKEN + token: process.env.GITHUB_TOKEN, + permissions: [ + { repo: 'cardstack/cardstack', permission: 'read' }, + { repo: 'cardstack/cardstack', permission: 'write' }, + { repo: 'cardstack/cardstack', permission: 'admin' } + ] } } },