From b83f5d903b17c1bf606b964acaaef7e273de3a6d Mon Sep 17 00:00:00 2001 From: HeonJe Lee Date: Tue, 25 Aug 2026 11:42:03 +0900 Subject: [PATCH] fix(posix): return empty config for GetBucketVersioning when unconfigured Validate bucket existence before checking the versioning state so a missing bucket still yields 404 NoSuchBucket. When no versioning directory is configured, respond with 200 and an empty configuration instead of VersioningNotConfigured, matching AWS semantics. GUI clients (e.g. S3 Browser) poll this API while browsing buckets and were failing on default deployments. PutBucketVersioning still rejects with VersioningNotConfigured. Fix a swapped test body between the Get/Put "not configured" cases, add integration coverage for GetBucketVersioning on a non-existent bucket, and add backend/posix unit tests guarding the validation order and the empty 200 response. Ref: exastor/versitygw!3 --- backend/posix/posix.go | 13 ++-- backend/posix/versioning_test.go | 102 +++++++++++++++++++++++++++++++ tests/integration/group-tests.go | 1 + tests/integration/versioning.go | 34 ++++++++--- 4 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 backend/posix/versioning_test.go diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 6259c74c..3a3dfbf7 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -890,10 +890,6 @@ func (p *Posix) GetBucketVersioning(ctx context.Context, bucket string) (s3respo } defer release() - if !p.versioningEnabled() { - return s3response.GetBucketVersioningOutput{}, s3err.GetAPIError(s3err.ErrVersioningNotConfigured) - } - if !p.isBucketValid(bucket) { return s3response.GetBucketVersioningOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } @@ -906,6 +902,15 @@ func (p *Posix) GetBucketVersioning(ctx context.Context, bucket string) (s3respo return s3response.GetBucketVersioningOutput{}, fmt.Errorf("stat bucket: %w", err) } + if !p.versioningEnabled() { + // No versioning directory is configured: report the bucket as + // never-versioned (200 with an empty configuration) to match AWS, + // instead of failing with VersioningNotConfigured. GUI clients + // (S3 Browser, CloudBerry, ...) poll this API while browsing + // buckets. PutBucketVersioning still rejects the request. + return s3response.GetBucketVersioningOutput{}, nil + } + vData, err := p.meta.RetrieveAttribute(nil, bucket, "", versioningKey) if errors.Is(err, meta.ErrNoSuchKey) { return s3response.GetBucketVersioningOutput{}, nil diff --git a/backend/posix/versioning_test.go b/backend/posix/versioning_test.go new file mode 100644 index 00000000..2edbe5c0 --- /dev/null +++ b/backend/posix/versioning_test.go @@ -0,0 +1,102 @@ +// Copyright 2026 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package posix + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/stretchr/testify/assert" + "github.com/versity/versitygw/backend/meta" + "github.com/versity/versitygw/s3err" +) + +// newUnversionedGateway creates a Posix backend over a temporary root +// directory with no versioning directory configured, i.e. gateway-level +// versioning disabled, matching the default gateway configuration. +func newUnversionedGateway(t *testing.T) *Posix { + t.Helper() + + p, err := New(t.TempDir(), meta.XattrMeta{}, PosixOpts{ + ValidateBucketNames: true, + }) + if err != nil { + t.Fatalf("init posix backend: %v", err) + } + return p +} + +// TestVersioningUnconfigured covers the bucket versioning behavior when the +// gateway has no versioning directory configured: bucket validation still +// applies first, GetBucketVersioning returns an empty configuration, and +// PutBucketVersioning is rejected. +func TestVersioningUnconfigured(t *testing.T) { + // New() chdirs into the gateway root; restore the original working + // directory when the test completes. + t.Chdir(t.TempDir()) + + t.Run("get bucket versioning invalid bucket name", func(t *testing.T) { + p := newUnversionedGateway(t) + + _, err := p.GetBucketVersioning(context.Background(), "bad/bucket") + if !errors.Is(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)) { + t.Errorf("expected InvalidBucketName, got %v", err) + } + }) + + t.Run("get bucket versioning no such bucket", func(t *testing.T) { + p := newUnversionedGateway(t) + + _, err := p.GetBucketVersioning(context.Background(), "does-not-exist") + if !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)) { + t.Errorf("expected NoSuchBucket, got %v", err) + } + }) + + t.Run("get bucket versioning returns empty config", func(t *testing.T) { + p := newUnversionedGateway(t) + + err := os.Mkdir("bucket", 0o755) + assert.NoError(t, err) + + res, err := p.GetBucketVersioning(context.Background(), "bucket") + assert.NoError(t, err) + assert.Nil(t, res.Status) + }) + + t.Run("put bucket versioning invalid bucket name", func(t *testing.T) { + p := newUnversionedGateway(t) + + err := p.PutBucketVersioning(context.Background(), "bad/bucket", types.BucketVersioningStatusEnabled) + if !errors.Is(err, s3err.GetAPIError(s3err.ErrInvalidBucketName)) { + t.Errorf("expected InvalidBucketName, got %v", err) + } + }) + + t.Run("put bucket versioning not configured", func(t *testing.T) { + p := newUnversionedGateway(t) + + err := os.Mkdir("bucket", 0o755) + assert.NoError(t, err) + + err = p.PutBucketVersioning(context.Background(), "bucket", types.BucketVersioningStatusEnabled) + if !errors.Is(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)) { + t.Errorf("expected VersioningNotConfigured, got %v", err) + } + }) +} diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 79baff1d..69b95702 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -1254,6 +1254,7 @@ func TestVersioning(ts *TestState) { func TestVersioningDisabled(ts *TestState) { ts.Run(VersioningDisabled_GetBucketVersioning_not_configured) + ts.Run(VersioningDisabled_GetBucketVersioning_no_such_bucket) ts.Run(VersioningDisabled_PutBucketVersioning_not_configured) } diff --git a/tests/integration/versioning.go b/tests/integration/versioning.go index 2662a45a..f0c0b2b7 100644 --- a/tests/integration/versioning.go +++ b/tests/integration/versioning.go @@ -3620,8 +3620,32 @@ func Versioning_AccessControl_GetObjectAttributes_policy(s *S3Conf) error { func VersioningDisabled_GetBucketVersioning_not_configured(s *S3Conf) error { testName := "VersioningDisabled_GetBucketVersioning_not_configured" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { - err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled) - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + res, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{ + Bucket: &bucket, + }) + cancel() + if err != nil { + return err + } + if res.Status != "" { + return fmt.Errorf("expected empty versioning status when versioning is not configured, instead got %v", + res.Status) + } + + return nil + }) +} + +func VersioningDisabled_GetBucketVersioning_no_such_bucket(s *S3Conf) error { + testName := "VersioningDisabled_GetBucketVersioning_no_such_bucket" + return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{ + Bucket: &bucket, + }) + cancel() + if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)); err != nil { return err } @@ -3632,11 +3656,7 @@ func VersioningDisabled_GetBucketVersioning_not_configured(s *S3Conf) error { func VersioningDisabled_PutBucketVersioning_not_configured(s *S3Conf) error { testName := "VersioningDisabled_PutBucketVersioning_not_configured" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { - ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) - _, err := s3client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{ - Bucket: &bucket, - }) - cancel() + err := putBucketVersioningStatus(s3client, bucket, types.BucketVersioningStatusEnabled) if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)); err != nil { return err }