feat(file): conceal file resource existence from other users

- feat: cross-user file access returns not-found instead of
  permission-denied
- fix: repository delete now only affects active rows and returns
  ErrNotFound when no row changes; repeated deletes yield not-found
- feat: parent directory operations (list, upload, create-dir, move)
  conceal ownership of other user's directories
- test: update handler, service, repository, and integration tests to
  assert not-found behavior for cross-user and missing file operations
This commit is contained in:
2026-07-16 00:06:31 +08:00
parent bb86950632
commit 6604ecb026
8 changed files with 292 additions and 42 deletions
+32 -3
View File
@@ -588,6 +588,16 @@ func TestFileHandler_Delete(t *testing.T) {
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusNoContent, rec.Body.String())
}
// Repeated deletes return not found.
req = httptest.NewRequest(http.MethodDelete, "/api/v1/files/"+uploaded.ID, nil)
req.Header.Set(testUserIDHeader, "user1")
rec = httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("repeated delete status = %d, want %d; body = %s", rec.Code, http.StatusNotFound, rec.Body.String())
}
// Verify gone.
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
req.Header.Set(testUserIDHeader, "user1")
@@ -599,7 +609,20 @@ func TestFileHandler_Delete(t *testing.T) {
}
}
func TestFileHandler_ForbiddenAccess(t *testing.T) {
func TestFileHandler_DeleteMissingReturnsNotFound(t *testing.T) {
r := setupFileRouter(t)
req := httptest.NewRequest(http.MethodDelete, "/api/v1/files/missing-file", nil)
req.Header.Set(testUserIDHeader, "user1")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusNotFound, rec.Body.String())
}
}
func TestFileHandler_CrossUserAccessReturnsNotFound(t *testing.T) {
r := setupFileRouter(t)
// Upload as user1.
@@ -624,7 +647,13 @@ func TestFileHandler_ForbiddenAccess(t *testing.T) {
rec = httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
if !strings.Contains(rec.Body.String(), "file not found") {
t.Errorf("body = %s, want file not found message", rec.Body.String())
}
if strings.Contains(rec.Body.String(), "access denied") {
t.Errorf("body leaks authorization result: %s", rec.Body.String())
}
}