mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55: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
@@ -1,144 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import path from "path";
|
||||
|
||||
// Stub db, logger, fs, and AuthManager before importing the route module
|
||||
const mockSelect = vi.fn();
|
||||
const mockDelete = vi.fn();
|
||||
const mockInsert = vi.fn();
|
||||
|
||||
vi.mock("../../../database/db/index.js", () => ({
|
||||
db: {
|
||||
select: mockSelect,
|
||||
delete: mockDelete,
|
||||
insert: mockInsert,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../../utils/logger.js", () => ({
|
||||
apiLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), success: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("../../../utils/auth-manager.js", () => ({
|
||||
AuthManager: {
|
||||
getInstance: () => ({
|
||||
createAuthMiddleware:
|
||||
() => (_req: unknown, _res: unknown, next: () => void) =>
|
||||
next(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
// The route module calls PermissionManager.getInstance() at import time and
|
||||
// pulls in the repository factory, which loads the drizzle schema and the
|
||||
// better-sqlite3 native binding. Importing that tree costs seconds under a
|
||||
// concurrent full run — enough to blow the 5s test timeout — and none of it is
|
||||
// under test here.
|
||||
vi.mock("../../../utils/permission-manager.js", () => ({
|
||||
PermissionManager: {
|
||||
getInstance: () => ({
|
||||
canAccessHost: vi.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
createCurrentSessionRecordingRepository: vi.fn(),
|
||||
createCurrentSettingsRepository: vi.fn(),
|
||||
getCurrentSettingValue: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockReadFile = vi.fn();
|
||||
const mockStat = vi.fn();
|
||||
const mockUnlink = vi.fn();
|
||||
const mockExistsSync = vi.fn();
|
||||
|
||||
vi.mock("fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("fs")>();
|
||||
return {
|
||||
...actual,
|
||||
promises: { readFile: mockReadFile, unlink: mockUnlink },
|
||||
existsSync: mockExistsSync,
|
||||
statSync: mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
// Build a chainable drizzle-like query stub
|
||||
function makeChain(resolveValue: unknown) {
|
||||
const chain: Record<string, unknown> = {};
|
||||
const methods = [
|
||||
"from",
|
||||
"leftJoin",
|
||||
"where",
|
||||
"orderBy",
|
||||
"limit",
|
||||
"set",
|
||||
"values",
|
||||
];
|
||||
for (const m of methods) {
|
||||
chain[m] = vi.fn(() => chain);
|
||||
}
|
||||
(chain as unknown as Promise<unknown>).then = (cb: (v: unknown) => unknown) =>
|
||||
Promise.resolve(resolveValue).then(cb);
|
||||
(chain as unknown as Promise<unknown>).catch = (
|
||||
cb: (e: unknown) => unknown,
|
||||
) => Promise.resolve(resolveValue).catch(cb);
|
||||
return chain;
|
||||
}
|
||||
|
||||
describe("session-log-routes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env.DATA_DIR = "/data";
|
||||
});
|
||||
|
||||
describe("GET / - list logs", () => {
|
||||
it("returns logs for the authenticated user with file size", async () => {
|
||||
const rows = [
|
||||
{
|
||||
id: 1,
|
||||
hostId: 10,
|
||||
userId: "u1",
|
||||
startedAt: "2026-01-01T00:00:00Z",
|
||||
endedAt: "2026-01-01T00:05:00Z",
|
||||
duration: 300,
|
||||
recordingPath: "/data/session_logs/u1/abc.log",
|
||||
hostName: "my-server",
|
||||
hostIp: "10.0.0.1",
|
||||
},
|
||||
];
|
||||
const chain = makeChain(rows);
|
||||
mockSelect.mockReturnValue(chain);
|
||||
mockStat.mockReturnValue({ size: 4096 });
|
||||
|
||||
// Directly call the route handler extracted from the module
|
||||
const { default: router } =
|
||||
await import("../../../database/routes/session-log-routes.js");
|
||||
expect(router).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("path traversal guard", () => {
|
||||
it("rejects paths outside the allowed session_logs directory", () => {
|
||||
const allowedBase = path.resolve("/data", "session_logs");
|
||||
const malicious = path.resolve("/data/session_logs/../../etc/passwd");
|
||||
expect(malicious.startsWith(allowedBase)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows a legitimate session log path", () => {
|
||||
const allowedBase = path.resolve("/data", "session_logs");
|
||||
const valid = path.resolve("/data/session_logs/user1/abc.log");
|
||||
expect(valid.startsWith(allowedBase)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatters (pure logic)", () => {
|
||||
it("stat returns size when file exists", () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockStat.mockReturnValue({ size: 1234 });
|
||||
const exists = mockExistsSync("/some/file.log");
|
||||
const { size } = mockStat("/some/file.log");
|
||||
expect(exists).toBe(true);
|
||||
expect(size).toBe(1234);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user