diff --git a/src/utils/string-utils.ts b/src/utils/string-utils.ts new file mode 100644 index 0000000..d98c103 --- /dev/null +++ b/src/utils/string-utils.ts @@ -0,0 +1,465 @@ +import { createHash, randomBytes } from "node:crypto"; +import { IGNORE_CASE_COMPARER, ORDINAL_COMPARER } from "@/utils/comparison"; + +/** + * Returns the input value converted to a string. + * + * If the input value is already a string, it is returned as-is. + * Otherwise, the output of `String()` is returned. + * + * @param s - The input value to convert to a string. + * + * @returns The input value as a string. + */ +export function asString(s: unknown): string { + return typeof s === "string" ? s : String(s); +} + +/** + * A regular expression that matches a string consisting of a single letter character. + */ +export const IS_LETTER_REGEX = /^\p{L}$/u; + +/** + * Checks if the provided string is a single letter. + * + * @param s - The string to check. + * + * @returns `true` if the string is a single letter; otherwise, `false`. + */ +export function isLetter(s: string): boolean { + return s?.length === 1 && IS_LETTER_REGEX.test(s); +} + +/** + * A regular expression that matches a string consisting of a single digit character. + */ +export const IS_DIGIT_REGEX = /^\d$/; + +/** + * Checks if the provided string is a single digit. + * + * @param s - The string to check. + * + * @returns `true` if the string is a single digit; otherwise, `false`. + */ +export function isDigit(s: string): boolean { + return s?.length === 1 && s >= "0" && s <= "9"; +} + +/** + * A regular expression that matches a string consisting of a single letter or digit character. + */ +export const IS_LETTER_OR_DIGIT_REGEX = /^(?:\p{L}|\d)$/u; + +/** + * Checks if the provided string is a single letter or digit. + * + * @param s - The string to check. + * + * @returns `true` if the string is a single letter or digit; otherwise, `false`. + */ +export function isLetterOrDigit(s: string): boolean { + return s?.length === 1 && IS_LETTER_OR_DIGIT_REGEX.test(s); +} + +/** + * A regular expression that matches strings containing only uppercase characters + * and not containing any lowercase Unicode characters. + */ +export const IS_UPPER_CASE_REGEX = /^[^\p{Ll}]*$/u; + +/** + * Checks if a string contains only uppercase characters. + * + * @param s - The string to check. + * + * @returns `true` if the input string contains only uppercase characters; otherwise, `false`. + */ +export function isUpperCase(s: string): boolean { + return IS_UPPER_CASE_REGEX.test(s); +} + +/** + * A regular expression that matches strings containing only lowercase characters + * and not containing any uppercase Unicode characters. + */ +export const IS_LOWER_CASE_REGEX = /^[^\p{Lu}]*$/u; + +/** + * Checks if a string contains only lowercase characters. + * + * @param s - The string to check. + * + * @returns `true` if the input string contains only lowercase characters; otherwise, `false`. + */ +export function isLowerCase(s: string): boolean { + return IS_LOWER_CASE_REGEX.test(s); +} + +/** + * Checks if a given string represents a valid number. + * + * @param s - The string to be checked. + * + * @returns `true` if the string represents a valid number; otherwise, `false`. + */ +export function isNumberString(s: string): boolean { + return String(+s) === s; +} + +/** + * Checks if a given string represents a valid integer number. + * + * @param s - The string to be checked. + * + * @returns `true` if the string represents a valid integer number; otherwise, `false`. + */ +export function isIntegerString(s: string): boolean { + return String(parseInt(s)) === s; +} + +/** + * Options for comparing strings. + */ +export interface StringComparisonOptions { + /** + * Indicates whether the comparison should ignore the case of the strings being compared. + * If `ignoreCase` is `true`, the comparison will use lexicographical sort rules while + * ignoring the case of the strings being compared. + */ + ignoreCase?: boolean; +} + +/** + * Compares two strings lexicographically and returns a value indicating whether one string is less than, equal to, or greater than the other. + * + * @param left - The first string to compare. + * @param right - The second string to compare. + * @param options - Options for comparing strings. + * + * @returns A value indicating the comparison result: + * + * - A value less than 0 indicates that `left` is less than `right`. + * - 0 indicates that `left` is equal to `right`. + * - A value greater than 0 indicates that `left` is greater than `right`. + */ +export function stringCompare(left: string, right: string, options?: StringComparisonOptions): number { + const comparer = options?.ignoreCase ? IGNORE_CASE_COMPARER : ORDINAL_COMPARER; + return comparer.compare(left, right); +} + +/** + * Compares two strings. + * + * @param left - The first string to compare. + * @param right - The second string to compare. + * @param options - Options for comparing strings. + * + * @returns `true` if the strings are equal; otherwise, `false`. + */ +export function stringEquals(left: string, right: string, options?: StringComparisonOptions): boolean { + return stringCompare(left, right, options) === 0; +} + +/** + * Capitalizes the first letter of a string. + * + * @param s - The string to capitalize. + * + * @returns The capitalized string. + */ +export function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +/** + * Converts the first character of a string to lowercase. + * + * @param s - The input string. + * + * @returns The input string with the first character converted to lowercase. + */ +export function uncapitalize(s: string): string { + return s.charAt(0).toLowerCase() + s.slice(1); +} + +/** + * Converts a string to PascalCase. + * + * This function can handle input strings in the following formats: + * - PascalCase + * - camelCase + * - kebab-case + * - snake_case + * - SCREAMING_SNAKE_CASE + * + * @param s - The input string to be converted to PascalCase. + * + * @returns The input string converted to PascalCase. + */ +export function toPascalCase(s: string): string { + // Convert input to lowercase if the entire string is in uppercase (SCREAMING_SNAKE_CASE) + if (isUpperCase(s)) { + s = s.toLowerCase(); + } + + return s + // Replace any character following a non-word character (such as - or _) with its uppercase counterpart + .replace(/(?:^|[\s_-])(\w)/g, (_, char) => char.toUpperCase()) + // Remove any non-word characters (such as - or _) from the result + .replace(/[\s_-]/g, ""); +} + +/** + * Specifies how the results should be transformed when splitting a string into substrings. + */ +export interface StringSplitOptions { + /** + * Remove empty (zero-length) substrings from the result. + */ + removeEmptyEntries?: boolean; + + /** + * Trim whitespace from each substring in the result. + */ + trimEntries?: boolean; +} + +/** + * Splits a string into an array of substrings based on a separator. + * + * @param s - The input string to split. + * @param separator - The separator to split the string by. + * @param options - Options for splitting the string. + * + * @returns An array of substrings from the original string. + */ +export function split(s: string, separator: string, options?: StringSplitOptions): string[]; + +/** + * Splits a string into an array of substrings based on the given separators. + * + * @param s - The input string to split. + * @param separators - The array of separators to split the string by. + * @param options - Options for splitting the string. + * + * @returns An array of substrings from the original string. + */ +export function split(s: string, separators: readonly string[], options?: StringSplitOptions): string[]; + +/** + * Returns an array of substrings that were delimited by strings in the original input that + * match against the `splitter` regular expression. + * + * @param s - The input string to split. + * @param splitter - The regular expression to split the string by. + * @param options - Options for splitting the string. + * + * @returns An array of substrings from the original string. + */ +export function split(s: string, splitter: RegExp, options?: StringSplitOptions): string[]; + +/** + * Splits a string into an array of substrings based on a separator. + * + * @param s - The string to split. + * @param separator - The separator to split the string by. Can be a string, a regular expression, or an array of strings. + * @param options - Options for splitting the string. + * + * @returns An array of substrings from the original string. + */ +export function split(s: string, separator: string | RegExp | readonly string[], options?: StringSplitOptions): string[] { + if (!s) { + return []; + } + + if (Array.isArray(separator)) { + return splitByArrayOfStrings(s, separator, options); + } + return splitByStringOrRegex(s, separator as string | RegExp, options); +} + +/** + * Split a string into substrings using the specified separator and return them as an array. + * + * @param s - The string to split. + * @param separator - The separator to split the string by. Can be a string, or a regular expression. + * @param options - Options for splitting the string. + * + * @returns An array of substrings from the original string. + */ +function splitByStringOrRegex(s: string, separator: string | RegExp, options?: StringSplitOptions): string[] { + const trimEntries = options?.trimEntries ?? false; + const removeEmptyEntries = options?.removeEmptyEntries ?? false; + const parts = s.split(separator); + + if (trimEntries) { + for (let i = 0; i < parts.length; ++i) { + parts[i] = parts[i].trim(); + } + } + return removeEmptyEntries ? parts.filter(x => x) : parts; +} + +/** + * Splits a string into an array of substrings based on the given separators. + * + * @param s - The input string to split. + * @param separators - The array of separators to split the string by. + * @param options - Options for splitting the string. + * + * @returns An array of substrings from the original string. + */ +function splitByArrayOfStrings(s: string, separators: readonly string[], options?: StringSplitOptions): string[] { + const trimEntries = options?.trimEntries ?? false; + const removeEmptyEntries = options?.removeEmptyEntries ?? false; + const splitted = []; + + let previousIndex = -1; + for (let i = 0; i < s.length; ++i) { + if (!separators.includes(s.charAt(i))) { + continue; + } + + let part = s.substring(previousIndex + 1, i); + previousIndex = i; + + if (trimEntries) { + part = part.trim(); + } + + if (part || !removeEmptyEntries) { + splitted.push(part); + } + } + + let lastPart = s.substring(previousIndex + 1); + + if (trimEntries) { + lastPart = lastPart.trim(); + } + + if (lastPart || !removeEmptyEntries) { + splitted.push(lastPart); + } + + return splitted; +} + +/** + * Represents options for splitting a string into multiple lines. + */ +export interface StringSplitLineOptions { + /** + * The maximum length of each line. + */ + maxLength?: number; +} + +/** + * Splits a string into an array of lines. + * + * @param text - The input string to split. + * @param options - An optional object that specifies the options for splitting the string, including the maximum line length. + * + * @returns An array of strings, where each string represents a line from the input string. If the `maxLength` option is specified, the lines will be truncated at the specified length. + */ +export function splitLines(text: string, options?: StringSplitLineOptions): string[] { + const maxLength = options?.maxLength || 0; + + const lines = text.split(/\r?\n/); + if (maxLength <= 0) { + return lines; + } + + const shortenedLines = lines.flatMap(line => { + if (line.length <= maxLength) { + return line; + } + + const words = line.split(" "); + const linesFromCurrentLine = [] as string[]; + let currentLine = ""; + + for (const word of words) { + if (currentLine.length + word.length <= maxLength) { + currentLine += (currentLine ? " " : "") + word; + } else { + linesFromCurrentLine.push(currentLine); + currentLine = word; + } + } + + linesFromCurrentLine.push(currentLine); + return linesFromCurrentLine; + }); + + return shortenedLines; +} + +/** + * Represents options for padding a string with spaces or a specific fill character. + */ +export interface StringPadOptions { + /** + * The fill character to use when padding the string. If not specified, spaces will be used. + */ + fillString?: string; + + /** + * The alignment of the padded string within the specified maximum length. + */ + align?: "left" | "center" | "right"; +} + +/** + * Pads a string with spaces or a specific fill character to the specified maximum length. + * + * @param s - The input string to pad. + * @param maxLength - The maximum length of the padded string. + * @param options - An optional object that specifies the options for padding the string. + * + * @returns A string that represents the padded input string according to the specified options. + */ +export function pad(s: string, maxLength: number, options?: StringPadOptions): string { + s ||= ""; + + switch (options?.align) { + case "left": + return s.padEnd(maxLength, options?.fillString); + case "right": + return s.padStart(maxLength, options?.fillString); + default: + const availableLength = maxLength - s.length; + if (availableLength <= 0) { + return s; + } + const padStartLength = (availableLength >> 1) + s.length; + return s.padStart(padStartLength, options?.fillString).padEnd(maxLength, options?.fillString); + } +} + +/** + * Generates a secure random string of a specified length. + * + * @param length - The desired length of the generated string. + * + * @returns The secure random string in hexadecimal format. + */ +export function generateSecureRandomString(length: number): string { + const bytes = randomBytes(Math.ceil(length / 2)); + return bytes.toString("hex").slice(0, length); +} + +/** + * Hashes a string using the specified algorithm. + * + * @param input - The string to be hashed. + * @param algorithm - The hashing algorithm to use (default: "sha256"). + * + * @returns The hashed string in hexadecimal format. + */ +export function hashString(input: string, algorithm: string = "sha256"): string { + return createHash(algorithm).update(input).digest("hex"); +} diff --git a/tests/unit/utils/string-utils.spec.ts b/tests/unit/utils/string-utils.spec.ts new file mode 100644 index 0000000..b3e6f22 --- /dev/null +++ b/tests/unit/utils/string-utils.spec.ts @@ -0,0 +1,446 @@ +import { + asString, + isLetter, + IS_LETTER_REGEX, + isDigit, + IS_DIGIT_REGEX, + isLetterOrDigit, + IS_LETTER_OR_DIGIT_REGEX, + isUpperCase, + IS_UPPER_CASE_REGEX, + isLowerCase, + IS_LOWER_CASE_REGEX, + isNumberString, + isIntegerString, + stringCompare, + stringEquals, + capitalize, + uncapitalize, + toPascalCase, + split, + splitLines, + pad, + generateSecureRandomString, + hashString, +} from "@/utils/string-utils"; +import { createHash } from "node:crypto"; + +describe("asString", () => { + test("returns string as is", () => { + expect(asString("test")).toBe("test"); + }); + + test("converts non-string value to string", () => { + expect(asString(123)).toBe("123"); + }); + + test("converts undefined value to string", () => { + expect(asString(undefined)).toBe("undefined"); + }); + + test("converts null value to string", () => { + expect(asString(null)).toBe("null"); + }); +}); + +describe("isLetter", () => { + test("returns true for a single letter", () => { + expect(isLetter("a")).toBe(true); + }); + + test("returns false for a string of multiple letters", () => { + expect(isLetter("abc")).toBe(false); + }); + + test("returns false for a digit", () => { + expect(isLetter("1")).toBe(false); + }); + + test("returns false for an empty string", () => { + expect(isLetter("")).toBe(false); + }); +}); + +describe("IS_LETTER_REGEX", () => { + test("matches a single letter", () => { + expect(IS_LETTER_REGEX.test("a")).toBe(true); + }); + + test("does not match a string of multiple letters", () => { + expect(IS_LETTER_REGEX.test("abc")).toBe(false); + }); + + test("does not match a digit", () => { + expect(IS_LETTER_REGEX.test("1")).toBe(false); + }); + + test("does not match an empty string", () => { + expect(IS_LETTER_REGEX.test("")).toBe(false); + }); +}); + +describe("isDigit", () => { + test("returns true for a single digit", () => { + expect(isDigit("1")).toBe(true); + }); + + test("returns false for a string of multiple digits", () => { + expect(isDigit("123")).toBe(false); + }); + + test("returns false for a letter", () => { + expect(isDigit("a")).toBe(false); + }); + + test("returns false for an empty string", () => { + expect(isDigit("")).toBe(false); + }); +}); + +describe("IS_DIGIT_REGEX", () => { + test("matches a single digit", () => { + expect(IS_DIGIT_REGEX.test("1")).toBe(true); + }); + + test("does not match a string of multiple digits", () => { + expect(IS_DIGIT_REGEX.test("123")).toBe(false); + }); + + test("does not match a letter", () => { + expect(IS_DIGIT_REGEX.test("a")).toBe(false); + }); + + test("does not match an empty string", () => { + expect(IS_DIGIT_REGEX.test("")).toBe(false); + }); +}); + +describe("isLetterOrDigit", () => { + test("returns true for a single letter", () => { + expect(isLetterOrDigit("a")).toBe(true); + }); + + test("returns true for a single digit", () => { + expect(isLetterOrDigit("1")).toBe(true); + }); + + test("returns false for a string of multiple letters or digits", () => { + expect(isLetterOrDigit("123")).toBe(false); + expect(isLetterOrDigit("abc")).toBe(false); + }); + + test("returns false for a non-letter and non-digit character", () => { + expect(isLetterOrDigit("@")).toBe(false); + }); + + test("returns false for an empty string", () => { + expect(isLetterOrDigit("")).toBe(false); + }); +}); + +describe("IS_LETTER_OR_DIGIT_REGEX", () => { + test("matches a single letter", () => { + expect(IS_LETTER_OR_DIGIT_REGEX.test("a")).toBe(true); + }); + + test("matches a single digit", () => { + expect(IS_LETTER_OR_DIGIT_REGEX.test("1")).toBe(true); + }); + + test("does not match a string of multiple letters or digits", () => { + expect(IS_LETTER_OR_DIGIT_REGEX.test("123")).toBe(false); + expect(IS_LETTER_OR_DIGIT_REGEX.test("abc")).toBe(false); + }); + + test("does not match a non-letter and non-digit character", () => { + expect(IS_LETTER_OR_DIGIT_REGEX.test("@")).toBe(false); + }); + + test("does not match an empty string", () => { + expect(IS_LETTER_OR_DIGIT_REGEX.test("")).toBe(false); + }); +}); + +describe("isUpperCase", () => { + test("returns true for a string of uppercase letters", () => { + expect(isUpperCase("ABC")).toBe(true); + }); + + test("returns true for a string of uppercase letters and non-letter characters", () => { + expect(isUpperCase("HELLO WORLD! 42 IS THE ANSWER.")).toBe(true); + }); + + test("returns true for an empty string", () => { + expect(isUpperCase("")).toBe(true); + }); + + test("returns false for a string of lowercase letters", () => { + expect(isUpperCase("abc")).toBe(false); + }); + + test("returns false for a mixed-case string", () => { + expect(isUpperCase("AbC")).toBe(false); + }); +}); + +describe("IS_UPPER_CASE_REGEX", () => { + test("matches a string of uppercase letters", () => { + expect(IS_UPPER_CASE_REGEX.test("ABC")).toBe(true); + }); + + test("matches a string of uppercase letters and non-letter characters", () => { + expect(IS_UPPER_CASE_REGEX.test("HELLO WORLD! 42 IS THE ANSWER.")).toBe(true); + }); + + test("matches an empty string", () => { + expect(IS_UPPER_CASE_REGEX.test("")).toBe(true); + }); + + test("does not match a string of lowercase letters", () => { + expect(IS_UPPER_CASE_REGEX.test("abc")).toBe(false); + }); + + test("does not match a mixed-case string", () => { + expect(IS_UPPER_CASE_REGEX.test("AbC")).toBe(false); + }); +}); + +describe("isLowerCase", () => { + test("returns true for a string of lowercase letters", () => { + expect(isLowerCase("abc")).toBe(true); + }); + + test("returns true for a string of lowercase letters and non-letter characters", () => { + expect(isLowerCase("hello world! 42 is the answer.")).toBe(true); + }); + + test("returns true for an empty string", () => { + expect(isLowerCase("")).toBe(true); + }); + + test("returns false for a string of uppercase letters", () => { + expect(isLowerCase("ABC")).toBe(false); + }); + + test("returns false for a mixed-case string", () => { + expect(isLowerCase("AbC")).toBe(false); + }); +}); + +describe("IS_LOWER_CASE_REGEX", () => { + test("matches a string of lowercase letters", () => { + expect(IS_LOWER_CASE_REGEX.test("abc")).toBe(true); + }); + + test("matches a string of lowercase letters and non-letter characters", () => { + expect(IS_LOWER_CASE_REGEX.test("hello world! 42 is the answer.")).toBe(true); + }); + + test("matches an empty string", () => { + expect(IS_LOWER_CASE_REGEX.test("")).toBe(true); + }); + + test("does not match a string of uppercase letters", () => { + expect(IS_LOWER_CASE_REGEX.test("ABC")).toBe(false); + }); + + test("does not match a mixed-case string", () => { + expect(IS_LOWER_CASE_REGEX.test("AbC")).toBe(false); + }); +}); + +describe("isNumberString", () => { + test("returns true for a valid number string", () => { + expect(isNumberString("123")).toBe(true); + expect(isNumberString("0.5")).toBe(true); + }); + + test("returns false for a non-number string", () => { + expect(isNumberString("abc")).toBe(false); + }); +}); + +describe("isIntegerString", () => { + test("returns true for a valid integer string", () => { + expect(isIntegerString("123")).toBe(true); + }); + + test("returns false for a non-number string", () => { + expect(isIntegerString("abc")).toBe(false); + }); + + test("returns false for a non-integer string", () => { + expect(isIntegerString("0.5")).toBe(false); + }); +}); + +describe("stringCompare", () => { + test("compares two strings", () => { + expect(stringCompare("abc", "def")).toBeLessThan(0); + expect(stringCompare("def", "abc")).toBeGreaterThan(0); + expect(stringCompare("abc", "abc")).toBe(0); + }); + + test("compares two strings ignoring case", () => { + expect(stringCompare("abc", "DEF", { ignoreCase: true })).toBeLessThan(0); + expect(stringCompare("ABC", "def", { ignoreCase: true })).toBeLessThan(0); + expect(stringCompare("DEF", "abc", { ignoreCase: true })).toBeGreaterThan(0); + expect(stringCompare("def", "ABC", { ignoreCase: true })).toBeGreaterThan(0); + expect(stringCompare("abc", "ABC", { ignoreCase: true })).toBe(0); + expect(stringCompare("ABC", "abc", { ignoreCase: true })).toBe(0); + }); +}); + +describe("stringEquals", () => { + test("compares two strings for equality", () => { + expect(stringEquals("abc", "abc")).toBe(true); + expect(stringEquals("abc", "def")).toBe(false); + }); + + test("compares two strings for equality ignoring case", () => { + expect(stringEquals("abc", "ABC", { ignoreCase: true })).toBe(true); + expect(stringEquals("ABC", "abc", { ignoreCase: true })).toBe(true); + }); +}); + +describe("capitalize", () => { + test("capitalizes the first letter of a string", () => { + expect(capitalize("abc")).toBe("Abc"); + }); + + test("leaves the string as is if the first letter is already capitalized", () => { + expect(capitalize("Abc")).toBe("Abc"); + }); +}); + +describe("uncapitalize", () => { + test("uncapitalizes the first letter of a string", () => { + expect(uncapitalize("Abc")).toBe("abc"); + }); + + test("leave the string as is if the first letter is already uncapitalized", () => { + expect(uncapitalize("abc")).toBe("abc"); + }); +}); + +describe("toPascalCase", () => { + test("converts strings to PascalCase", () => { + expect(toPascalCase("PascalCase")).toBe("PascalCase"); + expect(toPascalCase("camelCase")).toBe("CamelCase"); + expect(toPascalCase("kebab-case")).toBe("KebabCase"); + expect(toPascalCase("snake_case")).toBe("SnakeCase"); + expect(toPascalCase("SCREAMING_SNAKE_CASE")).toBe("ScreamingSnakeCase"); + }); +}); + +describe("split", () => { + describe("with string separator", () => { + test("splits a string by a given separator", () => { + expect(split("a,b,c", ",")).toEqual(["a", "b", "c"]); + }); + + test("removes empty entries if specified", () => { + expect(split("a,,b,,c", ",", { removeEmptyEntries: true })).toEqual(["a", "b", "c"]); + }); + + test("trims entries if specified", () => { + expect(split(" a , b , c ", ",", { trimEntries: true })).toEqual(["a", "b", "c"]); + }); + }); + + describe("with array of string separators", () => { + test("splits a string by a given separators", () => { + expect(split("a,b|c", [",", "|"])).toEqual(["a", "b", "c"]); + }); + + test("removes empty entries if specified", () => { + expect(split("a|,b,|c", [",", "|"], { removeEmptyEntries: true })).toEqual(["a", "b", "c"]); + }); + + test("trims entries if specified", () => { + expect(split(" a , b | c ", [",", "|"], { trimEntries: true })).toEqual(["a", "b", "c"]); + }); + }); + + describe("with regex separator", () => { + test("splits a string by a given separator", () => { + expect(split("a1b2c", /\d/)).toEqual(["a", "b", "c"]); + }); + + test("removes empty entries if specified", () => { + expect(split("a12b34c5", /\d/, { removeEmptyEntries: true })).toEqual(["a", "b", "c"]); + }); + + test("trims entries if specified", () => { + expect(split(" a 1 b 2 c ", /\d/, { trimEntries: true })).toEqual(["a", "b", "c"]); + }); + }); +}); + +describe("splitLines", () => { + test("splits a string into lines", () => { + expect(splitLines("a\nb\nc")).toEqual(["a", "b", "c"]); + expect(splitLines("a\r\nb\r\nc")).toEqual(["a", "b", "c"]); + }); + + test("splits a string into lines of a specified maximum length", () => { + expect(splitLines("abcd efg\nhijk lmn\nopq", { maxLength: 5 })).toEqual(["abcd", "efg", "hijk", "lmn", "opq"]); + }); +}); + +describe("pad", () => { + test("pads a string to a given length", () => { + expect(pad("abc", 5)).toBe(" abc "); + }); + + test("pads a string with the given fill character", () => { + expect(pad("abc", 5, { fillString: "*" })).toBe("*abc*"); + }); + + test("pads a string to the left", () => { + expect(pad("abc", 5, { align: "left" })).toBe("abc "); + }); + + test("pads a string to the right", () => { + expect(pad("abc", 5, { align: "right" })).toBe(" abc"); + }); + + test("pads a string to the center", () => { + expect(pad("abc", 5, { align: "center" })).toBe(" abc "); + }); +}); + +describe("generateSecureRandomString", () => { + test("generates a random string of the specified length", () => { + expect(generateSecureRandomString(1)).toHaveLength(1); + expect(generateSecureRandomString(2)).toHaveLength(2); + expect(generateSecureRandomString(10)).toHaveLength(10); + expect(generateSecureRandomString(11)).toHaveLength(11); + }); + + test("returns an empty string if called with zero", () => { + expect(generateSecureRandomString(0)).toBe(""); + }); + + test("generates different strings for different calls", () => { + expect(generateSecureRandomString(10)).not.toBe(generateSecureRandomString(10)); + }); +}); + +describe("hashString", () => { + test("hashes a string using the 'sha256' algorithm", () => { + const hashed = hashString("hello", "sha256"); + + expect(hashed).toBe(createHash("sha256").update("hello").digest("hex")); + }); + + test("hashes a string using the 'md5' algorithm", () => { + const hashed = hashString("hello", "md5"); + + expect(hashed).toBe(createHash("md5").update("hello").digest("hex")); + }); + + test("throws an error for an unknown algorithm", () => { + expect(() => hashString("hello", "unknown")).toThrow(); + }); +});