UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

140 lines (124 loc) • 6.63 kB
import { describe, expect, it } from "bun:test" import { addr, dec, i32, key, str, u256 } from "../schema" import { InvalidAttributeNameError, InvalidValueError } from "../schema/errors" import { and, eq, exists, gt, startsWith } from "./expression" import { toRpcSelect } from "./selection" const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" const ENTITY_KEY = `0x${"ab".repeat(32)}` as const /** * The query string is built by concatenation, so every value and name that reaches it is checked * here against the shapes that would change what the node parses rather than what it matches. */ describe("string literals cannot break out of their quotes", () => { it("doubles every quote, which is the language's only escape", () => { expect(String(eq("x", str("a' OR '1'='1")))).toBe("x = str('a'' OR ''1''=''1')") expect(String(eq("x", str("')")))).toBe("x = str(''')')") expect(String(eq("x", str("'")))).toBe("x = str('''')") expect(String(startsWith("x", "pre' OR '1"))).toBe("x STARTSWITH str('pre'' OR ''1')") }) it("leaves a backslash literal — the language has no backslash escape to subvert", () => { // If `\` escaped anything, `\''` would close the string. It does not: `\` is an ordinary char, // and the quote after it is still doubled. expect(String(eq("x", str("a\\")))).toBe("x = str('a\\')") // Backslash, then a doubled quote, then the terminator — the backslash buys nothing. expect(String(eq("x", str("a\\'")))).toBe("x = str('a\\''')") }) it("keeps a comment token harmless inside the quotes", () => { expect(String(eq("x", str("a -- comment")))).toBe("x = str('a -- comment')") }) it("keeps control characters inside the quotes, where the node reads them verbatim", () => { // The node's lexer takes every character up to the closing quote as part of the string, so a // newline cannot end the literal and turn the rest of the value into query text. for (const code of [0x00, 0x09, 0x0a, 0x0d, 0x1f, 0x7f]) { const char = String.fromCharCode(code) const hostile = `a${char}' AND flagged = true` expect(String(eq("x", hostile))).toBe(`x = str('a${char}'' AND flagged = true')`) expect(String(startsWith("x", hostile))).toBe( `x STARTSWITH str('a${char}'' AND flagged = true')`, ) } }) it("still allows every printable character, including the awkward ones", () => { for (const value of ["a b", "a\\b", "a--b", "a)b(", "a=b", "élan", "日本語", 'a"b']) { expect(() => str(value)).not.toThrow() } }) }) describe("attribute names cannot smuggle a comment", () => { it("rejects a name containing the comment token", () => { // `-` is a legal name character, so `a--b` passes the grammar — and renders a query whose // entire remainder is commented out. Verified before the fix: `and(eq("approved", true), // eq("a--b", 1), eq("level", 5))` rendered `approved = true AND a--b = i32(1) AND level = // i32(5)`, in which the node sees only `approved = true AND a`. expect(() => eq("a--b", 1)).toThrow(InvalidAttributeNameError) expect(() => eq("a--b", 1)).toThrow(/opens a comment in the query language/) expect(() => exists("owner-name--x")).toThrow(InvalidAttributeNameError) // Rejected on the write path too, so nothing unqueryable can be stored in the first place. expect(() => str("fine")).not.toThrow() }) it("keeps a single dash, which is unambiguous", () => { expect(String(eq("a-b", 1))).toBe("a-b = i32(1)") expect(String(eq("a-", 1))).toBe("a- = i32(1)") }) it("rejects everything else outside the grammar", () => { for (const name of ["x' OR '1", "x)", "a b", "x'", "x(", "1x", "x;", "a".repeat(33)]) { expect(() => eq(name, 1)).toThrow(InvalidAttributeNameError) } }) }) describe("typed literals are closed by construction", () => { it("renders numerics as digits and nothing else", () => { expect(String(eq("x", i32(-2147483648)))).toBe("x = i32(-2147483648)") expect(String(eq("x", u256(2n ** 256n - 1n)))).toBe( "x = u256(115792089237316195423570985008687907853269984665640564039457584007913129639935)", ) // Large and small decimals stay in positional notation — no exponent for a parser to choke on. expect(String(eq("x", dec("1000000000000000000000")))).toBe("x = dec(1000000000000000000000)") expect(String(eq("x", dec("-0.000000000000000001")))).toBe("x = dec(-0.000000000000000001)") expect(String(eq("x", u256(10n ** 30n)))).not.toMatch(/[eE+]/) }) it("renders the byte-shaped types as fixed-width hex", () => { expect(String(eq("x", key(ENTITY_KEY)))).toMatch(/^x = key\(0x[0-9a-f]{64}\)$/) expect(String(eq("x", addr(ACCOUNT)))).toMatch(/^x = addr\(0x[0-9a-fA-F]{40}\)$/) }) it("refuses hostile input at the constructor, before it can be rendered", () => { expect(() => u256("1) OR true --" as string)).toThrow(InvalidValueError) expect(() => addr("0x0') OR true --")).toThrow(InvalidValueError) expect(() => key("0xab') OR true --" as `0x${string}`)).toThrow(InvalidValueError) expect(() => dec("1) OR true")).toThrow(InvalidValueError) expect(() => i32("1) OR true" as unknown as number)).toThrow(InvalidValueError) }) it("cannot be handed a hostile value through an untyped object either", () => { // toValue re-runs the constructor, so a look-alike carrying a string gets validated, not // interpolated. expect(() => eq("x", { type: "u256", value: "1) OR true --" } as unknown as string)).toThrow( InvalidValueError, ) }) }) describe("the assembled query", () => { it("keeps every predicate intact through a hostile filter set", () => { const query = String( and( eq("approved", true), eq("note", str("a') OR true --")), gt("level", i32(5)), eq("$owner", addr(ACCOUNT)), ), ) expect(query).toBe( "approved = true AND note = str('a'') OR true --') AND level > i32(5) AND " + `$owner = addr(${ACCOUNT})`, ) // The guard predicates survive the hostile one, in both directions. expect(query).toContain("approved = true") expect(query).toContain("level > i32(5)") }) }) describe("the selection", () => { it("checks named attributes rather than forwarding them", () => { expect(() => toRpcSelect({ $key: true, "a' OR '1": true })).toThrow(InvalidAttributeNameError) expect(() => toRpcSelect({ $key: true, "a--b": true })).toThrow(InvalidAttributeNameError) expect(toRpcSelect({ $key: true, ok: true })).toMatchObject({ attributes: { ok: true } }) }) })