Files
seaweedfs/weed/s3api/iceberg/iceberg_invalid_name_test.go
T
Chris Lu c191b2fe01 iceberg: let clients select their table bucket as the catalog warehouse (#10549)
* iceberg: accept bare bucket names and ARNs as the catalog warehouse

Only s3://<bucket>/ was recognized. A warehouse spelled as a bare table
bucket name or as the s3tables bucket ARN -- the two forms users reach for
first, the latter being what AWS S3 Tables itself takes -- was silently
dropped, so every call landed on the default "warehouse" bucket and failed
with "table bucket warehouse not found".

* iceberg: report a missing table bucket as 404, not 500

Pointing a client at a table bucket that does not exist -- which every
client with no warehouse set does, since the default bucket "warehouse"
rarely exists -- returned InternalServerError with a message naming a
bucket the client never asked for. Answer 404 and say how to select one.

* admin: show the warehouse in the PyIceberg example

The example connected without one, so it always resolved to the default
table bucket and every client that copied it failed on the first call.

* test: pin bearer auth against a table bucket that exists

The subtest called the catalog with no warehouse and accepted 500 as proof
that auth had passed, since the default bucket does not exist. A missing
table bucket now answers 404, which the test read as an auth failure. Give
it a real table bucket so only 200 passes.

* test: assert the missing-bucket guidance reaches the client

The status and error type were checked but not the message, which is the
part of the mapping that tells a user how to select a table bucket.

* test: encode the warehouse query value

The ARN case pasted raw colons and slashes into the query string. Go's
parser tolerates them, so the test passed without modelling how a client
actually sends the request.
2026-08-03 13:25:37 -07:00

88 lines
3.5 KiB
Go

package iceberg
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/apache/iceberg-go"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
)
func TestNameValidationError(t *testing.T) {
cases := []struct {
err error
want bool
}{
{nil, false},
// Wrapped exactly as the s3tables manager surfaces it.
{fmt.Errorf("all filers failed, last error: invalid namespace name: only 'a-z', '0-9', and '_' are allowed"), true},
{fmt.Errorf("invalid table name: only 'a-z', '0-9', and '_' are allowed"), true},
{fmt.Errorf("namespace name must start with a letter or digit"), true},
{fmt.Errorf("namespace name cannot start with reserved prefix 'aws'"), true},
{fmt.Errorf("table name must be between 1 and 255 characters"), true},
{fmt.Errorf("namespace not found"), false},
{fmt.Errorf("all filers failed, last error: rpc timeout"), false},
// Unrelated faults that merely mention a name must stay 500.
{fmt.Errorf("failed to resolve table name from index"), false},
{fmt.Errorf("error fetching namespace name mapping"), false},
}
for _, c := range cases {
if got := nameValidationError(c.err); got != c.want {
t.Errorf("nameValidationError(%v) = %v, want %v", c.err, got, c.want)
}
}
}
func TestWriteManagerError(t *testing.T) {
cases := []struct {
name string
err error
wantCode int
wantType string
wantMessage []string
}{
{name: "invalid name is a client error", err: fmt.Errorf("invalid namespace name: only 'a-z', '0-9', and '_' are allowed"), wantCode: http.StatusBadRequest, wantType: "BadRequestException"},
{name: "rejected schema is a client error", err: fmt.Errorf("%w: for v2: variant is not supported until v3", iceberg.ErrInvalidSchema), wantCode: http.StatusBadRequest, wantType: "BadRequestException"},
{name: "bad format version is a client error", err: fmt.Errorf("%w: 4", iceberg.ErrInvalidFormatVersion), wantCode: http.StatusBadRequest, wantType: "BadRequestException"},
{
name: "missing table bucket is a client error",
err: fmt.Errorf("all filers failed, last error: %w", &s3tables.S3TablesError{Type: s3tables.ErrCodeNoSuchBucket, Message: "table bucket warehouse not found"}),
wantCode: http.StatusNotFound,
wantType: "NoSuchNamespaceException",
// The guidance is the point of the mapping: the bucket the request
// resolved to is one the client never named, so the response has to
// say how to name a real one.
wantMessage: []string{"table bucket warehouse not found", "warehouse=s3://<table-bucket>/", "/v1/<table-bucket>/"},
},
{name: "everything else is a server fault", err: fmt.Errorf("all filers failed, last error: connection refused"), wantCode: http.StatusInternalServerError, wantType: "InternalServerError"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
rec := httptest.NewRecorder()
writeManagerError(rec, c.err)
if rec.Code != c.wantCode {
t.Fatalf("status = %d, want %d", rec.Code, c.wantCode)
}
var resp ErrorResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Error.Type != c.wantType {
t.Fatalf("error type = %q, want %q", resp.Error.Type, c.wantType)
}
if resp.Error.Code != c.wantCode {
t.Fatalf("error code = %d, want %d", resp.Error.Code, c.wantCode)
}
for _, want := range c.wantMessage {
if !strings.Contains(resp.Error.Message, want) {
t.Errorf("message %q does not contain %q", resp.Error.Message, want)
}
}
})
}
}