feat(rabbitmq): add message queue management support
This commit is contained in:
1 parent
503d6748d4
commit
39e554369f
76 files changed
+14915
-707
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: mocks.invoke,
|
||||
}));
|
||||
|
||||
const nsRoot = { tenant: "_rabbitmq", namespace: "/" };
|
||||
|
||||
describe("mq client connections/channels tauri API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("invokes mq_list_client_connections with the namespace ref", async () => {
|
||||
const { mqListClientConnections } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ name: "192.168.1.10:51000 -> 192.168.1.126:5672", user: "jjsd", peerHost: "192.168.1.10", peerPort: 51000, state: "running", channels: 2 }]);
|
||||
|
||||
const result = await mqListClientConnections("conn-1", nsRoot);
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_client_connections", { connectionId: "conn-1", ns: nsRoot });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.peerHost).toBe("192.168.1.10");
|
||||
});
|
||||
|
||||
it("invokes mq_list_client_channels with optional connection filter", async () => {
|
||||
const { mqListClientChannels } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ name: "conn (1)", state: "running", prefetch: 10, messagesUnacked: 0, consumerCount: 1 }]);
|
||||
|
||||
const result = await mqListClientChannels("conn-1", nsRoot, "conn");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_client_channels", { connectionId: "conn-1", ns: nsRoot, connection: "conn" });
|
||||
expect(result[0]?.consumerCount).toBe(1);
|
||||
|
||||
await mqListClientChannels("conn-1", nsRoot);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_client_channels", { connectionId: "conn-1", ns: nsRoot, connection: undefined });
|
||||
});
|
||||
|
||||
it("invokes mq_close_client_connection with the connection name", async () => {
|
||||
const { mqCloseClientConnection } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqCloseClientConnection("conn-1", nsRoot, "192.168.1.10:51000 -> 192.168.1.126:5672");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_close_client_connection", { connectionId: "conn-1", ns: nsRoot, name: "192.168.1.10:51000 -> 192.168.1.126:5672" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("mq client connections/channels HTTP API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function stubFetch() {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
function lastCall(fetchMock: ReturnType<typeof stubFetch>): { url: string; body: Record<string, unknown> } {
|
||||
const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit];
|
||||
return { url, body: JSON.parse(String(init.body)) as Record<string, unknown> };
|
||||
}
|
||||
|
||||
it("posts to the client-connections endpoints", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListClientConnections, mqCloseClientConnection } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqListClientConnections("conn-1", nsRoot);
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/client-connections/list", body: { connectionId: "conn-1", ns: nsRoot } });
|
||||
|
||||
await mqCloseClientConnection("conn-1", nsRoot, "conn-name");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/client-connections/close", body: { connectionId: "conn-1", ns: nsRoot, name: "conn-name" } });
|
||||
});
|
||||
|
||||
it("posts to the channels list endpoint with optional connection filter", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListClientChannels } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqListClientChannels("conn-1", nsRoot, "conn");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/channels/list", body: { connectionId: "conn-1", ns: nsRoot, connection: "conn" } });
|
||||
|
||||
await mqListClientChannels("conn-1", nsRoot);
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/channels/list", body: { connectionId: "conn-1", ns: nsRoot, connection: undefined } });
|
||||
});
|
||||
|
||||
it("surfaces HTTP errors with the response detail", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: vi.fn().mockResolvedValue("connection not found"),
|
||||
}),
|
||||
);
|
||||
const { mqCloseClientConnection } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await expect(mqCloseClientConnection("conn-1", nsRoot, "gone")).rejects.toThrow("connection not found");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: mocks.invoke,
|
||||
}));
|
||||
|
||||
const NS = { tenant: "_rabbitmq", namespace: "/" };
|
||||
|
||||
describe("mq exchanges/bindings tauri API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("invokes mq_list_exchanges with the namespace ref", async () => {
|
||||
const { mqListExchanges } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ name: "dbx-events", type: "topic", durable: true, autoDelete: false, internal: false }]);
|
||||
|
||||
const result = await mqListExchanges("conn-1", NS);
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_exchanges", { connectionId: "conn-1", ns: NS });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.name).toBe("dbx-events");
|
||||
});
|
||||
|
||||
it("invokes mq_create_exchange with flattened fields", async () => {
|
||||
const { mqCreateExchange } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqCreateExchange("conn-1", NS, { name: "dbx-events", type: "topic", durable: true, autoDelete: false });
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_create_exchange", {
|
||||
connectionId: "conn-1",
|
||||
ns: NS,
|
||||
name: "dbx-events",
|
||||
exchangeType: "topic",
|
||||
durable: true,
|
||||
autoDelete: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("invokes mq_delete_exchange with the exchange name", async () => {
|
||||
const { mqDeleteExchange } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqDeleteExchange("conn-1", NS, "dbx-events");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_delete_exchange", { connectionId: "conn-1", ns: NS, name: "dbx-events" });
|
||||
});
|
||||
|
||||
it("invokes mq_list_bindings with optional filters", async () => {
|
||||
const { mqListBindings } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([]);
|
||||
|
||||
await mqListBindings("conn-1", NS, { exchange: "dbx-events" });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_bindings", { connectionId: "conn-1", ns: NS, exchange: "dbx-events", queue: undefined });
|
||||
|
||||
await mqListBindings("conn-1", NS);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_bindings", { connectionId: "conn-1", ns: NS, exchange: undefined, queue: undefined });
|
||||
});
|
||||
|
||||
it("invokes mq_bind and mq_unbind with the binding object", async () => {
|
||||
const { mqBind, mqUnbind } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
const binding = { source: "dbx-events", destination: "dbx-queue", destinationType: "queue", routingKey: "orders.*" };
|
||||
|
||||
await mqBind("conn-1", NS, binding);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_bind", { connectionId: "conn-1", ns: NS, binding });
|
||||
|
||||
await mqUnbind("conn-1", NS, binding);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_unbind", { connectionId: "conn-1", ns: NS, binding });
|
||||
});
|
||||
});
|
||||
|
||||
describe("mq exchanges/bindings HTTP API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function stubFetch() {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
function lastCall(fetchMock: ReturnType<typeof stubFetch>): { url: string; body: Record<string, unknown> } {
|
||||
const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit];
|
||||
return { url, body: JSON.parse(String(init.body)) as Record<string, unknown> };
|
||||
}
|
||||
|
||||
it("posts to the exchanges endpoints", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListExchanges, mqCreateExchange, mqDeleteExchange } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqListExchanges("conn-1", NS);
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/exchanges/list", body: { connectionId: "conn-1", ns: NS } });
|
||||
|
||||
await mqCreateExchange("conn-1", NS, { name: "dbx-events", type: "fanout", durable: false, autoDelete: true });
|
||||
expect(lastCall(fetchMock)).toEqual({
|
||||
url: "/api/mq/exchanges/create",
|
||||
body: { connectionId: "conn-1", ns: NS, name: "dbx-events", exchangeType: "fanout", durable: false, autoDelete: true },
|
||||
});
|
||||
|
||||
await mqDeleteExchange("conn-1", NS, "dbx-events");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/exchanges/delete", body: { connectionId: "conn-1", ns: NS, name: "dbx-events" } });
|
||||
});
|
||||
|
||||
it("posts to the bindings endpoints", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListBindings, mqBind, mqUnbind } = await import("@/lib/backend/mq-http");
|
||||
const binding = { source: "dbx-events", destination: "dbx-queue", destinationType: "queue", routingKey: "orders.*" };
|
||||
|
||||
await mqListBindings("conn-1", NS, { queue: "dbx-queue" });
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/bindings/list", body: { connectionId: "conn-1", ns: NS, exchange: undefined, queue: "dbx-queue" } });
|
||||
|
||||
await mqBind("conn-1", NS, binding);
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/bindings/bind", body: { connectionId: "conn-1", ns: NS, binding } });
|
||||
|
||||
await mqUnbind("conn-1", NS, binding);
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/bindings/unbind", body: { connectionId: "conn-1", ns: NS, binding } });
|
||||
});
|
||||
|
||||
it("surfaces HTTP errors with the response detail", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: vi.fn().mockResolvedValue("cannot delete built-in exchange"),
|
||||
}),
|
||||
);
|
||||
const { mqDeleteExchange } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await expect(mqDeleteExchange("conn-1", NS, "amq.direct")).rejects.toThrow("cannot delete built-in exchange");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: mocks.invoke,
|
||||
}));
|
||||
|
||||
describe("mq policies tauri API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("invokes mq_list_policies with flattened filters", async () => {
|
||||
const { mqListPolicies } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ name: "dbx-ttl", vhost: "/", pattern: "^dbx-", applyTo: "queues", priority: 0, definition: { "message-ttl": 60000 } }]);
|
||||
|
||||
const result = await mqListPolicies("conn-1", { virtualHost: "/" });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_policies", { connectionId: "conn-1", virtualHost: "/", allVhosts: undefined });
|
||||
expect(result[0]?.name).toBe("dbx-ttl");
|
||||
expect(result[0]?.definition).toEqual({ "message-ttl": 60000 });
|
||||
|
||||
await mqListPolicies("conn-1", { allVhosts: true });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_policies", { connectionId: "conn-1", virtualHost: undefined, allVhosts: true });
|
||||
|
||||
await mqListPolicies("conn-1");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_policies", { connectionId: "conn-1", virtualHost: undefined, allVhosts: undefined });
|
||||
});
|
||||
|
||||
it("invokes mq_set_policy with flattened fields", async () => {
|
||||
const { mqSetPolicy } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqSetPolicy("conn-1", "/", { name: "dbx-ttl", pattern: "^dbx-", applyTo: "queues", priority: 10, definition: { "message-ttl": 60000 } });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_set_policy", {
|
||||
connectionId: "conn-1",
|
||||
virtualHost: "/",
|
||||
name: "dbx-ttl",
|
||||
pattern: "^dbx-",
|
||||
applyTo: "queues",
|
||||
priority: 10,
|
||||
definition: { "message-ttl": 60000 },
|
||||
});
|
||||
|
||||
await mqSetPolicy("conn-1", "/", { name: "dbx-dlx", pattern: ".*", definition: { "dead-letter-exchange": "dbx-dlx" } });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_set_policy", {
|
||||
connectionId: "conn-1",
|
||||
virtualHost: "/",
|
||||
name: "dbx-dlx",
|
||||
pattern: ".*",
|
||||
applyTo: undefined,
|
||||
priority: undefined,
|
||||
definition: { "dead-letter-exchange": "dbx-dlx" },
|
||||
});
|
||||
});
|
||||
|
||||
it("invokes mq_delete_policy with vhost and name", async () => {
|
||||
const { mqDeletePolicy } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqDeletePolicy("conn-1", "/", "dbx-ttl");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_delete_policy", { connectionId: "conn-1", virtualHost: "/", name: "dbx-ttl" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("mq overview/nodes tauri API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("invokes mq_get_overview with only the connection id", async () => {
|
||||
const { mqGetOverview } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue({ messagesReady: 3, messagesUnacked: 1, publishRate: 0.5, totalQueues: 2 });
|
||||
|
||||
const result = await mqGetOverview("conn-1");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_get_overview", { connectionId: "conn-1" });
|
||||
expect(result.messagesReady).toBe(3);
|
||||
expect(result.totalQueues).toBe(2);
|
||||
});
|
||||
|
||||
it("invokes mq_list_nodes with only the connection id", async () => {
|
||||
const { mqListNodes } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ name: "rabbit@dbx", running: true, memUsed: 1024, memLimit: 4096, uptimeMs: 60000 }]);
|
||||
|
||||
const result = await mqListNodes("conn-1");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_nodes", { connectionId: "conn-1" });
|
||||
expect(result[0]?.name).toBe("rabbit@dbx");
|
||||
expect(result[0]?.running).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mq policies/overview/nodes HTTP API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function stubFetch(payload: unknown = []) {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue(payload),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
function lastCall(fetchMock: ReturnType<typeof stubFetch>): { url: string; body: Record<string, unknown> } {
|
||||
const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit];
|
||||
return { url, body: JSON.parse(String(init.body)) as Record<string, unknown> };
|
||||
}
|
||||
|
||||
it("posts to the policies endpoints", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListPolicies, mqSetPolicy, mqDeletePolicy } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqListPolicies("conn-1", { allVhosts: true });
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/policies/list", body: { connectionId: "conn-1", virtualHost: undefined, allVhosts: true } });
|
||||
|
||||
await mqSetPolicy("conn-1", "/", { name: "dbx-ttl", pattern: "^dbx-", applyTo: "queues", priority: 10, definition: { "message-ttl": 60000 } });
|
||||
expect(lastCall(fetchMock)).toEqual({
|
||||
url: "/api/mq/policies/set",
|
||||
body: { connectionId: "conn-1", virtualHost: "/", name: "dbx-ttl", pattern: "^dbx-", applyTo: "queues", priority: 10, definition: { "message-ttl": 60000 } },
|
||||
});
|
||||
|
||||
await mqDeletePolicy("conn-1", "/", "dbx-ttl");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/policies/delete", body: { connectionId: "conn-1", virtualHost: "/", name: "dbx-ttl" } });
|
||||
});
|
||||
|
||||
it("posts to the overview and nodes endpoints", async () => {
|
||||
const fetchMock = stubFetch({});
|
||||
const { mqGetOverview, mqListNodes } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqGetOverview("conn-1");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/overview", body: { connectionId: "conn-1" } });
|
||||
|
||||
await mqListNodes("conn-1");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/nodes", body: { connectionId: "conn-1" } });
|
||||
});
|
||||
|
||||
it("surfaces HTTP errors with the response detail", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: vi.fn().mockResolvedValue('virtual host "*" is a listing sentinel'),
|
||||
}),
|
||||
);
|
||||
const { mqSetPolicy } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await expect(mqSetPolicy("conn-1", "*", { name: "dbx-ttl", pattern: ".*", definition: {} })).rejects.toThrow('virtual host "*" is a listing sentinel');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: mocks.invoke,
|
||||
}));
|
||||
|
||||
describe("mq users/permissions tauri API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("invokes mq_list_users with only the connection id", async () => {
|
||||
const { mqListUsers } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ name: "dbx-app", tags: ["administrator"] }]);
|
||||
|
||||
const result = await mqListUsers("conn-1");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_users", { connectionId: "conn-1" });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.name).toBe("dbx-app");
|
||||
expect(result[0]?.tags).toEqual(["administrator"]);
|
||||
});
|
||||
|
||||
it("invokes mq_create_user with name, password and optional tags", async () => {
|
||||
const { mqCreateUser } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqCreateUser("conn-1", "dbx-app", "secret", ["monitoring", "management"]);
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_create_user", { connectionId: "conn-1", name: "dbx-app", password: "secret", tags: ["monitoring", "management"] });
|
||||
|
||||
await mqCreateUser("conn-1", "dbx-app2", "secret2");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_create_user", { connectionId: "conn-1", name: "dbx-app2", password: "secret2", tags: undefined });
|
||||
});
|
||||
|
||||
it("invokes mq_delete_user with the user name", async () => {
|
||||
const { mqDeleteUser } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqDeleteUser("conn-1", "dbx-app");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_delete_user", { connectionId: "conn-1", name: "dbx-app" });
|
||||
});
|
||||
|
||||
it("invokes mq_list_user_permissions with flattened filters", async () => {
|
||||
const { mqListUserPermissions } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue([{ user: "dbx-app", vhost: "/", configure: ".*", write: ".*", read: ".*" }]);
|
||||
|
||||
const result = await mqListUserPermissions("conn-1", { virtualHost: "/" });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_user_permissions", { connectionId: "conn-1", virtualHost: "/", user: undefined, allVhosts: undefined });
|
||||
expect(result[0]?.vhost).toBe("/");
|
||||
|
||||
await mqListUserPermissions("conn-1", { allVhosts: true, user: "dbx-app" });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_user_permissions", { connectionId: "conn-1", virtualHost: undefined, user: "dbx-app", allVhosts: true });
|
||||
|
||||
await mqListUserPermissions("conn-1");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_list_user_permissions", { connectionId: "conn-1", virtualHost: undefined, user: undefined, allVhosts: undefined });
|
||||
});
|
||||
|
||||
it("invokes mq_grant_user_permission with optional patterns", async () => {
|
||||
const { mqGrantUserPermission } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqGrantUserPermission("conn-1", "dbx-app", "/", { configure: "^dbx-", write: ".*", read: ".*" });
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_grant_user_permission", { connectionId: "conn-1", user: "dbx-app", virtualHost: "/", configure: "^dbx-", write: ".*", read: ".*" });
|
||||
|
||||
await mqGrantUserPermission("conn-1", "dbx-app", "/");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_grant_user_permission", { connectionId: "conn-1", user: "dbx-app", virtualHost: "/", configure: undefined, write: undefined, read: undefined });
|
||||
});
|
||||
|
||||
it("invokes mq_revoke_user_permission with user and vhost", async () => {
|
||||
const { mqRevokeUserPermission } = await import("@/lib/backend/mq-tauri");
|
||||
mocks.invoke.mockResolvedValue(undefined);
|
||||
|
||||
await mqRevokeUserPermission("conn-1", "dbx-app", "/");
|
||||
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("mq_revoke_user_permission", { connectionId: "conn-1", user: "dbx-app", virtualHost: "/" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("mq users/permissions HTTP API", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function stubFetch() {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
function lastCall(fetchMock: ReturnType<typeof stubFetch>): { url: string; body: Record<string, unknown> } {
|
||||
const [url, init] = fetchMock.mock.calls.at(-1) as [string, RequestInit];
|
||||
return { url, body: JSON.parse(String(init.body)) as Record<string, unknown> };
|
||||
}
|
||||
|
||||
it("posts to the users endpoints", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListUsers, mqCreateUser, mqDeleteUser } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqListUsers("conn-1");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/users/list", body: { connectionId: "conn-1" } });
|
||||
|
||||
await mqCreateUser("conn-1", "dbx-app", "secret", ["monitoring"]);
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/users/create", body: { connectionId: "conn-1", name: "dbx-app", password: "secret", tags: ["monitoring"] } });
|
||||
|
||||
await mqDeleteUser("conn-1", "dbx-app");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/users/delete", body: { connectionId: "conn-1", name: "dbx-app" } });
|
||||
});
|
||||
|
||||
it("posts to the user-permissions endpoints", async () => {
|
||||
const fetchMock = stubFetch();
|
||||
const { mqListUserPermissions, mqGrantUserPermission, mqRevokeUserPermission } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await mqListUserPermissions("conn-1", { virtualHost: "/" });
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/user-permissions/list", body: { connectionId: "conn-1", virtualHost: "/", user: undefined, allVhosts: undefined } });
|
||||
|
||||
await mqGrantUserPermission("conn-1", "dbx-app", "/", { configure: "^dbx-" });
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/user-permissions/grant", body: { connectionId: "conn-1", user: "dbx-app", virtualHost: "/", configure: "^dbx-", write: undefined, read: undefined } });
|
||||
|
||||
await mqRevokeUserPermission("conn-1", "dbx-app", "/");
|
||||
expect(lastCall(fetchMock)).toEqual({ url: "/api/mq/user-permissions/revoke", body: { connectionId: "conn-1", user: "dbx-app", virtualHost: "/" } });
|
||||
});
|
||||
|
||||
it("surfaces HTTP errors with the response detail", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: vi.fn().mockResolvedValue("cannot delete the current connection user"),
|
||||
}),
|
||||
);
|
||||
const { mqDeleteUser } = await import("@/lib/backend/mq-http");
|
||||
|
||||
await expect(mqDeleteUser("conn-1", "jjsd")).rejects.toThrow("cannot delete the current connection user");
|
||||
});
|
||||
});
|
||||
@@ -414,6 +414,15 @@ export const mqDeleteTopic = forward("mqDeleteTopic");
|
||||
export const mqUpdatePartitions = forward("mqUpdatePartitions");
|
||||
export const mqGetTopicStats = forward("mqGetTopicStats");
|
||||
export const mqGetTopicInternalStats = forward("mqGetTopicInternalStats");
|
||||
export const mqListExchanges = forward("mqListExchanges");
|
||||
export const mqCreateExchange = forward("mqCreateExchange");
|
||||
export const mqDeleteExchange = forward("mqDeleteExchange");
|
||||
export const mqListBindings = forward("mqListBindings");
|
||||
export const mqBind = forward("mqBind");
|
||||
export const mqUnbind = forward("mqUnbind");
|
||||
export const mqListClientConnections = forward("mqListClientConnections");
|
||||
export const mqListClientChannels = forward("mqListClientChannels");
|
||||
export const mqCloseClientConnection = forward("mqCloseClientConnection");
|
||||
export const mqListSubscriptions = forward("mqListSubscriptions");
|
||||
export const mqCreateSubscription = forward("mqCreateSubscription");
|
||||
export const mqDeleteSubscription = forward("mqDeleteSubscription");
|
||||
@@ -449,6 +458,17 @@ export const mqQueryMessagesByTopic = forward("mqQueryMessagesByTopic");
|
||||
export const mqQueryMessageTrace = forward("mqQueryMessageTrace");
|
||||
export const mqRawRequest = forward("mqRawRequest");
|
||||
export const mqSendMessage = forward("mqSendMessage");
|
||||
export const mqListUsers = forward("mqListUsers");
|
||||
export const mqCreateUser = forward("mqCreateUser");
|
||||
export const mqDeleteUser = forward("mqDeleteUser");
|
||||
export const mqListUserPermissions = forward("mqListUserPermissions");
|
||||
export const mqGrantUserPermission = forward("mqGrantUserPermission");
|
||||
export const mqRevokeUserPermission = forward("mqRevokeUserPermission");
|
||||
export const mqListPolicies = forward("mqListPolicies");
|
||||
export const mqSetPolicy = forward("mqSetPolicy");
|
||||
export const mqDeletePolicy = forward("mqDeletePolicy");
|
||||
export const mqGetOverview = forward("mqGetOverview");
|
||||
export const mqListNodes = forward("mqListNodes");
|
||||
|
||||
// MongoDB
|
||||
export const documentListDatabases = forward("documentListDatabases");
|
||||
|
||||
@@ -36,6 +36,21 @@ import type {
|
||||
MqRawResponse,
|
||||
SendMessageRequest,
|
||||
SendMessageResponse,
|
||||
MqExchangeInfo,
|
||||
MqExchangeCreateRequest,
|
||||
MqBindingInfo,
|
||||
MqBindingListFilter,
|
||||
MqClientConnectionInfo,
|
||||
MqChannelInfo,
|
||||
MqUserInfo,
|
||||
MqVhostPermission,
|
||||
MqUserPermissionListFilter,
|
||||
MqUserPermissionPatterns,
|
||||
MqPolicyInfo,
|
||||
MqPolicyListFilter,
|
||||
MqPolicyUpsertRequest,
|
||||
MqOverviewInfo,
|
||||
MqNodeInfo,
|
||||
} from "@/types/mq";
|
||||
|
||||
async function post<T>(path: string, body: unknown): Promise<T> {
|
||||
@@ -55,6 +70,89 @@ export async function mqTestConnection(connectionId: string): Promise<MqClusterI
|
||||
return post("/api/mq/test-connection", { connectionId });
|
||||
}
|
||||
|
||||
export async function mqListExchanges(connectionId: string, ns: NamespaceRef): Promise<MqExchangeInfo[]> {
|
||||
return post("/api/mq/exchanges/list", { connectionId, ns });
|
||||
}
|
||||
|
||||
export async function mqCreateExchange(connectionId: string, ns: NamespaceRef, exchange: MqExchangeCreateRequest): Promise<void> {
|
||||
return post("/api/mq/exchanges/create", { connectionId, ns, name: exchange.name, exchangeType: exchange.type, durable: exchange.durable, autoDelete: exchange.autoDelete });
|
||||
}
|
||||
|
||||
export async function mqDeleteExchange(connectionId: string, ns: NamespaceRef, name: string): Promise<void> {
|
||||
return post("/api/mq/exchanges/delete", { connectionId, ns, name });
|
||||
}
|
||||
|
||||
export async function mqListBindings(connectionId: string, ns: NamespaceRef, filter?: MqBindingListFilter): Promise<MqBindingInfo[]> {
|
||||
return post("/api/mq/bindings/list", { connectionId, ns, exchange: filter?.exchange, queue: filter?.queue });
|
||||
}
|
||||
|
||||
export async function mqBind(connectionId: string, ns: NamespaceRef, binding: MqBindingInfo): Promise<void> {
|
||||
return post("/api/mq/bindings/bind", { connectionId, ns, binding });
|
||||
}
|
||||
|
||||
export async function mqUnbind(connectionId: string, ns: NamespaceRef, binding: MqBindingInfo): Promise<void> {
|
||||
return post("/api/mq/bindings/unbind", { connectionId, ns, binding });
|
||||
}
|
||||
|
||||
export async function mqListClientConnections(connectionId: string, ns: NamespaceRef): Promise<MqClientConnectionInfo[]> {
|
||||
return post("/api/mq/client-connections/list", { connectionId, ns });
|
||||
}
|
||||
|
||||
export async function mqListClientChannels(connectionId: string, ns: NamespaceRef, connection?: string): Promise<MqChannelInfo[]> {
|
||||
return post("/api/mq/channels/list", { connectionId, ns, connection });
|
||||
}
|
||||
|
||||
export async function mqCloseClientConnection(connectionId: string, ns: NamespaceRef, name: string): Promise<void> {
|
||||
return post("/api/mq/client-connections/close", { connectionId, ns, name });
|
||||
}
|
||||
|
||||
// Users & vhost permissions (RabbitMQ)
|
||||
export async function mqListUsers(connectionId: string): Promise<MqUserInfo[]> {
|
||||
return post("/api/mq/users/list", { connectionId });
|
||||
}
|
||||
|
||||
export async function mqCreateUser(connectionId: string, name: string, password: string, tags?: string[]): Promise<void> {
|
||||
return post("/api/mq/users/create", { connectionId, name, password, tags });
|
||||
}
|
||||
|
||||
export async function mqDeleteUser(connectionId: string, name: string): Promise<void> {
|
||||
return post("/api/mq/users/delete", { connectionId, name });
|
||||
}
|
||||
|
||||
export async function mqListUserPermissions(connectionId: string, filter?: MqUserPermissionListFilter): Promise<MqVhostPermission[]> {
|
||||
return post("/api/mq/user-permissions/list", { connectionId, virtualHost: filter?.virtualHost, user: filter?.user, allVhosts: filter?.allVhosts });
|
||||
}
|
||||
|
||||
export async function mqGrantUserPermission(connectionId: string, user: string, virtualHost: string, patterns?: MqUserPermissionPatterns): Promise<void> {
|
||||
return post("/api/mq/user-permissions/grant", { connectionId, user, virtualHost, configure: patterns?.configure, write: patterns?.write, read: patterns?.read });
|
||||
}
|
||||
|
||||
export async function mqRevokeUserPermission(connectionId: string, user: string, virtualHost: string): Promise<void> {
|
||||
return post("/api/mq/user-permissions/revoke", { connectionId, user, virtualHost });
|
||||
}
|
||||
|
||||
// Policies (RabbitMQ)
|
||||
export async function mqListPolicies(connectionId: string, filter?: MqPolicyListFilter): Promise<MqPolicyInfo[]> {
|
||||
return post("/api/mq/policies/list", { connectionId, virtualHost: filter?.virtualHost, allVhosts: filter?.allVhosts });
|
||||
}
|
||||
|
||||
export async function mqSetPolicy(connectionId: string, virtualHost: string, policy: MqPolicyUpsertRequest): Promise<void> {
|
||||
return post("/api/mq/policies/set", { connectionId, virtualHost, name: policy.name, pattern: policy.pattern, applyTo: policy.applyTo, priority: policy.priority, definition: policy.definition });
|
||||
}
|
||||
|
||||
export async function mqDeletePolicy(connectionId: string, virtualHost: string, name: string): Promise<void> {
|
||||
return post("/api/mq/policies/delete", { connectionId, virtualHost, name });
|
||||
}
|
||||
|
||||
// Cluster overview & nodes (RabbitMQ)
|
||||
export async function mqGetOverview(connectionId: string): Promise<MqOverviewInfo> {
|
||||
return post("/api/mq/overview", { connectionId });
|
||||
}
|
||||
|
||||
export async function mqListNodes(connectionId: string): Promise<MqNodeInfo[]> {
|
||||
return post("/api/mq/nodes", { connectionId });
|
||||
}
|
||||
|
||||
export async function mqListTenants(connectionId: string): Promise<TenantInfo[]> {
|
||||
return post("/api/mq/tenants/list", { connectionId });
|
||||
}
|
||||
|
||||
@@ -36,6 +36,21 @@ import type {
|
||||
MqRawResponse,
|
||||
SendMessageRequest,
|
||||
SendMessageResponse,
|
||||
MqExchangeInfo,
|
||||
MqExchangeCreateRequest,
|
||||
MqBindingInfo,
|
||||
MqBindingListFilter,
|
||||
MqClientConnectionInfo,
|
||||
MqChannelInfo,
|
||||
MqUserInfo,
|
||||
MqVhostPermission,
|
||||
MqUserPermissionListFilter,
|
||||
MqUserPermissionPatterns,
|
||||
MqPolicyInfo,
|
||||
MqPolicyListFilter,
|
||||
MqPolicyUpsertRequest,
|
||||
MqOverviewInfo,
|
||||
MqNodeInfo,
|
||||
} from "@/types/mq";
|
||||
|
||||
// Connectivity
|
||||
@@ -43,6 +58,91 @@ export async function mqTestConnection(connectionId: string): Promise<MqClusterI
|
||||
return invoke("mq_test_connection", { connectionId });
|
||||
}
|
||||
|
||||
// Exchanges / Bindings (RabbitMQ)
|
||||
export async function mqListExchanges(connectionId: string, ns: NamespaceRef): Promise<MqExchangeInfo[]> {
|
||||
return invoke("mq_list_exchanges", { connectionId, ns });
|
||||
}
|
||||
|
||||
export async function mqCreateExchange(connectionId: string, ns: NamespaceRef, exchange: MqExchangeCreateRequest): Promise<void> {
|
||||
return invoke("mq_create_exchange", { connectionId, ns, name: exchange.name, exchangeType: exchange.type, durable: exchange.durable, autoDelete: exchange.autoDelete });
|
||||
}
|
||||
|
||||
export async function mqDeleteExchange(connectionId: string, ns: NamespaceRef, name: string): Promise<void> {
|
||||
return invoke("mq_delete_exchange", { connectionId, ns, name });
|
||||
}
|
||||
|
||||
export async function mqListBindings(connectionId: string, ns: NamespaceRef, filter?: MqBindingListFilter): Promise<MqBindingInfo[]> {
|
||||
return invoke("mq_list_bindings", { connectionId, ns, exchange: filter?.exchange, queue: filter?.queue });
|
||||
}
|
||||
|
||||
export async function mqBind(connectionId: string, ns: NamespaceRef, binding: MqBindingInfo): Promise<void> {
|
||||
return invoke("mq_bind", { connectionId, ns, binding });
|
||||
}
|
||||
|
||||
export async function mqUnbind(connectionId: string, ns: NamespaceRef, binding: MqBindingInfo): Promise<void> {
|
||||
return invoke("mq_unbind", { connectionId, ns, binding });
|
||||
}
|
||||
|
||||
// Client connections / channels (RabbitMQ)
|
||||
export async function mqListClientConnections(connectionId: string, ns: NamespaceRef): Promise<MqClientConnectionInfo[]> {
|
||||
return invoke("mq_list_client_connections", { connectionId, ns });
|
||||
}
|
||||
|
||||
export async function mqListClientChannels(connectionId: string, ns: NamespaceRef, connection?: string): Promise<MqChannelInfo[]> {
|
||||
return invoke("mq_list_client_channels", { connectionId, ns, connection });
|
||||
}
|
||||
|
||||
export async function mqCloseClientConnection(connectionId: string, ns: NamespaceRef, name: string): Promise<void> {
|
||||
return invoke("mq_close_client_connection", { connectionId, ns, name });
|
||||
}
|
||||
|
||||
// Users & vhost permissions (RabbitMQ)
|
||||
export async function mqListUsers(connectionId: string): Promise<MqUserInfo[]> {
|
||||
return invoke("mq_list_users", { connectionId });
|
||||
}
|
||||
|
||||
export async function mqCreateUser(connectionId: string, name: string, password: string, tags?: string[]): Promise<void> {
|
||||
return invoke("mq_create_user", { connectionId, name, password, tags });
|
||||
}
|
||||
|
||||
export async function mqDeleteUser(connectionId: string, name: string): Promise<void> {
|
||||
return invoke("mq_delete_user", { connectionId, name });
|
||||
}
|
||||
|
||||
export async function mqListUserPermissions(connectionId: string, filter?: MqUserPermissionListFilter): Promise<MqVhostPermission[]> {
|
||||
return invoke("mq_list_user_permissions", { connectionId, virtualHost: filter?.virtualHost, user: filter?.user, allVhosts: filter?.allVhosts });
|
||||
}
|
||||
|
||||
export async function mqGrantUserPermission(connectionId: string, user: string, virtualHost: string, patterns?: MqUserPermissionPatterns): Promise<void> {
|
||||
return invoke("mq_grant_user_permission", { connectionId, user, virtualHost, configure: patterns?.configure, write: patterns?.write, read: patterns?.read });
|
||||
}
|
||||
|
||||
export async function mqRevokeUserPermission(connectionId: string, user: string, virtualHost: string): Promise<void> {
|
||||
return invoke("mq_revoke_user_permission", { connectionId, user, virtualHost });
|
||||
}
|
||||
|
||||
// Policies (RabbitMQ)
|
||||
export async function mqListPolicies(connectionId: string, filter?: MqPolicyListFilter): Promise<MqPolicyInfo[]> {
|
||||
return invoke("mq_list_policies", { connectionId, virtualHost: filter?.virtualHost, allVhosts: filter?.allVhosts });
|
||||
}
|
||||
|
||||
export async function mqSetPolicy(connectionId: string, virtualHost: string, policy: MqPolicyUpsertRequest): Promise<void> {
|
||||
return invoke("mq_set_policy", { connectionId, virtualHost, name: policy.name, pattern: policy.pattern, applyTo: policy.applyTo, priority: policy.priority, definition: policy.definition });
|
||||
}
|
||||
|
||||
export async function mqDeletePolicy(connectionId: string, virtualHost: string, name: string): Promise<void> {
|
||||
return invoke("mq_delete_policy", { connectionId, virtualHost, name });
|
||||
}
|
||||
|
||||
// Cluster overview & nodes (RabbitMQ)
|
||||
export async function mqGetOverview(connectionId: string): Promise<MqOverviewInfo> {
|
||||
return invoke("mq_get_overview", { connectionId });
|
||||
}
|
||||
|
||||
export async function mqListNodes(connectionId: string): Promise<MqNodeInfo[]> {
|
||||
return invoke("mq_list_nodes", { connectionId });
|
||||
}
|
||||
|
||||
// Tenants
|
||||
export async function mqListTenants(connectionId: string): Promise<TenantInfo[]> {
|
||||
return invoke("mq_list_tenants", { connectionId });
|
||||
|
||||
Reference in new issue
Block a user