Something went wrong. Try again.
Automations and webhooks for the AT Protocol airglow.run
automation webhook atproto atprotocol
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487import { describe, it, expect } from "vitest";import { applyJsonPatch, validateJsonPatchOps, validateJsonPatchOpsShape, JsonPatchError,} from "./json-patch.js";
describe("applyJsonPatch", () => { describe("object operations", () => { it("adds a new top-level field", () => { const result = applyJsonPatch({ a: 1 }, [{ op: "add", path: "/b", value: 2 }]); expect(result).toEqual({ a: 1, b: 2 }); });
it("replaces an existing nested field without disturbing siblings", () => { const base = { profile: { displayName: "old", bio: "preserved" }, extra: true }; const result = applyJsonPatch(base, [ { op: "replace", path: "/profile/displayName", value: "new" }, ]); expect(result).toEqual({ profile: { displayName: "new", bio: "preserved" }, extra: true }); });
it("removes an object member", () => { const result = applyJsonPatch({ a: 1, b: 2 }, [{ op: "remove", path: "/a" }]); expect(result).toEqual({ b: 2 }); });
it("does not mutate the input target", () => { const base = { a: { b: 1 } }; applyJsonPatch(base, [{ op: "replace", path: "/a/b", value: 99 }]); expect(base).toEqual({ a: { b: 1 } }); }); });
describe("array operations", () => { it("replaces a specific array index", () => { const base = { layers: [{ equipped: false }, { equipped: false }, { equipped: false }] }; const result = applyJsonPatch(base, [ { op: "replace", path: "/layers/2/equipped", value: true }, ]); expect(result).toEqual({ layers: [{ equipped: false }, { equipped: false }, { equipped: true }], }); });
it("adds at an existing array index (shifts elements right)", () => { const result = applyJsonPatch([1, 2, 3], [{ op: "add", path: "/1", value: 99 }]); expect(result).toEqual([1, 99, 2, 3]); });
it("adds at index = length (appends)", () => { const result = applyJsonPatch([1, 2], [{ op: "add", path: "/2", value: 3 }]); expect(result).toEqual([1, 2, 3]); });
it("adds with '-' segment (appends)", () => { const result = applyJsonPatch([1, 2], [{ op: "add", path: "/-", value: 3 }]); expect(result).toEqual([1, 2, 3]); });
it("rejects replace on array index out of range", () => { expect(() => applyJsonPatch([1, 2], [{ op: "replace", path: "/5", value: 99 }])).toThrow( JsonPatchError, ); try { applyJsonPatch([1, 2], [{ op: "replace", path: "/5", value: 99 }]); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as JsonPatchError).opIndex).toBe(0); expect((err as Error).message).toMatch(/out of range/); } });
it("rejects remove on array index out of range", () => { expect(() => applyJsonPatch([1], [{ op: "remove", path: "/3" }])).toThrow(JsonPatchError); });
it("removes a specific array index", () => { const result = applyJsonPatch([1, 2, 3], [{ op: "remove", path: "/1" }]); expect(result).toEqual([1, 3]); }); });
describe("move and copy", () => { it("moves a value from one path to another", () => { const result = applyJsonPatch({ a: 1, b: 2 }, [{ op: "move", from: "/a", path: "/c" }]); expect(result).toEqual({ b: 2, c: 1 }); });
it("copies a value, leaving the source intact", () => { const result = applyJsonPatch({ a: 1 }, [{ op: "copy", from: "/a", path: "/b" }]); expect(result).toEqual({ a: 1, b: 1 }); });
it("rejects move when source does not exist", () => { expect(() => applyJsonPatch({ a: 1 }, [{ op: "move", from: "/missing", path: "/c" }]), ).toThrow(JsonPatchError); }); });
describe("test op", () => { it("passes when value at path deep-equals expected", () => { const ops = [ { op: "test" as const, path: "/v", value: 1 }, { op: "replace" as const, path: "/v", value: 2 }, ]; const result = applyJsonPatch({ v: 1 }, ops); expect(result).toEqual({ v: 2 }); });
it("fails the whole patch when test does not match", () => { const ops = [ { op: "test" as const, path: "/v", value: 1 }, { op: "replace" as const, path: "/v", value: 2 }, ]; try { applyJsonPatch({ v: 5 }, ops); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as JsonPatchError).opIndex).toBe(0); expect((err as Error).message).toMatch(/test failed/); } });
it("deep-equals nested objects and arrays", () => { const result = applyJsonPatch({ items: [{ a: 1, b: [2, 3] }] }, [ { op: "test", path: "/items", value: [{ a: 1, b: [2, 3] }] }, ]); expect(result).toEqual({ items: [{ a: 1, b: [2, 3] }] }); }); });
describe("JSON Pointer escapes", () => { it("decodes ~1 as literal /", () => { const result = applyJsonPatch({ "foo/bar": 1 }, [ { op: "replace", path: "/foo~1bar", value: 2 }, ]); expect(result).toEqual({ "foo/bar": 2 }); });
it("decodes ~0 as literal ~", () => { const result = applyJsonPatch({ "a~b": 1 }, [{ op: "replace", path: "/a~0b", value: 2 }]); expect(result).toEqual({ "a~b": 2 }); }); });
describe("error op indexing", () => { it("reports the failing op's index in the error", () => { const ops = [ { op: "replace" as const, path: "/a", value: 1 }, { op: "replace" as const, path: "/missing", value: 2 }, ]; try { applyJsonPatch({ a: 0 }, ops); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as JsonPatchError).opIndex).toBe(1); expect((err as Error).message).toMatch(/op 1/); } }); });
describe("prototype pollution defense", () => { // The pointer parser inherits `isUnsafeKey` from the shared placeholders // module, which rejects __proto__/constructor/prototype. These tests pin that // contract so a future refactor of resolveParent or resolveValue can't // silently reopen the attack surface. it.each([ ["/__proto__/admin", "add"], ["/__proto__", "remove"], ["/constructor/prototype/x", "add"], ] as const)("rejects %s on %s", (path, op) => { const base = {}; const ops = op === "add" ? [{ op: "add" as const, path, value: true }] : [{ op: "remove" as const, path }]; expect(() => applyJsonPatch(base, ops)).toThrow(JsonPatchError); expect((base as Record<string, unknown>).admin).toBeUndefined(); });
it("does not pollute Object.prototype after a rejected patch", () => { try { applyJsonPatch({}, [{ op: "add", path: "/__proto__/polluted", value: true }]); } catch { // expected } expect(({} as Record<string, unknown>).polluted).toBeUndefined(); }); });
describe("JSONPath paths", () => { it("replace with a JSONPath that resolves to exactly one node", () => { const result = applyJsonPatch({ profile: { displayName: "old" } }, [ { op: "replace", path: "$.profile.displayName", value: "new" }, ]); expect(result).toEqual({ profile: { displayName: "new" } }); });
it("remove with a JSONPath filter that matches one element", () => { const base = { users: [ { id: "a", active: false }, { id: "b", active: false }, ], }; const result = applyJsonPatch(base, [{ op: "remove", path: "$.users[?@.id=='a']" }]); expect(result).toEqual({ users: [{ id: "b", active: false }] }); });
it("replace via JSONPath filter flips the matched object's field", () => { const base = { users: [ { id: "a", active: false }, { id: "b", active: false }, ], }; const result = applyJsonPatch(base, [ { op: "replace", path: "$.users[?@.id=='b'].active", value: true }, ]); expect(result).toEqual({ users: [ { id: "a", active: false }, { id: "b", active: true }, ], }); });
it("add with a JSONPath matching an existing array index splice-inserts (sibling shifts right)", () => { const result = applyJsonPatch({ items: [1, 2, 3] }, [ { op: "add", path: "$.items[1]", value: 99 }, ]); expect(result).toEqual({ items: [1, 99, 2, 3] }); });
it("add with a JSONPath matching an existing object key overwrites it", () => { const result = applyJsonPatch({ profile: { displayName: "old" } }, [ { op: "add", path: "$.profile.displayName", value: "new" }, ]); expect(result).toEqual({ profile: { displayName: "new" } }); });
it("test op accepts JSONPath as a precondition for following ops", () => { const base = { items: [{ id: "x", on: true }] }; const testSuccessResult = applyJsonPatch(base, [ { op: "test", path: "$.items[?@.id=='x'].on", value: true }, { op: "replace", path: "$.items[?@.id=='x'].on", value: false }, ]); // Replace occurs expect(testSuccessResult).toEqual({ items: [{ id: "x", on: false }] });
try { applyJsonPatch(base, [ { op: "test", path: "$.items[?@.id=='x'].on", value: false }, { op: "replace", path: "$.items[?@.id=='x'].on", value: false }, ]); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as JsonPatchError).opIndex).toBe(0); expect((err as Error).message).toMatch(/test failed at/); } });
it("move resolves JSONPath destination AFTER the source remove (post-remove indices)", () => { // Without re-resolution, `$.users[1]` would point at the wrong post- // remove element. With re-resolution it points at what was originally // index 2 (which slides into index 1 after the remove). const base = { users: [{ id: "a" }, { id: "b" }, { id: "c" }], }; const result = applyJsonPatch(base, [ { op: "move", from: "$.users[?@.id=='a']", path: "$.users[1]" }, ]); // After remove: [b, c]. After add at index 1: [b, a, c]. expect(result).toEqual({ users: [{ id: "b" }, { id: "a" }, { id: "c" }] }); });
it("fails when the JSONPath matches no nodes", () => { try { applyJsonPatch({ users: [{ id: "a" }] }, [ { op: "replace", path: "$.users[?@.id=='missing'].id", value: "x" }, ]); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as JsonPatchError).opIndex).toBe(0); expect((err as Error).message).toMatch(/matched nothing/); } });
it("fails when the JSONPath matches multiple nodes", () => { try { applyJsonPatch({ users: [{ active: false }, { active: false }] }, [ { op: "replace", path: "$.users[*].active", value: true }, ]); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as JsonPatchError).opIndex).toBe(0); expect((err as Error).message).toMatch(/matched 2 nodes \(expected exactly 1\)/); } });
it("fails with a clear message when the JSONPath has invalid syntax", () => { try { applyJsonPatch({ a: 1 }, [{ op: "replace", path: "$.[unclosed", value: 2 }]); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as Error).message).toMatch(/invalid JSONPath expression/); } });
it("fails with a clear message on a leading-zero index (JSONPathIndexError)", () => { try { applyJsonPatch({ a: [1, 2] }, [{ op: "replace", path: "$.a[01]", value: 2 }]); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as Error).message).toMatch(/invalid JSONPath expression/); } });
it("a $-path that resolves to no node cannot pollute Object.prototype", () => { // The JSON.parse clone strips any non-own __proto__, so the query // returns nothing and the op fails. The important assertion is that // Object.prototype is unchanged, independent of error wording. try { applyJsonPatch({ a: 1 }, [{ op: "add", path: "$['__proto__'].polluted", value: true }]); } catch { // expected (either matched-nothing or unsafe-key) } expect(({} as Record<string, unknown>).polluted).toBeUndefined(); });
it("root selector '$' resolves to the whole document (replace at root)", () => { const result = applyJsonPatch({ a: 1 }, [{ op: "replace", path: "$", value: { b: 2 } }]); expect(result).toEqual({ b: 2 }); });
it("recursive descent matching exactly one node", () => { const result = applyJsonPatch({ outer: { inner: { target: "old" } } }, [ { op: "replace", path: "$..target", value: "new" }, ]); expect(result).toEqual({ outer: { inner: { target: "new" } } }); });
it.each([ ["wildcard", "$.users[*]"], ["slice", "$.users[0:2]"], ["union", "$.users[0,2]"], ])("multi-match guard fires on the %s selector", (_label, path) => { const base = { users: [{ a: 1 }, { a: 2 }, { a: 3 }] }; try { applyJsonPatch(base, [{ op: "remove", path }]); throw new Error("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(JsonPatchError); expect((err as Error).message).toMatch(/matched \d+ nodes \(expected exactly 1\)/); } });
it("negative array index resolves to the last element", () => { // json-p3 must convert [-1] to a concrete index (here /items/2) so our // parser sees a well-formed pointer — RFC 6901 doesn't allow negative // indices natively. const result = applyJsonPatch({ items: ["a", "b", "c"] }, [ { op: "replace", path: "$.items[-1]", value: "z" }, ]); expect(result).toEqual({ items: ["a", "b", "z"] }); });
it("filter comparing against null matches the null-valued node", () => { const result = applyJsonPatch( { items: [ { id: "a", deletedAt: null }, { id: "b", deletedAt: "2024-01-01" }, ], }, [{ op: "remove", path: "$.items[?@.deletedAt == null]" }], ); expect(result).toEqual({ items: [{ id: "b", deletedAt: "2024-01-01" }] }); }); });});
describe("validateJsonPatchOps", () => { it("accepts a well-formed op array", () => { const result = validateJsonPatchOps([ { op: "replace", path: "/a", value: 1 }, { op: "remove", path: "/b" }, ]); expect(result.ok).toBe(true); });
it("rejects a non-array input", () => { const result = validateJsonPatchOps({ op: "replace", path: "/a", value: 1 }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/array/); });
it("rejects an empty array", () => { const result = validateJsonPatchOps([]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/at least one op/); });
it("rejects an unknown op", () => { const result = validateJsonPatchOps([{ op: "wat", path: "/a", value: 1 }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/unknown op/); });
it("rejects a non-string path", () => { const result = validateJsonPatchOps([{ op: "replace", path: 42, value: 1 }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/path/); });
it("rejects a path that doesn't start with '/'", () => { const result = validateJsonPatchOps([{ op: "replace", path: "no-slash", value: 1 }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/start with "\//); });
it("rejects add/replace/test missing value", () => { const result = validateJsonPatchOps([{ op: "add", path: "/a" }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/value/); });
it("rejects move/copy missing from", () => { const result = validateJsonPatchOps([{ op: "move", path: "/a" }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/from/); });
it("accepts a JSONPath expression as path", () => { const result = validateJsonPatchOps([ { op: "replace", path: "$.users[?@.id=='a'].name", value: "x" }, ]); expect(result.ok).toBe(true); });
it("accepts a JSONPath expression as from", () => { const result = validateJsonPatchOps([{ op: "move", from: "$.a", path: "/b" }]); expect(result.ok).toBe(true); });
it("rejects a JSONPath with invalid syntax (compile-checked at validation time)", () => { const result = validateJsonPatchOps([{ op: "replace", path: "$.[unclosed", value: 1 }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/invalid JSONPath/); });
it("rejects a JSONPath with a leading-zero index", () => { const result = validateJsonPatchOps([{ op: "replace", path: "$.a[01]", value: 1 }]); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toMatch(/op 0: invalid JSONPath in "path"/); });});
describe("validateJsonPatchOpsShape", () => { it("accepts shape-valid ops without inspecting path/value/from", () => { // Save-time check used by validateTemplate: it must tolerate the // placeholder-substitution sentinel that drops the leading "/" from // templated paths. Anything with a known op and object shape passes. const result = validateJsonPatchOpsShape([ { op: "replace", path: "__placeholder__" }, { op: "move", path: "__placeholder__", from: "__placeholder__" }, ]); expect(result.ok).toBe(true); });
it("rejects empty arrays and unknown ops just like the full validator", () => { expect(validateJsonPatchOpsShape([]).ok).toBe(false); expect(validateJsonPatchOpsShape([{ op: "wat", path: "/a" }]).ok).toBe(false); expect(validateJsonPatchOpsShape({ op: "replace", path: "/a", value: 1 }).ok).toBe(false); });});