Files
2026-07-13 13:09:14 +08:00

132 lines
4.3 KiB
TypeScript
Vendored

import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "vitest";
import Database from "better-sqlite3";
import type { ConnectionConfig } from "../src/connections.js";
import { describeTable, executeQuery, listTables } from "../src/database.js";
function sqliteConfig(path: string): ConnectionConfig {
return {
id: "sqlite-test",
name: "local-sqlite",
db_type: "sqlite",
host: path,
port: 0,
username: "",
password: "",
ssh_enabled: false,
ssl: false,
};
}
function mysqlSshConfig(): ConnectionConfig {
return {
id: "mysql-ssh-test",
name: "mysql-over-ssh",
db_type: "mysql",
host: "10.0.0.10",
port: 3306,
username: "root",
password: "secret",
ssl: false,
transport_layers: [
{
type: "ssh",
id: "bastion",
enabled: true,
host: "bastion.example.com",
port: 22,
user: "root",
password: "ssh-secret",
},
],
};
}
test("queries SQLite connections without the DBX bridge", async () => {
const dir = mkdtempSync(join(tmpdir(), "dbx-mcp-sqlite-"));
const path = join(dir, "app.db");
const db = new Database(path);
db.exec("create table users (id integer primary key, name text not null); insert into users (name) values ('Ada');");
db.close();
try {
const result = await executeQuery(sqliteConfig(path), "select id, name from users");
assert.deepEqual(result.columns, ["id", "name"]);
assert.deepEqual(result.rows, [{ id: 1, name: "Ada" }]);
assert.equal(result.row_count, 1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("applies query row limits to SQLite connections", async () => {
const dir = mkdtempSync(join(tmpdir(), "dbx-mcp-sqlite-"));
const path = join(dir, "app.db");
const db = new Database(path);
db.exec("create table users (id integer primary key, name text not null); insert into users (name) values ('Ada'), ('Grace');");
db.close();
try {
const result = await executeQuery(sqliteConfig(path), "select id, name from users order by id", { maxRows: 1 });
assert.deepEqual(result.columns, ["id", "name"]);
assert.deepEqual(result.rows, [{ id: 1, name: "Ada" }]);
assert.equal(result.row_count, 1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("lists and describes SQLite tables without the DBX bridge", async () => {
const dir = mkdtempSync(join(tmpdir(), "dbx-mcp-sqlite-"));
const path = join(dir, "app.db");
const db = new Database(path);
db.exec("create table users (id integer primary key, name text not null);");
db.close();
try {
const tables = await listTables(sqliteConfig(path));
const columns = await describeTable(sqliteConfig(path), "users");
assert.deepEqual(tables, [{ name: "users", type: "table" }]);
assert.deepEqual(
columns.map((column) => ({
name: column.name,
data_type: column.data_type,
is_nullable: column.is_nullable,
is_primary_key: column.is_primary_key,
})),
[
{ name: "id", data_type: "INTEGER", is_nullable: true, is_primary_key: true },
{ name: "name", data_type: "TEXT", is_nullable: false, is_primary_key: false },
],
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("routes SSH transport layer connections through the DBX bridge", async () => {
const dir = mkdtempSync(join(tmpdir(), "dbx-mcp-bridge-home-"));
const originalHome = process.env.HOME;
const originalDbxDataDir = process.env.DBX_DATA_DIR;
process.env.HOME = dir;
process.env.DBX_DATA_DIR = dir;
try {
await assert.rejects(() => executeQuery(mysqlSshConfig(), "select 1"), /DBX desktop app is not running/);
await assert.rejects(() => listTables(mysqlSshConfig()), /DBX desktop app is not running/);
await assert.rejects(() => describeTable(mysqlSshConfig(), "users"), /DBX desktop app is not running/);
} finally {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalDbxDataDir === undefined) delete process.env.DBX_DATA_DIR;
else process.env.DBX_DATA_DIR = originalDbxDataDir;
rmSync(dir, { recursive: true, force: true });
}
});