- P1: add path traversal guard to getMessageDir (reject .., /, \) - P2: treat unknown part types as non-content in messageHasContentFromSDK
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, afterAll, mock } from "bun:test"
|
|
import { mkdirSync, rmSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import { tmpdir } from "node:os"
|
|
import { randomUUID } from "node:crypto"
|
|
|
|
const TEST_STORAGE = join(tmpdir(), `omo-msgdir-test-${randomUUID()}`)
|
|
const TEST_MESSAGE_STORAGE = join(TEST_STORAGE, "message")
|
|
|
|
mock.module("./opencode-storage-paths", () => ({
|
|
OPENCODE_STORAGE: TEST_STORAGE,
|
|
MESSAGE_STORAGE: TEST_MESSAGE_STORAGE,
|
|
PART_STORAGE: join(TEST_STORAGE, "part"),
|
|
SESSION_STORAGE: join(TEST_STORAGE, "session"),
|
|
}))
|
|
|
|
mock.module("./opencode-storage-detection", () => ({
|
|
isSqliteBackend: () => false,
|
|
resetSqliteBackendCache: () => {},
|
|
}))
|
|
|
|
const { getMessageDir } = await import("./opencode-message-dir")
|
|
|
|
describe("getMessageDir", () => {
|
|
beforeEach(() => {
|
|
mkdirSync(TEST_MESSAGE_STORAGE, { recursive: true })
|
|
})
|
|
|
|
afterEach(() => {
|
|
try { rmSync(TEST_MESSAGE_STORAGE, { recursive: true, force: true }) } catch {}
|
|
})
|
|
|
|
afterAll(() => {
|
|
try { rmSync(TEST_STORAGE, { recursive: true, force: true }) } catch {}
|
|
})
|
|
|
|
it("returns null when sessionID does not start with ses_", () => {
|
|
//#given - sessionID without ses_ prefix
|
|
//#when
|
|
const result = getMessageDir("invalid")
|
|
//#then
|
|
expect(result).toBe(null)
|
|
})
|
|
|
|
it("returns null when MESSAGE_STORAGE does not exist", () => {
|
|
//#given
|
|
rmSync(TEST_MESSAGE_STORAGE, { recursive: true, force: true })
|
|
//#when
|
|
const result = getMessageDir("ses_123")
|
|
//#then
|
|
expect(result).toBe(null)
|
|
})
|
|
|
|
it("returns direct path when session exists directly", () => {
|
|
//#given
|
|
const sessionDir = join(TEST_MESSAGE_STORAGE, "ses_123")
|
|
mkdirSync(sessionDir, { recursive: true })
|
|
//#when
|
|
const result = getMessageDir("ses_123")
|
|
//#then
|
|
expect(result).toBe(sessionDir)
|
|
})
|
|
|
|
it("returns subdirectory path when session exists in subdirectory", () => {
|
|
//#given
|
|
const sessionDir = join(TEST_MESSAGE_STORAGE, "subdir", "ses_123")
|
|
mkdirSync(sessionDir, { recursive: true })
|
|
//#when
|
|
const result = getMessageDir("ses_123")
|
|
//#then
|
|
expect(result).toBe(sessionDir)
|
|
})
|
|
|
|
it("returns null for path traversal attempts with ..", () => {
|
|
//#given - sessionID containing path traversal
|
|
//#when
|
|
const result = getMessageDir("ses_../etc/passwd")
|
|
//#then
|
|
expect(result).toBe(null)
|
|
})
|
|
|
|
it("returns null for path traversal attempts with forward slash", () => {
|
|
//#given - sessionID containing forward slash
|
|
//#when
|
|
const result = getMessageDir("ses_foo/bar")
|
|
//#then
|
|
expect(result).toBe(null)
|
|
})
|
|
|
|
it("returns null for path traversal attempts with backslash", () => {
|
|
//#given - sessionID containing backslash
|
|
//#when
|
|
const result = getMessageDir("ses_foo\\bar")
|
|
//#then
|
|
expect(result).toBe(null)
|
|
})
|
|
|
|
it("returns null when session not found anywhere", () => {
|
|
//#given
|
|
mkdirSync(join(TEST_MESSAGE_STORAGE, "subdir1"), { recursive: true })
|
|
mkdirSync(join(TEST_MESSAGE_STORAGE, "subdir2"), { recursive: true })
|
|
//#when
|
|
const result = getMessageDir("ses_nonexistent")
|
|
//#then
|
|
expect(result).toBe(null)
|
|
})
|
|
}) |