Files
versitygw/tests/integration/PutBucketWebsite.go
T
915fc7a20f fix: return InvalidArgument for an empty website configuration
* fix: return InvalidArgument for an empty website configuration

`PutBucketWebsite` parsed an empty `<WebsiteConfiguration/>` into a zero-value config, and `WebsiteConfiguration.Validate` reported the nil `IndexDocument` as `MalformedXML`. S3 reports the missing index document as an `InvalidArgument` request error naming the argument instead:

    InvalidArgument: A value for IndexDocument Suffix must be provided if RedirectAllRequestsTo is empty

That branch now returns the error with `ArgumentName=IndexDocument` and `ArgumentValue=null`, matching the response in the report. Genuinely malformed XML, and a `RedirectAllRequestsTo` that conflicts with the other fields, still return `MalformedXML`.

Fixes #2260

* test: cover an empty website configuration in the PutBucketWebsite integration suite

Add a `PutBucketWebsite_empty_configuration` case that sends the empty `<WebsiteConfiguration/>` payload from the report and asserts the `InvalidArgument` response, including `ArgumentName=IndexDocument` and `ArgumentValue=null`.

The SDK collapses the error into a generic API error that drops the argument fields, so the request is signed by hand and the raw response checked with `checkHTTPResponseApiErr`, the same helper the other argument-field assertions use. The case fails against the previous behaviour with `expected error code to be InvalidArgument, instead got MalformedXML`.

---------

Co-authored-by: Tung Lam <lamphamabtung96@gmail.com>
2026-09-20 11:04:22 -07:00

500 lines
16 KiB
Go

// 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 integration
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/versity/versitygw/s3err"
)
const maxWebsiteConfigSize = 131072
func PutBucketWebsite_non_existing_bucket(s *S3Conf) error {
testName := "PutBucketWebsite_non_existing_bucket"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: getPtr("non-existing-bucket"),
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrNoSuchBucket))
})
}
func PutBucketWebsite_empty_suffix(s *S3Conf) error {
testName := "PutBucketWebsite_empty_suffix"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr(""),
},
},
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgIndexDocumentSuffix, ""))
})
}
func PutBucketWebsite_empty_configuration(s *S3Conf) error {
testName := "PutBucketWebsite_empty_configuration"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
// The payload from https://github.com/versity/versitygw/issues/2260
body := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<WebsiteConfiguration xmlns="https://s3.amazonaws.com/doc/2006-03-01/"></WebsiteConfiguration>`)
// The SDK collapses the error into a generic API error that drops the
// ArgumentName and ArgumentValue fields, so the request is signed by
// hand and the raw error body checked.
req, err := createSignedReq(http.MethodPut, s.endpoint,
fmt.Sprintf("%v?website=", bucket), s.awsID, s.awsSecret,
"s3", s.awsRegion, "", body, time.Now(),
map[string]string{"Content-Type": "application/xml"})
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
return checkHTTPResponseApiErr(resp,
s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingIndexDocumentSuffix, "null"))
})
}
func PutBucketWebsite_suffix_with_slash(s *S3Conf) error {
testName := "PutBucketWebsite_suffix_with_slash"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("/index.html"),
},
},
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgIndexDocumentSuffix, "/index.html"))
})
}
func PutBucketWebsite_invalid_redirect_protocol(s *S3Conf) error {
testName := "PutBucketWebsite_invalid_redirect_protocol"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
RedirectAllRequestsTo: &types.RedirectAllRequestsTo{
HostName: getPtr("example.com"),
Protocol: types.Protocol("ftp"),
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidWebsiteRedirectProtocol))
})
}
func PutBucketWebsite_redirectAll_index_error_routingRules(s *S3Conf) error {
testName := "PutBucketWebsite_redirectAll_index_error_routingRules"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, test := range []struct {
name string
config *types.WebsiteConfiguration
}{
{
name: "index document",
config: &types.WebsiteConfiguration{
RedirectAllRequestsTo: &types.RedirectAllRequestsTo{
HostName: getPtr("example.com"),
},
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
},
},
{
name: "error document",
config: &types.WebsiteConfiguration{
RedirectAllRequestsTo: &types.RedirectAllRequestsTo{
HostName: getPtr("example.com"),
},
ErrorDocument: &types.ErrorDocument{
Key: getPtr("error.html"),
},
},
},
{
name: "routing rules",
config: &types.WebsiteConfiguration{
RedirectAllRequestsTo: &types.RedirectAllRequestsTo{
HostName: getPtr("example.com"),
},
RoutingRules: []types.RoutingRule{
{
Redirect: &types.Redirect{
HostName: getPtr("redirect.example.com"),
},
},
},
},
},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: test.config,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
return fmt.Errorf("%s: %w", test.name, err)
}
}
return nil
})
}
func PutBucketWebsite_invalid_routing_rule_protocol(s *S3Conf) error {
testName := "PutBucketWebsite_invalid_routing_rule_protocol"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: []types.RoutingRule{
{
Redirect: &types.Redirect{
HostName: getPtr("example.com"),
Protocol: types.Protocol("ftp"),
},
},
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidWebsiteRedirectProtocol))
})
}
func PutBucketWebsite_empty_routing_rule_condition(s *S3Conf) error {
testName := "PutBucketWebsite_empty_routing_rule_condition"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: []types.RoutingRule{
{
Condition: &types.Condition{},
Redirect: &types.Redirect{
HostName: getPtr("example.com"),
},
},
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML))
})
}
func PutBucketWebsite_empty_routing_rule_redirect(s *S3Conf) error {
testName := "PutBucketWebsite_empty_routing_rule_redirect"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: []types.RoutingRule{
{
Condition: &types.Condition{
KeyPrefixEquals: getPtr("docs/"),
},
Redirect: &types.Redirect{},
},
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML))
})
}
func PutBucketWebsite_empty_error_document_key(s *S3Conf) error {
testName := "PutBucketWebsite_empty_error_document_key"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
ErrorDocument: &types.ErrorDocument{
Key: getPtr(""),
},
},
})
cancel()
return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgErrorDocumentKey, ""))
})
}
func PutBucketWebsite_too_many_routing_rules(s *S3Conf) error {
testName := "PutBucketWebsite_too_many_routing_rules"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
routingRules := make([]types.RoutingRule, 51)
for i := range routingRules {
routingRules[i] = types.RoutingRule{
Condition: &types.Condition{
KeyPrefixEquals: getPtr(fmt.Sprintf("prefix-%d/", i)),
},
Redirect: &types.Redirect{
ReplaceKeyPrefixWith: getPtr(fmt.Sprintf("replacement-%d/", i)),
},
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: routingRules,
},
})
cancel()
return checkApiErr(err, s3err.GetWebsiteRoutingRulesLimitedErr(51))
})
}
func PutBucketWebsite_routing_rule_replace_key_and_prefix(s *S3Conf) error {
testName := "PutBucketWebsite_routing_rule_replace_key_and_prefix"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: []types.RoutingRule{
{
Redirect: &types.Redirect{
ReplaceKeyWith: getPtr("replacement.html"),
ReplaceKeyPrefixWith: getPtr("replacement-prefix/"),
},
},
},
},
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrBothReplaceKeyAndPrefix))
})
}
func PutBucketWebsite_invalid_http_redirect_code(s *S3Conf) error {
testName := "PutBucketWebsite_invalid_http_redirect_code"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, test := range []struct {
code string
expectedErr s3err.S3Error
}{
{code: "300", expectedErr: s3err.GetInvalidRedirectCodeErr(300)},
{code: "306", expectedErr: s3err.GetInvalidRedirectCodeErr(306)},
{code: "309", expectedErr: s3err.GetInvalidRedirectCodeErr(309)},
{code: "399", expectedErr: s3err.GetInvalidRedirectCodeErr(399)},
{code: "jibberish", expectedErr: s3err.GetAPIError(s3err.ErrMalformedXML)},
{code: "3xx", expectedErr: s3err.GetAPIError(s3err.ErrMalformedXML)},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: []types.RoutingRule{
{
Redirect: &types.Redirect{
HostName: getPtr("example.com"),
HttpRedirectCode: getPtr(test.code),
},
},
},
},
})
cancel()
if err := checkApiErr(err, test.expectedErr); err != nil {
return fmt.Errorf("code %q: %w", test.code, err)
}
}
return nil
})
}
func PutBucketWebsite_invalid_http_error_code(s *S3Conf) error {
testName := "PutBucketWebsite_invalid_http_error_code"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, test := range []struct {
code string
expectedErr s3err.S3Error
}{
{code: "399", expectedErr: s3err.GetInvalidHTTPErrorCodeErr(399)},
{code: "418", expectedErr: s3err.GetInvalidHTTPErrorCodeErr(418)},
{code: "499", expectedErr: s3err.GetInvalidHTTPErrorCodeErr(499)},
{code: "506", expectedErr: s3err.GetInvalidHTTPErrorCodeErr(506)},
{code: "jibberish", expectedErr: s3err.GetAPIError(s3err.ErrMalformedXML)},
{code: "4xx", expectedErr: s3err.GetAPIError(s3err.ErrMalformedXML)},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: []types.RoutingRule{
{
Condition: &types.Condition{
HttpErrorCodeReturnedEquals: getPtr(test.code),
},
Redirect: &types.Redirect{
HostName: getPtr("example.com"),
},
},
},
},
})
cancel()
if err := checkApiErr(err, test.expectedErr); err != nil {
return fmt.Errorf("code %q: %w", test.code, err)
}
}
return nil
})
}
func PutBucketWebsite_request_too_large(s *S3Conf) error {
testName := "PutBucketWebsite_request_too_large"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
longValue := strings.Repeat("a", 2048)
routingRules := make([]types.RoutingRule, 50)
for i := range routingRules {
routingRules[i] = types.RoutingRule{
Condition: &types.Condition{
KeyPrefixEquals: getPtr(fmt.Sprintf("prefix-%d-%s", i, longValue)),
},
Redirect: &types.Redirect{
HostName: getPtr("example.com"),
ReplaceKeyWith: getPtr(fmt.Sprintf("replacement-%d-%s", i, longValue)),
HttpRedirectCode: getPtr("301"),
},
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
RoutingRules: routingRules,
},
})
cancel()
return checkApiErr(err, s3err.GetMaxMessageLengthExceeded(maxWebsiteConfigSize))
})
}
func PutBucketWebsite_success(s *S3Conf) error {
testName := "PutBucketWebsite_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
IndexDocument: &types.IndexDocument{
Suffix: getPtr("index.html"),
},
ErrorDocument: &types.ErrorDocument{
Key: getPtr("error.html"),
},
},
})
cancel()
if err != nil {
return err
}
return nil
})
}
func PutBucketWebsite_success_redirect_all(s *S3Conf) error {
testName := "PutBucketWebsite_success_redirect_all"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
Bucket: &bucket,
WebsiteConfiguration: &types.WebsiteConfiguration{
RedirectAllRequestsTo: &types.RedirectAllRequestsTo{
HostName: getPtr("example.com"),
Protocol: types.ProtocolHttps,
},
},
})
cancel()
if err != nil {
return err
}
return nil
})
}