mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat(snippets): bring back sharing, target hosts and drag reorder
This commit is contained in:
1 parent
796db8f0a8
commit
1fd89acd90
16 files changed
+1376
-53
No files matched your search
@@ -30,6 +30,8 @@ export async function startServer(
|
||||
permissions?: string[];
|
||||
users?: string[];
|
||||
capabilities?: string[];
|
||||
shareableUsers?: { id: string; username: string }[];
|
||||
shareableRoles?: { id: number; name: string; displayName: string }[];
|
||||
} = {},
|
||||
): Promise<TestServer> {
|
||||
const db = await createTestDb(pluginDir);
|
||||
@@ -45,6 +47,8 @@ export async function startServer(
|
||||
manifest,
|
||||
capabilities: options.capabilities ?? manifest.capabilities,
|
||||
db: db.database,
|
||||
shareableUsers: options.shareableUsers,
|
||||
shareableRoles: options.shareableRoles,
|
||||
router: () => (router = express.Router()),
|
||||
permissions: options.permissions ?? [
|
||||
"snippets.view",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { manifest, startServer, type TestServer } from "./helpers";
|
||||
|
||||
let server: TestServer | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
await server?.close();
|
||||
server = null;
|
||||
});
|
||||
|
||||
describe("share target routes", () => {
|
||||
it("lists users and roles to share with", async () => {
|
||||
server = await startServer({
|
||||
shareableUsers: [{ id: "user-2", username: "bob" }],
|
||||
shareableRoles: [{ id: 3, name: "ops", displayName: "Ops" }],
|
||||
});
|
||||
const users = await server.request("GET", "/share-targets/users");
|
||||
expect(users.status).toBe(200);
|
||||
expect(users.body).toEqual({ users: [{ id: "user-2", username: "bob" }] });
|
||||
|
||||
const roles = await server.request("GET", "/share-targets/roles");
|
||||
expect(roles.status).toBe(200);
|
||||
expect(roles.body).toEqual({
|
||||
roles: [{ id: 3, name: "ops", displayName: "Ops" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("needs the share permission", async () => {
|
||||
server = await startServer({ permissions: ["snippets.view"] });
|
||||
expect((await server.request("GET", "/share-targets/users")).status).toBe(
|
||||
403,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails without the hosts:write capability", async () => {
|
||||
server = await startServer({
|
||||
capabilities: manifest.capabilities.filter((c) => c !== "hosts:write"),
|
||||
});
|
||||
expect((await server.request("GET", "/share-targets/roles")).status).toBe(
|
||||
500,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,11 @@ const PERMISSIONS = [
|
||||
"snippets.create",
|
||||
"snippets.edit",
|
||||
"snippets.delete",
|
||||
"snippets.share",
|
||||
];
|
||||
|
||||
const HOSTS = [
|
||||
{ id: "5", name: "web-1", ip: "10.0.0.5", port: 22, username: "root" },
|
||||
];
|
||||
|
||||
function snippet(overrides: Record<string, unknown>) {
|
||||
@@ -37,10 +42,17 @@ function snippet(overrides: Record<string, unknown>) {
|
||||
|
||||
function fakeApi(snippets: unknown[], folders: unknown[] = []) {
|
||||
const api = {
|
||||
get: vi.fn(async (path: string) => ({
|
||||
data: path === "/folders" ? folders : snippets,
|
||||
get: vi.fn(async (path: string) => {
|
||||
if (path === "/folders") return { data: folders };
|
||||
if (path === "/share-targets/users")
|
||||
return { data: { users: [{ id: "u2", username: "bob" }] } };
|
||||
if (path === "/share-targets/roles") return { data: { roles: [] } };
|
||||
if (path.endsWith("/access")) return { data: [] };
|
||||
return { data: snippets };
|
||||
}),
|
||||
post: vi.fn(async (path: string) => ({
|
||||
data: path === "/execute" ? { success: true, output: "up 3 days" } : {},
|
||||
})),
|
||||
post: vi.fn(async () => ({ data: {} })),
|
||||
put: vi.fn(async () => ({ data: {} })),
|
||||
patch: vi.fn(async () => ({ data: {} })),
|
||||
delete: vi.fn(async () => ({ data: {} })),
|
||||
@@ -61,6 +73,7 @@ async function renderPanel(api: PluginApiClient) {
|
||||
locales,
|
||||
api,
|
||||
permissions: PERMISSIONS,
|
||||
hosts: HOSTS as never,
|
||||
});
|
||||
rendered.renderPanel("snippets", { active: true });
|
||||
}
|
||||
@@ -121,6 +134,7 @@ describe("snippets panel", () => {
|
||||
description: null,
|
||||
folder: null,
|
||||
isNote: false,
|
||||
hostFilter: null,
|
||||
}),
|
||||
);
|
||||
expect(await screen.findByPlaceholderText(/search snippets/i)).toBeTruthy();
|
||||
@@ -136,4 +150,63 @@ describe("snippets panel", () => {
|
||||
fireEvent.click(screen.getByText(locales.backToSnippets));
|
||||
expect(await screen.findByText(locales.emptyTitle)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shares a snippet with a user", async () => {
|
||||
const api = fakeApi([snippet({})]);
|
||||
await renderPanel(api);
|
||||
await screen.findByText("List files");
|
||||
fireEvent.click(screen.getByTitle(locales.shareSnippet));
|
||||
fireEvent.click(await screen.findByText("bob"));
|
||||
fireEvent.click(screen.getByRole("button", { name: /share with 1/i }));
|
||||
await waitFor(() =>
|
||||
expect(api.post).toHaveBeenCalledWith("/1/share", {
|
||||
targetType: "user",
|
||||
targetUserId: "u2",
|
||||
expiresAt: null,
|
||||
}),
|
||||
);
|
||||
expect(await screen.findByText(locales.notSharedYet)).toBeTruthy();
|
||||
fireEvent.click(screen.getByText(/share "list files"/i));
|
||||
expect(await screen.findByText("List files")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("runs a snippet with target hosts on those hosts", async () => {
|
||||
const api = fakeApi([snippet({ hostFilter: "[5]" })]);
|
||||
await renderPanel(api);
|
||||
expect(await screen.findByText("web-1")).toBeTruthy();
|
||||
fireEvent.click(screen.getByTitle(locales.runOnTargets));
|
||||
await waitFor(() =>
|
||||
expect(api.post).toHaveBeenCalledWith("/execute", {
|
||||
snippetId: 1,
|
||||
hostId: 5,
|
||||
inputValues: undefined,
|
||||
}),
|
||||
);
|
||||
expect(await screen.findByText("up 3 days")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("reorders snippets by dragging", async () => {
|
||||
const api = fakeApi([
|
||||
snippet({ id: 1, name: "First", order: 0 }),
|
||||
snippet({ id: 2, name: "Second", order: 1 }),
|
||||
]);
|
||||
await renderPanel(api);
|
||||
const row = (name: string) =>
|
||||
screen.getByText(name).closest("[draggable]") as HTMLElement;
|
||||
await screen.findByText("First");
|
||||
// jsdom has no layout, so a drop always lands below the target row.
|
||||
fireEvent.dragStart(row("First"), {
|
||||
dataTransfer: { effectAllowed: "" },
|
||||
});
|
||||
fireEvent.dragOver(row("Second"));
|
||||
fireEvent.drop(row("Second"));
|
||||
await waitFor(() =>
|
||||
expect(api.put).toHaveBeenCalledWith("/reorder", {
|
||||
snippets: [
|
||||
{ id: 2, order: 0, folder: "" },
|
||||
{ id: 1, order: 1, folder: "" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseHostFilter } from "../../src/frontend/types";
|
||||
|
||||
describe("parseHostFilter", () => {
|
||||
it("reads a stored list of host ids", () => {
|
||||
expect(parseHostFilter("[1,5]")).toEqual([1, 5]);
|
||||
});
|
||||
|
||||
it("treats empty, malformed or non-numeric values as no targets", () => {
|
||||
expect(parseHostFilter(null)).toEqual([]);
|
||||
expect(parseHostFilter("")).toEqual([]);
|
||||
expect(parseHostFilter("not json")).toEqual([]);
|
||||
expect(parseHostFilter('{"a":1}')).toEqual([]);
|
||||
expect(parseHostFilter('[1,"x",2]')).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user