mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 15:15:24 +00:00
feat: drop the 2.8 columns, moves and aliases kept for a downgrade
This commit is contained in:
1 parent
d3d8269cb7
commit
61f46d2517
196 files changed
+29909
-10511
No files matched your search
@@ -113,8 +113,8 @@ describe("generated schemas", () => {
|
||||
expect(auditFks).toHaveLength(1);
|
||||
expect(auditFks[0].onDelete).toBe("set null");
|
||||
|
||||
const folderFks = read(schema.sshFolders).foreignKeys;
|
||||
expect(folderFks.map((fk) => fk.onDelete).sort()).toEqual([
|
||||
const hostFks = read(schema.hosts).foreignKeys;
|
||||
expect([...new Set(hostFks.map((fk) => fk.onDelete))].sort()).toEqual([
|
||||
"cascade",
|
||||
"set null",
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import Database from "better-sqlite3";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* A database 2.9 left behind still has the 2.8 columns it kept for a
|
||||
* downgrade. Opening it in 26.10.0 drops them, keeps what still matters
|
||||
* (a sudo password kept in terminal_config, an SSO sign-in, the hosts and
|
||||
* folders themselves) and removes the settings rows the plugins copied.
|
||||
*/
|
||||
describe("dropping the columns 2.9.0 kept", () => {
|
||||
let dataDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-retire-"));
|
||||
vi.resetModules();
|
||||
process.env.DATA_DIR = dataDir;
|
||||
process.env.DB_FILE_ENCRYPTION = "false";
|
||||
process.env.ALLOW_EMPTY_DATA_DIR = "true";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.DATA_DIR;
|
||||
delete process.env.DB_FILE_ENCRYPTION;
|
||||
delete process.env.ALLOW_EMPTY_DATA_DIR;
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function write29Database(): void {
|
||||
const seed = new Database(":memory:");
|
||||
seed.exec(`
|
||||
CREATE TABLE plugins (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
tier TEXT NOT NULL DEFAULT 'available',
|
||||
source TEXT NOT NULL DEFAULT 'community',
|
||||
registry_id TEXT,
|
||||
state TEXT NOT NULL DEFAULT 'disabled',
|
||||
last_error TEXT,
|
||||
installed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
auto_update INTEGER NOT NULL DEFAULT 0,
|
||||
manifest_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_oidc INTEGER NOT NULL DEFAULT 0,
|
||||
oidc_identifier TEXT,
|
||||
client_secret TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
|
||||
CREATE TABLE ssh_credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
auth_type TEXT NOT NULL,
|
||||
username TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE ssh_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
ip TEXT NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
auth_type TEXT NOT NULL DEFAULT 'password',
|
||||
sudo_password TEXT,
|
||||
terminal_config TEXT,
|
||||
quick_actions TEXT,
|
||||
tunnel_connections TEXT,
|
||||
enable_docker INTEGER NOT NULL DEFAULT 0,
|
||||
stats_config TEXT,
|
||||
rdp_user TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE ssh_folders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT,
|
||||
credential_id INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE user_preferences (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
theme TEXT,
|
||||
terminal_defaults TEXT,
|
||||
rdp_defaults TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO users (id, username, password_hash, is_oidc, oidc_identifier)
|
||||
VALUES ('u1', 'alice', 'hash', 1, 'sub-1');
|
||||
INSERT INTO ssh_credentials (id, user_id, name, auth_type)
|
||||
VALUES (5, 'u1', 'cred', 'password');
|
||||
INSERT INTO ssh_data (id, user_id, name, ip, port, username,
|
||||
terminal_config, tunnel_connections, enable_docker, rdp_user)
|
||||
VALUES
|
||||
(1, 'u1', 'web', '10.0.0.1', 22, '$oidc.preferred_username',
|
||||
'{"sudoPassword":"s3cret","theme":"nord"}', '[]', 1, 'admin'),
|
||||
(2, 'u1', 'db', '10.0.0.2', 22, 'root', NULL, NULL, 0, NULL);
|
||||
INSERT INTO ssh_folders (id, user_id, name, color, credential_id)
|
||||
VALUES (3, 'u1', 'prod', '#fff', 5);
|
||||
INSERT INTO user_preferences (user_id, theme, terminal_defaults, rdp_defaults)
|
||||
VALUES ('u1', 'dark', '{}', '{}');
|
||||
INSERT INTO settings (key, value) VALUES
|
||||
('guac_url', 'guacd:4822'),
|
||||
('analytics_enabled', 'true'),
|
||||
('allow_registration', 'false');
|
||||
`);
|
||||
fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize());
|
||||
seed.close();
|
||||
}
|
||||
|
||||
const columns = (sqlite: Database.Database, table: string) =>
|
||||
(sqlite.prepare(`PRAGMA table_info(${table})`).all() as Array<{
|
||||
name: string;
|
||||
}>).map((col) => col.name);
|
||||
|
||||
it("drops the columns and rows and keeps the data that still matters", async () => {
|
||||
write29Database();
|
||||
const db = await import("../../../database/db/index.js");
|
||||
await db.initializeDatabase();
|
||||
const sqlite = db.getSqlite();
|
||||
|
||||
expect(columns(sqlite, "users")).not.toContain("oidc_identifier");
|
||||
expect(columns(sqlite, "users")).not.toContain("is_oidc");
|
||||
expect(columns(sqlite, "ssh_data")).not.toEqual(
|
||||
expect.arrayContaining(["terminal_config"]),
|
||||
);
|
||||
for (const gone of [
|
||||
"terminal_config",
|
||||
"quick_actions",
|
||||
"tunnel_connections",
|
||||
"enable_docker",
|
||||
"stats_config",
|
||||
]) {
|
||||
expect(columns(sqlite, "ssh_data")).not.toContain(gone);
|
||||
}
|
||||
// A login moved only once its owner signs in stays until then.
|
||||
expect(columns(sqlite, "ssh_data")).toContain("rdp_user");
|
||||
expect(columns(sqlite, "ssh_folders")).not.toContain("credential_id");
|
||||
expect(columns(sqlite, "user_preferences")).not.toContain("rdp_defaults");
|
||||
expect(columns(sqlite, "user_preferences")).not.toContain(
|
||||
"terminal_defaults",
|
||||
);
|
||||
|
||||
const hosts = sqlite
|
||||
.prepare("SELECT id, username, sudo_password FROM ssh_data ORDER BY id")
|
||||
.all();
|
||||
expect(hosts).toEqual([
|
||||
{ id: 1, username: "$external.username", sudo_password: "s3cret" },
|
||||
{ id: 2, username: "root", sudo_password: null },
|
||||
]);
|
||||
expect(
|
||||
sqlite.prepare("SELECT name, color FROM ssh_folders").all(),
|
||||
).toEqual([{ name: "prod", color: "#fff" }]);
|
||||
expect(
|
||||
sqlite.prepare("SELECT theme FROM user_preferences").get(),
|
||||
).toEqual({ theme: "dark" });
|
||||
|
||||
const keys = (
|
||||
sqlite.prepare("SELECT key FROM settings").all() as Array<{
|
||||
key: string;
|
||||
}>
|
||||
).map((row) => row.key);
|
||||
expect(keys).toContain("allow_registration");
|
||||
expect(keys).not.toContain("guac_url");
|
||||
expect(keys).not.toContain("analytics_enabled");
|
||||
});
|
||||
|
||||
it("refuses a 2.8 database", async () => {
|
||||
const seed = new Database(":memory:");
|
||||
seed.exec(`
|
||||
CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL);
|
||||
CREATE TABLE ssh_data (id INTEGER PRIMARY KEY, user_id TEXT NOT NULL);
|
||||
`);
|
||||
fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize());
|
||||
seed.close();
|
||||
|
||||
const db = await import("../../../database/db/index.js");
|
||||
await expect(db.initializeDatabase()).rejects.toThrow(/2\.8 or older/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
legacySudoMoves,
|
||||
legacySudoPassword,
|
||||
} from "../../../database/db/retired-columns.js";
|
||||
|
||||
describe("legacySudoPassword", () => {
|
||||
it("reads the sudo password out of a terminal_config value", () => {
|
||||
expect(legacySudoPassword('{"sudoPassword":"s3cret"}')).toBe("s3cret");
|
||||
expect(legacySudoPassword({ sudoPassword: "s3cret" })).toBe("s3cret");
|
||||
expect(legacySudoPassword(JSON.stringify('{"sudoPassword":"x"}'))).toBe(
|
||||
"x",
|
||||
);
|
||||
});
|
||||
|
||||
it("finds nothing in anything else", () => {
|
||||
expect(legacySudoPassword(null)).toBeNull();
|
||||
expect(legacySudoPassword("{")).toBeNull();
|
||||
expect(legacySudoPassword('{"sudoPassword":""}')).toBeNull();
|
||||
expect(legacySudoPassword("[1]")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacySudoMoves", () => {
|
||||
it("moves only hosts with no sudo password of their own", () => {
|
||||
expect(
|
||||
legacySudoMoves([
|
||||
{ id: 1, terminal_config: '{"sudoPassword":"a"}', sudo_password: null },
|
||||
{ id: 2, terminal_config: '{"sudoPassword":"b"}', sudo_password: "own" },
|
||||
{ id: 3, terminal_config: '{"theme":"nord"}', sudo_password: null },
|
||||
]),
|
||||
).toEqual([{ id: 1, sudoPassword: "a" }]);
|
||||
});
|
||||
});
|
||||
@@ -33,10 +33,28 @@ describe("share_ssh_auth backfill", () => {
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** A 2.6.0 database: hosts and shares exist, the column does not. */
|
||||
/**
|
||||
* Hosts and shares exist, the column does not. The plugins table is there
|
||||
* because 26.10.0 only opens a database 2.9 has already upgraded.
|
||||
*/
|
||||
function writePreUpgradeDatabase(): void {
|
||||
const seed = new Database(":memory:");
|
||||
seed.exec(`
|
||||
CREATE TABLE plugins (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
tier TEXT NOT NULL DEFAULT 'available',
|
||||
source TEXT NOT NULL DEFAULT 'community',
|
||||
registry_id TEXT,
|
||||
state TEXT NOT NULL DEFAULT 'disabled',
|
||||
last_error TEXT,
|
||||
installed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
auto_update INTEGER NOT NULL DEFAULT 0,
|
||||
manifest_json TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertNotPre29Database,
|
||||
PRE_29_DATABASE_ERROR,
|
||||
} from "../../../database/db/upgrade-guard.js";
|
||||
|
||||
const tables = (...names: string[]) => (name: string) => names.includes(name);
|
||||
|
||||
describe("assertNotPre29Database", () => {
|
||||
it("refuses a 2.8 database, which has hosts but no plugins table", async () => {
|
||||
await expect(assertNotPre29Database(tables("ssh_data"))).rejects.toThrow(
|
||||
PRE_29_DATABASE_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
it("opens a database 2.9 has upgraded", async () => {
|
||||
await expect(
|
||||
assertNotPre29Database(tables("ssh_data", "plugins")),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("opens a new database", async () => {
|
||||
await expect(assertNotPre29Database(tables())).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user