mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: move session sharing and collab rooms into a plugin (B12)
This commit is contained in:
1 parent
791d8656a1
commit
a276f6898c
207 files changed
+35771
-11072
No files matched your search
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* ctx.services with named providers: a plugin may only register the names
|
||||
* its manifest lists, a consumer reaches one provider by name, and each is
|
||||
* disposed with the plugin that provided it.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../../utils/audit-logger.js", () => ({ logAudit: async () => {} }));
|
||||
|
||||
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
|
||||
import {
|
||||
clearServiceRegistry,
|
||||
getRegistration,
|
||||
} from "../../plugins/service-registry.js";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
|
||||
function contextFor(pluginId: string, names?: string[]) {
|
||||
const manifest = {
|
||||
id: pluginId,
|
||||
name: pluginId,
|
||||
version: "1.0.0",
|
||||
description: "",
|
||||
author: { name: "test" },
|
||||
license: "MIT",
|
||||
category: "Productivity",
|
||||
engine: { termix: ">=2.9.0", api: "1" },
|
||||
capabilities: [],
|
||||
provides: [
|
||||
{
|
||||
service: "sample.live",
|
||||
version: "1.0.0",
|
||||
permission: `${pluginId}.use`,
|
||||
...(names ? { names } : {}),
|
||||
},
|
||||
],
|
||||
} as PluginManifest;
|
||||
const handle = createPluginHandle(pluginId, { activate: () => {} });
|
||||
return { ctx: createPluginContext(manifest, handle), handle };
|
||||
}
|
||||
|
||||
afterEach(() => clearServiceRegistry());
|
||||
|
||||
describe("ctx.services named providers", () => {
|
||||
it("registers a declared name and lists it", () => {
|
||||
const { ctx } = contextFor("terminal-like", ["ssh"]);
|
||||
ctx.services.provide("sample.live", { get: () => 1 }, { name: "ssh" });
|
||||
|
||||
expect(getRegistration("sample.live", "ssh")?.pluginId).toBe(
|
||||
"terminal-like",
|
||||
);
|
||||
expect(ctx.services.providers("sample.live")).toEqual(["ssh"]);
|
||||
});
|
||||
|
||||
it("refuses a name the manifest does not list", () => {
|
||||
const { ctx } = contextFor("terminal-like", ["ssh"]);
|
||||
expect(() =>
|
||||
ctx.services.provide("sample.live", {}, { name: "rdp" }),
|
||||
).toThrow(/provides\[\]\.names does not list it/);
|
||||
expect(() => ctx.services.provide("sample.live", {})).toThrow(
|
||||
/does not list it/,
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses a name when the manifest declares an unnamed service", () => {
|
||||
const { ctx } = contextFor("plain");
|
||||
expect(() =>
|
||||
ctx.services.provide("sample.live", {}, { name: "ssh" }),
|
||||
).toThrow(/does not list it/);
|
||||
});
|
||||
|
||||
it("reaches one provider by name through a handle", () => {
|
||||
const { ctx } = contextFor("terminal-like", ["ssh"]);
|
||||
ctx.services.provide("sample.live", { get: () => 1 }, { name: "ssh" });
|
||||
|
||||
const named = ctx.services.get<{ get: () => Promise<number> }>(
|
||||
"sample.live",
|
||||
{ provider: "ssh" },
|
||||
);
|
||||
const other = ctx.services.get("sample.live", { provider: "rdp" });
|
||||
expect("get" in named).toBe(true);
|
||||
expect("get" in other).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -589,6 +589,19 @@ describe("panels, dashboard cards and guest pages", () => {
|
||||
expect(errors).toMatch(/dashboardCards\[0\]\.titleKey/);
|
||||
});
|
||||
|
||||
it("accepts guest views on a guest plugin and refuses them otherwise", () => {
|
||||
expect(
|
||||
validateManifest(
|
||||
base({ contributes: { guest: true, guestViews: ["shared"] } }),
|
||||
),
|
||||
).toEqual([]);
|
||||
const errors = validateManifest(
|
||||
base({ contributes: { guestViews: ["Shared View"] } }),
|
||||
).join();
|
||||
expect(errors).toMatch(/contributes\.guestViews entries must be/);
|
||||
expect(errors).toMatch(/needs "contributes\.guest"/);
|
||||
});
|
||||
|
||||
it("rejects an unknown field inside a view and a non-boolean guest", () => {
|
||||
const errors = validateManifest(
|
||||
base({
|
||||
@@ -602,3 +615,41 @@ describe("panels, dashboard cards and guest pages", () => {
|
||||
expect(errors).toMatch(/contributes\.guest" must be a boolean/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("named service providers", () => {
|
||||
const provides = (names: unknown) => [
|
||||
{
|
||||
service: "sample.live",
|
||||
version: "1.0.0",
|
||||
permission: "sample-plugin.use",
|
||||
names,
|
||||
},
|
||||
];
|
||||
const permissions = {
|
||||
permissions: [
|
||||
{
|
||||
name: "use",
|
||||
titleKey: "permissions.use.title",
|
||||
descriptionKey: "permissions.use.description",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("accepts a list of provider names", () => {
|
||||
expect(
|
||||
validateManifest(
|
||||
base({ provides: provides(["ssh", "rdp"]), contributes: permissions }),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("refuses an empty, duplicated or malformed list", () => {
|
||||
for (const names of [[], ["ssh", "ssh"], ["SSH"], "ssh"]) {
|
||||
expect(
|
||||
validateManifest(
|
||||
base({ provides: provides(names), contributes: permissions }),
|
||||
).join(),
|
||||
).toMatch(/provides\[0\]\.names/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ const {
|
||||
createServiceHandle,
|
||||
getRegistration,
|
||||
getServiceImplementation,
|
||||
listProviderNames,
|
||||
listServices,
|
||||
provideService,
|
||||
resolveRequirements,
|
||||
@@ -99,6 +100,94 @@ describe("service registry", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("named providers", () => {
|
||||
it("keeps several providers of one service side by side", () => {
|
||||
const ssh = register({
|
||||
name: "ssh",
|
||||
implementation: { kind: async () => "ssh" },
|
||||
});
|
||||
const rdp = register({
|
||||
name: "rdp",
|
||||
pluginId: "other-plugin",
|
||||
implementation: { kind: async () => "rdp" },
|
||||
});
|
||||
|
||||
expect(listProviderNames("testplugin.greet").sort()).toEqual([
|
||||
"rdp",
|
||||
"ssh",
|
||||
]);
|
||||
expect(getRegistration("testplugin.greet", "ssh")).toBe(ssh);
|
||||
expect(getRegistration("testplugin.greet", "rdp")).toBe(rdp);
|
||||
expect(getRegistration("testplugin.greet")).toBeUndefined();
|
||||
expect(listServices()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("hands core a named implementation only by its name", () => {
|
||||
const ssh = register({ name: "ssh" });
|
||||
expect(
|
||||
getServiceImplementation("testplugin.greet", "^1.0.0", "ssh"),
|
||||
).toBe(ssh.implementation);
|
||||
expect(
|
||||
getServiceImplementation("testplugin.greet", "^1.0.0"),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("revokes one provider and leaves the other", () => {
|
||||
const ssh = register({ name: "ssh" });
|
||||
const rdp = register({ name: "rdp" });
|
||||
|
||||
expect(revokeService("testplugin.greet", ssh)).toBe(true);
|
||||
expect(getRegistration("testplugin.greet", "ssh")).toBeUndefined();
|
||||
expect(getRegistration("testplugin.greet", "rdp")).toBe(rdp);
|
||||
});
|
||||
|
||||
it("routes a named handle to its provider and fails typed once it goes", async () => {
|
||||
const ssh = register({
|
||||
name: "ssh",
|
||||
implementation: { hello: async () => "from ssh" },
|
||||
});
|
||||
register({
|
||||
name: "rdp",
|
||||
implementation: { hello: async () => "from rdp" },
|
||||
});
|
||||
const context = {
|
||||
resolveUserId: () => "user-1",
|
||||
hasPermission: async () => true,
|
||||
audit: () => {},
|
||||
};
|
||||
const handle = createServiceHandle<{ hello: () => Promise<string> }>(
|
||||
"testplugin.greet",
|
||||
"consumer-plugin",
|
||||
context,
|
||||
"ssh",
|
||||
);
|
||||
const unnamed = createServiceHandle<{ hello?: () => Promise<string> }>(
|
||||
"testplugin.greet",
|
||||
"consumer-plugin",
|
||||
context,
|
||||
);
|
||||
|
||||
await expect(handle.hello()).resolves.toBe("from ssh");
|
||||
expect("hello" in unnamed).toBe(false);
|
||||
|
||||
revokeService("testplugin.greet", ssh);
|
||||
expect(() => handle.hello()).toThrow(PluginServiceUnavailableError);
|
||||
});
|
||||
|
||||
it("satisfies a requirement with any compatible provider", () => {
|
||||
register({ name: "rdp", version: "2.0.0" });
|
||||
register({ name: "ssh", version: "1.2.0" });
|
||||
|
||||
expect(
|
||||
resolveRequirements(
|
||||
manifestRequiring([
|
||||
{ service: "testplugin.greet", versionRange: "^1.0.0" },
|
||||
]),
|
||||
).satisfied,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolution", () => {
|
||||
it("is satisfied when the version is in range", () => {
|
||||
register({ version: "1.2.0" });
|
||||
|
||||
Reference in new issue
Block a user