forked from mirrors/probot
Compare commits
24 Commits
master
...
ts-extract
| Author | SHA1 | Date |
|---|---|---|
|
|
67a9945954 | |
|
|
fed50764b1 | |
|
|
c270752bc2 | |
|
|
d5b2733fd7 | |
|
|
dc42f5457a | |
|
|
c56370d99d | |
|
|
9c5cc31ff3 | |
|
|
bfa3e683f3 | |
|
|
5611d862b6 | |
|
|
8928971e81 | |
|
|
c869a18b02 | |
|
|
c107a83563 | |
|
|
fe423e5dfe | |
|
|
f5c5e0d164 | |
|
|
2bfaea6282 | |
|
|
38b5c44a91 | |
|
|
cbd677a501 | |
|
|
e17e4a45d9 | |
|
|
220c2c6506 | |
|
|
6e3d8efb71 | |
|
|
8e7e0d4ef3 | |
|
|
8cbb87a4c9 | |
|
|
622a5996b3 | |
|
|
3349082e5d |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
20
src/index.ts
20
src/index.ts
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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-----
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/**/*"
|
||||
|
|
|
|||
Loading…
Reference in New Issue