Skip to content

Commit

Permalink
Update webhook to accept x-www-form-urlencoded requests
Browse files Browse the repository at this point in the history
Refactor content-type verification to the main handler instead of in each
integration. Add tests for form urlencoded cases. Add utils function
to generate mock web-form APIGateway event.

Fixes #33
  • Loading branch information
wperron committed Aug 8, 2020
1 parent 940c8df commit 8f0547c
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 20 deletions.
36 changes: 17 additions & 19 deletions api/webhook/github.ts
Expand Up @@ -13,7 +13,7 @@ import {
Context,
APIGatewayProxyResultV2,
} from "../../deps.ts";
import { respondJSON } from "../../utils/http.ts";
import { respondJSON, parseRequestBody } from "../../utils/http.ts";
import { Database } from "../../utils/database.ts";
import { getMeta, uploadMetaJson } from "../../utils/storage.ts";
import type { WebhookPayloadCreate } from "../../utils/webhooks.d.ts";
Expand Down Expand Up @@ -56,9 +56,25 @@ export async function handler(

const headers = new Headers(event.headers);

if (
!(headers.get("content-type") ?? "").startsWith("application/json") &&
!(headers.get("content-type") ?? "").startsWith("application/x-www-form-urlencoded")
) {
return respondJSON({
statusCode: 400,
body: JSON.stringify({
success: false,
error: "content-type is not json or x-www-form-urlencoded",
}),
});
}

// Check the GitHub event type.
const ghEvent = headers.get("x-github-event");

// Decode event body in the case the event is submitted as form-urlencoded
event = parseRequestBody(event);

switch (ghEvent) {
case "ping":
return pingEvent({ headers, moduleName, event });
Expand All @@ -83,15 +99,6 @@ async function pingEvent(
},
): Promise<APIGatewayProxyResultV2> {
// Get version, version type, and repository from event
if (!(headers.get("content-type") ?? "").startsWith("application/json")) {
return respondJSON({
statusCode: 400,
body: JSON.stringify({
success: false,
error: "content-type is not json",
}),
});
}
if (!event.body) {
return respondJSON({
statusCode: 400,
Expand Down Expand Up @@ -187,15 +194,6 @@ async function createEvent(
},
): Promise<APIGatewayProxyResultV2> {
// Get version, version type, and repository from event
if (!(headers.get("content-type") ?? "").startsWith("application/json")) {
return respondJSON({
statusCode: 400,
body: JSON.stringify({
success: false,
error: "content-type is not json",
}),
});
}
if (!event.body) {
return respondJSON({
statusCode: 400,
Expand Down
68 changes: 68 additions & 0 deletions api/webhook/github_create_test.ts
@@ -1,6 +1,7 @@
import { handler } from "./github.ts";
import {
createJSONWebhookEvent,
createJSONWebhookWebFormEvent,
createContext,
} from "../../utils/test_utils.ts";
import { Database } from "../../utils/database.ts";
Expand Down Expand Up @@ -204,6 +205,73 @@ Deno.test({
},
});

Deno.test({
name: "create event success - web form",
async fn() {
// Send create event
const resp = await handler(
createJSONWebhookWebFormEvent(
"create",
"/webhook/gh/ltest2",
createevent,
{ name: "ltest2" },
{},
),
createContext(),
);

const builds = await database._builds.find({});

// Check that a new build was queued
assertEquals(builds.length, 1);
assertEquals(
builds[0],
{
_id: builds[0]._id,
created_at: builds[0].created_at,
options: {
moduleName: "ltest2",
type: "github",
repository: "luca-rand/testing",
ref: "0.0.7",
version: "0.0.7",
},
status: "queued",
},
);

assertEquals(resp, {
body:
`{"success":true,"data":{"module":"ltest2","version":"0.0.7","repository":"luca-rand/testing","status_url":"https://deno.land/status/${
builds[0]._id.$oid
}"}}`,
headers: {
"content-type": "application/json",
},
statusCode: 200,
});

// Check that the database entry
assertEquals(
await database.getModule("ltest2"),
{
name: "ltest2",
type: "github",
repository: "luca-rand/testing",
description: "Move along, just for testing",
star_count: 2,
},
);

// Check that no versions.json file was created
assertEquals(await getMeta("ltest2", "versions.json"), undefined);

// Clean up
await database._builds.deleteMany({});
await database._modules.deleteMany({});
},
});

Deno.test({
name: "create event not a tag",
async fn() {
Expand Down
51 changes: 51 additions & 0 deletions api/webhook/github_ping_test.ts
@@ -1,6 +1,7 @@
import { handler } from "./github.ts";
import {
createJSONWebhookEvent,
createJSONWebhookWebFormEvent,
createContext,
} from "../../utils/test_utils.ts";
import { Database } from "../../utils/database.ts";
Expand Down Expand Up @@ -129,6 +130,56 @@ Deno.test({
},
});

Deno.test({
name: "ping event success - web form",
async fn() {
// Send ping event
const resp = await handler(
createJSONWebhookWebFormEvent(
"ping",
"/webhook/gh/ltest2",
pingevent,
{ name: "ltest2" },
{},
),
createContext(),
);
assertEquals(resp, {
body:
'{"success":true,"data":{"module":"ltest2","repository":"luca-rand/testing"}}',
headers: {
"content-type": "application/json",
},
statusCode: 200,
});

// Check that the database entry
assertEquals(
await database.getModule("ltest2"),
{
name: "ltest2",
type: "github",
repository: "luca-rand/testing",
description: "Move along, just for testing",
star_count: 2,
},
);

// Check that a versions.json file was created
assertEquals(
JSON.parse(decoder.decode(await getMeta("ltest2", "versions.json"))),
{ latest: null, versions: [] },
);

// Check that no new build was queued
assertEquals(await database._builds.find({}), []);

// Clean up
await s3.deleteObject("ltest2/meta/versions.json");
await database._modules.deleteMany({});
},
});

Deno.test({
name: "ping event max registered to repository",
async fn() {
Expand Down
9 changes: 9 additions & 0 deletions utils/http.ts
@@ -1,6 +1,7 @@
// Copyright 2020 the Deno authors. All rights reserved. MIT license.

import {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
APIGatewayProxyStructuredResultV2,
} from "../deps.ts";
Expand All @@ -16,3 +17,11 @@ export function respondJSON(
},
};
}

export function parseRequestBody(event: APIGatewayProxyEventV2): APIGatewayProxyEventV2 {
const headers = new Headers(event.headers);
if (headers.get("content-type") === "application/x-www-form-urlencoded" && event.body) {
event.body = atob(event.body);
}
return event;
}
23 changes: 22 additions & 1 deletion utils/test_utils.ts
Expand Up @@ -19,7 +19,7 @@ export function createAPIGatewayProxyEventV2(
version: "2",
routeKey: "",
headers: headers ?? {},
body: data ? JSON.stringify(data) : undefined,
body: data ? (typeof data === "string" ? data : JSON.stringify(data)) : undefined,
isBase64Encoded: false,
rawPath: rawPath,
rawQueryString: queryString,
Expand Down Expand Up @@ -68,6 +68,27 @@ export function createJSONWebhookEvent(
});
}

export function createJSONWebhookWebFormEvent(
event: string,
path: string,
payload: unknown,
pathParameters: KV,
queryStringParameters: KV,
): APIGatewayProxyEventV2 {
return createAPIGatewayProxyEventV2("POST", path, {
headers: {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "GitHub-Hookshot/f1aa6e4",
"X-GitHub-Delivery": "01b06e5c-d65c-11ea-9409-7e8b4a054eac",
"X-GitHub-Event": event,
},
data: btoa(JSON.stringify(payload)),
pathParameters,
queryStringParameters,
});
}

export function createContext(): Context {
return {
awsRequestId: "",
Expand Down

0 comments on commit 8f0547c

Please sign in to comment.