354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
import { describe, it } from "@effect/vitest"
|
|
import { deepStrictEqual, strictEqual } from "@effect/vitest/utils"
|
|
import { escapeToken, unescapeToken } from "effect/JsonPointer"
|
|
|
|
describe("JsonPointer", () => {
|
|
describe("escapeToken", () => {
|
|
describe("basic escaping", () => {
|
|
it("escapes tilde (~) to ~0", () => {
|
|
deepStrictEqual(escapeToken("a~b"), "a~0b")
|
|
deepStrictEqual(escapeToken("~"), "~0")
|
|
deepStrictEqual(escapeToken("~~"), "~0~0")
|
|
})
|
|
|
|
it("escapes forward slash (/) to ~1", () => {
|
|
deepStrictEqual(escapeToken("a/b"), "a~1b")
|
|
deepStrictEqual(escapeToken("/"), "~1")
|
|
deepStrictEqual(escapeToken("//"), "~1~1")
|
|
})
|
|
|
|
it("escapes both tilde and slash in same string", () => {
|
|
deepStrictEqual(escapeToken("a~b/c"), "a~0b~1c")
|
|
deepStrictEqual(escapeToken("path/to~key"), "path~1to~0key")
|
|
deepStrictEqual(escapeToken("~a/b~c/d~"), "~0a~1b~0c~1d~0")
|
|
})
|
|
})
|
|
|
|
describe("order of operations", () => {
|
|
it("replaces ~ before / to prevent double-escaping", () => {
|
|
const token = "a~b/c"
|
|
const result = escapeToken(token)
|
|
deepStrictEqual(result, "a~0b~1c")
|
|
deepStrictEqual(result.includes("~0~1"), false)
|
|
deepStrictEqual(result.includes("~1~0"), false)
|
|
})
|
|
|
|
it("handles ~ followed by / correctly", () => {
|
|
deepStrictEqual(escapeToken("a~/b"), "a~0~1b")
|
|
})
|
|
|
|
it("handles / followed by ~ correctly", () => {
|
|
deepStrictEqual(escapeToken("a/~b"), "a~1~0b")
|
|
})
|
|
|
|
it("handles consecutive special characters", () => {
|
|
deepStrictEqual(escapeToken("~~"), "~0~0")
|
|
deepStrictEqual(escapeToken("//"), "~1~1")
|
|
deepStrictEqual(escapeToken("~/"), "~0~1")
|
|
deepStrictEqual(escapeToken("/~"), "~1~0")
|
|
})
|
|
})
|
|
|
|
describe("edge cases", () => {
|
|
it("returns empty string unchanged", () => {
|
|
deepStrictEqual(escapeToken(""), "")
|
|
strictEqual(escapeToken(""), "")
|
|
})
|
|
|
|
it("returns input unchanged when no special characters", () => {
|
|
deepStrictEqual(escapeToken("abc"), "abc")
|
|
deepStrictEqual(escapeToken("hello world"), "hello world")
|
|
deepStrictEqual(escapeToken("123"), "123")
|
|
})
|
|
|
|
it("handles strings with only tildes", () => {
|
|
deepStrictEqual(escapeToken("~~~"), "~0~0~0")
|
|
deepStrictEqual(escapeToken("a~b~c"), "a~0b~0c")
|
|
})
|
|
|
|
it("handles strings with only slashes", () => {
|
|
deepStrictEqual(escapeToken("///"), "~1~1~1")
|
|
deepStrictEqual(escapeToken("a/b/c"), "a~1b~1c")
|
|
})
|
|
|
|
it("handles multiple occurrences of same character", () => {
|
|
deepStrictEqual(escapeToken("a~b~c~d"), "a~0b~0c~0d")
|
|
deepStrictEqual(escapeToken("a/b/c/d"), "a~1b~1c~1d")
|
|
})
|
|
|
|
it("handles special sequences that could be confused", () => {
|
|
deepStrictEqual(escapeToken("~01"), "~001")
|
|
deepStrictEqual(escapeToken("~10"), "~010")
|
|
deepStrictEqual(escapeToken("~00"), "~000")
|
|
deepStrictEqual(escapeToken("~11"), "~011")
|
|
})
|
|
})
|
|
|
|
describe("unicode and other characters", () => {
|
|
it("passes through unicode characters unchanged", () => {
|
|
deepStrictEqual(escapeToken("héllo"), "héllo")
|
|
deepStrictEqual(escapeToken("世界"), "世界")
|
|
deepStrictEqual(escapeToken("🚀"), "🚀")
|
|
})
|
|
|
|
it("passes through other special characters unchanged", () => {
|
|
deepStrictEqual(escapeToken("a.b"), "a.b")
|
|
deepStrictEqual(escapeToken("a-b"), "a-b")
|
|
deepStrictEqual(escapeToken("a_b"), "a_b")
|
|
deepStrictEqual(escapeToken("a@b"), "a@b")
|
|
deepStrictEqual(escapeToken("a#b"), "a#b")
|
|
deepStrictEqual(escapeToken("a$b"), "a$b")
|
|
deepStrictEqual(escapeToken("a%b"), "a%b")
|
|
deepStrictEqual(escapeToken("a&b"), "a&b")
|
|
deepStrictEqual(escapeToken("a*b"), "a*b")
|
|
deepStrictEqual(escapeToken("a+b"), "a+b")
|
|
deepStrictEqual(escapeToken("a=b"), "a=b")
|
|
deepStrictEqual(escapeToken("a?b"), "a?b")
|
|
deepStrictEqual(escapeToken("a!b"), "a!b")
|
|
})
|
|
|
|
it("handles mixed unicode and special characters", () => {
|
|
deepStrictEqual(escapeToken("héllo~world/test"), "héllo~0world~1test")
|
|
deepStrictEqual(escapeToken("世界/🌍~key"), "世界~1🌍~0key")
|
|
})
|
|
})
|
|
|
|
describe("immutability", () => {
|
|
it("does not mutate input string", () => {
|
|
const original = "a~b/c"
|
|
const originalCopy = "a~b/c"
|
|
escapeToken(original)
|
|
deepStrictEqual(original, originalCopy)
|
|
strictEqual(original, originalCopy)
|
|
})
|
|
|
|
it("returns a different token value when escaping changes the input", () => {
|
|
const input = "a~b"
|
|
const result = escapeToken(input)
|
|
strictEqual(result === input, false)
|
|
deepStrictEqual(result, "a~0b")
|
|
})
|
|
})
|
|
|
|
describe("RFC 6901 examples", () => {
|
|
it("handles examples from documentation", () => {
|
|
deepStrictEqual(escapeToken("name/alias"), "name~1alias")
|
|
deepStrictEqual(escapeToken("path/to~key"), "path~1to~0key")
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("unescapeToken", () => {
|
|
describe("basic unescaping", () => {
|
|
it("unescapes ~0 to ~", () => {
|
|
deepStrictEqual(unescapeToken("a~0b"), "a~b")
|
|
deepStrictEqual(unescapeToken("~0"), "~")
|
|
deepStrictEqual(unescapeToken("~0~0"), "~~")
|
|
})
|
|
|
|
it("unescapes ~1 to /", () => {
|
|
deepStrictEqual(unescapeToken("a~1b"), "a/b")
|
|
deepStrictEqual(unescapeToken("~1"), "/")
|
|
deepStrictEqual(unescapeToken("~1~1"), "//")
|
|
})
|
|
|
|
it("unescapes both ~0 and ~1 in same string", () => {
|
|
deepStrictEqual(unescapeToken("a~0b~1c"), "a~b/c")
|
|
deepStrictEqual(unescapeToken("path~1to~0key"), "path/to~key")
|
|
deepStrictEqual(unescapeToken("~0a~1b~0c~1d~0"), "~a/b~c/d~")
|
|
})
|
|
})
|
|
|
|
describe("order of operations", () => {
|
|
it("replaces ~1 before ~0 to prevent incorrect decoding", () => {
|
|
const token = "a~1b~0c"
|
|
const result = unescapeToken(token)
|
|
deepStrictEqual(result, "a/b~c")
|
|
})
|
|
|
|
it("handles ~01 sequence correctly (unescapes to ~1)", () => {
|
|
deepStrictEqual(unescapeToken("~01"), "~1")
|
|
})
|
|
|
|
it("handles ~10 sequence correctly (unescapes to /0)", () => {
|
|
deepStrictEqual(unescapeToken("~10"), "/0")
|
|
})
|
|
|
|
it("handles ~00 sequence correctly (unescapes to ~0)", () => {
|
|
deepStrictEqual(unescapeToken("~00"), "~0")
|
|
})
|
|
|
|
it("handles ~11 sequence correctly (unescapes to /1)", () => {
|
|
deepStrictEqual(unescapeToken("~11"), "/1")
|
|
})
|
|
})
|
|
|
|
describe("edge cases", () => {
|
|
it("returns empty string unchanged", () => {
|
|
deepStrictEqual(unescapeToken(""), "")
|
|
strictEqual(unescapeToken(""), "")
|
|
})
|
|
|
|
it("returns input unchanged when no escaped sequences", () => {
|
|
deepStrictEqual(unescapeToken("abc"), "abc")
|
|
deepStrictEqual(unescapeToken("hello world"), "hello world")
|
|
deepStrictEqual(unescapeToken("123"), "123")
|
|
})
|
|
|
|
it("handles strings with only ~0 sequences", () => {
|
|
deepStrictEqual(unescapeToken("~0~0~0"), "~~~")
|
|
deepStrictEqual(unescapeToken("a~0b~0c"), "a~b~c")
|
|
})
|
|
|
|
it("handles strings with only ~1 sequences", () => {
|
|
deepStrictEqual(unescapeToken("~1~1~1"), "///")
|
|
deepStrictEqual(unescapeToken("a~1b~1c"), "a/b/c")
|
|
})
|
|
|
|
it("handles multiple occurrences of same sequence", () => {
|
|
deepStrictEqual(unescapeToken("a~0b~0c~0d"), "a~b~c~d")
|
|
deepStrictEqual(unescapeToken("a~1b~1c~1d"), "a/b/c/d")
|
|
})
|
|
|
|
it("decodes escaped sequences at token boundaries and suffixes", () => {
|
|
deepStrictEqual(unescapeToken("~0"), "~")
|
|
deepStrictEqual(unescapeToken("~1"), "/")
|
|
deepStrictEqual(unescapeToken("a~0"), "a~")
|
|
deepStrictEqual(unescapeToken("a~1"), "a/")
|
|
})
|
|
})
|
|
|
|
describe("unicode and other characters", () => {
|
|
it("passes through unicode characters unchanged", () => {
|
|
deepStrictEqual(unescapeToken("héllo"), "héllo")
|
|
deepStrictEqual(unescapeToken("世界"), "世界")
|
|
deepStrictEqual(unescapeToken("🚀"), "🚀")
|
|
})
|
|
|
|
it("passes through other special characters unchanged", () => {
|
|
deepStrictEqual(unescapeToken("a.b"), "a.b")
|
|
deepStrictEqual(unescapeToken("a-b"), "a-b")
|
|
deepStrictEqual(unescapeToken("a_b"), "a_b")
|
|
deepStrictEqual(unescapeToken("a@b"), "a@b")
|
|
})
|
|
|
|
it("handles mixed unicode and escaped sequences", () => {
|
|
deepStrictEqual(unescapeToken("héllo~0world~1test"), "héllo~world/test")
|
|
deepStrictEqual(unescapeToken("世界~1🌍~0key"), "世界/🌍~key")
|
|
})
|
|
})
|
|
|
|
describe("immutability", () => {
|
|
it("does not mutate input string", () => {
|
|
const original = "a~0b~1c"
|
|
const originalCopy = "a~0b~1c"
|
|
unescapeToken(original)
|
|
deepStrictEqual(original, originalCopy)
|
|
strictEqual(original, originalCopy)
|
|
})
|
|
|
|
it("returns a different token value when unescaping changes the input", () => {
|
|
const input = "a~0b"
|
|
const result = unescapeToken(input)
|
|
strictEqual(result === input, false)
|
|
deepStrictEqual(result, "a~b")
|
|
})
|
|
})
|
|
|
|
describe("RFC 6901 examples", () => {
|
|
it("handles examples from documentation", () => {
|
|
deepStrictEqual(unescapeToken("name~1alias"), "name/alias")
|
|
deepStrictEqual(unescapeToken("path~1to~0key"), "path/to~key")
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("round-trip", () => {
|
|
it("unescapeToken(escapeToken(token)) === token for basic cases", () => {
|
|
const cases = [
|
|
"abc",
|
|
"a~b",
|
|
"a/b",
|
|
"a~b/c",
|
|
"path/to~key",
|
|
"name/alias",
|
|
"~",
|
|
"/",
|
|
"~~",
|
|
"//",
|
|
"~/",
|
|
"/~"
|
|
]
|
|
|
|
for (const token of cases) {
|
|
const escaped = escapeToken(token)
|
|
const unescaped = unescapeToken(escaped)
|
|
deepStrictEqual(unescaped, token, `Failed for token: ${token}`)
|
|
}
|
|
})
|
|
|
|
it("unescapeToken(escapeToken(token)) === token for edge cases", () => {
|
|
const cases = [
|
|
"",
|
|
"~01",
|
|
"~10",
|
|
"~00",
|
|
"~11",
|
|
"a~01b",
|
|
"a~10b",
|
|
"a~00b",
|
|
"a~11b"
|
|
]
|
|
|
|
for (const token of cases) {
|
|
const escaped = escapeToken(token)
|
|
const unescaped = unescapeToken(escaped)
|
|
deepStrictEqual(unescaped, token, `Failed for token: ${token}`)
|
|
}
|
|
})
|
|
|
|
it("unescapeToken(escapeToken(token)) === token for unicode", () => {
|
|
const cases = [
|
|
"héllo",
|
|
"世界",
|
|
"🚀",
|
|
"héllo~world/test",
|
|
"世界/🌍~key"
|
|
]
|
|
|
|
for (const token of cases) {
|
|
const escaped = escapeToken(token)
|
|
const unescaped = unescapeToken(escaped)
|
|
deepStrictEqual(unescaped, token, `Failed for token: ${token}`)
|
|
}
|
|
})
|
|
|
|
it("unescapeToken(escapeToken(token)) === token for complex sequences", () => {
|
|
const cases = [
|
|
"a~b/c~d/e",
|
|
"path/to~key/value",
|
|
"~a/b~c/d~",
|
|
"a~/b/c~d",
|
|
"a/~b/c~d"
|
|
]
|
|
|
|
for (const token of cases) {
|
|
const escaped = escapeToken(token)
|
|
const unescaped = unescapeToken(escaped)
|
|
deepStrictEqual(unescaped, token, `Failed for token: ${token}`)
|
|
}
|
|
})
|
|
|
|
it("handles multiple round-trips", () => {
|
|
const token = "a~b/c~d"
|
|
let current = token
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
current = escapeToken(current)
|
|
current = unescapeToken(current)
|
|
deepStrictEqual(current, token)
|
|
}
|
|
})
|
|
})
|
|
})
|