mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: plugin docs and env manifest fields, in-app docs links, SDK 1.0.3
This commit is contained in:
1 parent
7fcda3bb01
commit
cfab6f764c
65 files changed
+1665
-294
No files matched your search
@@ -14,7 +14,10 @@ import {
|
||||
isTermixCompatible,
|
||||
youtubeVideoId,
|
||||
pluginFeatures,
|
||||
pluginDocsUrl,
|
||||
pluginEnvVars,
|
||||
} from "../../plugins/manifest.js";
|
||||
import { pluginDocsPage } from "@termix-ssh/plugin-sdk/docs";
|
||||
|
||||
function base(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -929,3 +932,73 @@ describe("youtubeVideoId", () => {
|
||||
expect(youtubeVideoId(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("docs and env fields", () => {
|
||||
it("accepts an https docs link and well formed env entries", () => {
|
||||
expect(
|
||||
validateManifest(
|
||||
base({
|
||||
docs: "https://docs.termix.site/plugins/sample-plugin",
|
||||
env: [
|
||||
{ name: "SAMPLE_URL", description: "Where to connect." },
|
||||
{
|
||||
name: "SAMPLE_TOKEN",
|
||||
description: "Token.",
|
||||
secret: true,
|
||||
required: true,
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("refuses a docs link that is not https", () => {
|
||||
const errors = validateManifest(base({ docs: "http://example.com" }));
|
||||
expect(errors.join(" ")).toMatch(/"docs" must be an https URL/);
|
||||
});
|
||||
|
||||
it("refuses bad env entries", () => {
|
||||
const errors = validateManifest(
|
||||
base({
|
||||
env: [
|
||||
{ name: "lower_case", description: "x" },
|
||||
{ name: "DUP", description: "x" },
|
||||
{ name: "DUP", description: "x" },
|
||||
{ name: "NO_DESC", description: "" },
|
||||
{ name: "EXTRA", description: "x", other: 1 },
|
||||
{ name: "FLAG", description: "x", secret: "yes" },
|
||||
],
|
||||
}),
|
||||
).join(" ");
|
||||
expect(errors).toMatch(/env\[0\]\.name/);
|
||||
expect(errors).toMatch(/env\[2\]\.name" repeats DUP/);
|
||||
expect(errors).toMatch(/env\[3\]\.description/);
|
||||
expect(errors).toMatch(/Unknown field "other" in env\[4\]/);
|
||||
expect(errors).toMatch(/env\[5\]\.secret/);
|
||||
});
|
||||
|
||||
it("reads docs links and env entries leniently", () => {
|
||||
expect(pluginDocsUrl("https://docs.termix.site/plugins/x/")).toBe(
|
||||
"https://docs.termix.site/plugins/x",
|
||||
);
|
||||
expect(pluginDocsUrl("file:///etc/passwd")).toBeNull();
|
||||
expect(pluginDocsUrl(42)).toBeNull();
|
||||
expect(
|
||||
pluginEnvVars([
|
||||
{ name: "OK_VAR", description: "fine" },
|
||||
{ name: "bad", description: "x" },
|
||||
"nope",
|
||||
]),
|
||||
).toEqual([{ name: "OK_VAR", description: "fine" }]);
|
||||
});
|
||||
|
||||
it("joins pages and anchors onto a docs link", () => {
|
||||
const base = "https://docs.termix.site/plugins/docker";
|
||||
expect(pluginDocsPage(base)).toBe(base);
|
||||
expect(pluginDocsPage(base, "/setup/")).toBe(`${base}/setup`);
|
||||
expect(pluginDocsPage(base, "setup", "#ports")).toBe(`${base}/setup#ports`);
|
||||
expect(pluginDocsPage(base, "", "top")).toBe(`${base}#top`);
|
||||
});
|
||||
});
|
||||
@@ -155,6 +155,23 @@ describe("parseRegistryIndex", () => {
|
||||
expect(index.plugins[1].features).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps only https docs links", () => {
|
||||
const index = parseRegistryIndex({
|
||||
plugins: [
|
||||
{
|
||||
id: "docker",
|
||||
docs: "https://docs.termix.site/plugins/docker/",
|
||||
versions: [version()],
|
||||
},
|
||||
{ id: "other", docs: "javascript:alert(1)", versions: [version()] },
|
||||
],
|
||||
});
|
||||
expect(index.plugins[0].docs).toBe(
|
||||
"https://docs.termix.site/plugins/docker",
|
||||
);
|
||||
expect(index.plugins[1].docs).toBeUndefined();
|
||||
});
|
||||
|
||||
it("refuses something that is not an index", () => {
|
||||
expect(() => parseRegistryIndex({ nope: true })).toThrow(/no plugins/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
buildOpenApi,
|
||||
hasOpenApiPaths,
|
||||
TERMIX_SECURITY_SCHEMES,
|
||||
} from "@termix-ssh/plugin-sdk/openapi";
|
||||
|
||||
let dir: string | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
||||
dir = null;
|
||||
});
|
||||
|
||||
function source(files: Record<string, string>): string {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-openapi-"));
|
||||
for (const [name, text] of Object.entries(files)) {
|
||||
fs.writeFileSync(path.join(dir, name), text);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("buildOpenApi", () => {
|
||||
it("reads @openapi blocks from TypeScript sources", async () => {
|
||||
const root = source({
|
||||
"routes.ts": [
|
||||
"/**",
|
||||
" * @openapi",
|
||||
" * /plugin-api/sample/items:",
|
||||
" * get:",
|
||||
" * summary: List items",
|
||||
" * responses:",
|
||||
" * 200:",
|
||||
" * description: The items",
|
||||
" */",
|
||||
'router.get("/items", handler);',
|
||||
].join("\n"),
|
||||
});
|
||||
const spec = await buildOpenApi({
|
||||
title: "Sample API",
|
||||
version: "1.2.3",
|
||||
files: [path.join(root, "*.ts")],
|
||||
tags: [{ name: "Sample" }],
|
||||
});
|
||||
expect(hasOpenApiPaths(spec)).toBe(true);
|
||||
expect(Object.keys(spec.paths ?? {})).toEqual(["/plugin-api/sample/items"]);
|
||||
expect(spec).toMatchObject({
|
||||
openapi: "3.0.3",
|
||||
info: { title: "Sample API", version: "1.2.3" },
|
||||
tags: [{ name: "Sample" }],
|
||||
components: { securitySchemes: TERMIX_SECURITY_SCHEMES },
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a spec with no paths when nothing is documented", async () => {
|
||||
const root = source({ "index.ts": "export const x = 1;" });
|
||||
const spec = await buildOpenApi({
|
||||
title: "Empty",
|
||||
version: "1.0.0",
|
||||
files: [path.join(root, "*.ts")],
|
||||
});
|
||||
expect(hasOpenApiPaths(spec)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user