mirror of
https://github.com/pgsty/minio.git
synced 2026-11-09 17:21:51 +03:00
aa2d79daf2
A directory object always has the null version. DeleteObjects without a version ID chose whether to read the object before it turned the version into null, so with Object Lock and no replication or tiering it checked retention against empty metadata and removed a directory object under legal hold or retention. Choose the null version before the lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
58 lines
2.2 KiB
Go
58 lines
2.2 KiB
Go
// Copyright (c) 2026 PGSTY
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/minio/minio/internal/auth"
|
|
)
|
|
|
|
// A directory object always has the null version, and a bulk delete without a
|
|
// version ID removes it. The retention check must read that version's lock.
|
|
func TestBulkDeleteDirectoryObjectLock(t *testing.T) {
|
|
defer DetectTestLeak(t)()
|
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
|
|
t: t, endpoints: []string{"DeleteMultipleObjects"},
|
|
objAPITest: func(obj ObjectLayer, instance, bucket string, router http.Handler, cred auth.Credentials, t *testing.T) {
|
|
enableBucketObjectLock(t, bucket)
|
|
for _, kind := range append([]string{"unlocked"}, batchLockKinds...) {
|
|
key := "dir-" + kind + "/"
|
|
if _, err := obj.PutObject(t.Context(), bucket, key, mustGetPutObjReader(t, bytes.NewReader(nil), 0, "", ""),
|
|
ObjectOptions{Versioned: true, VersionID: nullVersionID, UserDefined: batchLockMeta(kind)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := encodeResponse(DeleteObjectsRequest{Objects: []ObjectToDelete{{ObjectV: ObjectV{ObjectName: key}}}})
|
|
req, err := newTestSignedRequestV4(http.MethodPost, getDeleteMultipleObjectsURL("", bucket), int64(len(body)), bytes.NewReader(body),
|
|
cred.AccessKey, cred.SecretKey, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
var resp DeleteObjectsResponse
|
|
if rec.Code != http.StatusOK || xml.Unmarshal(rec.Body.Bytes(), &resp) != nil {
|
|
t.Fatalf("%s: %s: status %d: %s", instance, key, rec.Code, rec.Body.String())
|
|
}
|
|
exists := batchVersionExists(t, obj, bucket, key, nullVersionID)
|
|
if kind == "unlocked" {
|
|
if exists || len(resp.DeletedObjects) != 1 {
|
|
t.Errorf("%s: %s: unlocked directory object was not deleted: %s", instance, key, rec.Body.String())
|
|
}
|
|
continue
|
|
}
|
|
if !exists || len(resp.Errors) != 1 || len(resp.DeletedObjects) != 0 ||
|
|
resp.Errors[0].Key != key || !strings.Contains(resp.Errors[0].Message, "WORM") {
|
|
t.Errorf("%s: %s: want the WORM error and the object kept (exists=%v): %s", instance, key, exists, rec.Body.String())
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|