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 }