Files
minio/docs/investigations/issue-77/issue77_review_test.go.txt
T
2026-09-12 17:52:25 +08:00

148 lines
7.4 KiB
Plaintext

package cmd
// Review-only tests against the unchanged 2026-09-12 main. Failures are evidence,
// not a production regression-test submission.
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth"
)
func TestIssue77CurrentSourceTimeAndDeletion(t *testing.T) {
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: review77SourceTimeAndDeletion})
}
func review77SourceTimeAndDeletion(obj ObjectLayer, backend, bucket string, _ http.Handler, cred auth.Credentials, t *testing.T) {
ctx := t.Context()
created := UTCNow().Add(-time.Hour)
putAt := created.Add(10 * time.Minute)
delAt := putAt.Add(time.Minute)
enc := func(s string) *string { v := base64.StdEncoding.EncodeToString([]byte(s)); return &v }
base := newBucketMetadata(bucket)
base.SetCreatedAt(created)
base.defaultTimestamps()
quotaJSON, err := json.Marshal(madmin.BucketQuota{Quota: 1024, Type: madmin.HardQuota})
if err != nil {
t.Fatal(err)
}
cases := []struct {
name, file string
put madmin.SRBucketMeta
value func(BucketMetadata) []byte
stamp func(BucketMetadata) time.Time
exported func(madmin.SRBucketInfo) time.Time
deletable bool
}{
{"policy", bucketPolicyConfig, madmin.SRBucketMeta{Type: madmin.SRBucketMetaTypePolicy, Policy: []byte(fmt.Sprintf(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::%s/*"}]}`, bucket))}, func(m BucketMetadata) []byte { return m.PolicyConfigJSON }, func(m BucketMetadata) time.Time { return m.PolicyConfigUpdatedAt }, func(m madmin.SRBucketInfo) time.Time { return m.PolicyUpdatedAt }, true},
{"tags", bucketTaggingConfig, madmin.SRBucketMeta{Type: madmin.SRBucketMetaTypeTags, Tags: enc(`<Tagging><TagSet><Tag><Key>key</Key><Value>old</Value></Tag></TagSet></Tagging>`)}, func(m BucketMetadata) []byte { return m.TaggingConfigXML }, func(m BucketMetadata) time.Time { return m.TaggingConfigUpdatedAt }, func(m madmin.SRBucketInfo) time.Time { return m.TagConfigUpdatedAt }, true},
{"sse", bucketSSEConfig, madmin.SRBucketMeta{Type: madmin.SRBucketMetaTypeSSEConfig, SSEConfig: enc(`<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ApplyServerSideEncryptionByDefault><SSEAlgorithm>AES256</SSEAlgorithm></ApplyServerSideEncryptionByDefault></Rule></ServerSideEncryptionConfiguration>`)}, func(m BucketMetadata) []byte { return m.EncryptionConfigXML }, func(m BucketMetadata) time.Time { return m.EncryptionConfigUpdatedAt }, func(m madmin.SRBucketInfo) time.Time { return m.SSEConfigUpdatedAt }, true},
{"quota", bucketQuotaConfigFile, madmin.SRBucketMeta{Type: madmin.SRBucketMetaTypeQuotaConfig, Quota: quotaJSON}, func(m BucketMetadata) []byte { return m.QuotaConfigJSON }, func(m BucketMetadata) time.Time { return m.QuotaConfigUpdatedAt }, func(m madmin.SRBucketInfo) time.Time { return m.QuotaConfigUpdatedAt }, true},
{"versioning", bucketVersioningConfig, madmin.SRBucketMeta{Type: madmin.SRBucketMetaTypeVersionConfig, Versioning: enc(`<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Status>Enabled</Status></VersioningConfiguration>`)}, func(m BucketMetadata) []byte { return m.VersioningConfigXML }, func(m BucketMetadata) time.Time { return m.VersioningConfigUpdatedAt }, nil, false},
{"objectlock", objectLockConfig, newSRBucketObjectLockMeta(bucket, enc(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled><Rule><DefaultRetention><Mode>GOVERNANCE</Mode><Days>30</Days></DefaultRetention></Rule></ObjectLockConfiguration>`), putAt), func(m BucketMetadata) []byte { return m.ObjectLockConfigXML }, func(m BucketMetadata) time.Time { return m.ObjectLockConfigUpdatedAt }, nil, false},
}
exporter := &SiteReplicationSys{enabled: true}
for _, tc := range cases {
t.Run(backend+"/"+tc.name, func(t *testing.T) {
if err := globalBucketMetadataSys.save(ctx, base); err != nil {
t.Fatal(err)
}
item := tc.put
item.Bucket, item.UpdatedAt = bucket, putAt
apply := func(item madmin.SRBucketMeta) {
t.Helper()
rec := applySRBucketMetaViaAdmin(t, cred, item)
if rec.Code != http.StatusOK {
t.Fatalf("admin apply returned %d: %s", rec.Code, rec.Body.String())
}
}
read := func() BucketMetadata {
t.Helper()
m, err := loadBucketMetadata(ctx, obj, bucket)
if err != nil {
t.Fatal(err)
}
return m
}
apply(item)
put := read()
if len(tc.value(put)) == 0 {
t.Fatal("PUT did not establish live config")
}
if !tc.stamp(put).Equal(putAt) {
t.Errorf("SOURCE_TIME: persisted %s, want source %s", tc.stamp(put), putAt)
}
apply(madmin.SRBucketMeta{Type: item.Type, Bucket: bucket, UpdatedAt: delAt})
after := read()
if !tc.deletable {
if !bytes.Equal(tc.value(put), tc.value(after)) || !tc.stamp(put).Equal(tc.stamp(after)) {
t.Error("NIL_NOOP: update-only config changed")
}
t.Log("nil payload is correctly a no-op")
return
}
if len(tc.value(after)) != 0 {
t.Error("NEWER_DELETE: HTTP 200, but live config remained after newer source DELETE")
}
if _, err := globalBucketMetadataSys.Delete(ctx, bucket, tc.file); err != nil {
t.Fatal(err)
}
tombstone := read()
if len(tc.value(tombstone)) != 0 {
t.Fatal("local DELETE failed to establish tombstone")
}
info, err := exporter.SiteReplicationMetaInfo(ctx, obj, madmin.SRStatusOptions{Entity: madmin.SRBucketEntity, EntityValue: bucket})
if err != nil {
t.Fatal(err)
}
if !tc.exported(info.Buckets[bucket]).Equal(tc.stamp(tombstone)) {
t.Errorf("TOMBSTONE_EXPORT: got %s, want %s", tc.exported(info.Buckets[bucket]), tc.stamp(tombstone))
}
apply(item)
after = read()
if len(tc.value(after)) != 0 {
t.Error("STALE_RESURRECTION: older source PUT resurrected a locally deleted config")
} else {
t.Log("older source PUT did not resurrect config")
}
})
}
}
func TestIssue77CurrentBulkApplyOrdering(t *testing.T) {
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: func(obj ObjectLayer, backend, bucket string, _ http.Handler, cred auth.Credentials, t *testing.T) {
t.Run(backend, func(t *testing.T) {
ctx := t.Context()
meta, err := loadBucketMetadata(ctx, obj, bucket)
if err != nil {
t.Fatal(err)
}
newAt := meta.Created.Add(time.Hour)
newXML := `<Tagging><TagSet><Tag><Key>key</Key><Value>new</Value></Tag></TagSet></Tagging>`
meta.TaggingConfigXML, meta.TaggingConfigUpdatedAt = []byte(newXML), newAt
if err := globalBucketMetadataSys.save(ctx, meta); err != nil {
t.Fatal(err)
}
oldXML := base64.StdEncoding.EncodeToString([]byte(`<Tagging><TagSet><Tag><Key>key</Key><Value>old</Value></Tag></TagSet></Tagging>`))
item := madmin.SRBucketMeta{Bucket: bucket, Tags: &oldXML, UpdatedAt: newAt.Add(-time.Minute)}
rec := applySRBucketMetaViaAdmin(t, cred, item)
if rec.Code != http.StatusOK {
t.Fatalf("bulk apply returned %d: %s", rec.Code, rec.Body.String())
}
after, err := loadBucketMetadata(ctx, obj, bucket)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(after.TaggingConfigXML, []byte(newXML)) || !after.TaggingConfigUpdatedAt.Equal(newAt) {
t.Errorf("BULK_STALE_OVERWRITE: older bulk event overwrote newer config, value=%q time=%s", after.TaggingConfigXML, after.TaggingConfigUpdatedAt)
}
})
}})
}