mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-17 03:50:51 +02:00
* 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.
88 lines
3.5 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|