145 lines
5.2 KiB
TypeScript
Vendored
145 lines
5.2 KiB
TypeScript
Vendored
// @vitest-environment happy-dom
|
|
|
|
import { createApp, defineComponent, h, nextTick, type Component } from "vue";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { ConnectionConfig } from "@/types/database";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
ensureConnected: vi.fn(),
|
|
executeQuery: vi.fn(),
|
|
}));
|
|
|
|
function passthrough(tag: string): Component {
|
|
return defineComponent({
|
|
inheritAttrs: false,
|
|
setup(_, { attrs, slots }) {
|
|
return () => h(tag, attrs, slots.default?.());
|
|
},
|
|
});
|
|
}
|
|
|
|
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
|
|
vi.mock("@lucide/vue", () => {
|
|
const Icon = passthrough("span");
|
|
return {
|
|
AlertTriangle: Icon,
|
|
Check: Icon,
|
|
KeyRound: Icon,
|
|
Lock: Icon,
|
|
Loader2: Icon,
|
|
Plus: Icon,
|
|
RefreshCcw: Icon,
|
|
Search: Icon,
|
|
ShieldCheck: Icon,
|
|
Trash2: Icon,
|
|
Unlock: Icon,
|
|
UserRound: Icon,
|
|
};
|
|
});
|
|
vi.mock("@/components/ui/button", () => ({ Button: passthrough("button") }));
|
|
vi.mock("@/components/ui/badge", () => ({ Badge: passthrough("span") }));
|
|
vi.mock("@/components/ui/dialog", () => ({
|
|
Dialog: passthrough("div"),
|
|
DialogContent: passthrough("div"),
|
|
DialogFooter: passthrough("div"),
|
|
DialogHeader: passthrough("div"),
|
|
DialogTitle: passthrough("div"),
|
|
}));
|
|
vi.mock("@/components/ui/input", () => ({ Input: passthrough("input") }));
|
|
vi.mock("@/components/ui/PasswordInput.vue", () => ({ default: passthrough("input") }));
|
|
vi.mock("@/components/ui/select", () => ({
|
|
Select: passthrough("div"),
|
|
SelectContent: passthrough("div"),
|
|
SelectItem: passthrough("div"),
|
|
SelectTrigger: passthrough("div"),
|
|
SelectValue: passthrough("span"),
|
|
}));
|
|
vi.mock("@/stores/connectionStore", () => ({
|
|
useConnectionStore: () => ({ ensureConnected: mocks.ensureConnected }),
|
|
}));
|
|
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
|
vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: (sql: string) => sql }) }));
|
|
vi.mock("@/lib/backend/api", () => ({
|
|
executeQuery: mocks.executeQuery,
|
|
executeMulti: vi.fn(),
|
|
listDatabases: vi.fn(),
|
|
listSchemas: vi.fn(),
|
|
}));
|
|
|
|
import DatabaseUserAdmin from "@/components/admin/DatabaseUserAdmin.vue";
|
|
|
|
const connection: ConnectionConfig = {
|
|
id: "oceanbase",
|
|
name: "OceanBase",
|
|
db_type: "jdbc",
|
|
driver_profile: "mysql",
|
|
host: "localhost",
|
|
port: 2881,
|
|
username: "root",
|
|
password: "",
|
|
};
|
|
|
|
let app: ReturnType<typeof createApp> | undefined;
|
|
let root: HTMLDivElement | undefined;
|
|
|
|
afterEach(() => {
|
|
app?.unmount();
|
|
root?.remove();
|
|
app = undefined;
|
|
root = undefined;
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("DatabaseUserAdmin MySQL grant loading", () => {
|
|
it("syncs privilege buttons and grant option from loaded SHOW GRANTS rows", async () => {
|
|
mocks.ensureConnected.mockResolvedValue(undefined);
|
|
mocks.executeQuery.mockResolvedValueOnce({ columns: ["user", "host", "plugin"], rows: [["root", "%", "mysql_native_password"]] }).mockResolvedValueOnce({ columns: ["Grants for root@%"], rows: [["GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION"]] });
|
|
|
|
root = document.createElement("div");
|
|
document.body.append(root);
|
|
app = createApp(DatabaseUserAdmin, { connection });
|
|
app.mount(root);
|
|
|
|
await vi.waitFor(() => expect(mocks.executeQuery).toHaveBeenCalledTimes(2));
|
|
await nextTick();
|
|
|
|
const privilegeButton = Array.from(root.querySelectorAll("button")).find((button) => button.textContent?.trim() === "INSERT");
|
|
const grantOptionLabel = Array.from(root.querySelectorAll("label")).find((label) => label.textContent?.includes("userAdmin.grantOption"));
|
|
const grantOptionInput = grantOptionLabel?.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
|
|
|
expect(privilegeButton?.className).toContain("border-primary");
|
|
expect(grantOptionInput?.checked).toBe(true);
|
|
});
|
|
|
|
it("falls back to the current Doris user when SHOW ALL GRANTS requires GRANT_PRIV", async () => {
|
|
const dorisConnection: ConnectionConfig = {
|
|
...connection,
|
|
id: "doris-limited",
|
|
name: "Doris limited",
|
|
db_type: "doris",
|
|
driver_profile: "doris",
|
|
port: 9030,
|
|
username: "dbx_limited",
|
|
};
|
|
const currentUserGrant = {
|
|
columns: ["UserIdentity", "Comment", "Password", "Roles", "GlobalPrivs", "DatabasePrivs", "TablePrivs"],
|
|
rows: [["'dbx_limited'@'%'", "", "Yes", null, null, "internal.analytics: Select_priv", null]],
|
|
};
|
|
mocks.ensureConnected.mockResolvedValue(undefined);
|
|
mocks.executeQuery.mockRejectedValueOnce(new Error("Access denied; you need the (GRANT) privilege"));
|
|
mocks.executeQuery.mockResolvedValueOnce(currentUserGrant).mockResolvedValueOnce(currentUserGrant);
|
|
|
|
root = document.createElement("div");
|
|
document.body.append(root);
|
|
app = createApp(DatabaseUserAdmin, { connection: dorisConnection });
|
|
app.mount(root);
|
|
|
|
await vi.waitFor(() => expect(mocks.executeQuery).toHaveBeenCalledTimes(3));
|
|
await nextTick();
|
|
|
|
expect(mocks.executeQuery.mock.calls.map((call) => call[2])).toEqual(["SHOW ALL GRANTS;", "SHOW GRANTS;", "SHOW GRANTS FOR 'dbx_limited'@'%';"]);
|
|
expect(root.textContent).toContain("dbx_limited@%");
|
|
expect(root.textContent).not.toContain("Access denied");
|
|
});
|
|
});
|