Skip to content

Commit 5919ccb

Browse files
GenerQAQclaude
andcommitted
test: add UploadFromSandbox unit + E2E IDOR test coverage
Covers the disk ownership check added in PR #460 with: - Unit tests for 404 on cross-project disk access and 400 on invalid body - E2E test for cross-project upload_from_sandbox denial Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 33ef45a commit 5919ccb

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

src/server/api/go/internal/modules/handler/artifact_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,68 @@ func TestArtifactHandler_GlobArtifacts(t *testing.T) {
920920
}
921921
}
922922

923+
func TestArtifactHandler_UploadFromSandbox(t *testing.T) {
924+
gin.SetMode(gin.TestMode)
925+
926+
t.Run("returns 404 when disk belongs to different project", func(t *testing.T) {
927+
mockService := new(MockArtifactService)
928+
mockDiskRepo := new(MockDiskRepo)
929+
930+
projectID := uuid.New()
931+
diskID := uuid.New()
932+
933+
// Disk not found for this project (IDOR check)
934+
mockDiskRepo.On("GetByProjectAndID", mock.Anything, projectID, diskID).
935+
Return(nil, fmt.Errorf("record not found"))
936+
937+
handler := NewArtifactHandler(mockService, mockDiskRepo, createDefaultTestConfig(), nil, nil)
938+
939+
w := httptest.NewRecorder()
940+
c, _ := gin.CreateTestContext(w)
941+
c.Set("project", &model.Project{ID: projectID})
942+
943+
body := `{"sandbox_id":"` + uuid.New().String() + `","sandbox_path":"/tmp","sandbox_filename":"test.txt","file_path":"/"}`
944+
c.Request = httptest.NewRequest("POST", "/disk/"+diskID.String()+"/artifact/upload_from_sandbox", bytes.NewBufferString(body))
945+
c.Request.Header.Set("Content-Type", "application/json")
946+
c.Params = gin.Params{{Key: "disk_id", Value: diskID.String()}}
947+
948+
handler.UploadFromSandbox(c)
949+
950+
assert.Equal(t, http.StatusNotFound, w.Code)
951+
mockService.AssertNotCalled(t, "Create")
952+
mockDiskRepo.AssertExpectations(t)
953+
})
954+
955+
t.Run("returns 400 for invalid request body", func(t *testing.T) {
956+
mockService := new(MockArtifactService)
957+
mockDiskRepo := new(MockDiskRepo)
958+
959+
projectID := uuid.New()
960+
diskID := uuid.New()
961+
962+
// Disk found for this project
963+
mockDiskRepo.On("GetByProjectAndID", mock.Anything, projectID, diskID).
964+
Return(&model.Disk{ID: diskID, ProjectID: projectID}, nil)
965+
966+
handler := NewArtifactHandler(mockService, mockDiskRepo, createDefaultTestConfig(), nil, nil)
967+
968+
w := httptest.NewRecorder()
969+
c, _ := gin.CreateTestContext(w)
970+
c.Set("project", &model.Project{ID: projectID})
971+
972+
// Empty JSON body — missing required fields
973+
c.Request = httptest.NewRequest("POST", "/disk/"+diskID.String()+"/artifact/upload_from_sandbox", bytes.NewBufferString(`{}`))
974+
c.Request.Header.Set("Content-Type", "application/json")
975+
c.Params = gin.Params{{Key: "disk_id", Value: diskID.String()}}
976+
977+
handler.UploadFromSandbox(c)
978+
979+
assert.Equal(t, http.StatusBadRequest, w.Code)
980+
mockService.AssertNotCalled(t, "Create")
981+
mockDiskRepo.AssertExpectations(t)
982+
})
983+
}
984+
923985
func TestArtifactHandler_DownloadArtifact(t *testing.T) {
924986
gin.SetMode(gin.TestMode)
925987

src/server/tests/e2e/test_project_isolation.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,27 @@ async def test_cross_project_upload_artifact(test_project: ProjectCredentials, s
250250
await client.delete(f"{API_URL}/api/v1/disk/{disk_id}", headers=test_project.headers)
251251

252252

253+
@pytest.mark.asyncio
254+
async def test_cross_project_upload_from_sandbox(test_project: ProjectCredentials, second_project: ProjectCredentials):
255+
assert await wait_for_services()
256+
async with httpx.AsyncClient() as client:
257+
disk_id = await create_disk(client, test_project.headers)
258+
259+
resp = await client.post(
260+
f"{API_URL}/api/v1/disk/{disk_id}/artifact/upload_from_sandbox",
261+
json={
262+
"sandbox_id": str(uuid.uuid4()),
263+
"sandbox_path": "/tmp",
264+
"sandbox_filename": "test.txt",
265+
"file_path": "/",
266+
},
267+
headers=second_project.headers,
268+
)
269+
assert resp.status_code in DENIED_STATUSES
270+
271+
await client.delete(f"{API_URL}/api/v1/disk/{disk_id}", headers=test_project.headers)
272+
273+
253274
@pytest.mark.asyncio
254275
async def test_cross_project_download_artifact(test_project: ProjectCredentials, second_project: ProjectCredentials):
255276
assert await wait_for_services()

0 commit comments

Comments
 (0)