feat(rabbitmq): add message queue management support

This commit is contained in:
Yann丶 authored and GitHub committed 2026-07-22 23:52:09 +08:00
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");
});
});
+20
View File
@@ -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");
+98
View File
@@ -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 });
}
+100
View File
@@ -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 });