forked from mirrors/probot
feat: `probot.on()` / `probot.receive()` / `probot.auth()` (#1407)
This commit is contained in:
parent
de3adc1169
commit
1812cfeb2f
33
src/index.ts
33
src/index.ts
|
@ -31,6 +31,8 @@ import { getProbotOctokitWithDefaults } from "./octokit/get-probot-octokit-with-
|
|||
import { aliasLog } from "./helpers/alias-log";
|
||||
import { logWarningsForObsoleteEnvironmentVariables } from "./helpers/log-warnings-for-obsolete-environment-variables";
|
||||
import { getWebhooks } from "./octokit/get-webhooks";
|
||||
import { webhookEventCheck } from "./helpers/webhook-event-check";
|
||||
import { auth } from "./auth";
|
||||
|
||||
logWarningsForObsoleteEnvironmentVariables();
|
||||
|
||||
|
@ -53,6 +55,10 @@ export interface Options {
|
|||
port?: number;
|
||||
host?: string;
|
||||
webhookProxy?: string;
|
||||
/**
|
||||
* @deprecated set `Octokit` to `ProbotOctokit.defaults({ throttle })` instead
|
||||
*/
|
||||
throttleOptions?: any;
|
||||
}
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
@ -156,6 +162,11 @@ export class Probot {
|
|||
public webhooks: ProbotWebhooks;
|
||||
public log: DeprecatedLogger;
|
||||
public version: String;
|
||||
public on: ProbotWebhooks["on"];
|
||||
public auth: (
|
||||
installationId?: number,
|
||||
log?: Logger
|
||||
) => Promise<InstanceType<typeof ProbotOctokit>>;
|
||||
|
||||
// These need to be public for the tests to work.
|
||||
public options: Options;
|
||||
|
@ -216,12 +227,11 @@ export class Probot {
|
|||
cache,
|
||||
log: this.log,
|
||||
redisConfig: options.redisConfig,
|
||||
throttleOptions: options.throttleOptions,
|
||||
});
|
||||
const octokit = new Octokit();
|
||||
|
||||
this.state = {
|
||||
id: options.id,
|
||||
privateKey: options.privateKey,
|
||||
cache,
|
||||
githubToken: options.githubToken,
|
||||
log: this.log,
|
||||
|
@ -231,10 +241,29 @@ export class Probot {
|
|||
path: options.webhookPath,
|
||||
secret: options.secret,
|
||||
},
|
||||
id: options.id,
|
||||
privateKey: options.privateKey,
|
||||
};
|
||||
|
||||
this.auth = auth.bind(null, this.state);
|
||||
|
||||
this.webhooks = getWebhooks(this.state);
|
||||
|
||||
this.on = (eventNameOrNames, callback) => {
|
||||
// when an app subscribes to an event using `app.on(event, callback)`, Probot sends a request to `GET /app` and
|
||||
// verifies if the app is subscribed to the event and logs a warning if it is not.
|
||||
//
|
||||
// This feature will be moved out of Probot core as it has side effects and does not work in a stateless environment.
|
||||
webhookEventCheck(this.state, eventNameOrNames);
|
||||
|
||||
if (eventNameOrNames === "*") {
|
||||
// @ts-ignore this workaround is only to surpress a warning. The `.on()` method will be deprecated soon anyway.
|
||||
return this.webhooks.onAny(callback);
|
||||
}
|
||||
|
||||
return this.webhooks.on(eventNameOrNames, callback);
|
||||
};
|
||||
|
||||
this.server = createServer({
|
||||
webhook: (this.webhooks as any).middleware,
|
||||
logger: this.log,
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import Stream from "stream";
|
||||
|
||||
import { WebhookEvent } from "@octokit/webhooks";
|
||||
import Bottleneck from "bottleneck";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import request = require("supertest");
|
||||
import nock from "nock";
|
||||
import pino from "pino";
|
||||
|
||||
import { Application, Probot, ProbotOctokit } from "../src";
|
||||
import { Application, Probot, ProbotOctokit, Context } from "../src";
|
||||
|
||||
import path = require("path");
|
||||
import { WebhookEvents } from "@octokit/webhooks";
|
||||
|
@ -27,8 +31,17 @@ describe("Probot", () => {
|
|||
name: WebhookEvents;
|
||||
payload: any;
|
||||
};
|
||||
let output: any;
|
||||
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear log output
|
||||
output = [];
|
||||
process.env.DISABLE_WEBHOOK_EVENT_CHECK = "true";
|
||||
probot = new Probot({ githubToken: "faketoken" });
|
||||
|
||||
|
@ -249,7 +262,7 @@ describe("Probot", () => {
|
|||
return request(probot.server).get("/").expect(200, "foo");
|
||||
});
|
||||
|
||||
it("isolates apps from affecting eachother", async () => {
|
||||
it("isolates apps from affecting each other", async () => {
|
||||
["foo", "bar"].forEach((name) => {
|
||||
probot.load(({ app, getRouter }) => {
|
||||
const router = getRouter("/" + name);
|
||||
|
@ -580,4 +593,307 @@ describe("Probot", () => {
|
|||
probot.load([app, app]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on", () => {
|
||||
beforeEach(() => {
|
||||
event = {
|
||||
id: "123-456",
|
||||
name: "pull_request",
|
||||
payload: {
|
||||
action: "opened",
|
||||
installation: { id: 1 },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("calls callback when no action is specified", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on("pull_request", spy);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
await probot.receive(event);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy.mock.calls[0][0]).toBeInstanceOf(Context);
|
||||
expect(spy.mock.calls[0][0].payload).toBe(event.payload);
|
||||
});
|
||||
|
||||
it("calls callback with same action", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on("pull_request.opened", spy);
|
||||
|
||||
await probot.receive(event);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call callback with different action", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on("pull_request.closed", spy);
|
||||
|
||||
await probot.receive(event);
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("calls callback with *", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on("*", spy);
|
||||
|
||||
await probot.receive(event);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls callback x amount of times when an array of x actions is passed", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const event2: WebhookEvent = {
|
||||
id: "123",
|
||||
name: "issues",
|
||||
payload: {
|
||||
action: "opened",
|
||||
installation: { id: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on(["pull_request.opened", "issues.opened"], spy);
|
||||
|
||||
await probot.receive(event);
|
||||
await probot.receive(event2);
|
||||
expect(spy.mock.calls.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("adds a logger on the context", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
log: pino(streamLogsToOutput),
|
||||
});
|
||||
|
||||
const handler = jest.fn().mockImplementation((context) => {
|
||||
expect(context.log.info).toBeDefined();
|
||||
context.log.info("testing");
|
||||
|
||||
expect(output[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: context.id,
|
||||
msg: "testing",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
probot.on("pull_request", handler);
|
||||
await probot.receive(event).catch(console.error);
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an authenticated client for installation.created", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
event = {
|
||||
id: "123-456",
|
||||
name: "installation",
|
||||
payload: {
|
||||
action: "created",
|
||||
installation: { id: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const mock = nock("https://api.github.com")
|
||||
.post("/app/installations/1/access_tokens")
|
||||
.reply(201, {
|
||||
token: "v1.1f699f1069f60xxx",
|
||||
permissions: {
|
||||
issues: "write",
|
||||
contents: "read",
|
||||
},
|
||||
})
|
||||
.get("/")
|
||||
.matchHeader("authorization", "token v1.1f699f1069f60xxx")
|
||||
.reply(200, {});
|
||||
|
||||
probot.on("installation.created", async (context) => {
|
||||
await context.github.request("/");
|
||||
});
|
||||
|
||||
await probot.receive(event);
|
||||
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("returns an unauthenticated client for installation.deleted", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
event = {
|
||||
id: "123-456",
|
||||
name: "installation",
|
||||
payload: {
|
||||
action: "deleted",
|
||||
installation: { id: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/")
|
||||
.matchHeader("authorization", (value) => value === undefined)
|
||||
.reply(200, {});
|
||||
|
||||
probot.on("installation.deleted", async (context) => {
|
||||
await context.github.request("/");
|
||||
});
|
||||
|
||||
await probot.receive(event).catch(console.log);
|
||||
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("returns an authenticated client for events without an installation", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
event = {
|
||||
id: "123-456",
|
||||
name: "check_run",
|
||||
payload: {
|
||||
/* no installation */
|
||||
},
|
||||
};
|
||||
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/")
|
||||
.matchHeader("authorization", (value) => value === undefined)
|
||||
.reply(200, {});
|
||||
|
||||
probot.on("check_run", async (context) => {
|
||||
await context.github.request("/");
|
||||
});
|
||||
|
||||
await probot.receive(event).catch(console.log);
|
||||
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("receive", () => {
|
||||
beforeEach(() => {
|
||||
event = {
|
||||
id: "123-456",
|
||||
name: "pull_request",
|
||||
payload: {
|
||||
action: "opened",
|
||||
installation: { id: 1 },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("delivers the event", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on("pull_request", spy);
|
||||
|
||||
await probot.receive(event);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("waits for async events to resolve", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
probot.on("pull_request", () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
spy();
|
||||
resolve();
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
|
||||
await probot.receive(event);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a reject errors thrown in apps", async () => {
|
||||
const probot = new Probot({
|
||||
id,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
probot.on("pull_request", () => {
|
||||
throw new Error("error from app");
|
||||
});
|
||||
|
||||
try {
|
||||
await probot.receive(event);
|
||||
throw new Error("expected error to be raised from app");
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch(/error from app/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth", () => {
|
||||
it("throttleOptions", async () => {
|
||||
const probot = new Probot({
|
||||
Octokit: ProbotOctokit.plugin((octokit: any, options: any) => {
|
||||
return {
|
||||
pluginLoaded: true,
|
||||
test() {
|
||||
expect(options.throttle.id).toBe(1);
|
||||
expect(options.throttle.foo).toBe("bar");
|
||||
},
|
||||
};
|
||||
}),
|
||||
id: 1,
|
||||
privateKey: "private key",
|
||||
secret: "secret",
|
||||
throttleOptions: {
|
||||
foo: "bar",
|
||||
onAbuseLimit: () => true,
|
||||
onRateLimit: () => true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await probot.auth(1);
|
||||
expect(result.pluginLoaded).toEqual(true);
|
||||
result.test();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue