mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 08:02:44 +00:00
782 lines
24 KiB
TypeScript
782 lines
24 KiB
TypeScript
/* eslint-disable max-lines -- Why: import tests cover local copy, SSH routing,
|
|
symlink safety, and runtime-upload staging against one shared IPC fixture. */
|
|
import path from 'path'
|
|
import { constants } from 'fs'
|
|
import { Readable, Writable } from 'stream'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const handlers = new Map<string, (_event: unknown, args: unknown) => Promise<unknown>>()
|
|
const {
|
|
handleMock,
|
|
lstatMock,
|
|
mkdirMock,
|
|
realpathMock,
|
|
copyFileMock,
|
|
openMock,
|
|
readFileMock,
|
|
readdirMock,
|
|
rmMock,
|
|
unlinkMock
|
|
} = vi.hoisted(() => ({
|
|
handleMock: vi.fn(),
|
|
lstatMock: vi.fn(),
|
|
mkdirMock: vi.fn(),
|
|
realpathMock: vi.fn(),
|
|
copyFileMock: vi.fn(),
|
|
openMock: vi.fn(),
|
|
readFileMock: vi.fn(),
|
|
readdirMock: vi.fn(),
|
|
rmMock: vi.fn(),
|
|
unlinkMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('electron', () => ({
|
|
ipcMain: { handle: handleMock }
|
|
}))
|
|
|
|
vi.mock('fs/promises', () => ({
|
|
lstat: lstatMock,
|
|
mkdir: mkdirMock,
|
|
open: openMock,
|
|
rename: vi.fn(),
|
|
writeFile: vi.fn(),
|
|
realpath: realpathMock,
|
|
copyFile: copyFileMock,
|
|
readFile: readFileMock,
|
|
readdir: readdirMock,
|
|
rm: rmMock,
|
|
unlink: unlinkMock
|
|
}))
|
|
|
|
import { registerFilesystemMutationHandlers } from './filesystem-mutations'
|
|
|
|
const REPO_PATH = path.resolve('/workspace/repo')
|
|
const WORKSPACE_DIR = path.resolve('/workspace')
|
|
|
|
const store = {
|
|
getRepos: () => [
|
|
{ id: 'repo-1', path: REPO_PATH, displayName: 'repo', badgeColor: '#000', addedAt: 0 }
|
|
],
|
|
getSettings: () => ({ workspaceDir: WORKSPACE_DIR })
|
|
}
|
|
|
|
function enoent(): Error {
|
|
return Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
|
}
|
|
|
|
describe('fs:importExternalPaths', () => {
|
|
const destDir = path.resolve('/workspace/repo/src')
|
|
|
|
function mockSourceFile(filePath: string): void {
|
|
const resolvedPath = path.resolve(filePath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return {
|
|
size: 12,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => true,
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
}
|
|
|
|
function mockSourceDir(dirPath: string, entries: { name: string; isDir: boolean }[]): void {
|
|
const resolvedDir = path.resolve(dirPath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedDir) {
|
|
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
|
|
}
|
|
const entry = entries.find((e) => path.join(resolvedDir, e.name) === p)
|
|
if (entry) {
|
|
return {
|
|
size: entry.isDir ? 0 : 12,
|
|
ino: entry.isDir ? 2 : 3,
|
|
dev: 1,
|
|
isFile: () => !entry.isDir,
|
|
isDirectory: () => entry.isDir,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockImplementation(async () => {
|
|
return entries.map((e) => ({
|
|
name: e.name,
|
|
isDirectory: () => e.isDir,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => !e.isDir
|
|
}))
|
|
})
|
|
}
|
|
|
|
function mockSymlinkSource(filePath: string): void {
|
|
const resolvedPath = path.resolve(filePath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return { isFile: () => false, isDirectory: () => false, isSymbolicLink: () => true }
|
|
}
|
|
throw enoent()
|
|
})
|
|
}
|
|
|
|
function mockLocalCopyOpenSuccess(content = Buffer.from('file-content')): void {
|
|
openMock.mockImplementation(async (_p: string, flags: unknown) => {
|
|
if (flags === 'wx') {
|
|
const written: Buffer[] = []
|
|
return {
|
|
createWriteStream: () =>
|
|
new Writable({
|
|
write(chunk, _encoding, callback) {
|
|
written.push(Buffer.from(chunk))
|
|
callback()
|
|
}
|
|
}),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
written
|
|
}
|
|
}
|
|
return {
|
|
stat: vi.fn().mockResolvedValue({
|
|
size: content.byteLength,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => true
|
|
}),
|
|
createReadStream: () => Readable.from([content]),
|
|
close: vi.fn().mockResolvedValue(undefined)
|
|
}
|
|
})
|
|
}
|
|
|
|
beforeEach(() => {
|
|
handlers.clear()
|
|
handleMock.mockReset()
|
|
lstatMock.mockReset()
|
|
mkdirMock.mockReset()
|
|
realpathMock.mockReset()
|
|
copyFileMock.mockReset()
|
|
openMock.mockReset()
|
|
readFileMock.mockReset()
|
|
readdirMock.mockReset()
|
|
rmMock.mockReset()
|
|
unlinkMock.mockReset()
|
|
|
|
handleMock.mockImplementation((channel: string, handler: never) => {
|
|
handlers.set(channel, handler)
|
|
})
|
|
|
|
realpathMock.mockImplementation(async (p: string) => p)
|
|
lstatMock.mockRejectedValue(enoent())
|
|
mkdirMock.mockResolvedValue(undefined)
|
|
copyFileMock.mockResolvedValue(undefined)
|
|
mockLocalCopyOpenSuccess()
|
|
readFileMock.mockResolvedValue(Buffer.from('file-content'))
|
|
readdirMock.mockResolvedValue([])
|
|
rmMock.mockResolvedValue(undefined)
|
|
unlinkMock.mockResolvedValue(undefined)
|
|
|
|
registerFilesystemMutationHandlers(store as never)
|
|
})
|
|
|
|
it('imports a single file', async () => {
|
|
const sourcePath = '/tmp/dropped/logo.png'
|
|
mockSourceFile(sourcePath)
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: unknown[] }
|
|
|
|
expect(result.results).toHaveLength(1)
|
|
expect(result.results[0]).toMatchObject({
|
|
status: 'imported',
|
|
kind: 'file',
|
|
renamed: false,
|
|
destPath: path.join(destDir, 'logo.png')
|
|
})
|
|
expect(openMock).toHaveBeenCalledWith(
|
|
path.resolve(sourcePath),
|
|
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
|
|
)
|
|
expect(openMock).toHaveBeenCalledWith(path.join(destDir, 'logo.png'), 'wx')
|
|
expect(copyFileMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fails local import instead of clobbering when the chosen destination appears late', async () => {
|
|
const sourcePath = '/tmp/dropped/logo.png'
|
|
mockSourceFile(sourcePath)
|
|
openMock.mockImplementation(async (_p: string, flags: unknown) => {
|
|
if (flags === 'wx') {
|
|
throw Object.assign(new Error('EEXIST'), { code: 'EEXIST' })
|
|
}
|
|
return {
|
|
stat: vi.fn().mockResolvedValue({
|
|
size: 12,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => true
|
|
}),
|
|
createReadStream: () => Readable.from([Buffer.from('file-content')]),
|
|
close: vi.fn().mockResolvedValue(undefined)
|
|
}
|
|
})
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; reason?: string }[] }
|
|
|
|
expect(result.results[0]).toMatchObject({ status: 'failed', reason: 'EEXIST' })
|
|
expect(openMock).toHaveBeenCalledWith(
|
|
path.resolve(sourcePath),
|
|
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
|
|
)
|
|
expect(openMock).toHaveBeenCalledWith(path.join(destDir, 'logo.png'), 'wx')
|
|
})
|
|
|
|
it('imports multiple files in one batch', async () => {
|
|
const sources = ['/tmp/dropped/a.txt', '/tmp/dropped/b.txt']
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
const resolved = [path.resolve(sources[0]), path.resolve(sources[1])]
|
|
if (resolved.includes(p)) {
|
|
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
|
|
}
|
|
throw enoent()
|
|
})
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: sources,
|
|
destDir
|
|
})) as { results: { status: string }[] }
|
|
|
|
expect(result.results).toHaveLength(2)
|
|
expect(result.results[0]).toMatchObject({ status: 'imported' })
|
|
expect(result.results[1]).toMatchObject({ status: 'imported' })
|
|
})
|
|
|
|
it('imports a directory recursively', async () => {
|
|
const sourcePath = '/tmp/dropped/assets'
|
|
mockSourceDir(sourcePath, [
|
|
{ name: 'icon.png', isDir: false },
|
|
{ name: 'fonts', isDir: true }
|
|
])
|
|
readdirMock
|
|
.mockResolvedValueOnce([
|
|
{
|
|
name: 'icon.png',
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => true
|
|
},
|
|
{ name: 'fonts', isDirectory: () => true, isSymbolicLink: () => false, isFile: () => false }
|
|
])
|
|
.mockResolvedValue([])
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; kind?: string }[] }
|
|
|
|
expect(result.results).toHaveLength(1)
|
|
expect(result.results[0]).toMatchObject({
|
|
status: 'imported',
|
|
kind: 'directory',
|
|
renamed: false
|
|
})
|
|
expect(mkdirMock).toHaveBeenCalled()
|
|
})
|
|
|
|
it('deconflicts top-level filename collisions', async () => {
|
|
const sourcePath = '/tmp/dropped/logo.png'
|
|
const existingDest = path.join(destDir, 'logo.png')
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === path.resolve(sourcePath)) {
|
|
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
|
|
}
|
|
if (p === existingDest) {
|
|
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
|
|
}
|
|
throw enoent()
|
|
})
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; destPath?: string; renamed?: boolean }[] }
|
|
|
|
expect(result.results[0]).toMatchObject({
|
|
status: 'imported',
|
|
destPath: path.join(destDir, 'logo copy.png'),
|
|
renamed: true
|
|
})
|
|
})
|
|
|
|
it('deconflicts top-level directory collisions', async () => {
|
|
const sourcePath = '/tmp/dropped/assets'
|
|
const existingDest = path.join(destDir, 'assets')
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === path.resolve(sourcePath)) {
|
|
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
|
|
}
|
|
if (p === existingDest) {
|
|
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockResolvedValue([])
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; destPath?: string; renamed?: boolean }[] }
|
|
|
|
expect(result.results[0]).toMatchObject({
|
|
status: 'imported',
|
|
destPath: path.join(destDir, 'assets copy'),
|
|
renamed: true
|
|
})
|
|
})
|
|
|
|
it('rejects top-level symlink sources before canonicalization', async () => {
|
|
const sourcePath = '/tmp/dropped/link.txt'
|
|
mockSymlinkSource(sourcePath)
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; reason?: string }[] }
|
|
|
|
expect(result.results[0]).toMatchObject({ status: 'skipped', reason: 'symlink' })
|
|
expect(copyFileMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips a dropped directory with nested symlinks without leaving partial output', async () => {
|
|
const sourcePath = '/tmp/dropped/mixeddir'
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === path.resolve(sourcePath)) {
|
|
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockResolvedValue([
|
|
{
|
|
name: 'normal.txt',
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => true
|
|
},
|
|
{
|
|
name: 'bad-link',
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => true,
|
|
isFile: () => false
|
|
}
|
|
])
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; reason?: string }[] }
|
|
|
|
expect(result.results[0]).toMatchObject({ status: 'skipped', reason: 'symlink' })
|
|
expect(copyFileMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fails and removes output if a local directory entry becomes a symlink after pre-scan', async () => {
|
|
const sourcePath = '/tmp/dropped/mixeddir'
|
|
const resolvedSource = path.resolve(sourcePath)
|
|
const childPath = path.join(resolvedSource, 'normal.txt')
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedSource) {
|
|
return { isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false }
|
|
}
|
|
if (p === childPath) {
|
|
return { isFile: () => false, isDirectory: () => false, isSymbolicLink: () => true }
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockResolvedValue([
|
|
{
|
|
name: 'normal.txt',
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => true
|
|
}
|
|
])
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir
|
|
})) as { results: { status: string; reason?: string }[] }
|
|
|
|
expect(result.results[0]).toMatchObject({
|
|
status: 'failed',
|
|
reason: "Symlink not allowed in 'normal.txt'"
|
|
})
|
|
expect(openMock).not.toHaveBeenCalledWith(childPath, expect.anything())
|
|
expect(rmMock).toHaveBeenCalledWith(path.join(destDir, 'mixeddir'), {
|
|
recursive: true,
|
|
force: true
|
|
})
|
|
})
|
|
|
|
it('rejects unauthorized destinations', async () => {
|
|
const sourcePath = '/tmp/dropped/file.txt'
|
|
mockSourceFile(sourcePath)
|
|
|
|
realpathMock.mockImplementation(async (p: string) => {
|
|
if (p === path.resolve('/outside/evil')) {
|
|
return path.resolve('/outside/evil')
|
|
}
|
|
return p
|
|
})
|
|
|
|
await expect(
|
|
handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: [sourcePath],
|
|
destDir: '/outside/evil'
|
|
})
|
|
).rejects.toThrow('Access denied')
|
|
})
|
|
|
|
it('returns per-item results including rename metadata', async () => {
|
|
const sources = ['/tmp/dropped/a.txt', '/tmp/dropped/missing.txt']
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === path.resolve(sources[0])) {
|
|
return { isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false }
|
|
}
|
|
throw enoent()
|
|
})
|
|
|
|
const result = (await handlers.get('fs:importExternalPaths')!(null, {
|
|
sourcePaths: sources,
|
|
destDir
|
|
})) as {
|
|
results: {
|
|
sourcePath: string
|
|
status: string
|
|
reason?: string
|
|
renamed?: boolean
|
|
}[]
|
|
}
|
|
|
|
expect(result.results).toHaveLength(2)
|
|
expect(result.results[0]).toMatchObject({
|
|
sourcePath: sources[0],
|
|
status: 'imported',
|
|
renamed: false
|
|
})
|
|
expect(result.results[1]).toMatchObject({
|
|
sourcePath: sources[1],
|
|
status: 'skipped',
|
|
reason: 'missing'
|
|
})
|
|
})
|
|
|
|
it('stages external files for runtime upload without copying into the local worktree', async () => {
|
|
const sourcePath = '/tmp/dropped/logo.png'
|
|
const resolvedPath = path.resolve(sourcePath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return {
|
|
size: 4,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => true,
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
const closeMock = vi.fn().mockResolvedValue(undefined)
|
|
const readFileHandleMock = vi.fn().mockResolvedValue(Buffer.from('png'))
|
|
openMock.mockResolvedValue({
|
|
stat: vi.fn().mockResolvedValue({
|
|
size: 4,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => true
|
|
}),
|
|
readFile: readFileHandleMock,
|
|
close: closeMock
|
|
})
|
|
|
|
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
|
|
sourcePaths: [sourcePath]
|
|
})) as { sources: unknown[] }
|
|
|
|
expect(result.sources).toEqual([
|
|
{
|
|
sourcePath,
|
|
status: 'staged',
|
|
name: 'logo.png',
|
|
kind: 'file',
|
|
entries: [{ relativePath: '', kind: 'file', contentBase64: 'cG5n' }]
|
|
}
|
|
])
|
|
expect(copyFileMock).not.toHaveBeenCalled()
|
|
expect(readFileHandleMock).toHaveBeenCalled()
|
|
expect(closeMock).toHaveBeenCalled()
|
|
})
|
|
|
|
it('stages runtime upload directories whose child names start with dotdot characters', async () => {
|
|
const sourcePath = '/tmp/dropped/project'
|
|
const resolvedPath = path.resolve(sourcePath)
|
|
const childDir = path.join(resolvedPath, '..assets')
|
|
const childFile = path.join(childDir, 'icon.txt')
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath || p === childDir) {
|
|
return {
|
|
size: 0,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => false,
|
|
isDirectory: () => true,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
if (p === childFile) {
|
|
return {
|
|
size: 4,
|
|
ino: 2,
|
|
dev: 1,
|
|
isFile: () => true,
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return [
|
|
{
|
|
name: '..assets',
|
|
isDirectory: () => true,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => false
|
|
}
|
|
]
|
|
}
|
|
if (p === childDir) {
|
|
return [
|
|
{
|
|
name: 'icon.txt',
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => true
|
|
}
|
|
]
|
|
}
|
|
return []
|
|
})
|
|
openMock.mockResolvedValue({
|
|
stat: vi.fn().mockResolvedValue({
|
|
size: 4,
|
|
ino: 2,
|
|
dev: 1,
|
|
isFile: () => true
|
|
}),
|
|
readFile: vi.fn().mockResolvedValue(Buffer.from('icon')),
|
|
close: vi.fn().mockResolvedValue(undefined)
|
|
})
|
|
|
|
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
|
|
sourcePaths: [sourcePath]
|
|
})) as { sources: unknown[] }
|
|
|
|
expect(result.sources).toEqual([
|
|
{
|
|
sourcePath,
|
|
status: 'staged',
|
|
name: 'project',
|
|
kind: 'directory',
|
|
entries: [
|
|
{ relativePath: '', kind: 'directory' },
|
|
{ relativePath: '..assets', kind: 'directory' },
|
|
{ relativePath: '..assets/icon.txt', kind: 'file', contentBase64: 'aWNvbg==' }
|
|
]
|
|
}
|
|
])
|
|
})
|
|
|
|
it('skips runtime upload directories with nested symlinks during the staging traversal', async () => {
|
|
const sourcePath = '/tmp/dropped/project'
|
|
const resolvedPath = path.resolve(sourcePath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return {
|
|
size: 0,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => false,
|
|
isDirectory: () => true,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockResolvedValue([
|
|
{
|
|
name: 'link.txt',
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => true,
|
|
isFile: () => false
|
|
}
|
|
])
|
|
|
|
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
|
|
sourcePaths: [sourcePath]
|
|
})) as { sources: { status: string; reason?: string }[] }
|
|
|
|
expect(result.sources[0]).toMatchObject({ status: 'skipped', reason: 'symlink' })
|
|
expect(readdirMock).toHaveBeenCalledOnce()
|
|
expect(openMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('checks runtime upload directory byte budget before reading a file that exceeds the total cap', async () => {
|
|
const sourcePath = '/tmp/dropped/project'
|
|
const resolvedPath = path.resolve(sourcePath)
|
|
const filePaths = ['one.bin', 'two.bin', 'three.bin', 'four.bin', 'overflow.bin'].map((name) =>
|
|
path.join(resolvedPath, name)
|
|
)
|
|
const mib = 1024 * 1024
|
|
const regularSize = 25 * mib
|
|
const overflowSize = 1 * mib
|
|
const readFileMock = vi.fn().mockResolvedValue(Buffer.from('chunk'))
|
|
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return {
|
|
size: 0,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => false,
|
|
isDirectory: () => true,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
const fileIndex = filePaths.indexOf(p)
|
|
if (fileIndex !== -1) {
|
|
const size = fileIndex === filePaths.length - 1 ? overflowSize : regularSize
|
|
return {
|
|
size,
|
|
ino: fileIndex + 2,
|
|
dev: 1,
|
|
isFile: () => true,
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockResolvedValue(
|
|
filePaths.map((filePath) => ({
|
|
name: path.basename(filePath),
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false,
|
|
isFile: () => true
|
|
}))
|
|
)
|
|
openMock.mockImplementation(async (p: string) => {
|
|
const fileIndex = filePaths.indexOf(p)
|
|
if (fileIndex >= 0 && fileIndex < filePaths.length - 1) {
|
|
return {
|
|
stat: vi.fn().mockResolvedValue({
|
|
size: regularSize,
|
|
ino: fileIndex + 2,
|
|
dev: 1,
|
|
isFile: () => true
|
|
}),
|
|
readFile: readFileMock,
|
|
close: vi.fn().mockResolvedValue(undefined)
|
|
}
|
|
}
|
|
throw new Error(`unexpected open: ${p}`)
|
|
})
|
|
|
|
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
|
|
sourcePaths: [sourcePath]
|
|
})) as { sources: { status: string; reason?: string }[] }
|
|
|
|
expect(result.sources[0]).toMatchObject({
|
|
status: 'failed',
|
|
reason: 'Remote import is too large'
|
|
})
|
|
expect(readFileMock).toHaveBeenCalledTimes(4)
|
|
expect(openMock).not.toHaveBeenCalledWith(filePaths.at(-1), expect.anything())
|
|
})
|
|
|
|
it('fails runtime upload staging when a file changes between lstat and open', async () => {
|
|
const sourcePath = '/tmp/dropped/logo.png'
|
|
const resolvedPath = path.resolve(sourcePath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return {
|
|
size: 4,
|
|
ino: 1,
|
|
dev: 1,
|
|
isFile: () => true,
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
const readFileHandleMock = vi.fn().mockResolvedValue(Buffer.from('png'))
|
|
openMock.mockResolvedValue({
|
|
stat: vi.fn().mockResolvedValue({
|
|
size: 4,
|
|
ino: 2,
|
|
dev: 1,
|
|
isFile: () => true
|
|
}),
|
|
readFile: readFileHandleMock,
|
|
close: vi.fn().mockResolvedValue(undefined)
|
|
})
|
|
|
|
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
|
|
sourcePaths: [sourcePath]
|
|
})) as { sources: { status: string; reason?: string }[] }
|
|
|
|
expect(result.sources[0]).toMatchObject({
|
|
status: 'failed',
|
|
reason: "File changed during upload staging: ''"
|
|
})
|
|
expect(readFileHandleMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fails runtime upload staging when a checked directory resolves outside the upload root', async () => {
|
|
const sourcePath = '/tmp/dropped/assets'
|
|
const resolvedPath = path.resolve(sourcePath)
|
|
lstatMock.mockImplementation(async (p: string) => {
|
|
if (p === resolvedPath) {
|
|
return {
|
|
isFile: () => false,
|
|
isDirectory: () => true,
|
|
isSymbolicLink: () => false
|
|
}
|
|
}
|
|
throw enoent()
|
|
})
|
|
readdirMock.mockResolvedValue([])
|
|
realpathMock
|
|
.mockResolvedValueOnce(resolvedPath)
|
|
.mockResolvedValueOnce(path.resolve('/private/assets'))
|
|
|
|
const result = (await handlers.get('fs:stageExternalPathsForRuntimeUpload')!(null, {
|
|
sourcePaths: [sourcePath]
|
|
})) as { sources: { status: string; reason?: string }[] }
|
|
|
|
expect(result.sources[0]).toMatchObject({
|
|
status: 'failed',
|
|
reason: "Path escaped upload root during staging: ''"
|
|
})
|
|
})
|
|
})
|