Compare commits

...

24 Commits

Author SHA1 Message Date
Brandon Keepers 67a9945954
Move into adapter subdir 2018-10-08 16:38:31 -05:00
Brandon Keepers fed50764b1
Rename `github` => `adapter` 2018-10-07 13:08:45 -04:00
Brandon Keepers c270752bc2
Add private key fixture 2018-10-07 11:47:52 -04:00
Brandon Keepers d5b2733fd7
Merge remote-tracking branch 'origin/master' into ts-extract-github-adapter
* origin/master:
  Update octokit rest (#721)
2018-10-07 11:45:58 -04:00
Brandon Keepers dc42f5457a
Merge remote-tracking branch 'origin/master' into ts-extract-github-adapter
* origin/master: (51 commits)
  remove manual base64 encoding/decoding of private key (#717)
  chore(test): convert app/default test to TypeScript (#703)
  chore(test): convert Sentry test to TypeScript (#702)
  Detect color support (#708)
  Fix two typos (#709)
  7.2.0
  httpSSSSSSS (#701)
  chore(tests): convert serializers test to TypeScript (#695)
  Update development docs to explain GitHub App Manifests (#700)
  Probot Support for GitHub App Manifests (#650)
  Resolve Jest transform option deprecation warning (#694)
  chore(package): update nock to version 10.0.0
  7.1.2
  fix(package): require @octokit/webhooks@^5.0.2 (#691)
  Relax webhooks requirement
  fix(package): update @octokit/webhooks to version 5.0.1
  Update best-practices.md (#682)
  7.1.1
  add npm ignore for package-locks
  update octokit/rest and tests to use /app/installations (#681)
  ...
2018-10-07 11:43:05 -04:00
Brandon Keepers c56370d99d
Doc tweaks 2018-07-15 08:36:44 -05:00
Brandon Keepers 9c5cc31ff3
Remove @private 2018-07-15 08:21:00 -05:00
Brandon Keepers bfa3e683f3
Fix accidental change to catchErrors in constructor 2018-07-15 07:54:23 -05:00
Brandon Keepers 5611d862b6
Add deprecations for `app.app()` and `app.auth` 2018-07-15 07:53:05 -05:00
Brandon Keepers 8928971e81
Rename `adapter` to just `github` 2018-07-15 07:36:34 -05:00
Brandon Keepers c869a18b02
Merge remote-tracking branch 'origin/master' into ts-extract-github-adapter
* origin/master:
  chore(package): update ts-jest to version 23.0.0 (#602)
  chore(test): Convert application tests to TypeScript (#614)
  Clarifies the Probot requires a webhook secret (#621)
  chore(test): Run tslint against tests (#622)
  Tweak formatting of config docs (#603)
  Fix: Improve typing of Context (#617)
  fix: Make cacheErrors option optional (#616)
  7.0.1
  Update ocotokit/rest.js to v15.9.4 (#610)
  fix: Remove extra event setting (#611)
  Update eslint to the latest version 🚀 (#581)
  docs: Fix new contribution grammar (#609)
  update the example app id image (#606)
  Update links for our API (#604)
  Remove next tag (#595)
  7.0.0
  Various cleanup and fixes from TS checklist (#585)
  Add some documentation about TS to README and CONTRIBUTING (#590)
  chore: Use tslint-config-standard (#594)
2018-07-15 07:02:50 -05:00
Brandon Keepers c107a83563
Merge remote-tracking branch 'origin/master' into ts-extract-github-adapter
* origin/master:
  chore(package): update @types/jest to version 23.1.3 (#593)
  Revert "Configuration: DISABLE_STATS=true" (#591)
  chore(package): update @types/jest to version 23.0.0 (#555)
  docs(configuration): DISABLE_STATS=true (#578)
  fix: Fix path to GraphQL API when using GitHub Enterprise (#580)
  Adds error case (#589)
  doc: Clarify that `WEBHOOK_SECRET` needs to match GitHub settings
(#588)
  chore: Replace jsdoc with typedoc (#583)
  Adds missing awaits (#584)
  doc: Update robot => app in README (#586)
  docs: clarify GitHub apps as GA in 2.13 and above (#579)
  docs: Tweak payload info in testing.md (#575)
  add backticks (#574)
  docs: Typo (#572)
  fix: Throw if GHE_HOST includes a protocol (#570)
  feat: app.load() (#554)
  fix: Optimize IGNORED_ACCOUNTS check to reduce the number of API
requests that the stats endpoint makes for spammy accounts (#566)
  add env config docs (#544)
2018-06-27 22:38:11 -05:00
Brandon Keepers fe423e5dfe
Merge remote-tracking branch 'origin/master' into ts-extract-github-adapter
* origin/master:
  chore: Turn on esModuleInterop for TypeScript (#564)
2018-06-11 17:24:48 -05:00
Brandon Keepers f5c5e0d164
Make adapter private 2018-06-10 23:17:25 -05:00
Brandon Keepers 2bfaea6282
Rename to GitHubApp 2018-06-10 22:23:55 -05:00
Brandon Keepers 38b5c44a91
Bump octokit, fix logging 2018-06-10 22:21:38 -05:00
Brandon Keepers cbd677a501
Merge remote-tracking branch 'origin/master' into ts-extract-github-adapter
* origin/master:
  fix(package): update dotenv to version 6.0.0 (#556)
  chore: Remove noImplicitAny and add types to params (#546)
  chore: Convert Context tests to TypeScript (#541)
  chore(package): Update bottleneck to the latest version 🚀 (#562)
2018-06-10 22:19:28 -05:00
Brandon Keepers e17e4a45d9
Move jwt creation into adapter 2018-06-07 18:15:00 -07:00
Brandon Keepers 220c2c6506
Move the webhook handler setup out of the server 2018-06-07 17:41:54 -07:00
Brandon Keepers 6e3d8efb71
Pass adapter into Application 2018-06-07 17:41:49 -07:00
Brandon Keepers 8e7e0d4ef3
Move GitHubAdapter to separate class 2018-06-07 17:41:44 -07:00
Brandon Keepers 8cbb87a4c9
Move createContext to adapter 2018-06-07 17:41:27 -07:00
Brandon Keepers 622a5996b3
Extract GitHubAdapter class 2018-06-07 17:41:17 -07:00
Brandon Keepers 3349082e5d
Extract context creation to method 2018-06-07 17:41:04 -07:00
14 changed files with 276 additions and 258 deletions

View File

@ -73,6 +73,7 @@
"bunyan-sentry-stream": "^1.1.0",
"cache-manager": "^2.4.0",
"commander": "^2.11.0",
"deprecated-decorator": "^0.1.6",
"dotenv": "~6.0.0",
"express": "^4.16.2",
"express-async-errors": "^3.0.0",

121
src/adapter/github-app.ts Normal file
View File

@ -0,0 +1,121 @@
import { WebhookEvent } from '@octokit/webhooks'
import cacheManager from 'cache-manager'
import jwt from 'jsonwebtoken'
import { Context } from '../context'
import { GitHubAPI } from '../github'
import { logger } from '../logger'
import { LoggerWithTarget, wrapLogger } from '../wrap-logger'
// Some events can't get an authenticated client (#382):
function isUnauthenticatedEvent (event: WebhookEvent) {
return !event.payload.installation ||
(event.name === 'installation' && event.payload.action === 'deleted')
}
export class GitHubApp {
public log: LoggerWithTarget
public id: number
public cert: string
private cache: any
/**
* @param id - ID of the GitHub App
* @param cert - The private key of the GitHub App
*/
constructor (id: number, cert: string) {
this.id = id
this.cert = cert
this.log = wrapLogger(logger, logger)
this.cache = cacheManager.caching({
store: 'memory',
ttl: 60 * 60 // 1 hour
})
}
/**
* Create a new JWT, which is used to [authenticate as a GitHub
* App](https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app)
*/
public jwt () {
const payload = {
exp: Math.floor(Date.now() / 1000) + 60, // JWT expiration time
iat: Math.floor(Date.now() / 1000), // Issued at time
iss: this.id // GitHub App ID
}
// Sign with RSA SHA256
return jwt.sign(payload, this.cert, { algorithm: 'RS256' })
}
public async createContext (event: WebhookEvent) {
const log = this.log.child({ name: 'event', id: event.id })
let github
if (isUnauthenticatedEvent(event)) {
github = await this.auth()
log.debug('`context.github` is unauthenticated. See https://probot.github.io/docs/github-api/#unauthenticated-events')
} else {
github = await this.auth(event.payload.installation!.id, log)
}
return new Context(event, github, log)
}
/**
* Authenticate and get a GitHub client that can be used to make API calls.
*
* You'll probably want to use `context.github` instead.
*
* **Note**: `app.auth` is asynchronous, so it needs to be prefixed with a
* [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
* to wait for the magic to happen.
*
* ```js
* module.exports = (app) => {
* app.on('issues.opened', async context => {
* const github = await app.auth();
* });
* };
* ```
*
* @param id - ID of the installation, which can be extracted from
* `context.payload.installation.id`. If called without this parameter, the
* client wil authenticate [as the app](https://developer.github.com/apps/building-integrations/setting-up-and-registering-github-apps/about-authentication-options-for-github-apps/#authenticating-as-a-github-app)
* instead of as a specific installation, which means it can only be used for
* [app APIs](https://developer.github.com/v3/apps/).
*
* @returns An authenticated GitHub API client
* @private
*/
public async auth (id?: number, log = this.log): Promise<GitHubAPI> {
if (process.env.GHE_HOST && /^https?:\/\//.test(process.env.GHE_HOST)) {
throw new Error('Your \`GHE_HOST\` environment variable should not begin with https:// or http://')
}
const github = GitHubAPI({
baseUrl: process.env.GHE_HOST && `https://${process.env.GHE_HOST}/api/v3`,
debug: process.env.LOG_LEVEL === 'trace',
logger: log.child({ name: 'github', installation: String(id) })
})
// Cache for 1 minute less than GitHub expiry
const installationTokenTTL = parseInt(process.env.INSTALLATION_TOKEN_TTL || '3540', 10)
if (id) {
const res = await this.cache.wrap(`app:${id}:token`, () => {
log.trace(`creating token for installation`)
github.authenticate({ type: 'app', token: this.jwt() })
return github.apps.createInstallationToken({ installation_id: id })
}, { ttl: installationTokenTTL })
github.authenticate({ type: 'token', token: res.data.token })
} else {
github.authenticate({ type: 'app', token: this.jwt() })
}
return github
}
}

View File

@ -1,18 +1,14 @@
import { WebhookEvent } from '@octokit/webhooks'
import deprecated from 'deprecated-decorator'
import express from 'express'
import { EventEmitter } from 'promise-events'
import { ApplicationFunction } from '.'
import { GitHubApp } from './adapter/github-app'
import { Context } from './context'
import { GitHubAPI } from './github'
import { logger } from './logger'
import { LoggerWithTarget, wrapLogger } from './wrap-logger'
// Some events can't get an authenticated client (#382):
function isUnauthenticatedEvent (event: WebhookEvent) {
return !event.payload.installation ||
(event.name === 'installation' && event.payload.action === 'deleted')
}
/**
* The `app` parameter available to `ApplicationFunction`s
*
@ -20,18 +16,17 @@ function isUnauthenticatedEvent (event: WebhookEvent) {
*/
export class Application {
public events: EventEmitter
public app: () => string
public cache: Cache
public router: express.Router
public catchErrors: boolean
public log: LoggerWithTarget
private adapter: GitHubApp
constructor (options?: Options) {
const opts = options || {} as any
this.events = new EventEmitter()
this.log = wrapLogger(logger, logger)
this.app = opts.app
this.cache = opts.cache
this.adapter = opts.adapter
this.catchErrors = opts.catchErrors || false
this.router = opts.router || express.Router() // you can do this?
}
@ -125,25 +120,11 @@ export class Application {
*/
public on (eventName: string | string[], callback: (context: Context) => Promise<void>) {
if (typeof eventName === 'string') {
return this.events.on(eventName, async (event: WebhookEvent) => {
const log = this.log.child({ name: 'event', id: event.id })
try {
let github
if (isUnauthenticatedEvent(event)) {
github = await this.auth()
log.debug('`context.github` is unauthenticated. See https://probot.github.io/docs/github-api/#unauthenticated-events')
} else {
github = await this.auth(event.payload.installation!.id, log)
}
const context = new Context(event, github, log)
await callback(context)
await callback(await this.adapter.createContext(event))
} catch (err) {
log.error({ err, event })
this.log.error({ err, event, id: event.id })
if (!this.catchErrors) {
throw err
}
@ -154,74 +135,19 @@ export class Application {
}
}
/**
* Authenticate and get a GitHub client that can be used to make API calls.
*
* You'll probably want to use `context.github` instead.
*
* **Note**: `app.auth` is asynchronous, so it needs to be prefixed with a
* [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
* to wait for the magic to happen.
*
* ```js
* module.exports = (app) => {
* app.on('issues.opened', async context => {
* const github = await app.auth();
* });
* };
* ```
*
* @param id - ID of the installation, which can be extracted from
* `context.payload.installation.id`. If called without this parameter, the
* client wil authenticate [as the app](https://developer.github.com/apps/building-integrations/setting-up-and-registering-github-apps/about-authentication-options-for-github-apps/#authenticating-as-a-github-app)
* instead of as a specific installation, which means it can only be used for
* [app APIs](https://developer.github.com/v3/apps/).
*
* @returns An authenticated GitHub API client
* @private
*/
public async auth (id?: number, log = this.log): Promise<GitHubAPI> {
if (process.env.GHE_HOST && /^https?:\/\//.test(process.env.GHE_HOST)) {
throw new Error('Your \`GHE_HOST\` environment variable should not begin with https:// or http://')
}
@deprecated('adapter.jwt', '8.0.0')
public app (): string {
return this.adapter.jwt()
}
const github = GitHubAPI({
baseUrl: process.env.GHE_HOST && `https://${process.env.GHE_HOST}/api/v3`,
debug: process.env.LOG_LEVEL === 'trace',
logger: log.child({ name: 'github', installation: String(id) })
})
// Cache for 1 minute less than GitHub expiry
const installationTokenTTL = parseInt(process.env.INSTALLATION_TOKEN_TTL || '3540', 10)
if (id) {
const res = await this.cache.wrap(`app:${id}:token`, () => {
log.trace(`creating token for installation`)
github.authenticate({ type: 'app', token: this.app() })
return github.apps.createInstallationToken({ installation_id: id })
}, { ttl: installationTokenTTL })
github.authenticate({ type: 'token', token: res.data.token })
} else {
github.authenticate({ type: 'app', token: this.app() })
}
return github
@deprecated('adapter.auth', '8.0.0')
public auth (id?: number, log = this.log): Promise<GitHubAPI> {
return this.adapter.auth(id, log)
}
}
// The TypeScript definition for cache-manager does not export the Cache interface so we recreate it here
export interface Cache {
wrap<T> (key: string, wrapper: (callback: (error: any, result: T) => void) => any, options: CacheConfig): Promise<any>
}
export interface CacheConfig {
ttl: number
}
export interface Options {
app: () => string
cache: Cache
adapter: GitHubApp
router?: express.Router
catchErrors?: boolean
}

View File

@ -36,7 +36,7 @@ module.exports = async (app: any): Promise<void> => {
}
async function getInstallations (): Promise<Installation[]> {
const github = await app.auth()
const github = await app.adapter.auth()
const req = github.apps.getInstallations({ per_page: 100 })
return github.paginate(req, (res: AnyResponse) => res.data)
}
@ -51,7 +51,7 @@ module.exports = async (app: any): Promise<void> => {
return account
}
const github = await app.auth(installation.id)
const github = await app.adapter.auth(installation.id)
const req = github.apps.getInstallationRepositories({ per_page: 100 })
const repositories: Repository[] = await github.paginate(req, (res: AnyResponse) => {

View File

@ -1,19 +0,0 @@
import jwt from 'jsonwebtoken'
export const createApp = (options: AppOptions) => {
return () => {
const payload = {
exp: Math.floor(Date.now() / 1000) + 60, // JWT expiration time
iat: Math.floor(Date.now() / 1000), // Issued at time
iss: options.id // GitHub App ID
}
// Sign with RSA SHA256
return jwt.sign(payload, options.cert, { algorithm: 'RS256' })
}
}
export interface AppOptions {
id: number
cert: string
}

View File

@ -1,10 +1,9 @@
import Webhooks, { WebhookEvent } from '@octokit/webhooks'
import Logger from 'bunyan'
import cacheManager from 'cache-manager'
import express from 'express'
import { GitHubApp } from './adapter/github-app'
import { Application } from './application'
import { Context } from './context'
import { createApp } from './github-app'
import { logger } from './logger'
import { resolve } from './resolver'
import { createServer } from './server'
@ -14,11 +13,6 @@ import { createWebhookProxy } from './webhook-proxy'
// These needs types
const logRequestErrors = require('./middleware/log-request-errors')
const cache = cacheManager.caching({
store: 'memory',
ttl: 60 * 60 // 1 hour
})
const defaultAppFns: ApplicationFunction[] = [
require('./apps/default'),
require('./apps/sentry'),
@ -33,7 +27,7 @@ export class Probot {
private options: Options
private apps: Application[]
private app: () => string
private adapter: GitHubApp
constructor (options: Options) {
options.webhookPath = options.webhookPath || '/'
@ -42,8 +36,10 @@ export class Probot {
this.logger = logger
this.apps = []
this.webhook = new Webhooks({ path: options.webhookPath, secret: options.secret })
this.app = createApp({ id: options.id, cert: options.cert })
this.server = createServer({ webhook: this.webhook.middleware, logger })
this.server = createServer({ logger })
this.server.use(this.webhook.middleware)
this.adapter = new GitHubApp(options.id, options.cert)
// Log all received webhooks
this.webhook.on('*', (event: WebhookEvent) => {
@ -75,7 +71,7 @@ export class Probot {
appFn = resolve(appFn) as ApplicationFunction
}
const app = new Application({ app: this.app, cache, catchErrors: true })
const app = new Application({ adapter: this.adapter, catchErrors: true })
// Connect the router from the app to the server
this.server.use(app.router)
@ -126,4 +122,4 @@ export interface Options {
port?: number
}
export { Logger, Context, Application }
export { Logger, Context, Application, GitHubApp }

View File

@ -13,7 +13,6 @@ export const createServer = (args: ServerArgs) => {
app.use(logRequest({ logger: args.logger }))
app.use('/probot/static/', express.static(path.join(__dirname, '..', 'static')))
app.use(args.webhook)
app.set('view engine', 'hbs')
app.set('views', path.join(__dirname, '..', 'views'))
app.get('/ping', (req, res) => res.end('PONG'))
@ -22,6 +21,5 @@ export const createServer = (args: ServerArgs) => {
}
export interface ServerArgs {
webhook: express.Application
logger: Logger
}

View File

@ -0,0 +1,72 @@
import fs from 'fs'
import nock from 'nock'
import path from 'path'
import { GitHubApp } from '../../src'
describe('github-app', () => {
let adapter: GitHubApp
describe('auth', () => {
let scopeInstall: nock.Scope
beforeEach(() => {
const pem = path.join(__dirname, '..', 'fixtures', 'private-key.pem')
adapter = new GitHubApp(1, fs.readFileSync(pem).toString())
scopeInstall = nock('https://api.github.com')
.post('/app/installations/1/access_tokens')
.reply(200, { token: 'installation-bearer-authorization-token' })
})
afterEach(() => {
delete process.env.INSTALLATION_TOKEN_TTL
nock.cleanAll()
})
it('returns authenticated GitHub client', async () => {
const client = await adapter.auth(1)
expect(scopeInstall.isDone()).toEqual(true)
const scopeData = nock('https://api.github.com')
.matchHeader('authorization', 'token installation-bearer-authorization-token')
.get('/orgs/myorg')
.reply(200, {})
await client.orgs.get({ org: 'myorg' })
expect(scopeData.isDone()).toEqual(true)
})
it('requests an installation token once for two events', async () => {
await adapter.auth(1)
await adapter.auth(1)
expect(scopeInstall.isDone()).toEqual(true)
})
it('requests an installation token once for each event if not cached', async () => {
// Only cache token for 1 second
process.env.INSTALLATION_TOKEN_TTL = '1'
await adapter.auth(1)
// Sleep longer than ttl value to let token cache expire
await (new Promise(resolve => setTimeout(resolve, 1001)))
// Receive second event
const scopeInstallTwo = nock('https://api.github.com')
.post('/app/installations/1/access_tokens')
.reply(200, { token: 'second-installation-token' })
const scopeDataTwo = nock('https://api.github.com')
.matchHeader('authorization', 'token second-installation-token')
.get('/orgs/myorg')
.reply(200, {})
const client = await adapter.auth(1)
await client.orgs.get({ org: 'myorg' })
// our second token should have been requested and used
expect(scopeInstallTwo.isDone()).toEqual(true)
expect(scopeDataTwo.isDone()).toEqual(true)
})
})
})

View File

@ -1,12 +1,10 @@
import { WebhookEvent } from '@octokit/webhooks'
import cacheManager from 'cache-manager'
import nock from 'nock'
import { WebhookEvent } from '@octokit/webhooks'
import { Application } from '../src/application'
import { Context } from '../src/context'
import { Application, Context, GitHubApp } from '../src'
import { logger } from '../src/logger'
describe('Application', () => {
let adapter: GitHubApp
let app: Application
let event: WebhookEvent
let output: any
@ -25,8 +23,10 @@ describe('Application', () => {
// Clear log output
output = []
app = new Application({} as any)
app.auth = jest.fn().mockReturnValue({})
adapter = new GitHubApp(1, 'cert')
adapter.auth = jest.fn().mockReturnValue({})
app = new Application({ adapter })
event = {
id: '123-456',
@ -124,7 +124,7 @@ describe('Application', () => {
await app.receive(event)
expect(app.auth).toHaveBeenCalledWith(1, expect.anything())
expect(adapter.auth).toHaveBeenCalledWith(1, expect.anything())
})
it('returns an unauthenticated client for installation.deleted', async () => {
@ -143,7 +143,7 @@ describe('Application', () => {
await app.receive(event)
expect(app.auth).toHaveBeenCalledWith()
expect(adapter.auth).toHaveBeenCalledWith()
})
it('returns an authenticated client for events without an installation', async () => {
@ -159,7 +159,7 @@ describe('Application', () => {
await app.receive(event)
expect(app.auth).toHaveBeenCalledWith()
expect(adapter.auth).toHaveBeenCalledWith()
})
})
@ -232,7 +232,6 @@ describe('Application', () => {
beforeEach(() => {
error = new Error('testing')
app.log.error = jest.fn() as any
})
it('logs errors thrown from handlers', async () => {
@ -240,11 +239,7 @@ describe('Application', () => {
throw error
})
try {
await app.receive(event)
} catch (err) {
// Expected
}
await expect(app.receive(event)).rejects.toThrow(error)
expect(output.length).toBe(1)
expect(output[0].err.message).toEqual('testing')
@ -254,11 +249,7 @@ describe('Application', () => {
it('logs errors from rejected promises', async () => {
app.on('test', () => Promise.reject(error))
try {
await app.receive(event)
} catch (err) {
// Expected
}
await expect(app.receive(event)).rejects.toThrow(error)
expect(output.length).toBe(1)
expect(output[0].err.message).toEqual('testing')
@ -280,101 +271,17 @@ describe('Application', () => {
await app.receive({ name: 'real-event-name', event: 'deprecated', payload: { action: 'test' } } as any)
expect(spy).toHaveBeenCalled()
})
})
describe('auth cache', () => {
let scopeInstall: nock.Scope
let scopeData: nock.Scope
const cleanGlobals = () => {
delete process.env.INSTALLATION_TOKEN_TTL
nock.cleanAll()
}
beforeEach(() => {
cleanGlobals()
const cache = cacheManager.caching({
store: 'memory',
ttl: 60 * 60 // 1 hour
})
app = new Application({ cache } as any)
app.app = () => 'app-bearer-authorization-token'
scopeInstall = nock('https://api.github.com')
.post('/app/installations/1/access_tokens')
.reply(200, { token: 'installation-bearer-authorization-token' })
scopeData = nock('https://api.github.com')
.matchHeader('authorization', 'token installation-bearer-authorization-token')
.get('/orgs/myorg')
.reply(200, {})
test('app() calls adapter.jwt()', () => {
adapter.jwt = jest.fn().mockReturnValue('testing')
expect(app.app()).toEqual('testing')
expect(adapter.jwt).toHaveBeenCalled()
})
afterEach(() => {
expect(scopeInstall.isDone()).toEqual(true)
expect(scopeData.isDone()).toEqual(true)
cleanGlobals()
})
it('requests an installation token once for one event', async () => {
// Receive first event
app.on('test.foo', async context => {
await context.github.orgs.get({ org: 'myorg' })
})
await app.receive(event)
})
it('requests an installation token once for two events', async () => {
// Receive first event
app.on('test.foo', async context => {
await context.github.orgs.get({ org: 'myorg' })
})
await app.receive(event)
// Receive second event
const scopeInstallTwo = nock('https://api.github.com')
.post('/app/installations/1/access_tokens')
.reply(200, { token: 'token-should-not-be-requested' })
const scopeDataTwo = nock('https://api.github.com')
.matchHeader('authorization', 'token installation-bearer-authorization-token')
.get('/orgs/myorg')
.reply(200, {})
await app.receive(event)
// we should have not requested a second token, and just used the first one
expect(scopeInstallTwo.isDone()).toEqual(false)
expect(scopeDataTwo.isDone()).toEqual(true)
})
it('requests an installation token once for each event if not cached', async () => {
// Only cache token for 1 second
process.env.INSTALLATION_TOKEN_TTL = '1'
// Receive first event
app.on('test.foo', async context => {
await context.github.orgs.get({ org: 'myorg' })
})
await app.receive(event)
// Sleep longer than ttl value to let token cache expire
const sleep = async () => new Promise(resolve => setTimeout(resolve, 1200))
await sleep()
// Receive second event
const scopeInstallTwo = nock('https://api.github.com')
.post('/app/installations/1/access_tokens')
.reply(200, { token: 'second-installation-token' })
const scopeDataTwo = nock('https://api.github.com')
.matchHeader('authorization', 'token second-installation-token')
.get('/orgs/myorg')
.reply(200, {})
await app.receive(event)
// our second token should have been requested and used
expect(scopeInstallTwo.isDone()).toEqual(true)
expect(scopeDataTwo.isDone()).toEqual(true)
test('auth() calls adapter.auth()', async () => {
adapter.auth = jest.fn().mockReturnValue(Promise.resolve('a github client'))
expect(await app.auth(1, 'a logger' as any)).toEqual('a github client')
expect(adapter.auth).toHaveBeenCalledWith(1, 'a logger')
})
})
})

View File

@ -1,14 +1,11 @@
// FIXME: move this to a test helper that can be used by other apps
import cacheManager from 'cache-manager'
import { Application, ApplicationFunction } from '../../src'
const cache = cacheManager.caching({ store: 'memory', ttl: 0 })
const jwt = jest.fn().mockReturnValue('test')
import { Application, ApplicationFunction, GitHubApp } from '../../src'
export function newApp (): Application {
return new Application({ app: jwt, cache })
const adapter = new GitHubApp(1, 'test')
adapter.jwt = jest.fn().mockReturnValue('test')
return new Application({ adapter })
}
export function createApp (appFn?: ApplicationFunction) {

27
test/fixtures/private-key.pem vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAnK82xhtsAw1vdCHbpHRv+2De4Q5niOCQetS3Dn0CZou43Uhd
1xBPPaJZXWO8p+DV9g6f7rXIIE4MRWIYVLXD9nW8ZjYknE4EEgE6rUmb3FZ2Ek5J
6wyU9N0QWf7Aolba9x6Kt3kakpC8u7lXqw/7V7F3HjArTNHb2aZwehMXc7C7j3xq
1/T49vgzQe39n1/pLRXiBgTIVYMGD+n2KhmRQUv4Vwkl3c+GxeAv/C866xiOeTea
n2MipKakhXt3uo+i8/uTVrep4ohk+Xe+fk4zXck4Snks3x2GgXasHigIztQBAUfe
JJCEzaGktK4frVC8z6lpOctHxFwoZGn6b0a0DQIDAQABAoIBAQCYKTxbRbQufrS3
Jk5ktkMt79Ri4ZdAgT0uXDduFY7oQVaFZ0rimc8qfuikXkCPFDUVYPyGWPkCDmmy
U+JnGaI2Tf5pkLkbJlSjm5f9Q5fecgT2IBt+7dgWuDyx+UngHdNF295A8ZYRsgfn
ksYFtV6Uuh6BLaxPTRphk1Z15uaGjR2Iswyj3GFyG1S/Hs9wC1z2EAUJFkKHVUhx
4+ZqLDkXguN9pLSezKFC8+7+BhXfIism9a6qSbPMVaCUwBc856JAzsS/lV8Por2c
LV0XpACfbF2KoQEKy/M/Fd5z6Dp7ERH3UvgmzjKm7G4OG2rvW7/iGqEo/5hMnt7b
fLb1f15NAoGBAMkOu7UhxIo8ZIFvPbtwRg23puD8iZ2XVnpGVANYT48EIX1JIffg
56YjpH8ZADMGIsCkDZTWhDffJGYA6gKwt56RCi0AObCxHRyokDzU+mtI01dfYar5
7szeCJ4RnP35lAvzIko33WDdp6Co48Ij4e9pt0FlseboqFucD4ADG/TLAoGBAMeA
S8BvfjLb2Kt4Txfk3KyCM1QiL1oaax42AHEm5g0dx6kYdwAWWIvTMSQCmw73/2NX
alEVGqoFLBUmn9lb4y2sepD+ONPJF9rQHjmU47gLD8U1afVDlAG78gddZJNNAP7x
zUQdZHhdf/NoWZqOY7tiBKEFsfnA4YHvHDDgszeHAoGBALxFUJ5vzXRI0zClM2Bm
5SKJO/poYJEymucLHja3pmBc1ONV7ToJ38GilLHzfk1JTJepx5H9QnhzOslNx9PX
GUqhtK6pDFyZrZIdgluEcC6wVj718SZCvkwXCmjCQ4lMAUcjWJO5NlMznHmETSqJ
oVMoYlMZ4HpCmQyX2afcwXv7AoGAZanucGkgoWBBINhVTfLVtZ8/8u7cvIjb73BL
d14AO8ziMzyBX+0DQHXmA9jEFbOxVHl2d01O4jdyiHC/Yq+drGfJqduzL9G5M0t9
K5DBHsecSL34egDvCpcxNimUmC/UgxbLqJtl2KSlEHArwUQwdIv1meziDkkJYgJs
lkfbbD8CgYEAjPCdWXrNTkkJXhQdgsKM4eGY791p9Z7DoIUtseAqs8g4KDRxTRcc
r6UfAEJeDys8D74z+mVF+wW15CNHMEPvCysN6f/GPuQ7HWGRrORiNxsudBiLxlSi
FscpTB3Jjz3MrOpxNCJwHU6VWQTdwDSZuMc6ppCPIPos5y9va3clnI0=
-----END RSA PRIVATE KEY-----

View File

@ -183,7 +183,7 @@ describe('Probot', () => {
it('forwards events to each app', async () => {
const spy = jest.fn()
const app = probot.load(app => app.on('push', spy))
app.auth = jest.fn().mockReturnValue(Promise.resolve({}))
app.adapter.auth = jest.fn().mockReturnValue(Promise.resolve({}))
await probot.receive(event)
@ -212,7 +212,7 @@ describe('Probot', () => {
const spy = jest.fn()
const appFn = async app => {
const github = await app.auth()
const github = await app.adapter.auth()
const res = await github.apps.getInstallations({})
return spy(res)
}
@ -226,7 +226,7 @@ describe('Probot', () => {
process.env.GHE_HOST = 'https://notreallygithub.com'
try {
await app.auth()
await app.adapter.auth()
} catch (e) {
expect(e).toMatchSnapshot()
}

View File

@ -5,11 +5,9 @@ import { createServer } from '../src/server'
describe('server', () => {
let server: Application
let webhook: any
beforeEach(() => {
webhook = jest.fn((req, res, next) => next())
server = createServer({ webhook, logger })
server = createServer({ logger })
// Error handler to avoid printing logs
server.use((err: Error, req: Request, res: Response, next: NextFunction) => {
@ -23,13 +21,6 @@ describe('server', () => {
})
})
describe('webhook handler', () => {
it('should 500 on a webhook error', () => {
webhook.mockImplementation((req: Request, res: Response, callback: NextFunction) => callback(new Error('webhook error')))
return request(server).post('/').expect(500)
})
})
describe('with an unknown url', () => {
it('responds with 404', () => {
return request(server).get('/lolnotfound').expect(404)

View File

@ -15,7 +15,8 @@
"skipLibCheck": true,
"noImplicitAny": true,
"esModuleInterop": true,
"declaration": true // enable this once all files are .ts and we can remove allowJs
"declaration": true, // enable this once all files are .ts and we can remove allowJs
"experimentalDecorators": true
},
"include": [
"src/**/*"