mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
test: add directory marker Content-Type tests for #8712
Integration test (s3_list_empty_directory_test.go): - Reproduces exact scenario from #8712: PutObject with explicit Content-Type, then ListObjects with prefix - Covers ListV1/V2, with/without prefix, with/without delimiter - Verifies mixed MIME types coexist in listings - Asserts Size == 0 for all directory markers Unit test (s3api_object_handlers_list_test.go): - Tests doListFilerEntries with Mime set directly (pre-existing path) - Tests Mime empty but ExtMimeType set in extended attributes (the actual #8712 scenario after filer strips application/octet-stream)
This commit is contained in:
@@ -223,6 +223,141 @@ func TestS3ListObjectsEmptyDirectoryMarkers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestS3ListObjectsDirectoryMarkerWithContentType reproduces GitHub issue #8712:
|
||||
// Directory markers created with an explicit Content-Type (e.g. application/octet-stream)
|
||||
// must appear in ListObjects results, just like markers created without Content-Type.
|
||||
func TestS3ListObjectsDirectoryMarkerWithContentType(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
cluster, err := startMiniCluster(t)
|
||||
require.NoError(t, err)
|
||||
defer cluster.Stop()
|
||||
|
||||
bucketName := createTestBucket(t, cluster, "test-content-type-dirs-")
|
||||
|
||||
// Create a directory marker with explicit Content-Type (the scenario from #8712)
|
||||
_, err = cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String("test-content/empty/"),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
ContentType: aws.String("application/octet-stream"),
|
||||
})
|
||||
require.NoError(t, err, "failed to create directory marker with content-type")
|
||||
|
||||
// Verify the directory marker exists via HeadObject
|
||||
headResp, err := cluster.s3Client.HeadObject(&s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String("test-content/empty/"),
|
||||
})
|
||||
require.NoError(t, err, "directory marker should exist via HeadObject")
|
||||
assert.Equal(t, "application/octet-stream", aws.StringValue(headResp.ContentType),
|
||||
"Content-Type should be preserved")
|
||||
|
||||
// Test 1: ListObjectsV2 with prefix (no delimiter) — the exact scenario from #8712
|
||||
t.Run("ListV2_WithPrefix_NoDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("test-content"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
assert.Equal(t, []string{"test-content/empty/"}, keys,
|
||||
"directory marker with explicit Content-Type should be listed")
|
||||
|
||||
// Verify the directory marker has zero size
|
||||
require.Equal(t, 1, len(resp.Contents), "should have exactly one object in Contents")
|
||||
assert.Equal(t, int64(0), aws.Int64Value(resp.Contents[0].Size),
|
||||
"directory marker should have Size == 0")
|
||||
})
|
||||
|
||||
// Test 2: ListObjectsV1 with prefix
|
||||
t.Run("ListV1_WithPrefix_NoDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjects(&s3.ListObjectsInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("test-content"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeysV1(resp.Contents)
|
||||
assert.Equal(t, []string{"test-content/empty/"}, keys,
|
||||
"directory marker with explicit Content-Type should be listed")
|
||||
|
||||
// Verify the directory marker has zero size
|
||||
require.Equal(t, 1, len(resp.Contents), "should have exactly one object in Contents")
|
||||
assert.Equal(t, int64(0), aws.Int64Value(resp.Contents[0].Size),
|
||||
"directory marker should have Size == 0")
|
||||
})
|
||||
|
||||
// Test 3: Without prefix
|
||||
t.Run("ListV2_NoPrefix", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
assert.Contains(t, keys, "test-content/empty/",
|
||||
"directory marker with explicit Content-Type should appear in full listing")
|
||||
|
||||
// Find and verify the directory marker has zero size
|
||||
var dirMarker *s3.Object
|
||||
for _, obj := range resp.Contents {
|
||||
if aws.StringValue(obj.Key) == "test-content/empty/" {
|
||||
dirMarker = obj
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, dirMarker, "directory marker should be in Contents")
|
||||
assert.Equal(t, int64(0), aws.Int64Value(dirMarker.Size),
|
||||
"directory marker should have Size == 0")
|
||||
})
|
||||
|
||||
// Test 4: With delimiter — should appear as CommonPrefix
|
||||
t.Run("ListV2_WithDelimiter", func(t *testing.T) {
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Delimiter: aws.String("/"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
prefixes := collectPrefixes(resp.CommonPrefixes)
|
||||
assert.Contains(t, prefixes, "test-content/",
|
||||
"parent of directory marker should appear as CommonPrefix")
|
||||
})
|
||||
|
||||
// Test 5: Alongside a default-MIME directory marker — both should be listed
|
||||
t.Run("ListV2_MixedMimeTypes", func(t *testing.T) {
|
||||
_, err := cluster.s3Client.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String("test-content/default/"),
|
||||
Body: bytes.NewReader([]byte{}),
|
||||
// No ContentType — defaults to httpd/unix-directory
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := cluster.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
|
||||
Bucket: aws.String(bucketName),
|
||||
Prefix: aws.String("test-content"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := collectKeys(resp.Contents)
|
||||
sort.Strings(keys)
|
||||
assert.Equal(t, []string{"test-content/default/", "test-content/empty/"}, keys,
|
||||
"both directory markers (custom and default MIME) should be listed")
|
||||
|
||||
// Verify both directory markers have zero size
|
||||
require.Equal(t, 2, len(resp.Contents), "should have exactly two objects in Contents")
|
||||
for _, obj := range resp.Contents {
|
||||
assert.Equal(t, int64(0), aws.Int64Value(obj.Size),
|
||||
"directory marker %s should have Size == 0", aws.StringValue(obj.Key))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func collectKeys(contents []*s3.Object) []string {
|
||||
keys := make([]string, 0, len(contents))
|
||||
for _, obj := range contents {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/stretchr/testify/assert"
|
||||
grpc "google.golang.org/grpc"
|
||||
@@ -284,6 +285,67 @@ func TestAllowUnorderedParameterValidation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestDoListFilerEntries_DirectoryKeyObjectWithCustomMimeType is a regression test
|
||||
// for GitHub issue #8712: directory markers created via PutObject with an explicit
|
||||
// Content-Type (e.g., "application/octet-stream") must appear in ListObjects results.
|
||||
// Only the default "httpd/unix-directory" MIME was being listed before the fix.
|
||||
func TestDoListFilerEntries_DirectoryKeyObjectWithCustomMimeType(t *testing.T) {
|
||||
s3a := &S3ApiServer{
|
||||
option: &S3ApiServerOption{
|
||||
BucketsPath: "/buckets",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entry *filer_pb.Entry
|
||||
}{
|
||||
{"default FolderMimeType", &filer_pb.Entry{
|
||||
Name: "empty", IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{Mime: "httpd/unix-directory"},
|
||||
}},
|
||||
{"Mime set to application/octet-stream", &filer_pb.Entry{
|
||||
Name: "empty", IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{Mime: "application/octet-stream"},
|
||||
}},
|
||||
{"custom MIME", &filer_pb.Entry{
|
||||
Name: "empty", IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{Mime: "application/x-directory"},
|
||||
}},
|
||||
// The real-world scenario for #8712: the filer drops Mime on directories,
|
||||
// so only the ExtMimeType extended attribute survives.
|
||||
{"Mime empty but ExtMimeType set", &filer_pb.Entry{
|
||||
Name: "empty", IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{},
|
||||
Extended: map[string][]byte{s3_constants.ExtMimeType: []byte("application/octet-stream")},
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &testFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/buckets/bucket": {
|
||||
{Name: "test-content", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
"/buckets/bucket/test-content": {tt.entry},
|
||||
"/buckets/bucket/test-content/empty": {},
|
||||
},
|
||||
}
|
||||
|
||||
cursor := &ListingCursor{maxKeys: 1000}
|
||||
var seen []string
|
||||
_, err := s3a.doListFilerEntries(client, "/buckets/bucket", "test-content", cursor, "", "", false, "bucket", func(dir string, entry *filer_pb.Entry) {
|
||||
seen = append(seen, entry.Name)
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, seen, "empty",
|
||||
"directory key object should be listed for case %q", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoListFilerEntries_BucketRootPrefixSlashDelimiterSlash_ListsDirectories(t *testing.T) {
|
||||
// Regression test for a bug where doListFilerEntries returned early when
|
||||
// prefix == "/" && delimiter == "/", causing bucket-root folder listings
|
||||
|
||||
Reference in New Issue
Block a user