mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: give plugins their own settings and one settings screen
This commit is contained in:
1 parent
cc15559ea1
commit
49f0286a14
66 files changed
+36497
-451
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { parseManifest } from "@termix/plugin-sdk/manifest";
|
||||
import { createMockCtx } from "@termix/plugin-sdk/testing";
|
||||
|
||||
/**
|
||||
* The API key and base URL are this plugin's own settings now.
|
||||
*
|
||||
* They used to live in the core `settings` table behind a core route that
|
||||
* existed only for this plugin, with the key stored in plaintext. These tests
|
||||
* pin the shape core renders the page from, and that reading a plugin's own
|
||||
* settings needs no capability.
|
||||
*/
|
||||
const manifest = parseManifest(
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.resolve(import.meta.dirname, "../../manifest.json"),
|
||||
"utf8",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
describe("tailscale settings contribution", () => {
|
||||
it("parses cleanly", () => {
|
||||
expect(manifest.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("declares the API key as a secret, so it is encrypted and redacted", () => {
|
||||
const apiKey = manifest.manifest?.contributes?.settings?.admin?.find(
|
||||
(field) => field.key === "apiKey",
|
||||
);
|
||||
|
||||
expect(apiKey?.type).toBe("secret");
|
||||
});
|
||||
|
||||
it("declares the base URL as plain text, because it is not a secret", () => {
|
||||
const baseUrl = manifest.manifest?.contributes?.settings?.admin?.find(
|
||||
(field) => field.key === "apiBaseUrl",
|
||||
);
|
||||
|
||||
expect(baseUrl?.type).toBe("string");
|
||||
expect(baseUrl?.default).toBe("");
|
||||
});
|
||||
|
||||
it("puts both fields on the admin scope, not the user one", () => {
|
||||
const settings = manifest.manifest?.contributes?.settings;
|
||||
|
||||
expect(settings?.admin?.map((field) => field.key)).toContain("apiKey");
|
||||
expect(settings?.user).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps settings:read-core, which it still holds for core keys", () => {
|
||||
expect(manifest.manifest?.capabilities).toContain("settings:read-core");
|
||||
});
|
||||
});
|
||||
|
||||
describe("reading its own settings", () => {
|
||||
it("needs no capability", async () => {
|
||||
// Nothing granted at all: a plugin reading its own configuration must not
|
||||
// have to declare anything for it.
|
||||
const { ctx } = createMockCtx({
|
||||
pluginId: "tailscale",
|
||||
capabilities: [],
|
||||
settings: { apiKey: "tskey-api-test", apiBaseUrl: "" },
|
||||
});
|
||||
|
||||
await expect(ctx.settings.get("apiKey")).resolves.toBe("tskey-api-test");
|
||||
});
|
||||
|
||||
it("can write and read a value back", async () => {
|
||||
const { ctx } = createMockCtx({ pluginId: "tailscale", capabilities: [] });
|
||||
|
||||
await ctx.settings.set("apiBaseUrl", "https://headscale.example");
|
||||
|
||||
await expect(ctx.settings.get("apiBaseUrl")).resolves.toBe(
|
||||
"https://headscale.example",
|
||||
);
|
||||
});
|
||||
|
||||
it("still needs settings:read-core to reach a core key", async () => {
|
||||
const { ctx } = createMockCtx({ pluginId: "tailscale", capabilities: [] });
|
||||
|
||||
await expect(ctx.settings.readCore("app_name")).rejects.toThrow(
|
||||
/settings:read-core/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { AdminTailscaleSection } from "../../src/frontend/AdminTailscaleSection";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe("AdminTailscaleSection", () => {
|
||||
it("renders the API key and base URL fields and saves on click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleSave = vi.fn();
|
||||
render(
|
||||
<AdminTailscaleSection
|
||||
open
|
||||
onToggle={vi.fn()}
|
||||
tailscaleApiKey="tskey-api-abc"
|
||||
setTailscaleApiKey={vi.fn()}
|
||||
tailscaleApiBaseUrl="https://api.tailscale.com/api/v2"
|
||||
setTailscaleApiBaseUrl={vi.fn()}
|
||||
handleSaveTailscaleApiKey={handleSave}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
(
|
||||
screen.getByPlaceholderText(
|
||||
"tskey-api-... / hskey-api-...",
|
||||
) as HTMLInputElement
|
||||
).value,
|
||||
).toBe("tskey-api-abc");
|
||||
expect(
|
||||
(
|
||||
screen.getByPlaceholderText(
|
||||
"https://api.tailscale.com/api/v2",
|
||||
) as HTMLInputElement
|
||||
).value,
|
||||
).toBe("https://api.tailscale.com/api/v2");
|
||||
|
||||
await user.click(screen.getByText("common.save"));
|
||||
expect(handleSave).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not render its content when closed", () => {
|
||||
render(
|
||||
<AdminTailscaleSection
|
||||
open={false}
|
||||
onToggle={vi.fn()}
|
||||
tailscaleApiKey=""
|
||||
setTailscaleApiKey={vi.fn()}
|
||||
tailscaleApiBaseUrl=""
|
||||
setTailscaleApiBaseUrl={vi.fn()}
|
||||
handleSaveTailscaleApiKey={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByPlaceholderText("tskey-api-... / hskey-api-..."),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user