107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
base64ToBytes,
|
|
base64UrlToBytes,
|
|
bytesToBase64,
|
|
bytesToBase64Url,
|
|
bytesToHex,
|
|
convertLineEndings,
|
|
decodeText,
|
|
decodeUrlComponent,
|
|
encodeText,
|
|
encodeUrlComponent,
|
|
fromCodePoints,
|
|
hexToBytes,
|
|
inspectCodePoints,
|
|
inspectUrl,
|
|
normalizeUnicode,
|
|
parseFormEncoded,
|
|
segmentGraphemes,
|
|
stringifyFormEncoded,
|
|
transformCase,
|
|
} from "../../src/helpers";
|
|
|
|
describe("binary and text encoding", () => {
|
|
it("round-trips canonical Base64, Base64URL, hex, and text encodings", () => {
|
|
const bytes = encodeText("Hello, 🌍", "utf-8");
|
|
expect(bytesToBase64(bytes)).toBe("SGVsbG8sIPCfjI0=");
|
|
expect(decodeText(base64ToBytes("SGVsbG8sIPCfjI0="))).toBe("Hello, 🌍");
|
|
expect(bytesToBase64Url(new Uint8Array([251, 255]), false)).toBe("-_8");
|
|
expect(base64UrlToBytes("-_8")).toEqual(new Uint8Array([251, 255]));
|
|
expect(base64UrlToBytes("Zg==")).toEqual(new Uint8Array([102]));
|
|
expect(bytesToHex(bytes).startsWith("48656c6c6f")).toBe(true);
|
|
expect(hexToBytes("0x00 ff", { allowWhitespace: true })).toEqual(
|
|
new Uint8Array([0, 255]),
|
|
);
|
|
expect(decodeText(encodeText("Aé", "latin1"), "latin1")).toBe("Aé");
|
|
expect(decodeText(encodeText("A🌍", "utf-16le"), "utf-16le")).toBe("A🌍");
|
|
expect(decodeText(encodeText("A🌍", "utf-16be"), "utf-16be")).toBe("A🌍");
|
|
});
|
|
|
|
it("rejects ambiguous, malformed, and oversized encodings", () => {
|
|
expect(() => base64ToBytes("Zg")).toThrow(/padding/u);
|
|
expect(() => base64ToBytes("Zh==")).toThrow(/unused|canonical/u);
|
|
expect(() => base64UrlToBytes("+w")).toThrow(/Invalid/u);
|
|
expect(() => base64UrlToBytes("Zg=")).toThrow(/padding/u);
|
|
expect(() => base64ToBytes("Z g==")).toThrow(/whitespace/u);
|
|
expect(() => hexToBytes("abc")).toThrow(/pairs/u);
|
|
expect(() => hexToBytes("ffff", { maxOutputBytes: 1 })).toThrow(/limit/u);
|
|
expect(() => encodeText("€", "latin1")).toThrow(/Latin-1/u);
|
|
});
|
|
});
|
|
|
|
describe("URL, Unicode, case, and line helpers", () => {
|
|
it("handles URL components and repeated form fields without object coercion", () => {
|
|
expect(decodeUrlComponent(encodeUrlComponent("a b/✓"))).toBe("a b/✓");
|
|
const entries = parseFormEncoded("tag=one&tag=two+words&empty=");
|
|
expect(entries).toEqual([
|
|
["tag", "one"],
|
|
["tag", "two words"],
|
|
["empty", ""],
|
|
]);
|
|
expect(stringifyFormEncoded(entries)).toBe("tag=one&tag=two+words&empty=");
|
|
expect(
|
|
inspectUrl("../a?q=1#x", "https://example.test/base/"),
|
|
).toMatchObject({
|
|
href: "https://example.test/a?q=1#x",
|
|
protocol: "https:",
|
|
hostname: "example.test",
|
|
passwordPresent: false,
|
|
});
|
|
});
|
|
|
|
it("inspects scalar values and user-perceived graphemes", () => {
|
|
const points = inspectCodePoints("A🌍");
|
|
expect(points).toEqual([
|
|
{
|
|
character: "A",
|
|
codePoint: 65,
|
|
hex: "U+0041",
|
|
utf16Index: 0,
|
|
utf16Length: 1,
|
|
},
|
|
{
|
|
character: "🌍",
|
|
codePoint: 0x1f30d,
|
|
hex: "U+1F30D",
|
|
utf16Index: 1,
|
|
utf16Length: 2,
|
|
},
|
|
]);
|
|
expect(fromCodePoints(points.map((point) => point.codePoint))).toBe("A🌍");
|
|
expect(segmentGraphemes("👨👩👧👦é")).toHaveLength(2);
|
|
expect(normalizeUnicode("e\u0301", "NFC")).toBe("é");
|
|
expect(() => fromCodePoints([0xd800])).toThrow(/scalar/u);
|
|
expect(fromCodePoints(new Array(100_000).fill(0x61))).toHaveLength(100_000);
|
|
});
|
|
|
|
it("transforms case and line endings deterministically", () => {
|
|
expect(transformCase("XML http value", "camel")).toBe("xmlHttpValue");
|
|
expect(transformCase("hello-world", "pascal")).toBe("HelloWorld");
|
|
expect(transformCase("helloWorld", "snake")).toBe("hello_world");
|
|
expect(convertLineEndings("a\r\nb\rc\n", "lf")).toBe("a\nb\nc\n");
|
|
expect(convertLineEndings("a\nb", "crlf")).toBe("a\r\nb");
|
|
});
|
|
});
|