Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor [github] token persistence, again #1906

Merged
merged 8 commits into from
Aug 19, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/github-auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const { EventEmitter } = require('events')
const crypto = require('crypto')
const log = require('./log')
const secretIsValid = require('./sys/secret-is-valid')
Expand All @@ -15,6 +16,8 @@ let githubUserTokens = []
const reqRemaining = new Map() // From token to requests remaining.
const reqReset = new Map() // From token to timestamp.

const emitter = new EventEmitter()

// Personal tokens allow access to GitHub private repositories.
// You can manage your personal GitHub token at
// <https://github.com/settings/tokens>.
Expand Down Expand Up @@ -110,6 +113,7 @@ function setRoutes(server) {
}, 10000)
}
addGithubToken(data.token)
emitter.emit('token-added', data.token)
Copy link
Member

@PyvesB PyvesB Aug 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The listeners aren't actually using the token that is being passed around (as they call getAllTokenIds to get the whole list), but it probably doesn't hurt either to include it in the emitted event.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea… and the next iteration of this, with a redis backend, will use that. I believe Redis can do inserts and deletes instead of rewriting the whole thing.

end('Thanks!')
})
}
Expand Down Expand Up @@ -312,6 +316,7 @@ function githubRequest(request, url, query, cb) {
if (res.statusCode === 401) {
// Unauthorized.
rmGithubToken(githubToken)
emitter.emit('token-removed', githubToken)
} else {
const remaining = +res.headers['x-ratelimit-remaining']
// reset is in UTC epoch seconds.
Expand All @@ -331,6 +336,8 @@ module.exports = {
setRoutes,
serializeDebugInfo,
addGithubToken,
rmGithubToken,
getAllTokenIds,
removeAllTokens,
emitter,
}
54 changes: 33 additions & 21 deletions lib/token-persistence.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,55 @@
'use strict'

const autosave = require('json-autosave')
const fsos = require('fsos')
const githubAuth = require('./github-auth')
const log = require('./log')

// This is currently bound to the legacy github auth code. That will be
// replaced with a dependency-injected token provider.
class TokenPersistence {
constructor({ path }) {
this.path = path
this.save = undefined

this.noteTokenAdded = this.noteTokenAdded.bind(this)
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
}

async initialize() {
// This code is a bit difficult to understand, in part because it's
// working against the interface of `json-autosave` which wants to own the
// data structure.
const save = await autosave(this.path, { data: [] })
this.save = save
let contents
try {
contents = await fsos.get(this.path)
} catch (e) {
if (e.code === 'ENOENT') {
contents = '[]'
} else {
throw e
}
}

save.data.forEach(tokenString => {
const tokens = JSON.parse(contents)
tokens.forEach(tokenString => {
githubAuth.addGithubToken(tokenString)
})
}

async save() {
const tokens = githubAuth.getAllTokenIds()
await fsos.set(this.path, JSON.stringify(tokens))
}

// Override the autosave handler to refresh the token data before
// saving.
save.autosave = () => {
save.data = githubAuth.getAllTokenIds()
return save.save()
async noteTokenAdded(token) {
try {
await this.save()
} catch (e) {
log.error(e)
}
// Put the change in autosave handler into effect.
save.stop()
save.start()
}

async stop() {
if (this.save) {
this.save.stop()
await this.save.autosave()
this.save = undefined
async noteTokenRemoved(token) {
try {
await this.save()
} catch (e) {
log.error(e)
}
}
}
Expand Down
87 changes: 36 additions & 51 deletions lib/token-persistence.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,11 @@
const fs = require('fs')
const tmp = require('tmp')
const readFile = require('fs-readfile-promise')
const sinon = require('sinon')
const { sleep } = require('wait-promise')
const { expect } = require('chai')
const TokenPersistence = require('./token-persistence')
const githubAuth = require('./github-auth')

describe('Token persistence', function() {
// Fake timers must be set up before any timers are scheduled.
let clock
beforeEach(function() {
clock = sinon.useFakeTimers()
})
afterEach(function() {
clock.restore()
})

beforeEach(githubAuth.removeAllTokens)
afterEach(githubAuth.removeAllTokens)

Expand All @@ -27,67 +16,63 @@ describe('Token persistence', function() {
path = tmp.tmpNameSync()
persistence = new TokenPersistence({ path })
})
afterEach(function() {
if (persistence) {
persistence.stop()
persistence = null
}
})

context('when the file does not exist', function() {
it('creates it with an empty array', async function() {
it('does nothing', async function() {
await persistence.initialize()
const json = JSON.parse(await readFile(path))
expect(githubAuth.getAllTokenIds()).to.deep.equal([])
})

expect(json).to.deep.equal([])
it('saving creates an empty file', async function() {
await persistence.initialize()

await persistence.save()

const json = JSON.parse(await readFile(path))
expect(json).to.deep.deep.equal([])
})
})

context('when the file exists', function() {
it('loads the contents', async function() {
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))

beforeEach(async function() {
fs.writeFileSync(path, JSON.stringify(initialTokens))
})

it('loads the contents', async function() {
await persistence.initialize()

expect(githubAuth.getAllTokenIds()).to.deep.equal(initialTokens)
})
})

context('when shutting down', function() {
it('writes added tokens to the file', async function() {
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
fs.writeFileSync(path, JSON.stringify(initialTokens))

const newToken = 'e'.repeat(40)
const expected = initialTokens.slice()
expected.push(newToken)
context('when tokens are added', function() {
it('writes them to the file', async function() {
const newToken = 'e'.repeat(40)
const expected = initialTokens.slice()
expected.push(newToken)

await persistence.initialize()
githubAuth.addGithubToken(newToken)
await persistence.stop()
await persistence.initialize()
githubAuth.addGithubToken(newToken)
await persistence.noteTokenAdded(newToken)

const json = JSON.parse(await readFile(path))
expect(json).to.deep.equal(expected)
const json = JSON.parse(await readFile(path))
expect(json).to.deep.equal(expected)
})
})
})

context('time has elapsed', function() {
it('writes added tokens to the file', async function() {
const addedTokens = ['d', 'e'].map(char => char.repeat(40))
context('when tokens are removed', function() {
it('writes them to the file', async function() {
const expected = Array.from(initialTokens)
const toRemove = expected.pop()

await persistence.initialize()

addedTokens.forEach(githubAuth.addGithubToken)
await persistence.initialize()

// Fake time passing to trigger autosaving, then give the save a brief
// moment of real time to complete before reading.
clock.tick(5000)
clock.restore()
await sleep(200)
githubAuth.rmGithubToken(toRemove)
await persistence.noteTokenAdded(toRemove)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be noteTokenRemoved?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks, I caught that in #1939 but forgot to fix it here.


const json = JSON.parse(await readFile(path))
expect(json).to.deep.equal(addedTokens)
const json = JSON.parse(await readFile(path))
expect(json).to.deep.equal(expected)
})
})
})
})
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
"dot": "~1.1.2",
"emojic": "^1.1.14",
"escape-string-regexp": "^1.0.5",
"fsos": "^1.1.3",
"glob": "^7.1.1",
"gm": "^1.23.0",
"is-css-color": "^1.0.0",
"joi": "^13.3.0",
"js-yaml": "^3.11.0",
"json-autosave": "~1.1.2",
"jsonpath": "~1.0.0",
"lodash.countby": "^4.6.0",
"lodash.mapkeys": "^4.6.0",
Expand Down
16 changes: 12 additions & 4 deletions services/github/github-constellation.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ class GithubConstellation {
try {
await this.persistence.initialize()
} catch (e) {
// TODO Send to sentry.
console.error(e)
log.error(e)
}

githubAuth.emitter.on('token-added', this.persistence.noteTokenAdded)
githubAuth.emitter.on('token-removed', this.persistence.noteTokenRemoved)

setAdminRoutes(server)

if (serverSecrets && serverSecrets.gh_client_id) {
Expand All @@ -56,8 +58,14 @@ class GithubConstellation {
this.debugInterval = undefined
}

await this.persistence.stop()
this.persistence = undefined
githubAuth.emitter.removeListener(
'token-added',
this.persistence.noteTokenAdded
)
githubAuth.emitter.removeListener(
'token-removed',
this.persistence.noteTokenRemoved
)
}
}

Expand Down