diff --git a/README.md b/README.md index c3898ed7d..5d7cd8d2d 100644 --- a/README.md +++ b/README.md @@ -935,32 +935,6 @@ require("http").createServer(middleware).listen(3000); All exposed paths will be prefixed with the provided prefix. Defaults to `"/api/github/oauth"` - - - - - options.onUnhandledRequest - - - function - - - -Defaults to - -```js -function onUnhandledRequest(request, response) { - response.writeHead(404, { - "content-type": "application/json", - }); - response.end( - JSON.stringify({ - error: `Unknown route: ${request.method} ${request.url}`, - }) - ); -} -``` - @@ -1025,121 +999,6 @@ addEventListener("fetch", (event) => { All exposed paths will be prefixed with the provided prefix. Defaults to `"/api/github/oauth"` - - - - - options.onUnhandledRequest - - - function - - Defaults to - -```js -function onUnhandledRequest(request) { - return new Response( - JSON.stringify({ - error: `Unknown route: ${request.method} ${request.url}`, - }), - { - status: 404, - headers: { "content-type": "application/json" }, - } - ); -} -``` - - - - - - -### `createAWSLambdaAPIGatewayV2Handler(app, options)` - -Event handler for AWS Lambda using API Gateway V2 HTTP integration. - -```js -// worker.js -import { - OAuthApp, - createAWSLambdaAPIGatewayV2Handler, -} from "@octokit/oauth-app"; -const app = new OAuthApp({ - clientType: "oauth-app", - clientId: "1234567890abcdef1234", - clientSecret: "1234567890abcdef1234567890abcdef12345678", -}); - -export const handler = createAWSLambdaAPIGatewayV2Handler(app, { - pathPrefix: "/api/github/oauth", -}); - -// can now receive user authorization callbacks at /api/github/oauth/callback -``` - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/package.json b/package.json index 50901ac08..46c888ef4 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,6 @@ "@octokit/core": "^4.0.0", "@octokit/oauth-authorization-url": "^5.0.0", "@octokit/oauth-methods": "^2.0.0", - "@types/aws-lambda": "^8.10.83", - "fromentries": "^1.3.1", "universal-user-agent": "^6.0.0" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts index a129bb2ba..34b9a28f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,12 +50,19 @@ import type { Options, State, } from "./types"; + +// types required by external handlers (aws-lambda, etc) +export type { + HandlerOptions, + OctokitRequest, + OctokitResponse, +} from "./middleware/types"; + +// generic handlers +export { handleRequest } from "./middleware/handle-request"; + export { createNodeMiddleware } from "./middleware/node/index"; -export { - createCloudflareHandler, - createWebWorkerHandler, -} from "./middleware/web-worker/index"; -export { createAWSLambdaAPIGatewayV2Handler } from "./middleware/aws-lambda/api-gateway-v2"; +export { createWebWorkerHandler } from "./middleware/web-worker/index"; type Constructor = new (...args: any[]) => T; diff --git a/src/middleware/README.md b/src/middleware/README.md index d08c0ba08..541fbf9d3 100644 --- a/src/middleware/README.md +++ b/src/middleware/README.md @@ -11,7 +11,6 @@ middleware ├── types.ts ├── node/ ├── web-worker/ (Cloudflare Workers & Deno) -└── deno/ (to be implemented) ``` ## Generic HTTP Handler diff --git a/src/middleware/aws-lambda/api-gateway-v2-parse-request.ts b/src/middleware/aws-lambda/api-gateway-v2-parse-request.ts deleted file mode 100644 index 7817c0d3e..000000000 --- a/src/middleware/aws-lambda/api-gateway-v2-parse-request.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { OctokitRequest } from "../types"; -import type { APIGatewayProxyEventV2 } from "aws-lambda"; - -export function parseRequest(request: APIGatewayProxyEventV2): OctokitRequest { - const { method } = request.requestContext.http; - let url = request.rawPath; - const { stage } = request.requestContext; - if (url.startsWith("/" + stage)) url = url.substring(stage.length + 1); - if (request.rawQueryString) url += "?" + request.rawQueryString; - const headers = request.headers as Record; - const text = async () => request.body || ""; - return { method, url, headers, text }; -} diff --git a/src/middleware/aws-lambda/api-gateway-v2-send-response.ts b/src/middleware/aws-lambda/api-gateway-v2-send-response.ts deleted file mode 100644 index 073a790e2..000000000 --- a/src/middleware/aws-lambda/api-gateway-v2-send-response.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { OctokitResponse } from "../types"; -import type { APIGatewayProxyStructuredResultV2 } from "aws-lambda"; - -export function sendResponse( - octokitResponse: OctokitResponse -): APIGatewayProxyStructuredResultV2 { - return { - statusCode: octokitResponse.status, - headers: octokitResponse.headers, - body: octokitResponse.text, - }; -} diff --git a/src/middleware/aws-lambda/api-gateway-v2.ts b/src/middleware/aws-lambda/api-gateway-v2.ts deleted file mode 100644 index 873c9ae05..000000000 --- a/src/middleware/aws-lambda/api-gateway-v2.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { parseRequest } from "./api-gateway-v2-parse-request"; -import { sendResponse } from "./api-gateway-v2-send-response"; -import { handleRequest } from "../handle-request"; -import { onUnhandledRequestDefault } from "../on-unhandled-request-default"; -import { HandlerOptions } from "../types"; -import { OAuthApp } from "../../index"; -import { Options, ClientType } from "../../types"; -import type { - APIGatewayProxyEventV2, - APIGatewayProxyStructuredResultV2, -} from "aws-lambda"; - -async function onUnhandledRequestDefaultAWSAPIGatewayV2( - event: APIGatewayProxyEventV2 -): Promise { - const request = parseRequest(event); - const response = onUnhandledRequestDefault(request); - return sendResponse(response); -} - -export function createAWSLambdaAPIGatewayV2Handler( - app: OAuthApp>, - { - pathPrefix, - onUnhandledRequest = onUnhandledRequestDefaultAWSAPIGatewayV2, - }: HandlerOptions & { - onUnhandledRequest?: ( - event: APIGatewayProxyEventV2 - ) => Promise; - } = {} -) { - return async function (event: APIGatewayProxyEventV2) { - const request = parseRequest(event); - const response = await handleRequest(app, { pathPrefix }, request); - return response ? sendResponse(response) : onUnhandledRequest(event); - }; -} diff --git a/src/middleware/handle-request.ts b/src/middleware/handle-request.ts index 671133a3a..98983347a 100644 --- a/src/middleware/handle-request.ts +++ b/src/middleware/handle-request.ts @@ -1,14 +1,12 @@ import { OAuthApp } from "../index"; import { HandlerOptions, OctokitRequest, OctokitResponse } from "./types"; import { ClientType, Options } from "../types"; -// @ts-ignore - requires esModuleInterop flag -import fromEntries from "fromentries"; export async function handleRequest( app: OAuthApp>, { pathPrefix = "/api/github/oauth" }: HandlerOptions, request: OctokitRequest -): Promise { +): Promise { if (request.method === "OPTIONS") { return { status: 200, @@ -39,12 +37,18 @@ export async function handleRequest( // handle unknown routes if (!Object.values(routes).includes(route)) { - return null; + return { + status: 404, + headers: { "content-type": "application/json" }, + text: JSON.stringify({ + error: `Unknown route: ${request.method} ${request.url}`, + }), + }; } let json: any; try { - const text = await request.text(); + const text = request.text; json = text ? JSON.parse(text) : {}; } catch (error) { return { @@ -59,7 +63,7 @@ export async function handleRequest( }; } const { searchParams } = new URL(request.url as string, "http://localhost"); - const query = fromEntries(searchParams) as { + const query = Object.fromEntries(searchParams) as { state?: string; scopes?: string; code?: string; @@ -69,7 +73,7 @@ export async function handleRequest( error_description?: string; error_url?: string; }; - const headers = request.headers as { authorization?: string }; + const headers = (request.headers || {}) as { authorization?: string }; try { if (route === routes.getLogin) { diff --git a/src/middleware/node/index.ts b/src/middleware/node/index.ts index 9bc05f71f..50ae8e150 100644 --- a/src/middleware/node/index.ts +++ b/src/middleware/node/index.ts @@ -6,51 +6,23 @@ type ServerResponse = any; import { parseRequest } from "./parse-request"; import { sendResponse } from "./send-response"; -import { onUnhandledRequestDefault } from "../on-unhandled-request-default"; import { handleRequest } from "../handle-request"; import { OAuthApp } from "../../index"; import { HandlerOptions } from "../types"; import { ClientType, Options } from "../../types"; -function onUnhandledRequestDefaultNode( - request: IncomingMessage, - response: ServerResponse -) { - const octokitRequest = parseRequest(request); - const octokitResponse = onUnhandledRequestDefault(octokitRequest); - sendResponse(octokitResponse, response); -} - export function createNodeMiddleware( app: OAuthApp>, - { - pathPrefix, - onUnhandledRequest = onUnhandledRequestDefaultNode, - }: HandlerOptions & { - onUnhandledRequest?: ( - request: IncomingMessage, - response: ServerResponse - ) => void; - } = {} + options: HandlerOptions = {} ) { return async function ( request: IncomingMessage, response: ServerResponse, next?: Function ) { - const octokitRequest = parseRequest(request); - const octokitResponse = await handleRequest( - app, - { pathPrefix }, - octokitRequest - ); - - if (octokitResponse) { - sendResponse(octokitResponse, response); - } else if (typeof next === "function") { - next(); - } else { - onUnhandledRequest(request, response); - } + const octokitRequest = await parseRequest(request); + const octokitResponse = await handleRequest(app, options, octokitRequest); + if (octokitResponse.status === 404 && next) return next(); + sendResponse(octokitResponse, response); }; } diff --git a/src/middleware/node/parse-request.ts b/src/middleware/node/parse-request.ts index 5df274a26..c2f32d614 100644 --- a/src/middleware/node/parse-request.ts +++ b/src/middleware/node/parse-request.ts @@ -5,17 +5,16 @@ type IncomingMessage = any; import { OctokitRequest } from "../types"; -export function parseRequest(request: IncomingMessage): OctokitRequest { +export async function parseRequest( + request: IncomingMessage +): Promise { const { method, url, headers } = request; - async function text() { - const text = await new Promise((resolve, reject) => { - let bodyChunks: Uint8Array[] = []; - request - .on("error", reject) - .on("data", (chunk: Uint8Array) => bodyChunks.push(chunk)) - .on("end", () => resolve(Buffer.concat(bodyChunks).toString())); - }); - return text; - } + const text = await new Promise((resolve, reject) => { + let bodyChunks: Uint8Array[] = []; + request + .on("error", reject) + .on("data", (chunk: Uint8Array) => bodyChunks.push(chunk)) + .on("end", () => resolve(Buffer.concat(bodyChunks).toString())); + }); return { method, url, headers, text }; } diff --git a/src/middleware/node/send-response.ts b/src/middleware/node/send-response.ts index d753044b5..5ca636345 100644 --- a/src/middleware/node/send-response.ts +++ b/src/middleware/node/send-response.ts @@ -1,6 +1,6 @@ // remove type imports from http for Deno compatibility // see https://github.com/octokit/octokit.js/issues/2075#issuecomment-817361886 -// import { IncomingMessage, ServerResponse } from "http"; +// import { ServerResponse } from "http"; type ServerResponse = any; import { OctokitResponse } from "../types"; diff --git a/src/middleware/on-unhandled-request-default.ts b/src/middleware/on-unhandled-request-default.ts deleted file mode 100644 index 14dd11757..000000000 --- a/src/middleware/on-unhandled-request-default.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { OctokitRequest, OctokitResponse } from "./types"; - -export function onUnhandledRequestDefault( - request: OctokitRequest -): OctokitResponse { - return { - status: 404, - headers: { "content-type": "application/json" }, - text: JSON.stringify({ - error: `Unknown route: ${request.method} ${request.url}`, - }), - }; -} diff --git a/src/middleware/types.ts b/src/middleware/types.ts index f259376d8..80affd4c5 100644 --- a/src/middleware/types.ts +++ b/src/middleware/types.ts @@ -1,8 +1,8 @@ export type OctokitRequest = { method: string; url: string; - headers: Record; - text: () => Promise; + headers?: Record; + text?: string; }; export type OctokitResponse = { diff --git a/src/middleware/web-worker/index.ts b/src/middleware/web-worker/index.ts index 68511336a..d7ab1fe4b 100644 --- a/src/middleware/web-worker/index.ts +++ b/src/middleware/web-worker/index.ts @@ -1,47 +1,17 @@ import { parseRequest } from "./parse-request"; import { sendResponse } from "./send-response"; import { handleRequest } from "../handle-request"; -import { onUnhandledRequestDefault } from "../on-unhandled-request-default"; -import { OAuthApp } from "../../index"; -import { HandlerOptions } from "../types"; -import { ClientType, Options } from "../../types"; - -async function onUnhandledRequestDefaultWebWorker( - request: Request -): Promise { - const octokitRequest = parseRequest(request); - const octokitResponse = onUnhandledRequestDefault(octokitRequest); - return sendResponse(octokitResponse); -} +import type { OAuthApp } from "../../index"; +import type { HandlerOptions } from "../types"; +import type { ClientType, Options } from "../../types"; export function createWebWorkerHandler>( app: OAuthApp, - { - pathPrefix, - onUnhandledRequest = onUnhandledRequestDefaultWebWorker, - }: HandlerOptions & { - onUnhandledRequest?: (request: Request) => Response | Promise; - } = {} + options: HandlerOptions = {} ) { return async function (request: Request): Promise { - const octokitRequest = parseRequest(request); - const octokitResponse = await handleRequest( - app, - { pathPrefix }, - octokitRequest - ); - return octokitResponse - ? sendResponse(octokitResponse) - : await onUnhandledRequest(request); + const octokitRequest = await parseRequest(request); + const octokitResponse = await handleRequest(app, options, octokitRequest); + return sendResponse(octokitResponse); }; } - -/** @deprecated */ -export function createCloudflareHandler( - ...args: Parameters -) { - args[0].octokit.log.warn( - "[@octokit/oauth-app] `createCloudflareHandler` is deprecated, use `createWebWorkerHandler` instead" - ); - return createWebWorkerHandler(...args); -} diff --git a/src/middleware/web-worker/parse-request.ts b/src/middleware/web-worker/parse-request.ts index b11ed3272..b770feb11 100644 --- a/src/middleware/web-worker/parse-request.ts +++ b/src/middleware/web-worker/parse-request.ts @@ -1,12 +1,12 @@ import { OctokitRequest } from "../types"; -export function parseRequest(request: Request): OctokitRequest { +export async function parseRequest(request: Request): Promise { // @ts-ignore Worker environment supports fromEntries/entries. const headers = Object.fromEntries(request.headers.entries()); return { method: request.method, url: request.url, headers, - text: () => request.text(), + text: await request.text(), }; } diff --git a/test/aws-lambda-api-gateway-v2.test.ts b/test/aws-lambda-api-gateway-v2.test.ts deleted file mode 100644 index 3209f1902..000000000 --- a/test/aws-lambda-api-gateway-v2.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { OAuthApp, createAWSLambdaAPIGatewayV2Handler } from "../src/"; -import { URL } from "url"; -import { APIGatewayProxyEventV2 } from "aws-lambda"; - -describe("createAWSLambdaAPIGatewayV2Handler(app)", () => { - it("supports oauth app", async () => { - const app = new OAuthApp({ - clientType: "oauth-app", - clientId: "0123", - clientSecret: "0123secret", - }); - createAWSLambdaAPIGatewayV2Handler(app); - }); - - it("supports github app", async () => { - const app = new OAuthApp({ - clientType: "github-app", - clientId: "0123", - clientSecret: "0123secret", - }); - createAWSLambdaAPIGatewayV2Handler(app); - }); - - it("fail-over to default unhandled request handler", async () => { - const appMock = {}; - const handleRequest = createAWSLambdaAPIGatewayV2Handler( - appMock as unknown as OAuthApp - ); - - const response = await handleRequest({ - requestContext: { http: { method: "GET" }, stage: "prod" }, - rawPath: "/prod/unknown", - } as APIGatewayProxyEventV2); - - expect(response.statusCode).toBe(404); - }); - - it("allow pre-flight requests", async () => { - const app = new OAuthApp({ clientId: "0123", clientSecret: "0123secret" }); - const handleRequest = createAWSLambdaAPIGatewayV2Handler(app); - - const response = await handleRequest({ - requestContext: { http: { method: "OPTIONS" }, stage: "prod" }, - rawPath: "/prod/api/github/oauth/token", - } as APIGatewayProxyEventV2); - - expect(response.statusCode).toStrictEqual(200); - expect(response.headers!["access-control-allow-origin"]).toBe("*"); - expect(response.headers!["access-control-allow-methods"]).toBe("*"); - expect(response.headers!["access-control-allow-headers"]).toBe( - "Content-Type, User-Agent, Authorization" - ); - }); - - it("supports $default stage", async () => { - const app = new OAuthApp({ clientId: "0123", clientSecret: "0123secret" }); - const handleRequest = createAWSLambdaAPIGatewayV2Handler(app); - - const response = await handleRequest({ - requestContext: { http: { method: "GET" }, stage: "$default" }, - rawPath: "/api/github/oauth/login", - } as APIGatewayProxyEventV2); - - expect(response.statusCode).toBe(302); - const url = new URL(response.headers!.location as string); - expect(url.origin).toBe("https://github.com"); - expect(url.pathname).toBe("/login/oauth/authorize"); - expect(url.searchParams.get("client_id")).toBe("0123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toBeNull(); - }); - - it("supports named stage", async () => { - const app = new OAuthApp({ clientId: "0123", clientSecret: "0123secret" }); - const handleRequest = createAWSLambdaAPIGatewayV2Handler(app); - - const response = await handleRequest({ - requestContext: { http: { method: "GET" }, stage: "prod" }, - rawPath: "/prod/api/github/oauth/login", - } as APIGatewayProxyEventV2); - - expect(response.statusCode).toBe(302); - const url = new URL(response.headers!.location as string); - expect(url.origin).toBe("https://github.com"); - expect(url.pathname).toBe("/login/oauth/authorize"); - expect(url.searchParams.get("client_id")).toBe("0123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toBeNull(); - }); - - it("passes query string to generic request handler correctly", async () => { - const app = new OAuthApp({ clientId: "0123", clientSecret: "0123secret" }); - const handleRequest = createAWSLambdaAPIGatewayV2Handler(app); - - const response = await handleRequest({ - requestContext: { http: { method: "GET" }, stage: "prod" }, - rawPath: "/prod/api/github/oauth/login", - rawQueryString: "state=mystate123&scopes=one,two,three", - } as APIGatewayProxyEventV2); - - expect(response.statusCode).toBe(302); - const url = new URL(response.headers!.location as string); - expect(url.origin).toBe("https://github.com"); - expect(url.pathname).toBe("/login/oauth/authorize"); - expect(url.searchParams.get("client_id")).toBe("0123"); - expect(url.searchParams.get("state")).toBe("mystate123"); - expect(url.searchParams.get("scope")).toBe("one,two,three"); - }); -}); diff --git a/test/deprecations.test.ts b/test/deprecations.test.ts deleted file mode 100644 index 7d9c9e871..000000000 --- a/test/deprecations.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { URL } from "url"; -import * as nodeFetch from "node-fetch"; -import fromEntries from "fromentries"; -import { createCloudflareHandler, OAuthApp } from "../src"; -import { Octokit } from "@octokit/core"; - -describe("deprecations", () => { - beforeAll(() => { - Object.fromEntries ||= fromEntries; - (global as any).Request = nodeFetch.Request; - (global as any).Response = nodeFetch.Response; - }); - - afterAll(() => { - delete (global as any).Request; - delete (global as any).Response; - }); - - it("createCloudflareHandler works but logs out deprecation message", async () => { - const warn = jest.fn().mockResolvedValue(undefined); - const handleRequest = createCloudflareHandler( - new OAuthApp({ - clientType: "github-app", - clientId: "client_id_123", - clientSecret: "client_secret_456", - Octokit: Octokit.defaults({ - log: { - debug: () => undefined, - info: () => undefined, - warn, - error: () => undefined, - }, - }), - }) - ); - - expect(warn.mock.calls.length).toEqual(1); - expect(warn.mock.calls[0][0]).toEqual( - "[@octokit/oauth-app] `createCloudflareHandler` is deprecated, use `createWebWorkerHandler` instead" - ); - - const request = new Request("/api/github/oauth/login"); - const { status, headers } = await handleRequest(request); - - expect(status).toEqual(302); - const url = new URL(headers.get("location") as string); - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - expect(url.searchParams.get("client_id")).toEqual("client_id_123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toEqual(null); - }); -}); diff --git a/test/handle-request.test.ts b/test/handle-request.test.ts new file mode 100644 index 000000000..eef04fa39 --- /dev/null +++ b/test/handle-request.test.ts @@ -0,0 +1,654 @@ +import { URL } from "url"; +import * as nodeFetch from "node-fetch"; +import { handleRequest, OAuthApp } from "../src"; + +describe("handle request", () => { + beforeAll(() => { + (global as any).Request = nodeFetch.Request; + (global as any).Response = nodeFetch.Response; + }); + + afterAll(() => { + delete (global as any).Request; + delete (global as any).Response; + }); + + it("support both oauth-app and github-app", () => { + const oauthApp = new OAuthApp({ + clientType: "oauth-app", + clientId: "0123", + clientSecret: "0123secret", + }); + handleRequest(oauthApp, {}, { method: "GET", url: "" }); + + const githubApp = new OAuthApp({ + clientType: "github-app", + clientId: "0123", + clientSecret: "0123secret", + }); + handleRequest(githubApp, {}, { method: "GET", url: "" }); + }); + + it("allow pre-flight requests", async () => { + const app = new OAuthApp({ + clientId: "0123", + clientSecret: "0123secret", + }); + const request = { + method: "OPTIONS", + url: "/api/github/oauth/token", + }; + + const response = await handleRequest(app, {}, request); + expect(response).toBeTruthy(); + expect(response!.status).toStrictEqual(200); + }); + + it("GET /api/github/oauth/login", async () => { + const app = new OAuthApp({ + clientId: "0123", + clientSecret: "0123secret", + }); + const response = await handleRequest( + app, + {}, + { + method: "GET", + url: "/api/github/oauth/login", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(302); + const url = new URL(response!.headers!["location"] as string); + expect(url).toMatchObject({ + origin: "https://github.com", + pathname: "/login/oauth/authorize", + }); + expect(url.searchParams.get("client_id")).toEqual("0123"); + expect(url.searchParams.get("state")).toMatch(/^\w+$/); + expect(url.searchParams.get("scope")).toEqual(null); + }); + + it("GET /api/github/oauth/login with defaultScopes (#110)", async () => { + const app = new OAuthApp({ + clientId: "0123", + clientSecret: "0123secret", + defaultScopes: ["repo"], + }); + const response = await handleRequest( + app, + {}, + { + method: "GET", + url: "/api/github/oauth/login", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(302); + const url = new URL(response!.headers!["location"] as string); + expect(url).toMatchObject({ + origin: "https://github.com", + pathname: "/login/oauth/authorize", + }); + expect(url.searchParams.get("client_id")).toEqual("0123"); + expect(url.searchParams.get("state")).toMatch(/^\w+$/); + expect(url.searchParams.get("scope")).toEqual("repo"); + }); + + it("GET /api/github/oauth/login?state=mystate123&scopes=one,two,three", async () => { + const app = new OAuthApp({ + clientId: "0123", + clientSecret: "0123secret", + }); + const response = await handleRequest( + app, + {}, + { + method: "GET", + url: "/api/github/oauth/login?state=mystate123&scopes=one,two,three", + } + ); + + const request = new Request( + "/api/github/oauth/login?state=mystate123&scopes=one,two,three" + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(302); + const url = new URL(response!.headers!["location"] as string); + expect(url).toMatchObject({ + origin: "https://github.com", + pathname: "/login/oauth/authorize", + }); + expect(url.searchParams.get("client_id")).toEqual("0123"); + expect(url.searchParams.get("state")).toEqual("mystate123"); + expect(url.searchParams.get("scope")).toEqual("one,two,three"); + }); + + it("GET /api/github/oauth/callback?code=012345&state=mystate123", async () => { + const appMock = { + createToken: jest.fn().mockResolvedValue({ + authentication: { + type: "token", + tokenType: "oauth", + token: "token123", + }, + }), + }; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "GET", + url: "/api/github/oauth/callback?code=012345&state=state123", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(200); + expect(response!.text).toMatch(/token123/); + + expect(appMock.createToken.mock.calls.length).toEqual(1); + expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({ + code: "012345", + }); + }); + + it("POST /api/github/oauth/token", async () => { + const appMock = { + createToken: jest.fn().mockResolvedValue({ + authentication: { + type: "token", + tokenType: "oauth", + clientSecret: "secret123", + }, + }), + }; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "POST", + url: "/api/github/oauth/token", + headers: { "content-type": "application/json" }, + text: JSON.stringify({ + code: "012345", + redirectUrl: "http://example.com", + }), + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(201); + expect(JSON.parse(response!.text!)).toStrictEqual({ + authentication: { type: "token", tokenType: "oauth" }, + }); + + expect(appMock.createToken.mock.calls.length).toEqual(1); + expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({ + code: "012345", + redirectUrl: "http://example.com", + }); + }); + + it("GET /api/github/oauth/token", async () => { + const appMock = { + checkToken: jest.fn().mockResolvedValue({ + data: { id: 1 }, + authentication: { + type: "token", + tokenType: "oauth", + clientSecret: "secret123", + }, + }), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "GET", + url: "/api/github/oauth/token", + headers: { authorization }, + } + ); + + expect(response).toBeTruthy(); + + expect(response!.status).toEqual(200); + expect(JSON.parse(response!.text!)).toStrictEqual({ + data: { id: 1 }, + authentication: { type: "token", tokenType: "oauth" }, + }); + + expect(appMock.checkToken.mock.calls.length).toEqual(1); + expect(appMock.checkToken.mock.calls[0][0]).toStrictEqual({ token }); + }); + + it("POST /api/github/oauth/token/scoped", async () => { + const appMock = { + scopeToken: jest.fn().mockResolvedValue({ + data: { id: 1 }, + authentication: { + type: "token", + tokenType: "oauth", + clientSecret: "secret123", + }, + }), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "POST", + url: "/api/github/oauth/token/scoped", + headers: { authorization }, + text: JSON.stringify({ + target: "octokit", + repositories: ["oauth-methods.js"], + permissions: { issues: "write" }, + }), + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(200); + expect(JSON.parse(response!.text!)).toMatchInlineSnapshot(` + Object { + "authentication": Object { + "tokenType": "oauth", + "type": "token", + }, + "data": Object { + "id": 1, + }, + } + `); + + expect(appMock.scopeToken.mock.calls.length).toEqual(1); + expect(appMock.scopeToken.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "permissions": Object { + "issues": "write", + }, + "repositories": Array [ + "oauth-methods.js", + ], + "target": "octokit", + "token": "${token}", + } + `); + }); + + it("PATCH /api/github/oauth/refresh-token", async () => { + const appMock = { + refreshToken: jest.fn().mockResolvedValue({ + data: { id: 1 }, + authentication: { + type: "token", + tokenType: "oauth", + clientSecret: "secret123", + }, + }), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "PATCH", + url: "/api/github/oauth/refresh-token", + headers: { authorization }, + text: JSON.stringify({ refreshToken: "r1.refreshtoken123" }), + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(200); + expect(JSON.parse(response!.text!)).toStrictEqual({ + data: { id: 1 }, + authentication: { type: "token", tokenType: "oauth" }, + }); + + expect(appMock.refreshToken.mock.calls.length).toEqual(1); + expect(appMock.refreshToken.mock.calls[0][0]).toStrictEqual({ + refreshToken: "r1.refreshtoken123", + }); + }); + + it("PATCH /api/github/oauth/token", async () => { + const appMock = { + resetToken: jest.fn().mockResolvedValue({ + data: { id: 1 }, + authentication: { + type: "token", + tokenType: "oauth", + clientSecret: "secret123", + }, + }), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "PATCH", + url: "/api/github/oauth/token", + headers: { authorization }, + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(200); + expect(JSON.parse(response!.text!)).toStrictEqual({ + data: { id: 1 }, + authentication: { type: "token", tokenType: "oauth" }, + }); + + expect(appMock.resetToken.mock.calls.length).toEqual(1); + expect(appMock.resetToken.mock.calls[0][0]).toStrictEqual({ token }); + }); + + it("DELETE /api/github/oauth/token", async () => { + const appMock = { + deleteToken: jest.fn().mockResolvedValue(undefined), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "DELETE", + url: "/api/github/oauth/token", + headers: { authorization }, + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(204); + expect(appMock.deleteToken.mock.calls.length).toEqual(1); + expect(appMock.deleteToken.mock.calls[0][0]).toStrictEqual({ token }); + }); + + it("DELETE /api/github/oauth/grant", async () => { + const appMock = { + deleteAuthorization: jest.fn().mockResolvedValue(undefined), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "DELETE", + url: "/api/github/oauth/grant", + headers: { authorization }, + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(204); + expect(appMock.deleteAuthorization.mock.calls.length).toEqual(1); + expect(appMock.deleteAuthorization.mock.calls[0][0]).toStrictEqual({ + token, + }); + }); + + it("POST /unknown", async () => { + const response = await handleRequest( + {} as unknown as OAuthApp, + {}, + { + method: "POST", + url: "/unknown", + } + ); + expect(response).toBeTruthy(); + expect(response!.status).toEqual(404); + expect(JSON.parse(response!.text!)).toEqual({ + error: "Unknown route: POST /unknown", + }); + }); + + it("GET /api/github/oauth/callback without code", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "GET", + url: "/api/github/oauth/callback", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "code" parameter is required', + }); + }); + + it("GET /api/github/oauth/callback with error", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "GET", + url: "/api/github/oauth/callback?error=redirect_uri_mismatch&error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&error_uri=https://docs.github.com/en/developers/apps/troubleshooting-authorization-request-errors/%23redirect-uri-mismatch&state=xyz", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: + "[@octokit/oauth-app] redirect_uri_mismatch The redirect_uri MUST match the registered callback URL for this application.", + }); + }); + + it("POST /api/github/oauth/token without state or code", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "POST", + url: "/api/github/oauth/token", + headers: { "Content-Type": "application/json" }, + text: JSON.stringify({}), + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "code" parameter is required', + }); + }); + + it("POST /api/github/oauth/token with non-JSON request body", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "POST", + url: "/api/github/oauth/token", + headers: {}, + text: "foo", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: "[@octokit/oauth-app] request error", + }); + }); + + it("GET /api/github/oauth/token without Authorization header", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "GET", + url: "/api/github/oauth/token", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "Authorization" header is required', + }); + }); + + it("PATCH /api/github/oauth/token without authorization header", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "PATCH", + url: "/api/github/oauth/token", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "Authorization" header is required', + }); + }); + + it("POST /api/github/oauth/token/scoped without authorization header", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "POST", + url: "/api/github/oauth/token/scoped", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "Authorization" header is required', + }); + }); + + it("PATCH /api/github/oauth/refresh-token without authorization header", async () => { + const appMock = { + refreshToken: jest.fn().mockResolvedValue({ + ok: true, + }), + }; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "PATCH", + url: "/api/github/oauth/refresh-token", + headers: {}, + text: JSON.stringify({ + refreshToken: "r1.refreshtoken123", + }), + } + ); + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "Authorization" header is required', + }); + }); + + it("PATCH /api/github/oauth/refresh-token without refreshToken", async () => { + const appMock = { + refreshToken: jest.fn().mockResolvedValue({ + ok: true, + }), + }; + const token = "token123"; + const authorization = `token ${token}`; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "PATCH", + url: "/api/github/oauth/refresh-token", + headers: { authorization }, + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: "[@octokit/oauth-app] refreshToken must be sent in request body", + }); + }); + + it("DELETE /api/github/oauth/token without authorization header", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "DELETE", + url: "/api/github/oauth/token", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "Authorization" header is required', + }); + }); + + it("DELETE /api/github/oauth/grant without authorization header", async () => { + const appMock = {}; + const response = await handleRequest( + appMock as unknown as OAuthApp, + {}, + { + method: "DELETE", + url: "/api/github/oauth/grant", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(400); + expect(JSON.parse(response!.text!)).toStrictEqual({ + error: '[@octokit/oauth-app] "Authorization" header is required', + }); + }); + + it("web worker handler with options.pathPrefix", async () => { + const response = await handleRequest( + new OAuthApp({ + clientId: "0123", + clientSecret: "0123secret", + }), + { pathPrefix: "/test" }, + { + method: "GET", + url: "/test/login", + } + ); + + expect(response).toBeTruthy(); + expect(response!.status).toEqual(302); + }); +}); diff --git a/test/node-middleware.test.ts b/test/node-middleware.test.ts index 28b2827ae..9c663dbf2 100644 --- a/test/node-middleware.test.ts +++ b/test/node-middleware.test.ts @@ -2,726 +2,20 @@ import { createServer } from "http"; import { URL } from "url"; import fetch from "node-fetch"; -import { createNodeMiddleware, OAuthApp } from "../src/"; +import { createNodeMiddleware, OAuthApp } from "../src"; // import without types const express = require("express"); describe("createNodeMiddleware(app)", () => { - it("allow pre-flight requests", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - - const server = createServer(createNodeMiddleware(app)).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { method: "OPTIONS" } - ); - - server.close(); - - expect(response.status).toEqual(200); - }); - - it("GET /api/github/oauth/login", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - - const server = createServer(createNodeMiddleware(app)).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const { status, headers } = await fetch( - `http://localhost:${port}/api/github/oauth/login`, - { - redirect: "manual", - } - ); - - server.close(); - - expect(status).toEqual(302); - - const url = new URL(headers.get("location") as string); - - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - expect(url.searchParams.get("client_id")).toEqual("0123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toEqual(null); - }); - - it("GET /api/github/oauth/login with defaultScopes (#110)", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - defaultScopes: ["repo"], - }); - - const server = createServer(createNodeMiddleware(app)).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const { status, headers } = await fetch( - `http://localhost:${port}/api/github/oauth/login`, - { - redirect: "manual", - } - ); - - server.close(); - - expect(status).toEqual(302); - - const url = new URL(headers.get("location") as string); - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - expect(url.searchParams.get("client_id")).toEqual("0123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toEqual("repo"); - }); - - it("GET /api/github/oauth/login?state=mystate123&scopes=one,two,three", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - - const server = createServer(createNodeMiddleware(app)).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const { status, headers } = await fetch( - `http://localhost:${port}/api/github/oauth/login?state=mystate123&scopes=one,two,three`, - { - redirect: "manual", - } - ); - - server.close(); - - expect(status).toEqual(302); - - const url = new URL(headers.get("location") as string); - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - - expect(url.searchParams.get("client_id")).toEqual("0123"); - expect(url.searchParams.get("state")).toEqual("mystate123"); - expect(url.searchParams.get("scope")).toEqual("one,two,three"); - }); - - it("GET /api/github/oauth/callback?code=012345&state=mystate123", async () => { - const appMock = { - createToken: jest.fn().mockResolvedValue({ - authentication: { - type: "token", - tokenType: "oauth", - token: "token123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/callback?code=012345&state=state123` - ); - - server.close(); - - expect(response.status).toEqual(200); - expect(await response.text()).toMatch(/token123/); - - expect(appMock.createToken.mock.calls.length).toEqual(1); - expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({ - code: "012345", - }); - }); - - it("POST /api/github/oauth/token", async () => { - const appMock = { - createToken: jest.fn().mockResolvedValue({ - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "POST", - body: JSON.stringify({ - code: "012345", - redirectUrl: "http://example.com", - }), - } - ); - - server.close(); - - expect(response.status).toEqual(201); - expect(await response.json()).toStrictEqual({ - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.createToken.mock.calls.length).toEqual(1); - expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({ - code: "012345", - redirectUrl: "http://example.com", - }); - }); - - it("GET /api/github/oauth/token", async () => { - const appMock = { - checkToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - headers: { - authorization: "token token123", - }, - } - ); - - server.close(); - - expect(response.status).toEqual(200); - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.checkToken.mock.calls.length).toEqual(1); - expect(appMock.checkToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("PATCH /api/github/oauth/token", async () => { - const appMock = { - resetToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "PATCH", - headers: { - authorization: "token token123", - }, - } - ); - - server.close(); - - expect(response.status).toEqual(200); - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.resetToken.mock.calls.length).toEqual(1); - expect(appMock.resetToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("POST /api/github/oauth/token/scoped", async () => { - const appMock = { - scopeToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token/scoped`, - { - method: "POST", - headers: { - authorization: "token token123", - }, - body: JSON.stringify({ - target: "octokit", - repositories: ["oauth-methods.js"], - permissions: { issues: "write" }, - }), - } - ); - - server.close(); - - expect(response.status).toEqual(200); - expect(await response.json()).toMatchInlineSnapshot(` - Object { - "authentication": Object { - "tokenType": "oauth", - "type": "token", - }, - "data": Object { - "id": 1, - }, - } - `); - - expect(appMock.scopeToken.mock.calls.length).toEqual(1); - expect(appMock.scopeToken.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "permissions": Object { - "issues": "write", - }, - "repositories": Array [ - "oauth-methods.js", - ], - "target": "octokit", - "token": "token123", - } - `); - }); - - it("PATCH /api/github/oauth/refresh-token", async () => { - const appMock = { - refreshToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/refresh-token`, - { - method: "PATCH", - headers: { - authorization: "token token123", - }, - body: JSON.stringify({ - refreshToken: "r1.refreshtoken123", - }), - } - ); - - server.close(); - - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - expect(response.status).toEqual(200); - - expect(appMock.refreshToken.mock.calls.length).toEqual(1); - expect(appMock.refreshToken.mock.calls[0][0]).toStrictEqual({ - refreshToken: "r1.refreshtoken123", - }); - }); - it("PATCH /api/github/oauth/token", async () => { - const appMock = { - resetToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "PATCH", - headers: { - authorization: "token token123", - }, - } - ); - - server.close(); - - expect(response.status).toEqual(200); - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.resetToken.mock.calls.length).toEqual(1); - expect(appMock.resetToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("DELETE /api/github/oauth/token", async () => { - const appMock = { - deleteToken: jest.fn().mockResolvedValue(undefined), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "DELETE", - headers: { - authorization: "token token123", - }, - } - ); - - server.close(); - - expect(response.status).toEqual(204); - - expect(appMock.deleteToken.mock.calls.length).toEqual(1); - expect(appMock.deleteToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("DELETE /api/github/oauth/grant", async () => { - const appMock = { - deleteAuthorization: jest.fn().mockResolvedValue(undefined), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/grant`, - { - method: "DELETE", - headers: { - authorization: "token token123", - }, - } - ); - - server.close(); - - expect(response.status).toEqual(204); - - expect(appMock.deleteAuthorization.mock.calls.length).toEqual(1); - expect(appMock.deleteAuthorization.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("POST /unrelated", async () => { - expect.assertions(4); - - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - - const server = createServer( - createNodeMiddleware(app, { - onUnhandledRequest: (request, response) => { - expect(request.method).toEqual("POST"); - expect(request.url).toEqual("/unrelated"); - - // test that the request has not yet been consumed with .on("data") - expect(request.complete).toEqual(false); - - response.writeHead(200); - response.end(); - }, - }) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const { status, headers } = await fetch( - `http://localhost:${port}/unrelated`, - { - method: "POST", - body: JSON.stringify({ ok: true }), - headers: { - "content-type": "application/json", - }, - } - ); - - server.close(); - - expect(status).toEqual(200); - }); - - // errors - - it("GET /unknown", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch(`http://localhost:${port}/unknown`); - - server.close(); - - expect(response.status).toEqual(404); - }); - - it("GET /api/github/oauth/callback without code", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/callback` - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "code" parameter is required', - }); - }); - - it("GET /api/github/oauth/callback with error", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/callback?error=redirect_uri_mismatch&error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&error_uri=https://docs.github.com/en/developers/apps/troubleshooting-authorization-request-errors/%23redirect-uri-mismatch&state=xyz` - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: - "[@octokit/oauth-app] redirect_uri_mismatch The redirect_uri MUST match the registered callback URL for this application.", - }); - }); - - it("POST /api/github/oauth/token without state or code", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "POST", - body: JSON.stringify({}), - } - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "code" parameter is required', - }); - }); - - it("POST /api/github/oauth/token with non-JSON request body", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "POST", - body: "foo", - } - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: "[@octokit/oauth-app] request error", - }); - }); - - it("GET /api/github/oauth/token without Authorization header", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - headers: {}, - } - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("PATCH /api/github/oauth/token without authorization header", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "PATCH", - headers: {}, - } - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("POST /api/github/oauth/token/scoped without authorization header", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token/scoped`, - { - method: "POST", - headers: {}, - } - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("PATCH /api/github/oauth/refresh-token without authorization header", async () => { + it("POST /api/github/oauth/token", async () => { const appMock = { - refreshToken: jest.fn().mockResolvedValue({ - ok: true, + createToken: jest.fn().mockResolvedValue({ + authentication: { + type: "token", + tokenType: "oauth", + clientSecret: "secret123", + }, }), }; @@ -732,101 +26,27 @@ describe("createNodeMiddleware(app)", () => { const { port } = server.address(); const response = await fetch( - `http://localhost:${port}/api/github/oauth/refresh-token`, + `http://localhost:${port}/api/github/oauth/token`, { - method: "PATCH", + method: "POST", body: JSON.stringify({ - refreshToken: "r1.refreshtoken123", + code: "012345", + redirectUrl: "http://example.com", }), } ); server.close(); - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("PATCH /api/github/oauth/refresh-token without refreshToken", async () => { - const appMock = { - refreshToken: jest.fn().mockResolvedValue({ - ok: true, - }), - }; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/refresh-token`, - { - method: "PATCH", - headers: { - authorization: "token token123", - }, - } - ); - - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: "[@octokit/oauth-app] refreshToken must be sent in request body", - }); - }); - - it("DELETE /api/github/oauth/token without authorization header", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/token`, - { - method: "DELETE", - headers: {}, - } - ); - - server.close(); - - expect(response.status).toEqual(400); + expect(response.status).toEqual(201); expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', + authentication: { type: "token", tokenType: "oauth" }, }); - }); - - it("DELETE /api/github/oauth/grant without authorization header", async () => { - const appMock = {}; - - const server = createServer( - createNodeMiddleware(appMock as unknown as OAuthApp) - ).listen(); - // @ts-expect-error complains about { port } although it's included in returned AddressInfo interface - const { port } = server.address(); - - const response = await fetch( - `http://localhost:${port}/api/github/oauth/grant`, - { - method: "DELETE", - headers: {}, - } - ); - server.close(); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', + expect(appMock.createToken.mock.calls.length).toEqual(1); + expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({ + code: "012345", + redirectUrl: "http://example.com", }); }); @@ -929,13 +149,13 @@ describe("createNodeMiddleware(app)", () => { const app = express(); app.use( - "/test", + "/foo", createNodeMiddleware( new OAuthApp({ clientId: "0123", clientSecret: "0123secret", }), - { pathPrefix: "/test" } + { pathPrefix: "/bar" } ) ); app.all("*", (_request: any, response: any) => @@ -946,7 +166,7 @@ describe("createNodeMiddleware(app)", () => { const { port } = server.address(); - const { status } = await fetch(`http://localhost:${port}/test/test/login`, { + const { status } = await fetch(`http://localhost:${port}/foo/bar/login`, { redirect: "manual", }); diff --git a/test/web-worker-handler.test.ts b/test/web-worker-handler.test.ts index ba26da54c..2ce44a594 100644 --- a/test/web-worker-handler.test.ts +++ b/test/web-worker-handler.test.ts @@ -1,16 +1,9 @@ import { URL } from "url"; import * as nodeFetch from "node-fetch"; -import fromEntries from "fromentries"; -import { - createCloudflareHandler, - createWebWorkerHandler, - OAuthApp, -} from "../src"; -import { Octokit } from "@octokit/core"; +import { createWebWorkerHandler, OAuthApp } from "../src"; describe("createWebWorkerHandler(app)", () => { beforeAll(() => { - Object.fromEntries ||= fromEntries; (global as any).Request = nodeFetch.Request; (global as any).Response = nodeFetch.Response; }); @@ -20,129 +13,6 @@ describe("createWebWorkerHandler(app)", () => { delete (global as any).Response; }); - it("support both oauth-app and github-app", () => { - const oauthApp = new OAuthApp({ - clientType: "oauth-app", - clientId: "0123", - clientSecret: "0123secret", - }); - createWebWorkerHandler(oauthApp); - - const githubApp = new OAuthApp({ - clientType: "github-app", - clientId: "0123", - clientSecret: "0123secret", - }); - createWebWorkerHandler(githubApp); - }); - - it("allow pre-flight requests", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - const handleRequest = createWebWorkerHandler(app); - const request = new Request("/api/github/oauth/token", { - method: "OPTIONS", - }); - const response = await handleRequest(request); - expect(response.status).toStrictEqual(200); - }); - - it("GET /api/github/oauth/login", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - const handleRequest = createWebWorkerHandler(app); - - const request = new Request("/api/github/oauth/login"); - const { status, headers } = await handleRequest(request); - - expect(status).toEqual(302); - const url = new URL(headers.get("location") as string); - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - expect(url.searchParams.get("client_id")).toEqual("0123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toEqual(null); - }); - - it("GET /api/github/oauth/login with defaultScopes (#110)", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - defaultScopes: ["repo"], - }); - const handleRequest = createWebWorkerHandler(app); - - const request = new Request("/api/github/oauth/login"); - const { status, headers } = await handleRequest(request); - - expect(status).toEqual(302); - const url = new URL(headers.get("location") as string); - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - expect(url.searchParams.get("client_id")).toEqual("0123"); - expect(url.searchParams.get("state")).toMatch(/^\w+$/); - expect(url.searchParams.get("scope")).toEqual("repo"); - }); - - it("GET /api/github/oauth/login?state=mystate123&scopes=one,two,three", async () => { - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - const handleRequest = createWebWorkerHandler(app); - - const request = new Request( - "/api/github/oauth/login?state=mystate123&scopes=one,two,three" - ); - const { status, headers } = await handleRequest(request); - - expect(status).toEqual(302); - const url = new URL(headers.get("location") as string); - expect(url).toMatchObject({ - origin: "https://github.com", - pathname: "/login/oauth/authorize", - }); - expect(url.searchParams.get("client_id")).toEqual("0123"); - expect(url.searchParams.get("state")).toEqual("mystate123"); - expect(url.searchParams.get("scope")).toEqual("one,two,three"); - }); - - it("GET /api/github/oauth/callback?code=012345&state=mystate123", async () => { - const appMock = { - createToken: jest.fn().mockResolvedValue({ - authentication: { - type: "token", - tokenType: "oauth", - token: "token123", - }, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request( - "/api/github/oauth/callback?code=012345&state=state123" - ); - const response = await handleRequest(request); - - expect(response.status).toEqual(200); - expect(await response.text()).toMatch(/token123/); - - expect(appMock.createToken.mock.calls.length).toEqual(1); - expect(appMock.createToken.mock.calls[0][0]).toStrictEqual({ - code: "012345", - }); - }); - it("POST /api/github/oauth/token", async () => { const appMock = { createToken: jest.fn().mockResolvedValue({ @@ -177,500 +47,4 @@ describe("createWebWorkerHandler(app)", () => { redirectUrl: "http://example.com", }); }); - - it("GET /api/github/oauth/token", async () => { - const appMock = { - checkToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - headers: { - authorization: "token token123", - }, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(200); - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.checkToken.mock.calls.length).toEqual(1); - expect(appMock.checkToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("PATCH /api/github/oauth/token", async () => { - const appMock = { - resetToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "PATCH", - headers: { authorization: "token token123" }, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(200); - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.resetToken.mock.calls.length).toEqual(1); - expect(appMock.resetToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("POST /api/github/oauth/token/scoped", async () => { - const appMock = { - scopeToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token/scoped", { - method: "POST", - headers: { authorization: "token token123" }, - body: JSON.stringify({ - target: "octokit", - repositories: ["oauth-methods.js"], - permissions: { issues: "write" }, - }), - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(200); - expect(response.status).toEqual(200); - expect(await response.json()).toMatchInlineSnapshot(` - Object { - "authentication": Object { - "tokenType": "oauth", - "type": "token", - }, - "data": Object { - "id": 1, - }, - } - `); - - expect(appMock.scopeToken.mock.calls.length).toEqual(1); - expect(appMock.scopeToken.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "permissions": Object { - "issues": "write", - }, - "repositories": Array [ - "oauth-methods.js", - ], - "target": "octokit", - "token": "token123", - } - `); - }); - - it("PATCH /api/github/oauth/refresh-token", async () => { - const appMock = { - refreshToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/refresh-token", { - method: "PATCH", - headers: { authorization: "token token123" }, - body: JSON.stringify({ refreshToken: "r1.refreshtoken123" }), - }); - const response = await handleRequest(request); - - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - expect(response.status).toEqual(200); - - expect(appMock.refreshToken.mock.calls.length).toEqual(1); - expect(appMock.refreshToken.mock.calls[0][0]).toStrictEqual({ - refreshToken: "r1.refreshtoken123", - }); - }); - - it("PATCH /api/github/oauth/token", async () => { - const appMock = { - resetToken: jest.fn().mockResolvedValue({ - data: { id: 1 }, - authentication: { - type: "token", - tokenType: "oauth", - clientSecret: "secret123", - }, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "PATCH", - headers: { authorization: "token token123" }, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(200); - expect(await response.json()).toStrictEqual({ - data: { id: 1 }, - authentication: { type: "token", tokenType: "oauth" }, - }); - - expect(appMock.resetToken.mock.calls.length).toEqual(1); - expect(appMock.resetToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("DELETE /api/github/oauth/token", async () => { - const appMock = { - deleteToken: jest.fn().mockResolvedValue(undefined), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "DELETE", - headers: { authorization: "token token123" }, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(204); - - expect(appMock.deleteToken.mock.calls.length).toEqual(1); - expect(appMock.deleteToken.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("DELETE /api/github/oauth/grant", async () => { - const appMock = { - deleteAuthorization: jest.fn().mockResolvedValue(undefined), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/grant", { - method: "DELETE", - headers: { authorization: "token token123" }, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(204); - - expect(appMock.deleteAuthorization.mock.calls.length).toEqual(1); - expect(appMock.deleteAuthorization.mock.calls[0][0]).toStrictEqual({ - token: "token123", - }); - }); - - it("POST /unrelated", async () => { - expect.assertions(4); - - const app = new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }); - const handleRequest = createWebWorkerHandler(app, { - onUnhandledRequest: async (request: Request) => { - expect(request.method).toEqual("POST"); - expect(request.url).toEqual("/unrelated"); - const text = await request.text(); - expect(text).toEqual('{"ok":true}'); - return new Response(null, { status: 200 }); - }, - }); - - const request = new Request("/unrelated", { - method: "POST", - body: JSON.stringify({ ok: true }), - headers: { - "content-type": "application/json", - }, - }); - const { status } = await handleRequest(request); - - expect(status).toEqual(200); - }); - - // // errors - - it("GET /unknown", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/unknown"); - const response = await handleRequest(request); - expect(response.status).toEqual(404); - }); - - it("GET /api/github/oauth/callback without code", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/callback"); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "code" parameter is required', - }); - }); - - it("GET /api/github/oauth/callback with error", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request( - "/api/github/oauth/callback?error=redirect_uri_mismatch&error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&error_uri=https://docs.github.com/en/developers/apps/troubleshooting-authorization-request-errors/%23redirect-uri-mismatch&state=xyz" - ); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: - "[@octokit/oauth-app] redirect_uri_mismatch The redirect_uri MUST match the registered callback URL for this application.", - }); - }); - - it("POST /api/github/oauth/token without state or code", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "POST", - body: JSON.stringify({}), - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "code" parameter is required', - }); - }); - - it("POST /api/github/oauth/token with non-JSON request body", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "POST", - body: "foo", - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: "[@octokit/oauth-app] request error", - }); - }); - - it("GET /api/github/oauth/token without Authorization header", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - headers: {}, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("PATCH /api/github/oauth/token without authorization header", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "PATCH", - headers: {}, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("POST /api/github/oauth/token/scoped without authorization header", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token/scoped", { - method: "POST", - headers: {}, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("PATCH /api/github/oauth/refresh-token without authorization header", async () => { - const appMock = { - refreshToken: jest.fn().mockResolvedValue({ - ok: true, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/refresh-token", { - method: "PATCH", - body: JSON.stringify({ - refreshToken: "r1.refreshtoken123", - }), - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("PATCH /api/github/oauth/refresh-token without refreshToken", async () => { - const appMock = { - refreshToken: jest.fn().mockResolvedValue({ - ok: true, - }), - }; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/refresh-token", { - method: "PATCH", - headers: { - authorization: "token token123", - }, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: "[@octokit/oauth-app] refreshToken must be sent in request body", - }); - }); - - it("DELETE /api/github/oauth/token without authorization header", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/token", { - method: "DELETE", - headers: {}, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("DELETE /api/github/oauth/grant without authorization header", async () => { - const appMock = {}; - const handleRequest = createWebWorkerHandler( - appMock as unknown as OAuthApp - ); - - const request = new Request("/api/github/oauth/grant", { - method: "DELETE", - headers: {}, - }); - const response = await handleRequest(request); - - expect(response.status).toEqual(400); - expect(await response.json()).toStrictEqual({ - error: '[@octokit/oauth-app] "Authorization" header is required', - }); - }); - - it("web worker handler with options.pathPrefix", async () => { - const handleRequest = createWebWorkerHandler( - new OAuthApp({ - clientId: "0123", - clientSecret: "0123secret", - }), - { pathPrefix: "/test" } - ); - - const request = new Request("/test/login", { redirect: "manual" }); - const { status } = await handleRequest(request); - - expect(status).toEqual(302); - }); });
- name - - type - - description -
- app - - OAuthApp instance - - Required. -
- options.pathPrefix - - string - - -All exposed paths will be prefixed with the provided prefix. Defaults to `"/api/github/oauth"` - -
- options.onUnhandledRequest - - function - Defaults to returns: - -```js -function onUnhandledRequest(request) { - return - { - status: 404, - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - error: `Unknown route: [METHOD] [URL]`, - }) - } - ); -} -``` -