Skip to content

Commit 72d8450

Browse files
Merge branch 'main' into dev
2 parents d161f69 + 3661f0a commit 72d8450

File tree

3 files changed

+55
-40
lines changed

3 files changed

+55
-40
lines changed

backend/src/routes/files.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ import { getErrorMessage, getStatusCode } from '../utils/error-utils'
88
export function createFileRoutes() {
99
const app = new Hono()
1010

11-
app.get('/*/download-zip', async (c) => {
12-
try {
13-
const userPath = c.req.path.replace(/^\/api\/files\/(.*)\/download-zip$/, '$1')
11+
app.get('download-zip', async(c) => {
12+
return c.json({ error: 'No path provided' }, 400)
13+
})
1414

15-
if (!userPath) {
16-
return c.json({ error: 'No path provided' }, 400)
17-
}
15+
app.get(':path{.+}/download-zip', async (c) => {
16+
const userPath = c.req.param('path')
1817

18+
if (!userPath) {
19+
return c.json({ error: 'No path provided' }, 400)
20+
}
21+
22+
try {
1923
logger.info(`Starting ZIP archive creation for ${userPath}`)
2024

2125
const archivePath = await archiveService.createDirectoryArchive(userPath)
@@ -59,7 +63,7 @@ export function createFileRoutes() {
5963
const startLine = parseInt(startLineParam, 10)
6064
const endLine = parseInt(endLineParam, 10)
6165

62-
if (isNaN(startLine) || isNaN(endLine) || startLine < 0 || endLine < startLine) {
66+
if (isNaN(startLine) || isNaN(endLine) || startLine < 0 || endLine <= startLine) {
6367
return c.json({ error: 'Invalid line range parameters' }, 400)
6468
}
6569

backend/src/utils/error-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export function getErrorMessage(error: unknown): string {
66
if (isError(error)) {
77
return error.message
88
}
9+
if (error && typeof error === 'object' && 'message' in error) {
10+
return String(error.message)
11+
}
912
return String(error)
1013
}
1114

backend/test/routes/files.test.ts

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest'
1+
import { describe, it, expect, vi, beforeEach, type MockedFunction } from 'vitest'
22
import { createFileRoutes } from '../../src/routes/files'
33
import { Hono } from 'hono'
44
import type { ReadStream } from 'fs'
@@ -13,20 +13,6 @@ interface FileUploadResult {
1313
mimeType: string
1414
}
1515

16-
const getFile = vi.fn()
17-
const getRawFileContent = vi.fn()
18-
const getFileRange = vi.fn()
19-
const uploadFile = vi.fn()
20-
const createFileOrFolder = vi.fn()
21-
const deleteFileOrFolder = vi.fn()
22-
const renameOrMoveFile = vi.fn()
23-
const applyFilePatches = vi.fn()
24-
25-
const createDirectoryArchive = vi.fn()
26-
const getArchiveSize = vi.fn()
27-
const getArchiveStream = vi.fn()
28-
const deleteArchive = vi.fn()
29-
3016
vi.mock('../../src/utils/logger', () => ({
3117
logger: {
3218
info: vi.fn(),
@@ -43,23 +29,37 @@ vi.mock('@opencode-manager/shared/config/env', () => ({
4329
}))
4430

4531
vi.mock('../../src/services/files', () => ({
46-
getFile,
47-
getRawFileContent,
48-
getFileRange,
49-
uploadFile,
50-
createFileOrFolder,
51-
deleteFileOrFolder,
52-
renameOrMoveFile,
53-
applyFilePatches,
32+
getFile: vi.fn(),
33+
getRawFileContent: vi.fn(),
34+
getFileRange: vi.fn(),
35+
uploadFile: vi.fn(),
36+
createFileOrFolder: vi.fn(),
37+
deleteFileOrFolder: vi.fn(),
38+
renameOrMoveFile: vi.fn(),
39+
applyFilePatches: vi.fn(),
5440
}))
5541

5642
vi.mock('../../src/services/archive', () => ({
57-
createDirectoryArchive,
58-
getArchiveSize,
59-
getArchiveStream,
60-
deleteArchive,
43+
createDirectoryArchive: vi.fn(),
44+
getArchiveSize: vi.fn(),
45+
getArchiveStream: vi.fn(),
46+
deleteArchive: vi.fn(),
6147
}))
6248

49+
const getFile = fileService.getFile as MockedFunction<typeof fileService.getFile>
50+
const getRawFileContent = fileService.getRawFileContent as MockedFunction<typeof fileService.getRawFileContent>
51+
const getFileRange = fileService.getFileRange as MockedFunction<typeof fileService.getFileRange>
52+
const uploadFile = fileService.uploadFile as MockedFunction<typeof fileService.uploadFile>
53+
const createFileOrFolder = fileService.createFileOrFolder as MockedFunction<typeof fileService.createFileOrFolder>
54+
const deleteFileOrFolder = fileService.deleteFileOrFolder as MockedFunction<typeof fileService.deleteFileOrFolder>
55+
const renameOrMoveFile = fileService.renameOrMoveFile as MockedFunction<typeof fileService.renameOrMoveFile>
56+
const applyFilePatches = fileService.applyFilePatches as MockedFunction<typeof fileService.applyFilePatches>
57+
58+
const createDirectoryArchive = archiveService.createDirectoryArchive as MockedFunction<typeof archiveService.createDirectoryArchive>
59+
const getArchiveSize = archiveService.getArchiveSize as MockedFunction<typeof archiveService.getArchiveSize>
60+
const getArchiveStream = archiveService.getArchiveStream as MockedFunction<typeof archiveService.getArchiveStream>
61+
const deleteArchive = archiveService.deleteArchive as MockedFunction<typeof archiveService.deleteArchive>
62+
6363
describe('File Routes', () => {
6464
let app: Hono
6565
let filesApp: Hono
@@ -391,7 +391,9 @@ describe('File Routes', () => {
391391
})
392392

393393
it('should return error when deletion fails', async () => {
394-
deleteFileOrFolder.mockRejectedValue(new Error('File not found'))
394+
const error = new Error('File not found') as any
395+
error.statusCode = 404
396+
deleteFileOrFolder.mockRejectedValue(error)
395397

396398
const response = await app.request('/api/files/test-repo/nonexistent.ts', {
397399
method: 'DELETE',
@@ -436,7 +438,9 @@ describe('File Routes', () => {
436438
})
437439

438440
it('should return error when patch application fails', async () => {
439-
applyFilePatches.mockRejectedValue(new Error('Invalid patch'))
441+
const error = new Error('Invalid patch') as any
442+
error.statusCode = 404
443+
applyFilePatches.mockRejectedValue(error)
440444

441445
const response = await app.request('/api/files/test-repo/test.ts', {
442446
method: 'PATCH',
@@ -472,7 +476,9 @@ describe('File Routes', () => {
472476
})
473477

474478
it('should return error when rename fails', async () => {
475-
renameOrMoveFile.mockRejectedValue(new Error('File not found'))
479+
const error = new Error('File not found') as any
480+
error.statusCode = 404
481+
renameOrMoveFile.mockRejectedValue(error)
476482

477483
const response = await app.request('/api/files/test-repo/nonexistent.ts', {
478484
method: 'PATCH',
@@ -499,17 +505,19 @@ describe('File Routes', () => {
499505
}
500506

501507
it('should reject path with ../ segments via service error', async () => {
502-
getFile.mockRejectedValue({ message: 'Path traversal detected', statusCode: 403 })
508+
const error = { message: 'Path traversal detected', statusCode: 403 }
509+
getFile.mockRejectedValue(error)
503510

504-
const response = await app.request('/api/files/../../../etc/passwd')
511+
const response = await app.request('/api/files/test-repo/../etc/passwd')
505512
const body = await response.json() as { error: string }
506513

507514
expect(response.status).toBe(403)
508515
expect(body.error).toContain('Path traversal detected')
509516
})
510517

511518
it('should reject encoded path traversal attempts', async () => {
512-
getFile.mockRejectedValue({ message: 'Path traversal detected', statusCode: 403 })
519+
const error = { message: 'Path traversal detected', statusCode: 403 }
520+
getFile.mockRejectedValue(error)
513521

514522
const response = await app.request('/api/files/test-repo/..%2Fetc')
515523
const body = await response.json() as { error: string }

0 commit comments

Comments
 (0)