mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-30 08:40:52 +08:00
Cloudflare's single-page-application fallback percent-encodes the path on a fresh load or reload, so `rad:z4V1...` arrives as `rad%3Az4V1...` and leaks into links and the seed command. Only identifiers are decoded: revision and path segments stay encoded, because branch and tag lookups are keyed on the encoded name. The seed command needs a second fix: the repo path segment is an alias when one was used, and `rad seed <alias>` is not valid, the `rad seed` command only supports RIDs and not aliases. Co-Authored-By: Guillaume <guillaume.alabre@gmail.com>
481 lines
12 KiB
TypeScript
481 lines
12 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { testExports, type Route } from "@app/lib/router";
|
|
import config from "@tests/support/config.js";
|
|
|
|
// Defining the window.origin value, since vitest doesn't provide one.
|
|
window.origin = "http://localhost:3000";
|
|
|
|
describe("route invariant when parsed", () => {
|
|
const origin = "http://localhost:3000";
|
|
const node = {
|
|
hostname: "example.node.tld",
|
|
port: 8000,
|
|
scheme: "http",
|
|
};
|
|
|
|
test("nodes", () => {
|
|
expectParsingInvariant({
|
|
resource: "nodes",
|
|
params: {
|
|
// TODO: This only works with the value 0. The value is not actually
|
|
// extract.
|
|
repoPageIndex: 0,
|
|
baseUrl: node,
|
|
},
|
|
});
|
|
});
|
|
test("repos.tree", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
route: "",
|
|
});
|
|
});
|
|
|
|
test("repos.tree with peer", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "REPO",
|
|
peer: "PEER",
|
|
route: "",
|
|
});
|
|
});
|
|
|
|
test("repos.tree with peer and revision", () => {
|
|
const route: Route = {
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "REPO",
|
|
peer: "PEER",
|
|
revision: "REVISION",
|
|
route: "",
|
|
};
|
|
const path = testExports.routeToPath(route);
|
|
route.revision = undefined;
|
|
route.route = "REVISION";
|
|
expect(testExports.urlToRoute(new URL(path, origin))).toEqual(route);
|
|
});
|
|
|
|
test("repos.tree with peer and revision and path", () => {
|
|
const route: Route = {
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "REPO",
|
|
peer: "PEER",
|
|
path: "PATH",
|
|
revision: "REVISION",
|
|
route: "",
|
|
};
|
|
const path = testExports.routeToPath(route);
|
|
route.revision = undefined;
|
|
route.path = undefined;
|
|
route.route = "REVISION/PATH";
|
|
expect(testExports.urlToRoute(new URL(path, origin))).toEqual(route);
|
|
});
|
|
|
|
test("repos.history", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.history",
|
|
node,
|
|
repo: "REPO",
|
|
revision: "",
|
|
});
|
|
});
|
|
|
|
test("repos.history with revision", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.history",
|
|
node,
|
|
repo: "REPO",
|
|
revision: "REVISION",
|
|
});
|
|
});
|
|
|
|
test("repos.commits", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.commit",
|
|
node,
|
|
repo: "REPO",
|
|
commit: "COMMIT",
|
|
});
|
|
});
|
|
|
|
test("repos.issues", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.issues",
|
|
node,
|
|
repo: "REPO",
|
|
});
|
|
});
|
|
|
|
test("repos.issues with status", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.issues",
|
|
node,
|
|
repo: "REPO",
|
|
status: "closed",
|
|
});
|
|
});
|
|
|
|
test("repos.issue", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.issue",
|
|
node,
|
|
repo: "REPO",
|
|
issue: "ISSUE",
|
|
});
|
|
});
|
|
|
|
test("repos.patches", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patches",
|
|
node,
|
|
repo: "REPO",
|
|
search: "SEARCH",
|
|
});
|
|
});
|
|
|
|
test("repos.patches with search", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patches",
|
|
node,
|
|
repo: "REPO",
|
|
search: "SEARCH",
|
|
});
|
|
});
|
|
|
|
test("repos.patch default view", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patch",
|
|
node,
|
|
repo: "REPO",
|
|
patch: "PATCH",
|
|
});
|
|
});
|
|
|
|
test("repos.patch activity", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patch",
|
|
node,
|
|
repo: "REPO",
|
|
patch: "PATCH",
|
|
view: { name: "activity" },
|
|
});
|
|
});
|
|
|
|
test("repos.patch changes", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patch",
|
|
node,
|
|
repo: "REPO",
|
|
patch: "PATCH",
|
|
view: { name: "changes" },
|
|
});
|
|
});
|
|
|
|
test("repos.patch changes with revision", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patch",
|
|
node,
|
|
repo: "REPO",
|
|
patch: "PATCH",
|
|
view: { name: "changes", revision: "REVISION" },
|
|
});
|
|
});
|
|
|
|
test("repos.patch diff", () => {
|
|
expectParsingInvariant({
|
|
resource: "repo.patch",
|
|
node,
|
|
repo: "REPO",
|
|
patch: "PATCH",
|
|
view: {
|
|
name: "diff",
|
|
fromCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
toCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
},
|
|
});
|
|
});
|
|
|
|
function expectParsingInvariant(route: Route) {
|
|
const path = testExports.routeToPath(route);
|
|
expect(testExports.urlToRoute(new URL(path, origin))).toEqual(route);
|
|
}
|
|
});
|
|
|
|
describe("pathToRoute", () => {
|
|
test("non-existent", () => {
|
|
expectPathToRoute("/foo/baz/bar", null);
|
|
});
|
|
|
|
test("nodes", () => {
|
|
expectPathToRoute("/nodes/example.node.tld", {
|
|
resource: "nodes",
|
|
params: {
|
|
baseUrl: {
|
|
hostname: "example.node.tld",
|
|
scheme: "http",
|
|
port: config.nodes.defaultHttpdPort,
|
|
},
|
|
repoPageIndex: 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
test("repo with trailing slash", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/",
|
|
{
|
|
resource: "repo.source",
|
|
node: {
|
|
hostname: "example.node.tld",
|
|
scheme: "http",
|
|
port: config.nodes.defaultHttpdPort,
|
|
},
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
route: "",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("repo without trailing slash", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
{
|
|
resource: "repo.source",
|
|
node: {
|
|
hostname: "example.node.tld",
|
|
scheme: "http",
|
|
port: config.nodes.defaultHttpdPort,
|
|
},
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
route: "",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("non-existent repo route", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/nope",
|
|
null,
|
|
);
|
|
});
|
|
|
|
function expectPathToRoute(relativeUrl: string, route: Route | null) {
|
|
expect(
|
|
testExports.urlToRoute(new URL(relativeUrl, "http://localhost/")),
|
|
).toEqual(route);
|
|
}
|
|
});
|
|
|
|
describe("extractBaseUrl", () => {
|
|
test("hostname with explicit port", () => {
|
|
const result = testExports.extractBaseUrl("example.com:9000");
|
|
expect(result).toEqual({
|
|
hostname: "example.com",
|
|
port: 9000,
|
|
scheme: config.nodes.defaultHttpdScheme,
|
|
});
|
|
});
|
|
|
|
test("hostname without port uses default port and scheme", () => {
|
|
const result = testExports.extractBaseUrl("example.com");
|
|
expect(result).toEqual({
|
|
hostname: "example.com",
|
|
port: config.nodes.defaultHttpdPort,
|
|
scheme: config.nodes.defaultHttpdScheme,
|
|
});
|
|
});
|
|
|
|
test("localhost without port uses local port and http scheme", () => {
|
|
const result = testExports.extractBaseUrl("localhost");
|
|
expect(result).toEqual({
|
|
hostname: "localhost",
|
|
port: config.nodes.defaultLocalHttpdPort,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("localhost with explicit port uses http scheme", () => {
|
|
const result = testExports.extractBaseUrl("localhost:3000");
|
|
expect(result).toEqual({
|
|
hostname: "localhost",
|
|
port: 3000,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("wildcard localhost domain without port uses local port and http scheme", () => {
|
|
const result = testExports.extractBaseUrl("app.localhost");
|
|
expect(result).toEqual({
|
|
hostname: "app.localhost",
|
|
port: config.nodes.defaultLocalHttpdPort,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("127.0.0.1 without port uses local port and http scheme", () => {
|
|
const result = testExports.extractBaseUrl("127.0.0.1");
|
|
expect(result).toEqual({
|
|
hostname: "127.0.0.1",
|
|
port: config.nodes.defaultLocalHttpdPort,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("127.0.0.1 with explicit port uses http scheme", () => {
|
|
const result = testExports.extractBaseUrl("127.0.0.1:8080");
|
|
expect(result).toEqual({
|
|
hostname: "127.0.0.1",
|
|
port: 8080,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("onion domain without port uses default port and http scheme", () => {
|
|
const result = testExports.extractBaseUrl("example.onion");
|
|
expect(result).toEqual({
|
|
hostname: "example.onion",
|
|
port: config.nodes.defaultHttpdPort,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("onion domain with explicit port uses http scheme", () => {
|
|
const result = testExports.extractBaseUrl("example.onion:9050");
|
|
expect(result).toEqual({
|
|
hostname: "example.onion",
|
|
port: 9050,
|
|
scheme: "http",
|
|
});
|
|
});
|
|
|
|
test("URL-encoded hostname is decoded", () => {
|
|
const result = testExports.extractBaseUrl("example.com%3A8000");
|
|
expect(result).toEqual({
|
|
hostname: "example.com",
|
|
port: 8000,
|
|
scheme: config.nodes.defaultHttpdScheme,
|
|
});
|
|
});
|
|
});
|
|
|
|
// Cloudflare's single-page-application fallback percent-encodes the path on a
|
|
// fresh load or reload, so these paths are what the router actually receives
|
|
// in production for a URL the user sees and copies in its decoded form.
|
|
describe("percent-encoded path segments", () => {
|
|
const node = {
|
|
hostname: "example.node.tld",
|
|
scheme: "http",
|
|
port: config.nodes.defaultHttpdPort,
|
|
};
|
|
|
|
test("encoded repo id is decoded", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad%3AzKtT7DmF9H34KkvcKj9PHW19WzjT/patches/68579c8c8c792d39522c31544ed696b6e3a607fb",
|
|
{
|
|
resource: "repo.patch",
|
|
node,
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
patch: "68579c8c8c792d39522c31544ed696b6e3a607fb",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("decoded repo id is left as is", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
{
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
route: "",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("encoded did is decoded", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/users/did%3Akey%3Az6MktaNvN1tVAno62rCfqvV8bBoTeAyHjuJz9YZjmpBmnCwd",
|
|
{
|
|
resource: "users",
|
|
baseUrl: node,
|
|
did: "did:key:z6MktaNvN1tVAno62rCfqvV8bBoTeAyHjuJz9YZjmpBmnCwd",
|
|
},
|
|
);
|
|
});
|
|
|
|
// Repo aliases are operator-supplied and unvalidated, so a segment that is
|
|
// not a well-formed encoding must not throw out of route parsing.
|
|
test("undecodable repo id is passed through", () => {
|
|
expectPathToRoute("/nodes/example.node.tld/100%", {
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "100%",
|
|
route: "",
|
|
});
|
|
});
|
|
|
|
// Revision segments must stay encoded: branch and tag lookups are keyed on
|
|
// the encoded name, and decoding `%2F` here would turn a tag like
|
|
// `v1.0/release` into two path segments.
|
|
test("encoded tag name in a tree route stays encoded", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad%3AzKtT7DmF9H34KkvcKj9PHW19WzjT/tree/v1.0%2Frelease",
|
|
{
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
route: "v1.0%2Frelease",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("encoded tag name in a history route stays encoded", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad%3AzKtT7DmF9H34KkvcKj9PHW19WzjT/history/v1.0%2Frelease",
|
|
{
|
|
resource: "repo.history",
|
|
node,
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
revision: "v1.0%2Frelease",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("encoded tag name under an explicit peer stays encoded", () => {
|
|
expectPathToRoute(
|
|
"/nodes/example.node.tld/rad%3AzKtT7DmF9H34KkvcKj9PHW19WzjT/remotes/z6MktaNvN1tVAno62rCfqvV8bBoTeAyHjuJz9YZjmpBmnCwd/tree/v1.0%2Frelease",
|
|
{
|
|
resource: "repo.source",
|
|
node,
|
|
repo: "rad:zKtT7DmF9H34KkvcKj9PHW19WzjT",
|
|
peer: "z6MktaNvN1tVAno62rCfqvV8bBoTeAyHjuJz9YZjmpBmnCwd",
|
|
route: "v1.0%2Frelease",
|
|
},
|
|
);
|
|
});
|
|
|
|
// Decoding the repo id changes what `routeToPath` emits, which is what heals
|
|
// the address bar and the "Copy link" button on subsequent navigation.
|
|
test("an encoded path round-trips to its decoded form", () => {
|
|
const route = testExports.urlToRoute(
|
|
new URL(
|
|
"/nodes/example.node.tld/rad%3AzKtT7DmF9H34KkvcKj9PHW19WzjT/tree/v1.0%2Frelease",
|
|
"http://localhost/",
|
|
),
|
|
);
|
|
expect(testExports.routeToPath(route!)).toEqual(
|
|
"/nodes/example.node.tld/rad:zKtT7DmF9H34KkvcKj9PHW19WzjT/tree/v1.0%2Frelease",
|
|
);
|
|
});
|
|
|
|
function expectPathToRoute(relativeUrl: string, route: Route | null) {
|
|
expect(
|
|
testExports.urlToRoute(new URL(relativeUrl, "http://localhost/")),
|
|
).toEqual(route);
|
|
}
|
|
});
|