mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-28 16:00:45 +08:00
parseRepositoryId threw instead of returning undefined, so the seed form crashed on a typo instead of saying the RID is invalid.
276 lines
7.4 KiB
TypeScript
276 lines
7.4 KiB
TypeScript
import type { Config } from "@bindings/config/Config";
|
|
|
|
import { describe, expect, test, vi } from "vitest";
|
|
|
|
import {
|
|
coAuthors,
|
|
explorerHost,
|
|
explorerUrl,
|
|
formatRepositoryId,
|
|
formatTimestamp,
|
|
identityKey,
|
|
isCommit,
|
|
parseNodeId,
|
|
parseRepositoryId,
|
|
pluralize,
|
|
revisionPosition,
|
|
safeHttpUrl,
|
|
truncateDid,
|
|
unqualifyBranch,
|
|
} from "@app/lib/utils";
|
|
|
|
const rid = "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe";
|
|
const nid = "z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK";
|
|
|
|
describe("safeHttpUrl", () => {
|
|
test.each([
|
|
["http://example.com/", "http://example.com/"],
|
|
["https://example.com/path?a=1&b=2", "https://example.com/path?a=1&b=2"],
|
|
["HTTP://EXAMPLE.com/Path", "http://example.com/Path"],
|
|
["http://example.com/<script>", "http://example.com/%3Cscript%3E"],
|
|
])("accepts %j", (input, expected) => {
|
|
expect(safeHttpUrl(input)).toBe(expected);
|
|
});
|
|
|
|
test.each([
|
|
"javascript:alert(1)",
|
|
"\tjavascript:alert(1)",
|
|
"data:text/html,<script>alert(1)</script>",
|
|
"vbscript:msgbox(1)",
|
|
"file:///etc/passwd",
|
|
"//example.com",
|
|
"/relative/path",
|
|
"relative",
|
|
"",
|
|
"not a url",
|
|
])("rejects %j", input => {
|
|
expect(safeHttpUrl(input)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("coAuthors", () => {
|
|
test("reads trailers from the final paragraph", () => {
|
|
expect(
|
|
coAuthors(
|
|
"Fix the thing\n\nSome body text.\n\nSigned-off-by: A <a@x>\nCo-authored-by: Alice Lidell <alice@lidell.com>\nCo-authored-by: Bob <bob@example.com>",
|
|
),
|
|
).toEqual([
|
|
{ name: "Alice Lidell", email: "alice@lidell.com" },
|
|
{ name: "Bob", email: "bob@example.com" },
|
|
]);
|
|
});
|
|
|
|
test("is case-insensitive", () => {
|
|
expect(
|
|
coAuthors("Subject\n\nCO-AUTHORED-BY: Bob <bob@example.com>"),
|
|
).toEqual([{ name: "Bob", email: "bob@example.com" }]);
|
|
});
|
|
|
|
test("ignores a trailer that is not in the last paragraph", () => {
|
|
expect(
|
|
coAuthors(
|
|
"Subject\n\nCo-authored-by: Bob <bob@example.com>\n\nThat line was quoted, not a trailer.",
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("keeps the last paragraph rule for CRLF messages", () => {
|
|
expect(
|
|
coAuthors(
|
|
"Subject\r\n\r\nCo-authored-by: Bob <bob@example.com>\r\n\r\nThat line was quoted, not a trailer.\r\n",
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("reads a trailer that carries no name", () => {
|
|
expect(coAuthors("Subject\n\nCo-authored-by: <bob@example.com>")).toEqual([
|
|
{ name: "", email: "bob@example.com" },
|
|
]);
|
|
});
|
|
|
|
test("ignores malformed lines", () => {
|
|
expect(
|
|
coAuthors(
|
|
"Subject\n\nCo-authored-by: Bob\nCo-authored-by:\nNot-a-trailer: x <y@z>",
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("returns nothing when there are no trailers", () => {
|
|
expect(coAuthors("Subject\n\nJust a body.")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("parseRepositoryId", () => {
|
|
test("parses a RID with and without the rad: prefix", () => {
|
|
expect(parseRepositoryId(rid)).toEqual({
|
|
prefix: "rad:",
|
|
pubkey: "z3fpY7nttPPa6MBnAv2DccHzQJnqe",
|
|
});
|
|
expect(parseRepositoryId("z3fpY7nttPPa6MBnAv2DccHzQJnqe")).toEqual({
|
|
prefix: "rad:",
|
|
pubkey: "z3fpY7nttPPa6MBnAv2DccHzQJnqe",
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
"",
|
|
"rad:",
|
|
"rad:z3fpY7ntt",
|
|
`${rid} `,
|
|
"rad:x3fpY7nttPPa6MBn",
|
|
// Base58 has no 0, O, I or l.
|
|
"rad:z0OIl",
|
|
])("rejects %j", input => {
|
|
expect(parseRepositoryId(input)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("formatRepositoryId", () => {
|
|
test("truncates a valid RID", () => {
|
|
expect(formatRepositoryId(rid)).toBe("rad:z3fpY7…zQJnqe");
|
|
});
|
|
|
|
test("returns an invalid RID unchanged", () => {
|
|
expect(formatRepositoryId("not-a-rid")).toBe("not-a-rid");
|
|
});
|
|
});
|
|
|
|
describe("parseNodeId", () => {
|
|
test("parses a NID with and without the did:key: prefix", () => {
|
|
expect(parseNodeId(nid)).toEqual({ prefix: "did:key:", pubkey: nid });
|
|
expect(parseNodeId(`did:key:${nid}`)).toEqual({
|
|
prefix: "did:key:",
|
|
pubkey: nid,
|
|
});
|
|
});
|
|
|
|
test("rejects a RID", () => {
|
|
expect(parseNodeId("z3fpY7nttPPa6MBnAv2DccHzQJnqe")).toBeUndefined();
|
|
});
|
|
|
|
test("rejects input that is not base58", () => {
|
|
vi.spyOn(console, "error").mockReturnValue(undefined);
|
|
expect(parseNodeId("z0OIl")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
test("truncateDid", () => {
|
|
expect(truncateDid(`did:key:${nid}`)).toBe("did:key:z6MkqG…WTvLSK");
|
|
});
|
|
|
|
test.each([
|
|
["a".repeat(40), true],
|
|
["A".repeat(40), false],
|
|
["a".repeat(39), false],
|
|
["g".repeat(40), false],
|
|
])("isCommit(%j) is %j", (input, expected) => {
|
|
expect(isCommit(input)).toBe(expected);
|
|
});
|
|
|
|
test.each([
|
|
["refs/heads/main", "main"],
|
|
["refs/heads/feature/x", "feature/x"],
|
|
["main", "main"],
|
|
["refs/tags/v1", "refs/tags/v1"],
|
|
])("unqualifyBranch(%j) is %j", (input, expected) => {
|
|
expect(unqualifyBranch(input)).toBe(expected);
|
|
});
|
|
|
|
describe("formatTimestamp", () => {
|
|
const now = Date.UTC(2026, 0, 1);
|
|
const minute = 60 * 1000;
|
|
const hour = 60 * minute;
|
|
const day = 24 * hour;
|
|
|
|
test.each([
|
|
[0, "now"],
|
|
[59 * 1000, "now"],
|
|
[minute, "1m"],
|
|
[59 * minute, "59m"],
|
|
[hour, "1h"],
|
|
[23 * hour, "23h"],
|
|
[day, "1d"],
|
|
[30 * day, "30d"],
|
|
[31 * day, "1mo"],
|
|
[364 * day, "11mo"],
|
|
[365 * day, "1y"],
|
|
[3 * 365 * day, "3y"],
|
|
])("%j ms ago is %j", (elapsed, expected) => {
|
|
expect(formatTimestamp(now - elapsed, now)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
["issue", 0, "issues"],
|
|
["issue", 1, "issue"],
|
|
["issue", 2, "issues"],
|
|
["patch", 2, "patches"],
|
|
["box", 2, "boxes"],
|
|
["class", 2, "classes"],
|
|
])("pluralize(%j, %j) is %j", (singular, count, expected) => {
|
|
expect(pluralize(singular, count)).toBe(expected);
|
|
});
|
|
|
|
test("revisionPosition", () => {
|
|
const revisions = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
expect(revisionPosition(revisions, "a")).toBe(1);
|
|
expect(revisionPosition(revisions, "c")).toBe(3);
|
|
expect(revisionPosition(revisions, "x")).toBeUndefined();
|
|
});
|
|
|
|
describe("identityKey", () => {
|
|
test("matches identities by email case-insensitively", () => {
|
|
expect(identityKey({ name: "Alice", email: " Alice@Example.com " })).toBe(
|
|
identityKey({ name: "A. Lidell", email: "alice@example.com" }),
|
|
);
|
|
});
|
|
|
|
test("falls back to the name when there is no email", () => {
|
|
expect(identityKey({ name: "Alice", email: "" })).toBe("name:alice");
|
|
expect(identityKey({ name: "Alice", email: "" })).not.toBe(
|
|
identityKey({ name: "Bob", email: "" }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("explorer links", () => {
|
|
function config(overrides: Partial<Config>): Config {
|
|
return {
|
|
publicExplorer: "https://radicle.network/nodes/$host/$rid$path",
|
|
preferredSeeds: [],
|
|
...overrides,
|
|
} as Config;
|
|
}
|
|
|
|
test("uses the first preferred seed host", () => {
|
|
expect(
|
|
explorerUrl(
|
|
`${rid}/issues/abc`,
|
|
config({
|
|
preferredSeeds: [
|
|
`${nid}@seed.example.com:8776`,
|
|
`${nid}@other.example.com:8776`,
|
|
],
|
|
}),
|
|
),
|
|
).toBe(`https://radicle.network/nodes/seed.example.com/${rid}/issues/abc`);
|
|
});
|
|
|
|
test("falls back to the default seed", () => {
|
|
expect(explorerUrl(rid, config({}))).toBe(
|
|
`https://radicle.network/nodes/rosa.radicle.network/${rid}`,
|
|
);
|
|
});
|
|
|
|
test("explorerHost returns the host of the template", () => {
|
|
expect(explorerHost(config({}))).toBe("radicle.network");
|
|
});
|
|
|
|
test("explorerHost returns an unparseable template unchanged", () => {
|
|
expect(explorerHost(config({ publicExplorer: "not a url" }))).toBe(
|
|
"not a url",
|
|
);
|
|
});
|
|
});
|