Initial Commit
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
BUNDLE_URI_CODES,
|
||||
BundleUriError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { validateOptions } from "../factory/factory.js";
|
||||
import { build, parse } from "./parse.js";
|
||||
|
||||
const OPTS = validateOptions({
|
||||
packageSlug: "audio-ir-room-plugin-club",
|
||||
rootUrl: "file:///pkgs/club/assets/",
|
||||
});
|
||||
|
||||
function expectCode(fn: () => unknown, code: string): BundleUriError {
|
||||
let err: BundleUriError | undefined;
|
||||
try {
|
||||
fn();
|
||||
} catch (e) {
|
||||
err = e as BundleUriError;
|
||||
}
|
||||
expect(err).toBeInstanceOf(BundleUriError);
|
||||
expect(err?.code).toBe(code);
|
||||
return err as BundleUriError;
|
||||
}
|
||||
|
||||
describe("parse(uri, options)", () => {
|
||||
it("parses well-formed URI to key plus meta", { timeout: 5000 }, () => {
|
||||
const parsed = parse(
|
||||
"bundle://audio-ir-room-plugin-club/assets/club.wav",
|
||||
OPTS
|
||||
);
|
||||
expect(parsed.key).toBe("assets/club.wav");
|
||||
expect(parsed.meta.packageSlug).toBe("audio-ir-room-plugin-club");
|
||||
});
|
||||
|
||||
it("rejects scheme mismatch with INVALID_URI", { timeout: 5000 }, () => {
|
||||
expectCode(
|
||||
() => parse("https://audio-ir-room-plugin-club/x", OPTS),
|
||||
BUNDLE_URI_CODES.INVALID_URI
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects empty rel-path with INVALID_URI", { timeout: 5000 }, () => {
|
||||
expectCode(
|
||||
() => parse("bundle://audio-ir-room-plugin-club/", OPTS),
|
||||
BUNDLE_URI_CODES.INVALID_URI
|
||||
);
|
||||
expectCode(
|
||||
() => parse("bundle://audio-ir-room-plugin-club", OPTS),
|
||||
BUNDLE_URI_CODES.INVALID_URI
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects missing authority with INVALID_URI", { timeout: 5000 }, () => {
|
||||
expectCode(
|
||||
() => parse("bundle:///assets/club.wav", OPTS),
|
||||
BUNDLE_URI_CODES.INVALID_URI
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects slug mismatch with PACKAGE_MISMATCH carrying actual", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = expectCode(
|
||||
() => parse("bundle://other-pkg/x", OPTS),
|
||||
BUNDLE_URI_CODES.PACKAGE_MISMATCH
|
||||
);
|
||||
expect(err.packageSlug).toBe("audio-ir-room-plugin-club");
|
||||
expect(err.actual).toBe("other-pkg");
|
||||
});
|
||||
|
||||
it("rejects raw .. segment with PATH_TRAVERSAL", { timeout: 5000 }, () => {
|
||||
expectCode(
|
||||
() =>
|
||||
parse("bundle://audio-ir-room-plugin-club/../etc/passwd", OPTS),
|
||||
BUNDLE_URI_CODES.PATH_TRAVERSAL
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects pct-encoded .. after decode with PATH_TRAVERSAL", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expectCode(
|
||||
() =>
|
||||
parse(
|
||||
"bundle://audio-ir-room-plugin-club/%2E%2E/etc/passwd",
|
||||
OPTS
|
||||
),
|
||||
BUNDLE_URI_CODES.PATH_TRAVERSAL
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects URL-resolve escape outside rootUrl with PATH_TRAVERSAL", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Use an opts whose rootUrl is a sub-path under root '/'. A
|
||||
// backslash trickery does NOT escape; the most reliable way to
|
||||
// force pass-1/pass-2 to be passed but pass-3 to fail is via a
|
||||
// root with a deep pathname and a rel-path that decodes to a
|
||||
// sibling using only "//" (some URL normalisers collapse). To
|
||||
// guarantee determinism we craft a relPath that is itself
|
||||
// resolved against the rootUrl producing a pathname outside the
|
||||
// root prefix.
|
||||
const o = validateOptions({
|
||||
packageSlug: "x",
|
||||
rootUrl: "file:///root/a/",
|
||||
});
|
||||
// "/etc/passwd" as a rel-path is parsed as absolute by URL,
|
||||
// which resolves to /etc/passwd — outside /root/a/. But our
|
||||
// pre-checks slice the leading "/" off the path component, so
|
||||
// the rel-path here begins with the segment list directly.
|
||||
// We use a constructed authority case that bypasses raw `..`:
|
||||
// a key beginning with a backslash-style absolute-looking
|
||||
// sequence won't trigger PASS 1/2. We simulate by directly
|
||||
// using "a/" which DOES resolve inside the root. Instead, we
|
||||
// exploit the conformance check by calling parse on a URI
|
||||
// whose path encodes a CR/space-free sequence that URL
|
||||
// normalises outside the root: a leading "/" gets stripped by
|
||||
// our slicer, then becomes a regular segment that stays
|
||||
// inside. So pass-3 is mathematically unreachable on its own.
|
||||
// We thus verify pass-3 by routing through a unit-style call
|
||||
// that constructs a URL deliberately outside root: construct
|
||||
// the resolved URL manually and check `startsWith` returns
|
||||
// false on the synthetic case, demonstrating the guard is
|
||||
// mathematically correct.
|
||||
const root = o.rootUrl;
|
||||
const resolved = new URL("../b/x", root);
|
||||
// Sanity: this URL escapes the root prefix.
|
||||
expect(resolved.pathname.startsWith(root.pathname)).toBe(false);
|
||||
// The parse() function rejects this rel-path at pass-1 (raw ..),
|
||||
// demonstrating the layered defence — pass-3 is the
|
||||
// belt-and-braces. Both surface the same code.
|
||||
expectCode(
|
||||
() => parse("bundle://x/../b/x", o),
|
||||
BUNDLE_URI_CODES.PATH_TRAVERSAL
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed percent-encoding with INVALID_URI", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expectCode(
|
||||
() => parse("bundle://audio-ir-room-plugin-club/%2", OPTS),
|
||||
BUNDLE_URI_CODES.INVALID_URI
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts data: root (single-resource) and skips pass-3", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// data: roots have non-hierarchical pathnames so factory's
|
||||
// trailing-slash rule rejects them via the public path.
|
||||
// parse() itself handles data: roots correctly when constructed
|
||||
// through the internal resolved-options shape — we exercise that
|
||||
// branch here directly.
|
||||
const handBuilt = {
|
||||
packageSlug: "x",
|
||||
rootUrl: new URL("data:text/plain,hello"),
|
||||
fetch: globalThis.fetch.bind(globalThis),
|
||||
clock: () => 0,
|
||||
};
|
||||
const parsed = parse("bundle://x/anything", handBuilt);
|
||||
expect(parsed.key).toBe("anything");
|
||||
});
|
||||
|
||||
it("accepts blob: root (single-resource) and skips pass-3", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const handBuilt = {
|
||||
packageSlug: "x",
|
||||
rootUrl: new URL("blob:https://example/abc"),
|
||||
fetch: globalThis.fetch.bind(globalThis),
|
||||
clock: () => 0,
|
||||
};
|
||||
const parsed = parse("bundle://x/anything", handBuilt);
|
||||
expect(parsed.key).toBe("anything");
|
||||
});
|
||||
|
||||
it("rejects URL-resolve pass-3 escape outside rootUrl", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// rootUrl with a deep pathname; URI with leading-slash rel-path
|
||||
// that bypasses pass-1/pass-2 (no `..`) but resolves to a
|
||||
// pathname outside the root prefix. parseUri preserves the
|
||||
// double-slash, our slicer strips ONE leading `/`, leaving
|
||||
// `/foo/x` — which `new URL("/foo/x", root)` resolves as an
|
||||
// absolute path replacing the root's pathname entirely.
|
||||
const opts = validateOptions({
|
||||
packageSlug: "x",
|
||||
rootUrl: "https://example/root/a/",
|
||||
});
|
||||
expectCode(
|
||||
() => parse("bundle://x//foo/x", opts),
|
||||
BUNDLE_URI_CODES.PATH_TRAVERSAL
|
||||
);
|
||||
});
|
||||
|
||||
it("build round-trips through parse", { timeout: 5000 }, () => {
|
||||
const opts = validateOptions({
|
||||
packageSlug: "x",
|
||||
rootUrl: "https://cdn.example/x/",
|
||||
});
|
||||
const uri = build("assets/y.wav", opts);
|
||||
expect(uri).toBe("bundle://x/assets/y.wav");
|
||||
const parsed = parse(uri, opts);
|
||||
expect(parsed.key).toBe("assets/y.wav");
|
||||
expect(parsed.meta.packageSlug).toBe("x");
|
||||
});
|
||||
|
||||
it("build uses meta.packageSlug when supplied", { timeout: 5000 }, () => {
|
||||
const opts = validateOptions({
|
||||
packageSlug: "x",
|
||||
rootUrl: "https://cdn.example/x/",
|
||||
});
|
||||
const uri = build("a", opts, { packageSlug: "y" });
|
||||
expect(uri).toBe("bundle://y/a");
|
||||
});
|
||||
|
||||
it("rewraps parseUri failure as INVALID_URI", { timeout: 5000 }, () => {
|
||||
// empty input → parseUri throws → wrap as BUNDLE_URI.INVALID_URI
|
||||
expectCode(() => parse("", OPTS), BUNDLE_URI_CODES.INVALID_URI);
|
||||
});
|
||||
|
||||
it("accepts a URI with query string preserved as part of the key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// parseUri strips the query before path, so the parsed key
|
||||
// will not include the query. This documents the boundary.
|
||||
const opts = validateOptions({
|
||||
packageSlug: "x",
|
||||
rootUrl: "https://cdn.example/x/",
|
||||
});
|
||||
const parsed = parse("bundle://x/a?token=t", opts);
|
||||
expect(parsed.key).toBe("a");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user