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
+87 -15
View File
@@ -32,6 +32,18 @@ func assertAppError(t *testing.T, err error, wantKind model.ErrorKind) bool {
return true
}
func assertAppErrorMessage(t *testing.T, err error, wantKind model.ErrorKind, wantMessage string) {
t.Helper()
if !assertAppError(t, err, wantKind) {
return
}
var ae *model.AppError
errors.As(err, &ae)
if ae.Message != wantMessage {
t.Errorf("message = %q, want %q", ae.Message, wantMessage)
}
}
// memStorage is an in-memory implementation of storage.StorageBackend for tests.
type memStorage struct {
mu sync.RWMutex
@@ -274,7 +286,7 @@ func TestFileService_Download(t *testing.T) {
}
}
func TestFileService_DownloadForbidden(t *testing.T) {
func TestFileService_DownloadReturnsNotFoundForOtherUser(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -284,7 +296,7 @@ func TestFileService_DownloadForbidden(t *testing.T) {
}
_, _, err = svc.Download(ctx, "user2", info.ID)
assertAppError(t, err, model.KindPermissionDenied)
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}
func TestFileService_DownloadDirectory(t *testing.T) {
@@ -328,7 +340,7 @@ func TestFileService_GetNotFound(t *testing.T) {
assertAppError(t, err, model.KindNotFound)
}
func TestFileService_GetForbidden(t *testing.T) {
func TestFileService_GetReturnsNotFoundForOtherUser(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -338,7 +350,7 @@ func TestFileService_GetForbidden(t *testing.T) {
}
_, err = svc.Get(ctx, "user2", info.ID)
assertAppError(t, err, model.KindPermissionDenied)
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}
func TestFileService_List(t *testing.T) {
@@ -395,6 +407,19 @@ func TestFileService_Update(t *testing.T) {
}
}
func TestFileService_UpdateReturnsNotFoundForOtherUser(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
info, err := svc.Upload(ctx, "user1", nil, "private.txt", strings.NewReader("data"))
if err != nil {
t.Fatalf("Upload = %v", err)
}
_, err = svc.Update(ctx, "user2", info.ID, "stolen.txt", nil)
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}
func TestFileService_UpdateMove(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -458,7 +483,7 @@ func TestFileService_DeleteNonEmptyDirectory(t *testing.T) {
}
}
func TestFileService_DeleteForbidden(t *testing.T) {
func TestFileService_DeleteReturnsNotFoundForOtherUser(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -468,7 +493,14 @@ func TestFileService_DeleteForbidden(t *testing.T) {
}
err = svc.Delete(ctx, "user2", info.ID)
assertAppError(t, err, model.KindPermissionDenied)
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}
func TestFileService_DeleteReturnsNotFoundForMissingFile(t *testing.T) {
svc := setupFileService(t)
err := svc.Delete(context.Background(), "user1", "missing-file")
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}
func TestFileService_CreateDir(t *testing.T) {
@@ -502,7 +534,7 @@ func TestFileService_CreateDirDuplicateName(t *testing.T) {
}
}
func TestFileService_VerifyParentForbidden(t *testing.T) {
func TestFileService_ParentOperationsReturnNotFoundForOtherUser(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -510,9 +542,50 @@ func TestFileService_VerifyParentForbidden(t *testing.T) {
if err != nil {
t.Fatalf("CreateDir = %v", err)
}
otherFile, err := svc.Upload(ctx, "user2", nil, "other.txt", strings.NewReader("data"))
if err != nil {
t.Fatalf("Upload other file = %v", err)
}
_, err = svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data"))
assertAppError(t, err, model.KindPermissionDenied)
tests := []struct {
name string
run func() error
}{
{
name: "list",
run: func() error {
_, err := svc.List(ctx, "user2", &dir.ID, 0, 50)
return err
},
},
{
name: "upload",
run: func() error {
_, err := svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data"))
return err
},
},
{
name: "create directory",
run: func() error {
_, err := svc.CreateDir(ctx, "user2", &dir.ID, "stolen-dir")
return err
},
},
{
name: "move",
run: func() error {
_, err := svc.Update(ctx, "user2", otherFile.ID, "", &dir.ID)
return err
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertAppErrorMessage(t, tt.run(), model.KindNotFound, "parent directory not found")
})
}
}
func TestFileService_SoftDelete(t *testing.T) {
@@ -562,7 +635,7 @@ func TestFileService_SoftDeleteKeepsStorage(t *testing.T) {
}
}
func TestFileService_SoftDeleteIdempotent(t *testing.T) {
func TestFileService_SoftDeleteRepeatedReturnsNotFound(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -574,12 +647,11 @@ func TestFileService_SoftDeleteIdempotent(t *testing.T) {
if err := svc.Delete(ctx, "user1", info.ID); err != nil {
t.Fatalf("first Delete = %v", err)
}
if err := svc.Delete(ctx, "user1", info.ID); err != nil {
t.Fatalf("second Delete should be idempotent, got: %v", err)
}
err = svc.Delete(ctx, "user1", info.ID)
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}
func TestFileService_SoftDeleteForbidden(t *testing.T) {
func TestFileService_SoftDeleteReturnsNotFoundForOtherUser(t *testing.T) {
svc := setupFileService(t)
ctx := context.Background()
@@ -589,5 +661,5 @@ func TestFileService_SoftDeleteForbidden(t *testing.T) {
}
err = svc.Delete(ctx, "user2", info.ID)
assertAppError(t, err, model.KindPermissionDenied)
assertAppErrorMessage(t, err, model.KindNotFound, "file not found")
}