forked from mirrored/github-readme-stats
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d656449265 | ||
|
|
10d8a68ab7 | ||
|
|
49c0e2da28 | ||
|
|
4a065a36b5 |
+32
-13
@@ -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 {
|
||||
@@ -53,19 +82,9 @@ export default async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (locale && !isLocaleAvailable(locale)) {
|
||||
return res.send(
|
||||
renderError("Something went wrong", "Language not found", {
|
||||
title_color,
|
||||
text_color,
|
||||
bg_color,
|
||||
border_color,
|
||||
theme,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
validateQueryStringParams(req.query, QUERYSTRING_PARAMS_DATA_TYPE_MAP);
|
||||
|
||||
const showStats = parseArray(show);
|
||||
const stats = await fetchStats(
|
||||
username,
|
||||
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
export type DataTypes =
|
||||
| "enum"
|
||||
| "enum-array"
|
||||
| "array"
|
||||
| "string"
|
||||
| "boolean"
|
||||
| "number";
|
||||
|
||||
export function validateQueryStringParam(
|
||||
expectedDataType: DateTypes,
|
||||
param: string,
|
||||
value: string,
|
||||
): boolean;
|
||||
|
||||
export function validateQueryStringParams(
|
||||
queryStringParams: object,
|
||||
queryStringParamsDataTypeMap: Map<string, DataTypes>,
|
||||
): void;
|
||||
|
||||
export class InvalidQueryStringParamsError extends Error {
|
||||
constructor(message: string, secondaryMessage: string);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// @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.
|
||||
*/
|
||||
export 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",
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import("./validate").DataTypes} DataTypes The data types.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the secondary error message for an invalid query string param.
|
||||
*
|
||||
* @param {string} param - The invalid query string param.
|
||||
* @param {Map<string, DataTypes>} 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 a query string param.
|
||||
*
|
||||
* @param {DataTypes} expectedDataType - The expected data type of the query string param.
|
||||
* @param {string} param - The query string param.
|
||||
* @param {string} value - The query string param value.
|
||||
* @returns {boolean} Whether the query string param is valid.
|
||||
*/
|
||||
export const validateQueryStringParam = (expectedDataType, param, value) => {
|
||||
if (expectedDataType === "enum") {
|
||||
return QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value);
|
||||
} else if (expectedDataType === "enum-array") {
|
||||
return parseArray(value).every((value) =>
|
||||
QUERYSTRING_PARAMS_ENUM_VALUES[param].includes(value),
|
||||
);
|
||||
} else if (expectedDataType === "number") {
|
||||
return !isNaN(parseFloat(value));
|
||||
} else if (expectedDataType === "boolean") {
|
||||
return value === "true" || value === "false";
|
||||
} else if (expectedDataType === "array") {
|
||||
return Array.isArray(parseArray(value));
|
||||
} else if (expectedDataType === "string") {
|
||||
return typeof value === "string";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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, DataTypes>} queryStringParamsDataTypeMap - The query string params data type map.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const validateQueryStringParams = (
|
||||
queryStringParams,
|
||||
queryStringParamsDataTypeMap,
|
||||
) => {
|
||||
for (const [param, value] of Object.entries(queryStringParams)) {
|
||||
const expectedDataType = queryStringParamsDataTypeMap.get(param);
|
||||
if (!expectedDataType) {
|
||||
// Absence of data type means that the query string param is not supported.
|
||||
// Currently we allow addition of extra query string params.
|
||||
continue;
|
||||
}
|
||||
if (validateQueryStringParam(expectedDataType, param, value)) {
|
||||
continue;
|
||||
}
|
||||
throw new InvalidQueryStringParamsError(
|
||||
`Invalid query string param \`${param}\` value: ${value}`,
|
||||
getInvalidQueryStringParamsErrorSecondaryMessage(
|
||||
param,
|
||||
queryStringParamsDataTypeMap,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
+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",
|
||||
@@ -140,8 +141,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",
|
||||
@@ -270,8 +271,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",
|
||||
@@ -318,7 +319,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` value: asdf",
|
||||
`Expected: ${availableLocales.join(", ")}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -328,7 +332,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// @ts-check
|
||||
|
||||
import { expect, it, describe } from "@jest/globals";
|
||||
import {
|
||||
validateQueryStringParam,
|
||||
validateQueryStringParams,
|
||||
InvalidQueryStringParamsError,
|
||||
} from "../src/common/validate";
|
||||
import { themes } from "../themes/index.js";
|
||||
|
||||
/**
|
||||
* @typedef {import("../src/common/validate").DataTypes} DataTypes The data types.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Map<string, DataTypes>} The query string params data type map.
|
||||
*/
|
||||
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"],
|
||||
]);
|
||||
|
||||
describe("test query string param validation", () => {
|
||||
// Tests for `validateQueryStringParam` function.
|
||||
it("should validate a string", () => {
|
||||
expect(validateQueryStringParam("string", "username", "anuraghazra")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
it("should validate a boolean", () => {
|
||||
expect(validateQueryStringParam("boolean", "hide_title", "true")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(validateQueryStringParam("boolean", "hide_title", "false")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(validateQueryStringParam("boolean", "hide_title", "invalid")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it("should validate a number", () => {
|
||||
expect(validateQueryStringParam("number", "card_width", "300")).toBe(true);
|
||||
expect(validateQueryStringParam("number", "card_width", "300.000")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(validateQueryStringParam("number", "card_width", "invalid")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it("should validate an enum", () => {
|
||||
expect(validateQueryStringParam("enum", "theme", "dark")).toBe(true);
|
||||
expect(validateQueryStringParam("enum", "theme", "merko")).toBe(true);
|
||||
expect(validateQueryStringParam("enum", "theme", "invalid")).toBe(false);
|
||||
|
||||
expect(validateQueryStringParam("enum", "locale", "en")).toBe(true);
|
||||
expect(validateQueryStringParam("enum", "locale", "invalid")).toBe(false);
|
||||
|
||||
expect(validateQueryStringParam("enum", "number_format", "short")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(validateQueryStringParam("enum", "number_format", "invalid")).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
expect(validateQueryStringParam("enum", "rank_icon", "github")).toBe(true);
|
||||
expect(validateQueryStringParam("enum", "rank_icon", "invalid")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it("should validate an enum-array", () => {
|
||||
expect(
|
||||
validateQueryStringParam("enum-array", "hide", "stars,commits,prs"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateQueryStringParam(
|
||||
"enum-array",
|
||||
"show",
|
||||
"reviews,discussions_started",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
validateQueryStringParam(
|
||||
"enum-array",
|
||||
"hide",
|
||||
"stars,commits,prs,invalid",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(validateQueryStringParam("enum-array", "hide", "invalid")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
it("should validate an array", () => {
|
||||
expect(
|
||||
validateQueryStringParam("array", "exclude_repo", "repo1,repo2"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Tests for `validateQueryStringParams` function.
|
||||
it("should validate query string params", () => {
|
||||
expect(
|
||||
validateQueryStringParams(
|
||||
{
|
||||
username: "anuraghazra",
|
||||
hide: "stars,commits,prs",
|
||||
hide_title: "true",
|
||||
hide_border: "true",
|
||||
card_width: "300",
|
||||
hide_rank: "true",
|
||||
show_icons: "true",
|
||||
include_all_commits: "true",
|
||||
line_height: "25",
|
||||
title_color: "fff",
|
||||
ring_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
text_bold: "true",
|
||||
bg_color: "fff",
|
||||
theme: "dark",
|
||||
cache_seconds: "300",
|
||||
exclude_repo: "repo1,repo2",
|
||||
custom_title: "My Custom Title",
|
||||
locale: "en",
|
||||
disable_animations: "true",
|
||||
border_radius: "0",
|
||||
number_format: "long",
|
||||
border_color: "fff",
|
||||
rank_icon: "github",
|
||||
show: "reviews,discussions_started",
|
||||
},
|
||||
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
|
||||
),
|
||||
// This function returns void on successful run and throws an error on invalid query string params.
|
||||
).toBe(undefined);
|
||||
});
|
||||
it("should validate query string params with extra params", () => {
|
||||
expect(
|
||||
validateQueryStringParams(
|
||||
{
|
||||
username: "anuraghazra",
|
||||
hide: "stars,commits,prs",
|
||||
hide_title: "true",
|
||||
hide_border: "true",
|
||||
card_width: "300",
|
||||
hide_rank: "true",
|
||||
show_icons: "true",
|
||||
include_all_commits: "true",
|
||||
line_height: "25",
|
||||
title_color: "fff",
|
||||
ring_color: "fff",
|
||||
icon_color: "fff",
|
||||
text_color: "fff",
|
||||
text_bold: "true",
|
||||
bg_color: "fff",
|
||||
theme: "dark",
|
||||
cache_seconds: "300",
|
||||
exclude_repo: "repo1,repo2",
|
||||
custom_title: "My Custom Title",
|
||||
locale: "en",
|
||||
disable_animations: "true",
|
||||
border_radius: "0",
|
||||
number_format: "long",
|
||||
border_color: "fff",
|
||||
rank_icon: "github",
|
||||
show: "reviews,discussions_started",
|
||||
invalid: "invalid",
|
||||
},
|
||||
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
|
||||
),
|
||||
// This function returns void on successful run and throws an error on invalid query string params.
|
||||
).toBe(undefined);
|
||||
});
|
||||
it("should throw correct error on invalid boolean param", () => {
|
||||
expect(() =>
|
||||
validateQueryStringParams(
|
||||
{
|
||||
hide_title: "invalid",
|
||||
},
|
||||
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
|
||||
),
|
||||
).toThrow(
|
||||
new InvalidQueryStringParamsError(
|
||||
"Invalid query string param `hide_title` value: invalid",
|
||||
"Expected: true or false",
|
||||
),
|
||||
);
|
||||
});
|
||||
it("should throw correct error on invalid number param", () => {
|
||||
expect(() =>
|
||||
validateQueryStringParams(
|
||||
{
|
||||
card_width: "invalid",
|
||||
},
|
||||
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
|
||||
),
|
||||
).toThrow(
|
||||
new InvalidQueryStringParamsError(
|
||||
"Invalid query string param `card_width` value: invalid",
|
||||
"Expected: a number",
|
||||
),
|
||||
);
|
||||
});
|
||||
it("should throw correct error on invalid enum param", () => {
|
||||
expect(() =>
|
||||
validateQueryStringParams(
|
||||
{
|
||||
theme: "invalid",
|
||||
},
|
||||
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
|
||||
),
|
||||
).toThrow(
|
||||
new InvalidQueryStringParamsError(
|
||||
"Invalid query string param `theme` value: invalid",
|
||||
`Expected: ${Object.keys(themes).join(", ")}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
it("should throw correct error on invalid enum-array param", () => {
|
||||
expect(() =>
|
||||
validateQueryStringParams(
|
||||
{
|
||||
hide: "commits,prs,invalid",
|
||||
},
|
||||
QUERYSTRING_PARAMS_DATA_TYPE_MAP,
|
||||
),
|
||||
).toThrow(
|
||||
new InvalidQueryStringParamsError(
|
||||
"Invalid query string param `hide` value: commits,prs,invalid",
|
||||
"Expected: stars, commits, prs, issues, contribs",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user