mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: move session sharing and collab rooms into a plugin (B12)
This commit is contained in:
1 parent
791d8656a1
commit
a276f6898c
207 files changed
+35771
-11072
No files matched your search
@@ -0,0 +1,222 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
|
||||
import { pluginDir } from "./helpers";
|
||||
|
||||
// The four tables as core's SQLite bootstrap created them before 2.9.0,
|
||||
// including the guest token column and its index added later.
|
||||
const LEGACY_DDL = `
|
||||
CREATE TABLE session_shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
host_id INTEGER NOT NULL,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
protocol TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
tab_instance_id TEXT,
|
||||
share_type TEXT NOT NULL,
|
||||
target_user_id TEXT,
|
||||
link_token TEXT UNIQUE,
|
||||
permission_level TEXT NOT NULL DEFAULT 'read-only',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TEXT NOT NULL,
|
||||
revoked_at TEXT,
|
||||
last_joined_at TEXT,
|
||||
join_count INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE session_share_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
share_id TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
guest_label TEXT,
|
||||
joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
left_at TEXT,
|
||||
FOREIGN KEY (share_id) REFERENCES session_shares (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE collab_rooms (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
persistent INTEGER NOT NULL DEFAULT 0,
|
||||
presenter_user_id TEXT,
|
||||
stage_protocol TEXT,
|
||||
stage_host_id INTEGER,
|
||||
stage_share_id TEXT,
|
||||
guest_link_token TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TEXT,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (presenter_user_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (stage_host_id) REFERENCES ssh_data (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (stage_share_id) REFERENCES session_shares (id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE TABLE collab_room_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
room_role TEXT NOT NULL DEFAULT 'member',
|
||||
added_by TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (room_id, user_id),
|
||||
FOREIGN KEY (room_id) REFERENCES collab_rooms (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (added_by) REFERENCES users (id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_session_shares_session_id ON session_shares (session_id);
|
||||
CREATE INDEX idx_session_shares_host_id ON session_shares (host_id);
|
||||
CREATE UNIQUE INDEX idx_collab_rooms_guest_token ON collab_rooms (guest_link_token);
|
||||
`;
|
||||
|
||||
const TABLES = {
|
||||
session_shares: "p_session_sharing_shares",
|
||||
session_share_participants: "p_session_sharing_share_participants",
|
||||
collab_rooms: "p_session_sharing_rooms",
|
||||
collab_room_members: "p_session_sharing_room_members",
|
||||
};
|
||||
|
||||
let db: TestDb | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
db?.close();
|
||||
db = null;
|
||||
});
|
||||
|
||||
const exists = (type: "table" | "index", name: string) =>
|
||||
!!db!.sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = ? AND name = ?")
|
||||
.get(type, name);
|
||||
|
||||
function seedUsersAndHost(sqlite: TestDb["sqlite"]) {
|
||||
sqlite.exec(
|
||||
"INSERT INTO users (id, username) VALUES ('alice', 'alice'), ('bob', 'bob')",
|
||||
);
|
||||
sqlite.exec("INSERT INTO ssh_data (id) VALUES (7)");
|
||||
}
|
||||
|
||||
describe("adopting the session sharing tables", () => {
|
||||
it("keeps every row and index when the legacy tables are there", async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
seedUsersAndHost(sqlite);
|
||||
sqlite.exec(LEGACY_DDL);
|
||||
sqlite.exec(`
|
||||
INSERT INTO session_shares (id, host_id, owner_user_id, protocol, session_id, share_type, link_token, permission_level, expires_at, join_count)
|
||||
VALUES ('share-1', 7, 'alice', 'ssh', 'sess-1', 'link', 'tok-1', 'read-write', '2999-01-01T00:00:00.000Z', 3);
|
||||
INSERT INTO session_share_participants (share_id, user_id, guest_label) VALUES ('share-1', NULL, 'Guest');
|
||||
INSERT INTO collab_rooms (id, name, owner_user_id, persistent, stage_share_id, stage_host_id, guest_link_token)
|
||||
VALUES ('room-1', 'Standup', 'alice', 1, 'share-1', 7, 'guest-tok');
|
||||
INSERT INTO collab_room_members (room_id, user_id, room_role) VALUES ('room-1', 'alice', 'host'), ('room-1', 'bob', 'member');
|
||||
`);
|
||||
},
|
||||
});
|
||||
|
||||
expect(db.applied).toEqual(["0001_adopt_session_sharing"]);
|
||||
for (const [legacy, adopted] of Object.entries(TABLES)) {
|
||||
expect(exists("table", legacy)).toBe(false);
|
||||
expect(exists("table", adopted)).toBe(true);
|
||||
}
|
||||
|
||||
expect(
|
||||
db.sqlite
|
||||
.prepare(
|
||||
"SELECT id, link_token, permission_level, join_count FROM p_session_sharing_shares",
|
||||
)
|
||||
.all(),
|
||||
).toEqual([
|
||||
{
|
||||
id: "share-1",
|
||||
link_token: "tok-1",
|
||||
permission_level: "read-write",
|
||||
join_count: 3,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
db.sqlite
|
||||
.prepare(
|
||||
"SELECT name, persistent, stage_share_id, guest_link_token FROM p_session_sharing_rooms",
|
||||
)
|
||||
.get(),
|
||||
).toEqual({
|
||||
name: "Standup",
|
||||
persistent: 1,
|
||||
stage_share_id: "share-1",
|
||||
guest_link_token: "guest-tok",
|
||||
});
|
||||
expect(
|
||||
db.sqlite
|
||||
.prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members")
|
||||
.get(),
|
||||
).toEqual({ n: 2 });
|
||||
expect(
|
||||
db.sqlite
|
||||
.prepare("SELECT guest_label FROM p_session_sharing_share_participants")
|
||||
.get(),
|
||||
).toEqual({ guest_label: "Guest" });
|
||||
|
||||
for (const index of [
|
||||
"idx_session_shares_session_id",
|
||||
"idx_session_shares_host_id",
|
||||
"idx_collab_rooms_guest_token",
|
||||
"idx_collab_rooms_owner",
|
||||
"idx_collab_room_members_user",
|
||||
]) {
|
||||
expect(exists("index", index)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("follows the rename in the links between the tables", async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
seedUsersAndHost(sqlite);
|
||||
sqlite.exec(LEGACY_DDL);
|
||||
},
|
||||
});
|
||||
const ddl = (name: string) =>
|
||||
(
|
||||
db!.sqlite
|
||||
.prepare("SELECT sql FROM sqlite_master WHERE name = ?")
|
||||
.get(name) as { sql: string }
|
||||
).sql;
|
||||
|
||||
expect(ddl("p_session_sharing_rooms")).toContain(
|
||||
'"p_session_sharing_shares"',
|
||||
);
|
||||
expect(ddl("p_session_sharing_room_members")).toContain(
|
||||
'"p_session_sharing_rooms"',
|
||||
);
|
||||
});
|
||||
|
||||
it("creates the tables fresh when there are no legacy ones", async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => seedUsersAndHost(sqlite),
|
||||
});
|
||||
|
||||
for (const [legacy, adopted] of Object.entries(TABLES)) {
|
||||
expect(exists("table", legacy)).toBe(false);
|
||||
expect(exists("table", adopted)).toBe(true);
|
||||
}
|
||||
|
||||
db.sqlite.exec(`
|
||||
INSERT INTO p_session_sharing_shares (id, host_id, owner_user_id, protocol, session_id, share_type, expires_at)
|
||||
VALUES ('s', 7, 'alice', 'ssh', 'x', 'room', '2999-01-01');
|
||||
INSERT INTO p_session_sharing_rooms (id, name, owner_user_id, stage_share_id) VALUES ('r', 'R', 'alice', 's');
|
||||
INSERT INTO p_session_sharing_room_members (room_id, user_id) VALUES ('r', 'bob');
|
||||
`);
|
||||
// The hand-kept set-null link: deleting the share clears the stage.
|
||||
db.sqlite.exec("DELETE FROM p_session_sharing_shares WHERE id = 's'");
|
||||
expect(
|
||||
db.sqlite
|
||||
.prepare("SELECT stage_share_id FROM p_session_sharing_rooms")
|
||||
.get(),
|
||||
).toEqual({ stage_share_id: null });
|
||||
// And deleting a user cascades to their memberships.
|
||||
db.sqlite.exec("DELETE FROM users WHERE id = 'bob'");
|
||||
expect(
|
||||
db.sqlite
|
||||
.prepare("SELECT COUNT(*) AS n FROM p_session_sharing_room_members")
|
||||
.get(),
|
||||
).toEqual({ n: 0 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user