chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Tests for file serve API route
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { hybridAuthMockFns, storageServiceMock, storageServiceMockFns } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockVerifyFileAccess,
|
||||
mockReadFile,
|
||||
mockIsUsingCloudStorage,
|
||||
mockDownloadCopilotFile,
|
||||
mockInferContextFromKey,
|
||||
mockGetContentType,
|
||||
mockFindLocalFile,
|
||||
mockCreateFileResponse,
|
||||
mockCreateErrorResponse,
|
||||
FileNotFoundError,
|
||||
} = vi.hoisted(() => {
|
||||
class FileNotFoundErrorClass extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'FileNotFoundError'
|
||||
}
|
||||
}
|
||||
return {
|
||||
mockVerifyFileAccess: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockIsUsingCloudStorage: vi.fn(),
|
||||
mockDownloadCopilotFile: vi.fn(),
|
||||
mockInferContextFromKey: vi.fn(),
|
||||
mockGetContentType: vi.fn(),
|
||||
mockFindLocalFile: vi.fn(),
|
||||
mockCreateFileResponse: vi.fn(),
|
||||
mockCreateErrorResponse: vi.fn(),
|
||||
FileNotFoundError: FileNotFoundErrorClass,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: mockReadFile,
|
||||
access: vi.fn().mockResolvedValue(undefined),
|
||||
stat: vi.fn().mockResolvedValue({ isFile: () => true, size: 100 }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: mockVerifyFileAccess,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/uploads', () => ({
|
||||
CopilotFiles: {
|
||||
downloadCopilotFile: mockDownloadCopilotFile,
|
||||
},
|
||||
isUsingCloudStorage: mockIsUsingCloudStorage,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
|
||||
|
||||
vi.mock('@/lib/uploads/utils/file-utils', () => ({
|
||||
inferContextFromKey: mockInferContextFromKey,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/uploads/setup.server', () => ({}))
|
||||
|
||||
vi.mock('@/lib/execution/sandbox/run-task', () => ({
|
||||
runSandboxTask: vi
|
||||
.fn()
|
||||
.mockImplementation(async (taskId: string) =>
|
||||
taskId === 'pdf-generate' ? Buffer.from('%PDF-compiled') : Buffer.from('PK\x03\x04compiled')
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
|
||||
parseWorkspaceFileKey: vi.fn().mockReturnValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/api/files/utils', () => ({
|
||||
FileNotFoundError,
|
||||
createFileResponse: mockCreateFileResponse,
|
||||
createErrorResponse: mockCreateErrorResponse,
|
||||
getContentType: mockGetContentType,
|
||||
extractStorageKey: vi.fn().mockImplementation((path: string) => path.split('/').pop()),
|
||||
extractFilename: vi.fn().mockImplementation((path: string) => path.split('/').pop()),
|
||||
findLocalFile: mockFindLocalFile,
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/files/serve/[...path]/route'
|
||||
|
||||
describe('File Serve API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
mockVerifyFileAccess.mockResolvedValue(true)
|
||||
mockReadFile.mockResolvedValue(Buffer.from('test content'))
|
||||
mockIsUsingCloudStorage.mockReturnValue(false)
|
||||
storageServiceMockFns.mockHasCloudStorage.mockReturnValue(true)
|
||||
mockInferContextFromKey.mockReturnValue('workspace')
|
||||
mockGetContentType.mockReturnValue('text/plain')
|
||||
mockFindLocalFile.mockReturnValue('/test/uploads/test-file.txt')
|
||||
mockCreateFileResponse.mockImplementation(
|
||||
(file: { buffer: Buffer; contentType: string; filename: string }) => {
|
||||
return new Response(file.buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': file.contentType,
|
||||
'Content-Disposition': `inline; filename="${file.filename}"`,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
mockCreateErrorResponse.mockImplementation((error: Error) => {
|
||||
return new Response(JSON.stringify({ error: error.name, message: error.message }), {
|
||||
status: error.name === 'FileNotFoundError' ? 404 : 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should serve local file successfully', async () => {
|
||||
const req = new NextRequest(
|
||||
'http://localhost:3000/api/files/serve/workspace/test-workspace-id/test-file.txt'
|
||||
)
|
||||
const params = { path: ['workspace', 'test-workspace-id', 'test-file.txt'] }
|
||||
|
||||
const response = await GET(req, { params: Promise.resolve(params) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('Content-Type')).toBe('text/plain')
|
||||
const disposition = response.headers.get('Content-Disposition')
|
||||
expect(disposition).toContain('inline')
|
||||
expect(disposition).toContain('filename=')
|
||||
expect(disposition).toContain('test-file.txt')
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle nested paths correctly', async () => {
|
||||
mockFindLocalFile.mockReturnValue('/test/uploads/nested/path/file.txt')
|
||||
|
||||
const req = new NextRequest(
|
||||
'http://localhost:3000/api/files/serve/workspace/test-workspace-id/nested-path-file.txt'
|
||||
)
|
||||
const params = { path: ['workspace', 'test-workspace-id', 'nested-path-file.txt'] }
|
||||
|
||||
const response = await GET(req, { params: Promise.resolve(params) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith('/test/uploads/nested/path/file.txt')
|
||||
})
|
||||
|
||||
it('should serve cloud file by downloading and proxying', async () => {
|
||||
mockIsUsingCloudStorage.mockReturnValue(true)
|
||||
storageServiceMockFns.mockDownloadFile.mockResolvedValue(Buffer.from('test cloud file content'))
|
||||
mockGetContentType.mockReturnValue('image/png')
|
||||
|
||||
const req = new NextRequest(
|
||||
'http://localhost:3000/api/files/serve/workspace/test-workspace-id/1234567890-image.png'
|
||||
)
|
||||
const params = { path: ['workspace', 'test-workspace-id', '1234567890-image.png'] }
|
||||
|
||||
const response = await GET(req, { params: Promise.resolve(params) })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('Content-Type')).toBe('image/png')
|
||||
|
||||
expect(storageServiceMockFns.mockDownloadFile).toHaveBeenCalledWith({
|
||||
key: 'workspace/test-workspace-id/1234567890-image.png',
|
||||
context: 'workspace',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 404 when file not found', async () => {
|
||||
mockVerifyFileAccess.mockResolvedValue(false)
|
||||
mockFindLocalFile.mockReturnValue(null)
|
||||
|
||||
const req = new NextRequest(
|
||||
'http://localhost:3000/api/files/serve/workspace/test-workspace-id/nonexistent.txt'
|
||||
)
|
||||
const params = { path: ['workspace', 'test-workspace-id', 'nonexistent.txt'] }
|
||||
|
||||
const response = await GET(req, { params: Promise.resolve(params) })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
|
||||
const responseData = await response.json()
|
||||
expect(responseData).toEqual({
|
||||
error: 'FileNotFoundError',
|
||||
message: expect.stringContaining('File not found'),
|
||||
})
|
||||
})
|
||||
|
||||
describe('content type detection', () => {
|
||||
const contentTypeTests = [
|
||||
{ ext: 'pdf', contentType: 'application/pdf' },
|
||||
{ ext: 'json', contentType: 'application/json' },
|
||||
{ ext: 'jpg', contentType: 'image/jpeg' },
|
||||
{ ext: 'txt', contentType: 'text/plain' },
|
||||
{ ext: 'unknown', contentType: 'application/octet-stream' },
|
||||
]
|
||||
|
||||
for (const test of contentTypeTests) {
|
||||
it(`should serve ${test.ext} file with correct content type`, async () => {
|
||||
mockGetContentType.mockReturnValue(test.contentType)
|
||||
mockFindLocalFile.mockReturnValue(`/test/uploads/file.${test.ext}`)
|
||||
mockCreateFileResponse.mockImplementation(
|
||||
(obj: { buffer: Buffer; contentType: string; filename: string }) =>
|
||||
new Response(obj.buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': obj.contentType,
|
||||
'Content-Disposition': `inline; filename="${obj.filename}"`,
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const req = new NextRequest(
|
||||
`http://localhost:3000/api/files/serve/workspace/test-workspace-id/file.${test.ext}`
|
||||
)
|
||||
const params = { path: ['workspace', 'test-workspace-id', `file.${test.ext}`] }
|
||||
|
||||
const response = await GET(req, { params: Promise.resolve(params) })
|
||||
|
||||
expect(response.headers.get('Content-Type')).toBe(test.contentType)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user