Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setup tests #61

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add setup-proxy tests
  • Loading branch information
gladwindos committed Apr 10, 2024
commit 30ddf44a52d859721e901de6f47ab68639f4fdad
157 changes: 157 additions & 0 deletions packages/server/src/router/setup/setup-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { IncomingMessage } from "http";
import { Router, Request, Response } from "express";
import { setupProxy, buildTargetUrl, onProxyRes } from "./setup-proxy";
import { createProxyMiddleware, fixRequestBody } from "http-proxy-middleware";
import { Endpoint } from "../../config/config.types";
import { PolicyOption } from "@gateweaver/policies";

jest.mock("express", () => ({
Router: () => ({
use: jest.fn(),
}),
}));

jest.mock("http-proxy-middleware", () => ({
createProxyMiddleware: jest.fn(),
fixRequestBody: jest.fn(),
}));

describe("buildTargetUrl", () => {
it("should build target URL with query parameters", () => {
const endpoint: Endpoint = {
path: "/api",
target: {
url: "https://example.com",
},
request: {
query: {
param1: "value1",
param2: "value2",
},
},
};

const url = buildTargetUrl(endpoint);

expect(url).toEqual("https://example.com?param1=value1&param2=value2");
});
});

describe("onProxyRes", () => {
const req = {} as Request;
const res = {} as Response;

const createMockProxyRes = (headers: Record<string, string>) => {
const proxyRes = {
headers,
} as IncomingMessage;

return proxyRes;
};

it("should remove rate limit headers if RateLimit policy is applied", () => {
const proxyRes = createMockProxyRes({
"x-ratelimit-limit": "1000",
"x-ratelimit-remaining": "500",
"x-ratelimit-reset": "158300000",
});

const endpoint: Endpoint = {
path: "/api",
target: {
url: "https://example.com",
},
policies: [PolicyOption.RateLimit],
};

onProxyRes(proxyRes, req, res, endpoint);

expect(proxyRes.headers["x-ratelimit-limit"]).toBeUndefined();
expect(proxyRes.headers["x-ratelimit-remaining"]).toBeUndefined();
expect(proxyRes.headers["x-ratelimit-reset"]).toBeUndefined();
});

it("should remove CORS headers if Cors policy is applied", () => {
const proxyRes = createMockProxyRes({
"access-control-allow-origin": "*",
"access-control-allow-credentials": "true",
});

const endpoint: Endpoint = {
path: "/api",
target: {
url: "https://example.com",
},
policies: [PolicyOption.Cors],
};

onProxyRes(proxyRes, req, res, endpoint);

expect(proxyRes.headers["access-control-allow-origin"]).toBeUndefined();
expect(
proxyRes.headers["access-control-allow-credentials"],
).toBeUndefined();
});

it("should add response headers defined in the endpoint", () => {
const proxyRes = createMockProxyRes({});
const endpoint: Endpoint = {
path: "/api",
target: {
url: "https://example.com",
},
response: {
headers: {
"x-custom-header": "value",
},
},
};

onProxyRes(proxyRes, req, res, endpoint);

expect(proxyRes.headers["x-custom-header"]).toEqual("value");
});

it("should remove headers with empty values", () => {
const proxyRes = createMockProxyRes({
"x-empty-header": "",
});

const endpoint: Endpoint = {
path: "/api",
target: {
url: "https://example.com",
},
};

onProxyRes(proxyRes, req, res, endpoint);

expect(proxyRes.headers["x-empty-header"]).toBeUndefined();
});
});

describe("setupProxy", () => {
const router = Router();

it("should setup proxy middleware", () => {
const endpoint = {
path: "/api",
target: {
url: "https://example.com",
},
};

setupProxy(router, endpoint);

expect(createProxyMiddleware).toHaveBeenCalledWith(
expect.objectContaining({
target: "https://example.com",
changeOrigin: true,
pathRewrite: { "^/api": "" },
xfwd: true,
onProxyReq: fixRequestBody,
onProxyRes: expect.any(Function),
}),
);
});
});
105 changes: 57 additions & 48 deletions packages/server/src/router/setup/setup-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,66 +9,74 @@ import { PolicyOption } from "@gateweaver/policies";
import { Endpoint } from "../../config/config.types";
import { logger } from "../../logger";

const RATE_LIMIT_HEADERS = [
"X-Ratelimit-Limit",
"X-Ratelimit-Remaining",
"X-Ratelimit-Reset",
"RateLimit-Policy",
"RateLimit-Limit",
"RateLimit-Remaining",
"RateLimit-Reset",
"RateLimit",
];

const CORS_HEADERS = [
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
];

const deleteHeaders = (res: IncomingMessage, headers: string[]): void => {
headers.forEach((header) => {
delete res.headers[header];
delete res.headers[header.toLowerCase()];
});
};

export const setupProxy = (router: Router, endpoint: Endpoint): void => {
const buildTargetUrl = () => {
let url = endpoint.target.url;

if (endpoint.request?.query) {
const query = new URLSearchParams(endpoint.request.query);
url += `?${query}`;
}
export const buildTargetUrl = (endpoint: Endpoint) => {
let url = endpoint.target.url;

return url;
};
if (endpoint.request?.query) {
const query = new URLSearchParams(endpoint.request.query);
url += `?${query}`;
}

const onProxyRes = (
proxyRes: IncomingMessage,
_req: Request,
_res: Response,
) => {
proxyRes.headers = {
...proxyRes.headers,
...endpoint.response?.headers,
};
return url;
};

Object.keys(proxyRes.headers).forEach((key) => {
if (!proxyRes.headers[key]) {
delete proxyRes.headers[key];
}
});
export const onProxyRes = (
proxyRes: IncomingMessage,
_req: Request,
_res: Response,
endpoint: Endpoint,
) => {
proxyRes.headers = {
...proxyRes.headers,
...endpoint.response?.headers,
};

if (endpoint.policies?.includes(PolicyOption.RateLimit)) {
deleteHeaders(proxyRes, [
"X-Ratelimit-Limit",
"X-Ratelimit-Remaining",
"X-Ratelimit-Reset",
"RateLimit-Policy",
"RateLimit-Limit",
"RateLimit-Remaining",
"RateLimit-Reset",
"RateLimit",
]);
// Remove headers with empty values
Object.keys(proxyRes.headers).forEach((key) => {
if (!proxyRes.headers[key]) {
delete proxyRes.headers[key];
}
});

if (endpoint.policies?.includes(PolicyOption.Cors)) {
deleteHeaders(proxyRes, [
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
]);
}
};
// Remove rate limit headers to avoid conflicts with rate limit middleware
if (endpoint.policies?.includes(PolicyOption.RateLimit)) {
deleteHeaders(proxyRes, RATE_LIMIT_HEADERS);
}

// Remove CORS headers to avoid conflicts with CORS middleware
if (endpoint.policies?.includes(PolicyOption.Cors)) {
deleteHeaders(proxyRes, CORS_HEADERS);
}
};

export const setupProxy = (router: Router, endpoint: Endpoint): void => {
const logProvider = () => {
return {
debug: logger.debug.bind(logger),
Expand All @@ -80,13 +88,14 @@ export const setupProxy = (router: Router, endpoint: Endpoint): void => {
};

const proxyOptions: Options = {
target: buildTargetUrl(),
target: buildTargetUrl(endpoint),
changeOrigin: true,
pathRewrite: { [`^${endpoint.path}`]: "" },
headers: endpoint.request?.headers,
xfwd: true,
onProxyReq: fixRequestBody,
onProxyRes,
onProxyRes: (proxyRes, req, res) =>
onProxyRes(proxyRes, req, res, endpoint),
logProvider,
logLevel: "error",
};
Expand Down