forked from mirrored/github-readme-stats
feature: implement basic querystring params validation
This commit is contained in:
+32
-5
@@ -8,7 +8,36 @@ import {
|
||||
renderError,
|
||||
} from "../src/common/utils.js";
|
||||
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
import { validateQueryStringParams } from "../src/common/validate.js";
|
||||
|
||||
const QUERYSTRING_PARAMS_DATA_TYPE_MAP = new Map([
|
||||
["username", "string"],
|
||||
["hide", "enum-array"],
|
||||
["hide_title", "boolean"],
|
||||
["hide_border", "boolean"],
|
||||
["card_width", "number"],
|
||||
["hide_rank", "boolean"],
|
||||
["show_icons", "boolean"],
|
||||
["include_all_commits", "boolean"],
|
||||
["line_height", "number"],
|
||||
["title_color", "string"],
|
||||
["ring_color", "string"],
|
||||
["icon_color", "string"],
|
||||
["text_color", "string"],
|
||||
["text_bold", "boolean"],
|
||||
["bg_color", "string"],
|
||||
["theme", "enum"],
|
||||
["cache_seconds", "number"],
|
||||
["exclude_repo", "array"],
|
||||
["custom_title", "string"],
|
||||
["locale", "enum"],
|
||||
["disable_animations", "boolean"],
|
||||
["border_radius", "number"],
|
||||
["border_color", "string"],
|
||||
["number_format", "enum"],
|
||||
["rank_icon", "enum"],
|
||||
["show", "enum-array"],
|
||||
]);
|
||||
|
||||
export default async (req, res) => {
|
||||
const {
|
||||
@@ -45,11 +74,9 @@ export default async (req, res) => {
|
||||
return res.send(renderError("Something went wrong"));
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(renderError("Something went wrong", "Language not found"));
|
||||
}
|
||||
|
||||
try {
|
||||
validateQueryStringParams(req.query, QUERYSTRING_PARAMS_DATA_TYPE_MAP);
|
||||
|
||||
const showStats = parseArray(show);
|
||||
const stats = await fetchStats(
|
||||
username,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// @ts-check
|
||||
|
||||
import { availableLocales } from "../translations.js";
|
||||
import { themes } from "../../themes/index.js";
|
||||
import { parseArray } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Class for handling invalid query string param errors.
|
||||
*/
|
||||
class InvalidQueryStringParamsError extends Error {
|
||||
/**
|
||||
* Constructor for InvalidQueryStringParamsError.
|
||||
*
|
||||
* @param {string} message - The error message.
|
||||
* @param {string} secondaryMessage - The secondary error message.
|
||||
*/
|
||||
constructor(message, secondaryMessage) {
|
||||
super(message);
|
||||
this.secondaryMessage = secondaryMessage;
|
||||
}
|
||||
}
|
||||
|
||||
const QUERYSTRING_PARAMS_ENUM_VALUES = {
|
||||
hide: ["stars", "commits", "prs", "issues", "contribs"],
|
||||
theme: Object.keys(themes),
|
||||
locale: availableLocales,
|
||||
number_format: ["short", "long"],
|
||||
rank_icon: ["github", "percentile", "default"],
|
||||
show: [
|
||||
"reviews",
|
||||
"discussions_started",
|
||||
"discussions_answered",
|
||||
"prs_merged",
|
||||
"prs_merged_percentage",
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the secondary error message for an invalid query string param.
|
||||
*
|
||||
* @param {string} param - The invalid query string param.
|
||||
* @param {Map<string, string>} queryStringParamsDataTypeMap - The query string params data type map.
|
||||
* @returns {string} The secondary error message.
|
||||
*/
|
||||
const getInvalidQueryStringParamsErrorSecondaryMessage = (
|
||||
param,
|
||||
queryStringParamsDataTypeMap,
|
||||
) => {
|
||||
const expectedDataType = queryStringParamsDataTypeMap.get(param);
|
||||
if (expectedDataType === "enum" || expectedDataType === "enum-array") {
|
||||
return `Expected: ${QUERYSTRING_PARAMS_ENUM_VALUES[param].join(", ")}`;
|
||||
} else if (expectedDataType === "number") {
|
||||
return "Expected: a number";
|
||||
} else if (expectedDataType === "boolean") {
|
||||
return "Expected: true or false";
|
||||
} else if (expectedDataType === "array") {
|
||||
return "Expected: an array";
|
||||
} else if (expectedDataType === "string") {
|
||||
return "Expected: a string";
|
||||
} else {
|
||||
throw new Error("Unexpected behavior");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the query string params.
|
||||
* Throws an error if a query string param is invalid.
|
||||
* Does not return anything.
|
||||
*
|
||||
* @param {object} queryStringParams - The query string params.
|
||||
* @param {Map<string, string>} queryStringParamsDataTypeMap - The query string params data type map.
|
||||
* @returns {void}
|
||||
*/
|
||||
const validateQueryStringParams = (
|
||||
queryStringParams,
|
||||
queryStringParamsDataTypeMap,
|
||||
) => {
|
||||
for (const [param, value] of Object.entries(queryStringParams)) {
|
||||
const expectedDataType = queryStringParamsDataTypeMap.get(param);
|
||||
if (!expectedDataType) {
|
||||
throw new InvalidQueryStringParamsError(
|
||||
`Invalid query string param: ${param}`,
|
||||
"Expected: a valid query string param",
|
||||
);
|
||||
}
|
||||
if (expectedDataType === "enum") {
|
||||
if (QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value)) {
|
||||
continue;
|
||||
}
|
||||
} else if (expectedDataType === "enum-array") {
|
||||
const values = parseArray(value);
|
||||
if (
|
||||
values.every((value) =>
|
||||
QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (expectedDataType === "number") {
|
||||
if (!isNaN(value)) {
|
||||
continue;
|
||||
}
|
||||
} else if (expectedDataType === "boolean") {
|
||||
if (value === "true" || value === "false") {
|
||||
continue;
|
||||
}
|
||||
} else if (expectedDataType === "array") {
|
||||
if (Array.isArray(parseArray(value))) {
|
||||
continue;
|
||||
}
|
||||
} else if (expectedDataType === "string") {
|
||||
if (typeof value === "string") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new InvalidQueryStringParamsError(
|
||||
`Invalid query string param: ${param}`,
|
||||
getInvalidQueryStringParamsErrorSecondaryMessage(
|
||||
param,
|
||||
queryStringParamsDataTypeMap,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { validateQueryStringParams };
|
||||
+10
-6
@@ -6,6 +6,7 @@ import { calculateRank } from "../src/calculateRank.js";
|
||||
import { renderStatsCard } from "../src/cards/stats-card.js";
|
||||
import { CONSTANTS, renderError } from "../src/common/utils.js";
|
||||
import { expect, it, describe, afterEach } from "@jest/globals";
|
||||
import { availableLocales } from "../src/translations.js";
|
||||
|
||||
const stats = {
|
||||
name: "Anurag Hazra",
|
||||
@@ -125,8 +126,8 @@ describe("Test /api/", () => {
|
||||
{
|
||||
username: "anuraghazra",
|
||||
hide: "issues,prs,contribs",
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
show_icons: "true",
|
||||
hide_border: "true",
|
||||
line_height: 100,
|
||||
title_color: "fff",
|
||||
icon_color: "fff",
|
||||
@@ -255,8 +256,8 @@ describe("Test /api/", () => {
|
||||
{
|
||||
username: "anuraghazra",
|
||||
hide: "issues,prs,contribs",
|
||||
show_icons: true,
|
||||
hide_border: true,
|
||||
show_icons: "true",
|
||||
hide_border: "true",
|
||||
line_height: 100,
|
||||
title_color: "fff",
|
||||
ring_color: "0000ff",
|
||||
@@ -301,7 +302,10 @@ describe("Test /api/", () => {
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
|
||||
expect(res.send).toBeCalledWith(
|
||||
renderError("Something went wrong", "Language not found"),
|
||||
renderError(
|
||||
"Invalid query string param: locale",
|
||||
`Expected: ${availableLocales.join(", ")}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -311,7 +315,7 @@ describe("Test /api/", () => {
|
||||
.reply(200, { error: "Some test error message" });
|
||||
|
||||
const { req, res } = faker(
|
||||
{ username: "anuraghazra", include_all_commits: true },
|
||||
{ username: "anuraghazra", include_all_commits: "true" },
|
||||
data_stats,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user