mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-06 12:07:40 +00:00
feat(apierr): map upstream codes to API status/code and add Respond helper
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package apierr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"Noooste/garage-ui/internal/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// codeEntry defines how an upstream code translates to the API surface.
|
||||
type codeEntry struct {
|
||||
HTTPStatus int
|
||||
APICode string
|
||||
}
|
||||
|
||||
// upstreamCodeTable maps upstream codes (Garage + S3 share the same naming for
|
||||
// most codes) to (httpStatus, apiCode). Extend here as new upstream codes are
|
||||
// discovered.
|
||||
var upstreamCodeTable = map[string]codeEntry{
|
||||
"BucketNotEmpty": {409, models.ErrCodeBucketNotEmpty},
|
||||
"NoSuchBucket": {404, models.ErrCodeBucketNotFound},
|
||||
"BucketAlreadyExists": {409, models.ErrCodeBucketExists},
|
||||
"BucketAlreadyOwnedByYou": {409, models.ErrCodeBucketExists},
|
||||
"NoSuchKey": {404, models.ErrCodeObjectNotFound},
|
||||
"InvalidBucketName": {400, models.ErrCodeInvalidBucketName},
|
||||
"AccessDenied": {403, models.ErrCodeForbidden},
|
||||
}
|
||||
|
||||
// Map translates an error into (httpStatus, apiCode, message) for the API
|
||||
// response. Falls back to 500 / INTERNAL_ERROR for non-UpstreamError values.
|
||||
func Map(err error) (int, string, string) {
|
||||
var ue *UpstreamError
|
||||
if !errors.As(err, &ue) {
|
||||
return 500, models.ErrCodeInternalError, err.Error()
|
||||
}
|
||||
|
||||
if entry, ok := upstreamCodeTable[ue.Code]; ok {
|
||||
return entry.HTTPStatus, entry.APICode, ue.Message
|
||||
}
|
||||
|
||||
status := ue.HTTPStatus
|
||||
if status == 0 {
|
||||
status = 500
|
||||
}
|
||||
msg := ue.Message
|
||||
if msg == "" {
|
||||
msg = ue.Error()
|
||||
}
|
||||
return status, models.ErrCodeInternalError, msg
|
||||
}
|
||||
|
||||
// Respond writes the mapped error as a Fiber JSON response using the standard
|
||||
// APIResponse envelope. Handlers should call this from their err != nil
|
||||
// branches for all upstream failures.
|
||||
func Respond(c fiber.Ctx, err error) error {
|
||||
status, code, msg := Map(err)
|
||||
return c.Status(status).JSON(models.ErrorResponse(code, msg))
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package apierr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"Noooste/garage-ui/internal/models"
|
||||
)
|
||||
|
||||
func TestMap_TableDriven(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
err error
|
||||
wantStatus int
|
||||
wantCode string
|
||||
wantMsg string
|
||||
}{
|
||||
{
|
||||
name: "BucketNotEmpty",
|
||||
err: &UpstreamError{HTTPStatus: 409, Code: "BucketNotEmpty", Message: "Tried to delete a non-empty bucket", Source: "garage"},
|
||||
wantStatus: 409,
|
||||
wantCode: models.ErrCodeBucketNotEmpty,
|
||||
wantMsg: "Tried to delete a non-empty bucket",
|
||||
},
|
||||
{
|
||||
name: "NoSuchBucket",
|
||||
err: &UpstreamError{HTTPStatus: 404, Code: "NoSuchBucket", Message: "missing", Source: "s3"},
|
||||
wantStatus: 404,
|
||||
wantCode: models.ErrCodeBucketNotFound,
|
||||
wantMsg: "missing",
|
||||
},
|
||||
{
|
||||
name: "BucketAlreadyExists",
|
||||
err: &UpstreamError{HTTPStatus: 409, Code: "BucketAlreadyExists", Message: "dup", Source: "s3"},
|
||||
wantStatus: 409,
|
||||
wantCode: models.ErrCodeBucketExists,
|
||||
wantMsg: "dup",
|
||||
},
|
||||
{
|
||||
name: "BucketAlreadyOwnedByYou",
|
||||
err: &UpstreamError{HTTPStatus: 409, Code: "BucketAlreadyOwnedByYou", Message: "yours", Source: "s3"},
|
||||
wantStatus: 409,
|
||||
wantCode: models.ErrCodeBucketExists,
|
||||
wantMsg: "yours",
|
||||
},
|
||||
{
|
||||
name: "NoSuchKey",
|
||||
err: &UpstreamError{HTTPStatus: 404, Code: "NoSuchKey", Message: "gone", Source: "s3"},
|
||||
wantStatus: 404,
|
||||
wantCode: models.ErrCodeObjectNotFound,
|
||||
wantMsg: "gone",
|
||||
},
|
||||
{
|
||||
name: "InvalidBucketName",
|
||||
err: &UpstreamError{HTTPStatus: 400, Code: "InvalidBucketName", Message: "bad", Source: "s3"},
|
||||
wantStatus: 400,
|
||||
wantCode: models.ErrCodeInvalidBucketName,
|
||||
wantMsg: "bad",
|
||||
},
|
||||
{
|
||||
name: "AccessDenied",
|
||||
err: &UpstreamError{HTTPStatus: 403, Code: "AccessDenied", Message: "nope", Source: "garage"},
|
||||
wantStatus: 403,
|
||||
wantCode: models.ErrCodeForbidden,
|
||||
wantMsg: "nope",
|
||||
},
|
||||
{
|
||||
name: "UnknownCodeWithStatus",
|
||||
err: &UpstreamError{HTTPStatus: 503, Code: "SomethingWeird", Message: "weird", Source: "garage"},
|
||||
wantStatus: 503,
|
||||
wantCode: models.ErrCodeInternalError,
|
||||
wantMsg: "weird",
|
||||
},
|
||||
{
|
||||
name: "UnknownCodeNoStatus",
|
||||
err: &UpstreamError{HTTPStatus: 0, Code: "", Message: "boom", Source: "garage"},
|
||||
wantStatus: 500,
|
||||
wantCode: models.ErrCodeInternalError,
|
||||
wantMsg: "boom",
|
||||
},
|
||||
{
|
||||
name: "NonUpstreamError",
|
||||
err: errors.New("plain"),
|
||||
wantStatus: 500,
|
||||
wantCode: models.ErrCodeInternalError,
|
||||
wantMsg: "plain",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
status, code, msg := Map(tc.err)
|
||||
if status != tc.wantStatus {
|
||||
t.Errorf("status = %d, want %d", status, tc.wantStatus)
|
||||
}
|
||||
if code != tc.wantCode {
|
||||
t.Errorf("code = %q, want %q", code, tc.wantCode)
|
||||
}
|
||||
if msg != tc.wantMsg {
|
||||
t.Errorf("msg = %q, want %q", msg, tc.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user