Addresses Copilot's round-18 review on #2035. Two suppressed comments, both
holes in my own recent fixes rather than in the original code.
1. Round 16's grouping keyed off the length-announcing lead byte alone, so
it accepted sequences RFC 3629 forbids -- and the comment claiming they
fell back to per-triplet counting was simply wrong. Measured on 05bbfe50:
x{+v:1} "%C0%80x" => "x%C0%80" overlong
"%E0%80%80x" => "x%E0%80%80" overlong
"%ED%A0%80x" => "x%ED%A0%80" UTF-16 surrogate
"%F4%90%80%80x" => "x%F4%90%80%80" past U+10FFFF
The lead-byte ranges are now the well-formed ones -- C2-DF, E0 only with
A0-BF, ED only with 80-9F, F0 only with 90-BF, F4 only with 80-8F -- so
each of the above falls back to one triplet, which is what the fallback
was always documented to do. The legal boundary cases (%E0%A0%80,
%ED%9F%BF, %F4%8F%BF%BF) are tested alongside, so tightening the ranges
cannot quietly start splitting valid characters.
2. The preview never ran the read's template validation. It expands
expression by expression, so `x://{a}}/{b}` with both fields filled
previewed `x://1}/2` -- a URI whose stray `}` makes every read refuse.
Validation moved into an exported `templateError` that both callers
consult, so they cannot drift. It subsumes the narrower rule round 12
added here ("leave the invalid part standing"), which still let the REST
of such a template expand: the refusal is per template, not per
expression, so the preview now shows the template verbatim -- literals
unencoded included, since half-normalizing an unreadable template invents
a string the server never published. Two existing tests asserted the
weaker behavior and now assert this one.
npm run ci passes.
Signed-off-by: cliffhall <cliff@futurescale.com>
151 lines
5.9 KiB
TypeScript
151 lines
5.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { previewUriTemplate } from "./uriTemplate";
|
|
|
|
describe("previewUriTemplate", () => {
|
|
it("shows the template verbatim before anything is entered", () => {
|
|
expect(previewUriTemplate("file:///users/{userId}/profile", {})).toBe(
|
|
"file:///users/{userId}/profile",
|
|
);
|
|
});
|
|
|
|
it("substitutes only the expressions that are filled", () => {
|
|
expect(
|
|
previewUriTemplate("db://{tableName}/rows/{rowId}", {
|
|
tableName: "users",
|
|
rowId: "",
|
|
}),
|
|
).toBe("db://users/rows/{rowId}");
|
|
});
|
|
|
|
it("encodes the filled values the same way expansion does", () => {
|
|
expect(
|
|
previewUriTemplate("foobar://events/{topic}", { topic: "foo/bar" }),
|
|
).toBe("foobar://events/foo%2Fbar");
|
|
});
|
|
|
|
it("expands a partially-filled multi-name expression, as submitting would", () => {
|
|
// RFC 6570 drops the undefined names rather than the whole expression, so
|
|
// showing `{?one,two}` here would promise a URI the submit does not send.
|
|
expect(previewUriTemplate("x://a{?one,two}", { one: "1" })).toBe(
|
|
"x://a?one=1",
|
|
);
|
|
});
|
|
|
|
it("keeps a multi-name expression whole while none of its names is filled", () => {
|
|
expect(previewUriTemplate("x://a{?one,two}", {})).toBe("x://a{?one,two}");
|
|
});
|
|
|
|
it("expands each query expression independently, matching the submit", () => {
|
|
// Not `?one=1&two=2` -- see the expander's tests: the SDK's own matcher
|
|
// rejects that for this template. The preview must show what will be sent.
|
|
expect(
|
|
previewUriTemplate("x://a{?one}{?two}", { one: "1", two: "2" }),
|
|
).toBe("x://a?one=1?two=2");
|
|
});
|
|
|
|
it("restores a deferred expression that follows a resolved query expression", () => {
|
|
expect(previewUriTemplate("x://a{?one}{?two}", { one: "1" })).toBe(
|
|
"x://a?one=1{?two}",
|
|
);
|
|
});
|
|
|
|
it("falls back to the raw template when the SDK cannot parse it", () => {
|
|
expect(previewUriTemplate("x://a/{oops", {})).toBe("x://a/{oops");
|
|
});
|
|
});
|
|
|
|
describe("previewUriTemplate - multi-name expressions", () => {
|
|
it("applies the same correction the real expansion does", () => {
|
|
// Must match expandUriTemplate("x://{a,b}", ...) exactly, or the preview
|
|
// would promise a URI that submitting does not send.
|
|
expect(previewUriTemplate("x://{a,b}", { a: "foo/bar", b: "q" })).toBe(
|
|
"x://foo%2Fbar,q",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("preview with Object.prototype-colliding names", () => {
|
|
it("leaves a blank {?toString} standing rather than treating it as filled", () => {
|
|
// A bare `defined[name] !== undefined` finds Object.prototype.toString and
|
|
// would expand the expression instead of showing the placeholder.
|
|
expect(previewUriTemplate("x://a{?toString}", { toString: "" })).toBe(
|
|
"x://a{?toString}",
|
|
);
|
|
});
|
|
|
|
it("expands it once it really has a value", () => {
|
|
expect(previewUriTemplate("x://a{?toString}", { toString: "v" })).toBe(
|
|
"x://a?toString=v",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("previewUriTemplate - literals", () => {
|
|
// The preview promises the URI that submitting would send, so it applies the
|
|
// same RFC 6570 3.1 literal encoding the wire does.
|
|
it("percent-encodes a non-ASCII literal", () => {
|
|
expect(previewUriTemplate("café/{var}", { var: "value" })).toBe(
|
|
"caf%C3%A9/value",
|
|
);
|
|
});
|
|
|
|
it("encodes the literal even while an expression is still unfilled", () => {
|
|
expect(previewUriTemplate("café/{var}", {})).toBe("caf%C3%A9/{var}");
|
|
});
|
|
|
|
it("shows a template the read would refuse exactly as the server wrote it", () => {
|
|
// Not even the literal is encoded: an unreadable template is displayed
|
|
// verbatim rather than half-normalized into something never published.
|
|
expect(previewUriTemplate("x://café/{oops", {})).toBe("x://café/{oops");
|
|
});
|
|
});
|
|
|
|
describe("previewUriTemplate - it must never throw or over-promise", () => {
|
|
// Both of these were free when the preview expanded a rewritten template
|
|
// through the lenient `expandUriTemplate`; assembling part by part has to
|
|
// reinstate them explicitly.
|
|
it("falls back to the raw template when a value cannot be encoded", () => {
|
|
// An unpaired surrogate has no UTF-8 encoding, so `encodeURIComponent`
|
|
// raises URIError -- and this runs during render, where an escaping throw
|
|
// unmounts the panel rather than disabling its button. A paste delivers it.
|
|
expect(previewUriTemplate("x://{v}", { v: "\ud800" })).toBe("x://{v}");
|
|
});
|
|
|
|
it("leaves an invalid expression standing rather than expanding it", () => {
|
|
// `{a,}` keeps `a` in its varspecs, so expanding it would preview a URI
|
|
// for a template whose submission is refused outright.
|
|
expect(previewUriTemplate("x://{a,}", { a: "1" })).toBe("x://{a,}");
|
|
expect(previewUriTemplate("x://{id:abc}", { "id:abc": "1" })).toBe(
|
|
"x://{id:abc}",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
["an invalid varspec", "x://{a,}/{b}"],
|
|
["a stray closing brace", "x://{a}}/{b}"],
|
|
])(
|
|
"does not expand the valid expressions around %s either",
|
|
(_label, template) => {
|
|
// The refusal is per TEMPLATE, not per expression: every read of these
|
|
// is refused, so previewing `x://{a,}/2` or `x://1}/2` would promise a
|
|
// URI this form can never send. An earlier revision did exactly that.
|
|
expect(previewUriTemplate(template, { a: "1", b: "2" })).toBe(template);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("previewUriTemplate - the value ceiling", () => {
|
|
it("falls back before encoding an oversized value", () => {
|
|
// Without this the guard was half a guard: the read was refused while the
|
|
// preview still encoded the same value on every keystroke, so a large
|
|
// paste froze the UI anyway.
|
|
expect(previewUriTemplate("x://{v}", { v: "a".repeat(1_000_001) })).toBe(
|
|
"x://{v}",
|
|
);
|
|
});
|
|
|
|
it("still previews a value at the limit", () => {
|
|
const value = "a".repeat(1_000_000);
|
|
expect(previewUriTemplate("x://{v}", { v: value })).toBe(`x://${value}`);
|
|
});
|
|
});
|