[CORL-687] Webhooks (#2738)

* feat: initial webhook impl

* feat: added support for key rotation

* feat: harmonized fetcher

* feat: added expired secrets cleaning

* feat: event system refactor

* feat: added story event

* feat: simplfiied webhook handler

* feat: added ref's to locations where user events can be added

* feat: added UI to support webhooks

* fix: renaming some Webhook -> WebhookEndpoint

* fix: review comments to adjuist flow

* feat: added localizations

* fix: linting, updated snapshots

* fix: adapted for new fluent

* fix: rearranged folders

* fix: linting

* feat: added webhooks documentation

* feat: improved toc generation

* feat: added some tests to webhooks

* fix: chain transition hooks

* feat: added tests around webhook ui

* fix: renamed events

* fix: adjusted circle markdown linting

* fix: adjusted doctoc script call

* review: review fixes

* review: review comments

* review: adjusted signing secret confirmation

* review: adjusted styles to harmonize button usage

* fix: updated snapshots and tests

* review: move form out of webhooks

Moved the form out of the webhooks by relocating the layout used for the
route associated with the configure routes.

* fix: fixed bugs and snapshots with tests

* feat: revised slack message format to use block api

* fix: fixed a small text bug

Co-authored-by: Vinh <vinh@vinh.tech>
Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
Wyatt Johnson
2020-02-18 13:25:48 -05:00
committed by GitHub
co-authored by Vinh Kim Gardner
parent 34ba2da88d
commit e42c2b925d
137 changed files with 5633 additions and 1020 deletions
@@ -88,6 +88,15 @@ exports[`renders configure advanced 1`] = `
Slack
</a>
</li>
<li>
<a
className="Link-link"
href="/admin/configure/webhooks"
onClick={[Function]}
>
Webhooks
</a>
</li>
<li>
<a
className="Link-link Link-linkActive"
@@ -88,6 +88,15 @@ exports[`renders configure auth 1`] = `
Slack
</a>
</li>
<li>
<a
className="Link-link"
href="/admin/configure/webhooks"
onClick={[Function]}
>
Webhooks
</a>
</li>
<li>
<a
className="Link-link"
@@ -88,6 +88,15 @@ exports[`renders configure general 1`] = `
Slack
</a>
</li>
<li>
<a
className="Link-link"
href="/admin/configure/webhooks"
onClick={[Function]}
>
Webhooks
</a>
</li>
<li>
<a
className="Link-link"
@@ -88,6 +88,15 @@ exports[`renders configure moderation 1`] = `
Slack
</a>
</li>
<li>
<a
className="Link-link"
href="/admin/configure/webhooks"
onClick={[Function]}
>
Webhooks
</a>
</li>
<li>
<a
className="Link-link"
@@ -88,6 +88,15 @@ exports[`renders configure organization 1`] = `
Slack
</a>
</li>
<li>
<a
className="Link-link"
href="/admin/configure/webhooks"
onClick={[Function]}
>
Webhooks
</a>
</li>
<li>
<a
className="Link-link"
@@ -320,7 +329,7 @@ moderation questions.
Add a new site to your organization or edit an existing site's details.
</p>
<a
className="BaseButton-root Button-root Button-sizeLarge Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
data-color="regular"
data-variant="regular"
href="/admin/configure/organization/sites/new"
@@ -0,0 +1,525 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`displays a list of webhook endpoints that have been configured 1`] = `
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
data-testid="webhooks-container"
>
<div
className="Box-root ConfigBox-root"
>
<div
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
>
<div>
<label
className="Header-root"
htmlFor="configure-webhooks-header.title"
>
Configure webhook endpoint
</label>
</div>
<div />
</div>
<div
className="ConfigBox-content"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
>
<p
className="FormFieldDescription-root"
>
Configure an endpoint to send events to when events occur within
Coral. These events will be JSON encoded and signed. To learn more
about webhook signing, visit our
<a
className="ExternalLink-root"
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
rel="noopener noreferrer"
target="_blank"
>
Webhook Guide
</a>
.
</p>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
data-color="regular"
data-testid="add-webhook-endpoint"
data-variant="regular"
href="/admin/configure/webhooks/add"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<i
aria-hidden="true"
className="Icon-root Icon-md"
>
add
</i>
Add webhook endpoint
</a>
<h3
className="Subheader-root"
>
Endpoints
</h3>
<table
className="Table-root Table-fullWidth"
>
<thead
className="TableHead-root"
>
<tr
className="TableRow-root"
>
<th
className="TableCell-root TableCell-header"
>
URL
</th>
<th
className="TableCell-root TableCell-header"
>
Status
</th>
<th
className="TableCell-root TableCell-header"
/>
</tr>
</thead>
<tbody
className="TableBody-root"
>
<tr
className="TableRow-root TableRow-body"
data-testid="webhook-endpoint-webhook-endpoint-1"
>
<td
className="TableCell-root WebhookEndpointRow-urlColumn TableCell-body"
>
http://example.com/webhook-endpoint-1
</td>
<td
className="TableCell-root TableCell-body"
>
<span
className="Marker-root StatusMarker-success Marker-colorPending Marker-variantRegular"
>
Enabled
</span>
</td>
<td
className="TableCell-root TableCell-body"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantText Button-uppercase Button-iconRight"
data-color="regular"
data-variant="text"
href="/admin/configure/webhooks/endpoint/webhook-endpoint-1"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Details
<i
aria-hidden="true"
className="Icon-root Icon-sm"
>
keyboard_arrow_right
</i>
</a>
</div>
</td>
</tr>
<tr
className="TableRow-root TableRow-body"
data-testid="webhook-endpoint-webhook-endpoint-2"
>
<td
className="TableCell-root WebhookEndpointRow-urlColumn TableCell-body"
>
http://example.com/webhook-endpoint-2
</td>
<td
className="TableCell-root TableCell-body"
>
<span
className="Marker-root StatusMarker-error Marker-colorPending Marker-variantRegular"
>
Disabled
</span>
</td>
<td
className="TableCell-root TableCell-body"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantText Button-uppercase Button-iconRight"
data-color="regular"
data-variant="text"
href="/admin/configure/webhooks/endpoint/webhook-endpoint-2"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Details
<i
aria-hidden="true"
className="Icon-root Icon-sm"
>
keyboard_arrow_right
</i>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
exports[`goes to add new webhook endpoint when clicking add 1`] = `
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
data-testid="webhooks-container"
>
<div
className="Box-root ConfigBox-root"
>
<div
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
>
<div>
<label
className="Header-root"
htmlFor="configure-webhooks-header.title"
>
Configure webhook endpoint
</label>
</div>
<div />
</div>
<div
className="ConfigBox-content"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
>
<p
className="FormFieldDescription-root"
>
Configure an endpoint to send events to when events occur within
Coral. These events will be JSON encoded and signed. To learn more
about webhook signing, visit our
<a
className="ExternalLink-root"
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
rel="noopener noreferrer"
target="_blank"
>
Webhook Guide
</a>
.
</p>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
data-color="regular"
data-testid="add-webhook-endpoint"
data-variant="regular"
href="/admin/configure/webhooks/add"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<i
aria-hidden="true"
className="Icon-root Icon-md"
>
add
</i>
Add webhook endpoint
</a>
<h3
className="Subheader-root"
>
Endpoints
</h3>
<div
className="CallOut-root CallOut-colorRegular CallOut-fullWidth"
>
<div
className="CallOut-inner"
>
There are no webhook endpoints configured, add one above.
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`goes to the webhook endpoint configuration page when selected 1`] = `
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
data-testid="webhooks-container"
>
<div
className="Box-root ConfigBox-root"
>
<div
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
>
<div>
<label
className="Header-root"
htmlFor="configure-webhooks-header.title"
>
Configure webhook endpoint
</label>
</div>
<div />
</div>
<div
className="ConfigBox-content"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
>
<p
className="FormFieldDescription-root"
>
Configure an endpoint to send events to when events occur within
Coral. These events will be JSON encoded and signed. To learn more
about webhook signing, visit our
<a
className="ExternalLink-root"
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
rel="noopener noreferrer"
target="_blank"
>
Webhook Guide
</a>
.
</p>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
data-color="regular"
data-testid="add-webhook-endpoint"
data-variant="regular"
href="/admin/configure/webhooks/add"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<i
aria-hidden="true"
className="Icon-root Icon-md"
>
add
</i>
Add webhook endpoint
</a>
<h3
className="Subheader-root"
>
Endpoints
</h3>
<table
className="Table-root Table-fullWidth"
>
<thead
className="TableHead-root"
>
<tr
className="TableRow-root"
>
<th
className="TableCell-root TableCell-header"
>
URL
</th>
<th
className="TableCell-root TableCell-header"
>
Status
</th>
<th
className="TableCell-root TableCell-header"
/>
</tr>
</thead>
<tbody
className="TableBody-root"
>
<tr
className="TableRow-root TableRow-body"
data-testid="webhook-endpoint-webhook-endpoint-1"
>
<td
className="TableCell-root WebhookEndpointRow-urlColumn TableCell-body"
>
http://example.com/webhook-endpoint-1
</td>
<td
className="TableCell-root TableCell-body"
>
<span
className="Marker-root StatusMarker-success Marker-colorPending Marker-variantRegular"
>
Enabled
</span>
</td>
<td
className="TableCell-root TableCell-body"
>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantText Button-uppercase Button-iconRight"
data-color="regular"
data-variant="text"
href="/admin/configure/webhooks/endpoint/webhook-endpoint-1"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Details
<i
aria-hidden="true"
className="Icon-root Icon-sm"
>
keyboard_arrow_right
</i>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
exports[`renders webhooks 1`] = `
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
data-testid="webhooks-container"
>
<div
className="Box-root ConfigBox-root"
>
<div
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
>
<div>
<label
className="Header-root"
htmlFor="configure-webhooks-header.title"
>
Configure webhook endpoint
</label>
</div>
<div />
</div>
<div
className="ConfigBox-content"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
>
<p
className="FormFieldDescription-root"
>
Configure an endpoint to send events to when events occur within
Coral. These events will be JSON encoded and signed. To learn more
about webhook signing, visit our
<a
className="ExternalLink-root"
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
rel="noopener noreferrer"
target="_blank"
>
Webhook Guide
</a>
.
</p>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
data-color="regular"
data-testid="add-webhook-endpoint"
data-variant="regular"
href="/admin/configure/webhooks/add"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<i
aria-hidden="true"
className="Icon-root Icon-md"
>
add
</i>
Add webhook endpoint
</a>
<h3
className="Subheader-root"
>
Endpoints
</h3>
<div
className="CallOut-root CallOut-colorRegular CallOut-fullWidth"
>
<div
className="CallOut-inner"
>
There are no webhook endpoints configured, add one above.
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -88,6 +88,15 @@ exports[`renders configure wordList 1`] = `
Slack
</a>
</li>
<li>
<a
className="Link-link"
href="/admin/configure/webhooks"
onClick={[Function]}
>
Webhooks
</a>
</li>
<li>
<a
className="Link-link"
@@ -0,0 +1,182 @@
import { noop } from "lodash";
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import { settings, users } from "../fixtures";
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/configure/webhooks");
});
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return await act(async () => {
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("webhooks-container")
);
return { testRenderer, container, context };
});
}
it("renders webhooks", async () => {
const { container } = await createTestRenderer();
await act(async () => {
await wait(() => {
expect(within(container).toJSON()).toMatchSnapshot();
});
});
});
it("goes to add new webhook endpoint when clicking add", async () => {
const {
container,
context: { transitionControl },
} = await createTestRenderer();
// Prevent router transitions.
transitionControl.allowTransition = false;
act(() => {
within(container)
.getByText(/Add webhook endpoint/)
.props.onClick({ button: 0, preventDefault: noop });
});
// Expect a routing request was made to the right url.
await act(async () => {
await wait(() => {
expect(transitionControl.history[0].pathname).toBe(
"/admin/configure/webhooks/add"
);
});
});
await act(async () => {
await wait(() => {
expect(within(container).toJSON()).toMatchSnapshot();
});
});
});
it("displays a list of webhook endpoints that have been configured", async () => {
const resolvers = createResolversStub<GQLResolver>({
Query: {
settings: () =>
pureMerge<typeof settings>(settings, {
webhooks: {
endpoints: [
{
id: "webhook-endpoint-1",
enabled: true,
url: "http://example.com/webhook-endpoint-1",
all: true,
events: [],
},
{
id: "webhook-endpoint-2",
enabled: false,
url: "http://example.com/webhook-endpoint-2",
all: true,
events: [],
},
],
},
}),
},
});
const { container } = await createTestRenderer({ resolvers });
await act(async () => {
await wait(() => {
expect(within(container).toJSON()).toMatchSnapshot();
});
});
});
it("goes to the webhook endpoint configuration page when selected", async () => {
const resolvers = createResolversStub<GQLResolver>({
Query: {
settings: () =>
pureMerge<typeof settings>(settings, {
webhooks: {
endpoints: [
{
id: "webhook-endpoint-1",
enabled: true,
url: "http://example.com/webhook-endpoint-1",
all: true,
events: [],
},
],
},
}),
},
});
const {
container,
context: { transitionControl },
} = await createTestRenderer({ resolvers });
// Prevent router transitions.
transitionControl.allowTransition = false;
act(() => {
const row = within(container).getByTestID(
"webhook-endpoint-webhook-endpoint-1"
);
within(row)
.getByText(/Details/, {
selector: "a",
})
.props.onClick({ button: 0, preventDefault: noop });
});
// Expect a routing request was made to the right url.
await act(async () => {
await wait(() => {
expect(transitionControl.history[0].pathname).toBe(
"/admin/configure/webhooks/endpoint/webhook-endpoint-1"
);
});
});
await act(async () => {
await wait(() => {
expect(within(container).toJSON()).toMatchSnapshot();
});
});
});
+5
View File
@@ -23,6 +23,7 @@ import {
GQLUSER_ROLE,
GQLUSER_STATUS,
GQLUsersConnection,
GQLWEBHOOK_EVENT_NAME,
} from "coral-framework/schema";
import { createFixture, createFixtures } from "coral-framework/testHelpers";
@@ -152,6 +153,10 @@ export const settings = createFixture<GQLSettings>({
},
},
},
webhooks: {
endpoints: [],
},
webhookEvents: [GQLWEBHOOK_EVENT_NAME.STORY_CREATED],
stories: {
scraping: {
enabled: true,