mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 07:35:24 +00:00
feat: turn session recording into a plugin (B13)
This commit is contained in:
1 parent
a276f6898c
commit
0af2c94983
147 files changed
+25915
-2074
No files matched your search
@@ -0,0 +1,179 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { startServer, type TestServer } from "./helpers";
|
||||
|
||||
let server: TestServer | null = null;
|
||||
let dataDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dataDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "session-recording-routes-"),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server?.close();
|
||||
server = null;
|
||||
await fs.rm(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("session recording routes", () => {
|
||||
it("401s without an authenticated actor", async () => {
|
||||
server = await startServer();
|
||||
const res = await server.request("GET", "/", { user: "" });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("lists only the caller's own recordings", async () => {
|
||||
server = await startServer();
|
||||
|
||||
const table = await server.mock.ctx.db.client<{
|
||||
insert: (t: unknown) => {
|
||||
values: (v: unknown) => Promise<unknown>;
|
||||
};
|
||||
}>();
|
||||
void table;
|
||||
|
||||
// Seed through the repository the way activate() wired it: easiest is a
|
||||
// direct HTTP round trip isn't available for writes (only ssh-terminal
|
||||
// and remote-desktop write through the service), so seed the table
|
||||
// directly.
|
||||
server.db.sqlite
|
||||
.prepare(
|
||||
`INSERT INTO p_session_recording_session_recordings
|
||||
(host_id, user_id, recording_path, protocol, format, started_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
7,
|
||||
"user-1",
|
||||
"/tmp/a.cast",
|
||||
"ssh",
|
||||
"asciicast",
|
||||
new Date().toISOString(),
|
||||
);
|
||||
server.db.sqlite
|
||||
.prepare(
|
||||
`INSERT INTO p_session_recording_session_recordings
|
||||
(host_id, user_id, recording_path, protocol, format, started_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
7,
|
||||
"user-2",
|
||||
"/tmp/b.cast",
|
||||
"ssh",
|
||||
"asciicast",
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const res = await server.request("GET", "/", { user: "user-1" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.logs).toHaveLength(1);
|
||||
expect(res.body.logs[0].userId).toBe("user-1");
|
||||
});
|
||||
|
||||
it("404s getting another user's recording", async () => {
|
||||
server = await startServer();
|
||||
server.db.sqlite
|
||||
.prepare(
|
||||
`INSERT INTO p_session_recording_session_recordings
|
||||
(host_id, user_id, recording_path, protocol, format, started_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
7,
|
||||
"user-2",
|
||||
"/tmp/b.cast",
|
||||
"ssh",
|
||||
"asciicast",
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const res = await server.request("GET", "/1", { user: "user-1" });
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("serves file content within the plugin's data directory", async () => {
|
||||
server = await startServer();
|
||||
const filePath = path.join(dataDir, "session_logs", "user-1", "a.cast");
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, '{"version":2}\n[0,"o","hi"]\n');
|
||||
|
||||
server.db.sqlite
|
||||
.prepare(
|
||||
`INSERT INTO p_session_recording_session_recordings
|
||||
(host_id, user_id, recording_path, protocol, format, started_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(7, "user-1", filePath, "ssh", "asciicast", new Date().toISOString());
|
||||
|
||||
// Point ctx.files.dataDir() at the temp dir for this request.
|
||||
(
|
||||
server.mock.ctx as unknown as {
|
||||
files: { dataDir: () => Promise<string> };
|
||||
}
|
||||
).files = { dataDir: async () => dataDir };
|
||||
|
||||
const res = await server.requestBinary("GET", "/1/content", {
|
||||
user: "user-1",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.contentType).toContain("asciicast");
|
||||
});
|
||||
|
||||
it("refuses a path outside the plugin's data directory", async () => {
|
||||
server = await startServer();
|
||||
server.db.sqlite
|
||||
.prepare(
|
||||
`INSERT INTO p_session_recording_session_recordings
|
||||
(host_id, user_id, recording_path, protocol, format, started_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
7,
|
||||
"user-1",
|
||||
"/etc/passwd",
|
||||
"ssh",
|
||||
"asciicast",
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
(
|
||||
server.mock.ctx as unknown as {
|
||||
files: { dataDir: () => Promise<string> };
|
||||
}
|
||||
).files = { dataDir: async () => dataDir };
|
||||
|
||||
const res = await server.requestBinary("GET", "/1/content", {
|
||||
user: "user-1",
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("deletes only the caller's own recording", async () => {
|
||||
server = await startServer();
|
||||
server.db.sqlite
|
||||
.prepare(
|
||||
`INSERT INTO p_session_recording_session_recordings
|
||||
(host_id, user_id, recording_path, protocol, format, started_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
7,
|
||||
"user-1",
|
||||
"/tmp/a.cast",
|
||||
"ssh",
|
||||
"asciicast",
|
||||
new Date().toISOString(),
|
||||
);
|
||||
|
||||
const denied = await server.request("DELETE", "/1", { user: "user-2" });
|
||||
expect(denied.status).toBe(404);
|
||||
|
||||
const ok = await server.request("DELETE", "/1", { user: "user-1" });
|
||||
expect(ok.status).toBe(200);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user