mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
385 lines
10 KiB
TypeScript
385 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { PluginSummary, RegistryPluginEntry } from "@/api/plugins-api";
|
|
import {
|
|
categoriesOf,
|
|
describeContributions,
|
|
formatBytes,
|
|
formatCount,
|
|
groupChanges,
|
|
matchesQuery,
|
|
mergeReleaseNotes,
|
|
mergePlugins,
|
|
orderByRisk,
|
|
pluginSource,
|
|
pluginStatus,
|
|
reportIssueUrl,
|
|
sortPlugins,
|
|
uploadEntry,
|
|
youtubeEmbedUrl,
|
|
} from "@/plugins/plugin-model";
|
|
|
|
function summary(overrides: Partial<PluginSummary> = {}): PluginSummary {
|
|
return {
|
|
id: "docker",
|
|
name: "Docker",
|
|
version: "1.0.0",
|
|
enabled: true,
|
|
state: "active",
|
|
contributes: null,
|
|
capabilities: ["kv:own"],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function entry(
|
|
overrides: Partial<RegistryPluginEntry> = {},
|
|
): RegistryPluginEntry {
|
|
return {
|
|
id: "docker",
|
|
name: "Docker",
|
|
description: "Containers",
|
|
author: "Termix",
|
|
category: "Infrastructure",
|
|
versions: [
|
|
{
|
|
version: "1.1.0",
|
|
compatible: true,
|
|
capabilities: ["kv:own", "ssh:connect"],
|
|
publishedAt: "2026-10-02",
|
|
size: 1,
|
|
},
|
|
],
|
|
latestVersion: "1.1.0",
|
|
installed: true,
|
|
installedVersion: "1.0.0",
|
|
updateAvailable: true,
|
|
addedCapabilities: ["ssh:connect"],
|
|
pinnedVersion: null,
|
|
autoUpdate: false,
|
|
bundled: true,
|
|
installCount: null,
|
|
installCountSource: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("mergePlugins", () => {
|
|
it("joins an installed plugin with its registry entry", () => {
|
|
const [row] = mergePlugins([summary()], [entry()]);
|
|
expect(row).toMatchObject({
|
|
installed: true,
|
|
version: "1.0.0",
|
|
latestVersion: "1.1.0",
|
|
updateAvailable: true,
|
|
addedCapabilities: ["ssh:connect"],
|
|
capabilities: ["kv:own"],
|
|
category: "Infrastructure",
|
|
inRegistry: true,
|
|
status: "running",
|
|
});
|
|
});
|
|
|
|
it("lists registry plugins that are not installed with the latest capabilities", () => {
|
|
const rows = mergePlugins(
|
|
[],
|
|
[entry({ installed: false, installedVersion: null })],
|
|
);
|
|
expect(rows[0]).toMatchObject({
|
|
installed: false,
|
|
version: null,
|
|
status: null,
|
|
capabilities: ["kv:own", "ssh:connect"],
|
|
});
|
|
});
|
|
|
|
it("keeps installed plugins when the registry is unreachable", () => {
|
|
const rows = mergePlugins([summary()], null);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].inRegistry).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("pluginStatus", () => {
|
|
it("maps loader and stored states", () => {
|
|
expect(pluginStatus(summary({ state: "failed" }))).toBe("failed");
|
|
expect(pluginStatus(summary({ state: "blocked" }))).toBe("blocked");
|
|
expect(pluginStatus(summary({ enabled: false, state: "disabled" }))).toBe(
|
|
"disabled",
|
|
);
|
|
expect(pluginStatus(summary())).toBe("running");
|
|
});
|
|
});
|
|
|
|
describe("sorting and filtering", () => {
|
|
const rows = mergePlugins(
|
|
[],
|
|
[
|
|
entry({ id: "b", name: "Beta", category: "Monitoring" }),
|
|
entry({
|
|
id: "a",
|
|
name: "Alpha",
|
|
category: "Access",
|
|
description: "Sign in",
|
|
versions: [
|
|
{
|
|
version: "1.0.0",
|
|
compatible: true,
|
|
capabilities: [],
|
|
publishedAt: "2026-10-05",
|
|
size: 1,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
);
|
|
|
|
it("sorts by name, date and category", () => {
|
|
expect(sortPlugins(rows, "name").map((r) => r.id)).toEqual(["a", "b"]);
|
|
expect(sortPlugins(rows, "updated").map((r) => r.id)).toEqual(["a", "b"]);
|
|
expect(sortPlugins(rows, "category").map((r) => r.id)).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("searches name, description and id", () => {
|
|
expect(
|
|
rows.filter((r) => matchesQuery(r, "sign")).map((r) => r.id),
|
|
).toEqual(["a"]);
|
|
expect(rows.filter((r) => matchesQuery(r, " ")).length).toBe(2);
|
|
});
|
|
|
|
it("collects categories", () => {
|
|
expect(categoriesOf(rows)).toEqual(["Access", "Monitoring"]);
|
|
});
|
|
});
|
|
|
|
describe("helpers", () => {
|
|
it("orders capabilities worst first", () => {
|
|
expect(orderByRisk(["kv:own", "hosts:write", "credentials:read"])).toEqual([
|
|
"credentials:read",
|
|
"hosts:write",
|
|
"kv:own",
|
|
]);
|
|
});
|
|
|
|
it("counts contributions", () => {
|
|
expect(
|
|
describeContributions({
|
|
tabs: [{}, {}],
|
|
settings: { admin: [] },
|
|
auth: { loginMethods: [{}], secondFactors: [] },
|
|
panels: [],
|
|
}),
|
|
).toEqual([
|
|
{ kind: "tabs", count: 2 },
|
|
{ kind: "auth", count: 1 },
|
|
{ kind: "settings", count: 1 },
|
|
]);
|
|
});
|
|
|
|
it("formats sizes", () => {
|
|
expect(formatBytes(512)).toBe("512 B");
|
|
expect(formatBytes(2048)).toBe("2.0 KB");
|
|
expect(formatBytes(50 * 1024 * 1024)).toBe("50 MB");
|
|
});
|
|
});
|
|
|
|
describe("popularity", () => {
|
|
it("sorts by install count, unknown counts last", () => {
|
|
const rows = mergePlugins(
|
|
[],
|
|
[
|
|
entry({ id: "a", name: "A", installed: false, installCount: 5 }),
|
|
entry({ id: "b", name: "B", installed: false, installCount: null }),
|
|
entry({ id: "c", name: "C", installed: false, installCount: 90 }),
|
|
],
|
|
);
|
|
expect(sortPlugins(rows, "popular").map((r) => r.id)).toEqual([
|
|
"c",
|
|
"a",
|
|
"b",
|
|
]);
|
|
});
|
|
|
|
it("formats counts roughly", () => {
|
|
expect(formatCount(950)).toBe("950");
|
|
expect(formatCount(1234)).toBe("1.2k");
|
|
expect(formatCount(45_000)).toBe("45k");
|
|
expect(formatCount(2_500_000)).toBe("2.5M");
|
|
});
|
|
});
|
|
|
|
describe("source and trust", () => {
|
|
it("tells bundled, signed and unverified copies apart", () => {
|
|
expect(pluginSource(summary({ source: "bundled", tier: "bundled" }))).toBe(
|
|
"bundled",
|
|
);
|
|
expect(
|
|
pluginSource(
|
|
summary({ source: "user", tier: "official", signedBy: "k" }),
|
|
),
|
|
).toBe("official");
|
|
expect(pluginSource(summary({ source: "user", tier: "community" }))).toBe(
|
|
"unverified",
|
|
);
|
|
expect(
|
|
pluginSource(
|
|
summary({ source: "user", tier: "unverified", signedBy: "k" }),
|
|
),
|
|
).toBe("unverified");
|
|
});
|
|
|
|
it("marks an upload as unverified", () => {
|
|
const row = uploadEntry({
|
|
token: "t",
|
|
id: "notes",
|
|
name: "Notes",
|
|
version: "0.2.0",
|
|
description: "",
|
|
author: "me",
|
|
capabilities: ["kv:own"],
|
|
replaces: "0.1.0",
|
|
});
|
|
expect(row).toMatchObject({
|
|
unverified: true,
|
|
installed: true,
|
|
version: "0.1.0",
|
|
latestVersion: "0.2.0",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("reportIssueUrl", () => {
|
|
const base = {
|
|
id: "docker",
|
|
repository: "https://github.com/Termix-SSH/Plugin-Docker.git",
|
|
version: "1.0.0",
|
|
latestVersion: "1.1.0",
|
|
status: "failed" as const,
|
|
source: "official" as const,
|
|
lastError: "boom",
|
|
};
|
|
|
|
it("fills the bug form on an official plugin repo", () => {
|
|
const parsed = new URL(reportIssueUrl(base, "26.10.0")!);
|
|
expect(parsed.origin + parsed.pathname).toBe(
|
|
"https://github.com/Termix-SSH/Plugin-Docker/issues/new",
|
|
);
|
|
expect(parsed.searchParams.get("template")).toBe("bug_report.yml");
|
|
expect(parsed.searchParams.get("termix-version")).toBe("26.10.0");
|
|
expect(parsed.searchParams.get("plugin-version")).toBe("1.0.0");
|
|
expect(parsed.searchParams.get("environment")).toContain(
|
|
"Plugin docker, official, failed",
|
|
);
|
|
expect(parsed.searchParams.get("logs")).toBe("boom");
|
|
expect(parsed.searchParams.has("body")).toBe(false);
|
|
});
|
|
|
|
it("uses a plain body for an unverified plugin", () => {
|
|
const parsed = new URL(
|
|
reportIssueUrl({ ...base, source: "unverified" }, "26.10.0")!,
|
|
);
|
|
expect(parsed.searchParams.has("template")).toBe(false);
|
|
expect(parsed.searchParams.get("title")).toBe("[docker] ");
|
|
const body = parsed.searchParams.get("body")!;
|
|
expect(body).toContain("**Plugin:** docker");
|
|
expect(body).toContain("**Version:** 1.0.0");
|
|
expect(body).toContain("**Source:** unverified");
|
|
expect(body).toContain("**State:** failed");
|
|
expect(body).toContain("**Termix:** 26.10.0");
|
|
expect(body).toContain("boom");
|
|
});
|
|
|
|
it("gives nothing for a repo that is not on GitHub", () => {
|
|
expect(
|
|
reportIssueUrl({
|
|
...base,
|
|
repository: "https://gitlab.com/a/b",
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("mergeReleaseNotes", () => {
|
|
const registryVersion = (version: string, extra = {}) => ({
|
|
version,
|
|
compatible: true,
|
|
capabilities: [],
|
|
size: 1,
|
|
publishedAt: "2026-10-01T00:00:00Z",
|
|
...extra,
|
|
});
|
|
|
|
it("joins registry and local notes newest first, local winning", () => {
|
|
const rows = mergeReleaseNotes(
|
|
[
|
|
registryVersion("1.2.0", {
|
|
compatible: false,
|
|
notes: { changes: [{ type: "added", text: "Registry" }] },
|
|
}),
|
|
registryVersion("1.0.0", {
|
|
releaseNotesUrl: "https://github.com/x/y/releases/tag/v1.0.0",
|
|
notes: { changes: [{ type: "added", text: "Old" }] },
|
|
}),
|
|
],
|
|
[
|
|
{
|
|
version: "1.0.0",
|
|
date: "2026-09-01",
|
|
summary: "First.",
|
|
changes: [{ type: "added", text: "Local" }],
|
|
},
|
|
{ version: "0.9.0", changes: [{ type: "fixed", text: "Beta" }] },
|
|
],
|
|
);
|
|
expect(rows.map((r) => r.version)).toEqual(["1.2.0", "1.0.0", "0.9.0"]);
|
|
expect(rows[0]).toMatchObject({ compatible: false });
|
|
expect(rows[1]).toEqual({
|
|
version: "1.0.0",
|
|
date: "2026-09-01",
|
|
summary: "First.",
|
|
changes: [{ type: "added", text: "Local" }],
|
|
compatible: true,
|
|
releaseNotesUrl: "https://github.com/x/y/releases/tag/v1.0.0",
|
|
});
|
|
expect(rows[2]).toMatchObject({ date: null, compatible: true });
|
|
});
|
|
|
|
it("is empty with nothing to show", () => {
|
|
expect(mergeReleaseNotes([], [])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("groupChanges", () => {
|
|
it("groups by type in a fixed order", () => {
|
|
expect(
|
|
groupChanges([
|
|
{ type: "fixed", text: "b" },
|
|
{ type: "added", text: "a" },
|
|
{ type: "fixed", text: "c" },
|
|
]),
|
|
).toEqual([
|
|
{ type: "added", items: ["a"] },
|
|
{ type: "fixed", items: ["b", "c"] },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("youtubeEmbedUrl", () => {
|
|
it("builds a privacy embed from a video id only", () => {
|
|
expect(youtubeEmbedUrl("dQw4w9WgXcQ")).toBe(
|
|
"https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?rel=0",
|
|
);
|
|
});
|
|
|
|
it("refuses anything that is not an id", () => {
|
|
for (const value of [
|
|
undefined,
|
|
"",
|
|
"https://evil.example/x",
|
|
"dQw4w9WgXcQ/../../x",
|
|
"javascript:alert(1)",
|
|
]) {
|
|
expect(youtubeEmbedUrl(value)).toBeNull();
|
|
}
|
|
});
|
|
});
|