Compare commits

...
5 changed files with 448 additions and 19 deletions
+32 -13
View File
@@ -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,
+22
View File
@@ -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);
}
+128
View File
@@ -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
View File
@@ -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,
);
+256
View File
@@ -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",
),
);
});
});