mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
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);
|
|
});
|
|
});
|