mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-18 09:03:13 +00:00
af7af32d4e
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
429 lines
14 KiB
Go
429 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"Noooste/garage-ui/internal/models"
|
|
"Noooste/garage-ui/internal/services/mocks"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
)
|
|
|
|
func newBucketsTestApp(t *testing.T) (*fiber.App, *mocks.AdminMock) {
|
|
t.Helper()
|
|
admin := &mocks.AdminMock{}
|
|
h := NewBucketHandler(admin, nil) // s3 unused in this handler
|
|
app := fiber.New()
|
|
app.Get("/buckets", h.ListBuckets)
|
|
app.Post("/buckets", h.CreateBucket)
|
|
app.Get("/buckets/:name", h.GetBucketInfo)
|
|
app.Delete("/buckets/:name", h.DeleteBucket)
|
|
app.Post("/buckets/:name/permissions", h.GrantBucketPermission)
|
|
app.Put("/buckets/:name/website", h.UpdateBucketWebsite)
|
|
return app, admin
|
|
}
|
|
|
|
func decodeJSON(t *testing.T, r io.Reader, v any) {
|
|
t.Helper()
|
|
if err := json.NewDecoder(r).Decode(v); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- ListBuckets ---
|
|
|
|
func TestListBuckets_MapsAliasesAndStats(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) {
|
|
return []models.ListBucketsResponseItem{
|
|
{ID: "id-1", Created: time.Unix(0, 0), GlobalAliases: []string{"alpha"}},
|
|
{ID: "id-2", Created: time.Unix(0, 0), GlobalAliases: []string{}}, // skipped: no global alias
|
|
{ID: "id-3", Created: time.Unix(0, 0), GlobalAliases: []string{"gamma"}},
|
|
}, nil
|
|
}
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, alias string) (*models.GarageBucketInfo, error) {
|
|
switch alias {
|
|
case "alpha":
|
|
return &models.GarageBucketInfo{ID: "id-1", Objects: 10, Bytes: 100, WebsiteAccess: true}, nil
|
|
case "gamma":
|
|
return nil, errors.New("detail fetch failed") // degraded path
|
|
}
|
|
return nil, errors.New("unexpected alias: " + alias)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/buckets", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
var body struct {
|
|
Data models.BucketListResponse `json:"data"`
|
|
}
|
|
decodeJSON(t, resp.Body, &body)
|
|
if body.Data.Count != 2 {
|
|
t.Errorf("count = %d, want 2 (id-2 skipped)", body.Data.Count)
|
|
}
|
|
// alpha has stats; gamma degraded to no stats (ObjectCount/Size nil).
|
|
var foundAlpha, foundGamma bool
|
|
for _, b := range body.Data.Buckets {
|
|
if b.Name == "alpha" {
|
|
foundAlpha = true
|
|
if b.ObjectCount == nil || *b.ObjectCount != 10 {
|
|
t.Errorf("alpha.ObjectCount = %v, want 10", b.ObjectCount)
|
|
}
|
|
if !b.WebsiteAccess {
|
|
t.Error("alpha.WebsiteAccess false")
|
|
}
|
|
}
|
|
if b.Name == "gamma" {
|
|
foundGamma = true
|
|
if b.ObjectCount != nil {
|
|
t.Errorf("gamma.ObjectCount = %v, want nil (degraded)", *b.ObjectCount)
|
|
}
|
|
}
|
|
}
|
|
if !foundAlpha || !foundGamma {
|
|
t.Errorf("missing buckets: alpha=%v gamma=%v", foundAlpha, foundGamma)
|
|
}
|
|
}
|
|
|
|
func TestListBuckets_AdminErrorReturns500(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) {
|
|
return nil, errors.New("boom")
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want 500", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// --- CreateBucket ---
|
|
|
|
func TestCreateBucket_Success201(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.CreateBucketFn = func(_ context.Context, r models.CreateBucketAdminRequest) (*models.GarageBucketInfo, error) {
|
|
if r.GlobalAlias == nil || *r.GlobalAlias != "new-bucket" {
|
|
t.Errorf("GlobalAlias = %v, want 'new-bucket'", r.GlobalAlias)
|
|
}
|
|
return &models.GarageBucketInfo{ID: "id-new"}, nil
|
|
}
|
|
body, _ := json.Marshal(map[string]string{"name": "new-bucket"})
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("status = %d, want 201", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCreateBucket_MissingNameReturns400(t *testing.T) {
|
|
app, _ := newBucketsTestApp(t)
|
|
body, _ := json.Marshal(map[string]string{})
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCreateBucket_MalformedJSONReturns400(t *testing.T) {
|
|
app, _ := newBucketsTestApp(t)
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets", strings.NewReader("{not-json"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCreateBucket_AdminErrorReturns500(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.CreateBucketFn = func(_ context.Context, _ models.CreateBucketAdminRequest) (*models.GarageBucketInfo, error) {
|
|
return nil, errors.New("boom")
|
|
}
|
|
body, _ := json.Marshal(map[string]string{"name": "x"})
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want 500", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// --- GetBucketInfo ---
|
|
|
|
func TestGetBucketInfo_Success(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, alias string) (*models.GarageBucketInfo, error) {
|
|
return &models.GarageBucketInfo{ID: "id-1", Bytes: 1, Objects: 1}, nil
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets/alpha", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestGetBucketInfo_NotFound404(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return nil, nil // nil pointer, nil error → 404
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets/missing", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestGetBucketInfo_ServiceErrorReturns500(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return nil, errors.New("boom")
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/buckets/alpha", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want 500", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// --- DeleteBucket ---
|
|
|
|
func TestDeleteBucket_Success(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
admin.DeleteBucketFn = func(_ context.Context, id string) error {
|
|
if id != "id-1" {
|
|
t.Errorf("DeleteBucket id = %q, want id-1", id)
|
|
}
|
|
return nil
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/buckets/alpha", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestDeleteBucket_NotFound(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return nil, nil
|
|
}
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/buckets/missing", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestDeleteBucket_AdminDeleteErrorReturns500(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
admin.DeleteBucketFn = func(_ context.Context, _ string) error { return errors.New("boom") }
|
|
resp, err := app.Test(httptest.NewRequest(http.MethodDelete, "/buckets/alpha", nil))
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want 500", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// --- GrantBucketPermission ---
|
|
|
|
func TestGrantBucketPermission_Success(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
admin.AllowBucketKeyFn = func(_ context.Context, req models.BucketKeyPermRequest) (*models.GarageBucketInfo, error) {
|
|
if req.BucketID != "id-1" || req.AccessKeyID != "AKIA" {
|
|
t.Errorf("allow req = %+v", req)
|
|
}
|
|
if !req.Permissions.Read || !req.Permissions.Write || req.Permissions.Owner {
|
|
t.Errorf("allow perms = %+v", req.Permissions)
|
|
}
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
admin.DenyBucketKeyFn = func(_ context.Context, req models.BucketKeyPermRequest) (*models.GarageBucketInfo, error) {
|
|
if req.BucketID != "id-1" || req.AccessKeyID != "AKIA" {
|
|
t.Errorf("deny req = %+v", req)
|
|
}
|
|
if req.Permissions.Read || req.Permissions.Write || !req.Permissions.Owner {
|
|
t.Errorf("deny perms = %+v", req.Permissions)
|
|
}
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
body, _ := json.Marshal(models.GrantBucketPermissionRequest{
|
|
AccessKeyID: "AKIA",
|
|
Permissions: models.BucketKeyPermission{Read: true, Write: true},
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets/alpha/permissions", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestGrantBucketPermission_MissingAccessKey400(t *testing.T) {
|
|
app, _ := newBucketsTestApp(t)
|
|
body, _ := json.Marshal(map[string]any{"accessKeyId": "", "permissions": map[string]bool{"read": true}})
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets/alpha/permissions", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestGrantBucketPermission_BucketNotFound404(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return nil, nil
|
|
}
|
|
body, _ := json.Marshal(models.GrantBucketPermissionRequest{AccessKeyID: "AKIA"})
|
|
req := httptest.NewRequest(http.MethodPost, "/buckets/missing/permissions", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// --- UpdateBucketWebsite ---
|
|
|
|
func TestUpdateBucketWebsite_EnableWithIndexDocument(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
admin.UpdateBucketFn = func(_ context.Context, id string, req models.UpdateBucketRequest) (*models.GarageBucketInfo, error) {
|
|
if req.WebsiteAccess == nil || !req.WebsiteAccess.Enabled {
|
|
t.Errorf("WebsiteAccess = %+v", req.WebsiteAccess)
|
|
}
|
|
if req.WebsiteAccess.IndexDocument == nil || *req.WebsiteAccess.IndexDocument != "index.html" {
|
|
t.Errorf("IndexDocument = %v", req.WebsiteAccess.IndexDocument)
|
|
}
|
|
return &models.GarageBucketInfo{ID: id, WebsiteAccess: true}, nil
|
|
}
|
|
body, _ := json.Marshal(models.UpdateBucketWebsiteRequest{Enabled: true, IndexDocument: "index.html"})
|
|
req := httptest.NewRequest(http.MethodPut, "/buckets/alpha/website", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestUpdateBucketWebsite_EnableWithoutIndexDocumentReturns400(t *testing.T) {
|
|
app, _ := newBucketsTestApp(t)
|
|
body, _ := json.Marshal(models.UpdateBucketWebsiteRequest{Enabled: true})
|
|
req := httptest.NewRequest(http.MethodPut, "/buckets/alpha/website", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestUpdateBucketWebsite_Disable(t *testing.T) {
|
|
app, admin := newBucketsTestApp(t)
|
|
admin.GetBucketInfoByAliasFn = func(_ context.Context, _ string) (*models.GarageBucketInfo, error) {
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
admin.UpdateBucketFn = func(_ context.Context, _ string, req models.UpdateBucketRequest) (*models.GarageBucketInfo, error) {
|
|
if req.WebsiteAccess == nil || req.WebsiteAccess.Enabled {
|
|
t.Errorf("expected Enabled=false, got %+v", req.WebsiteAccess)
|
|
}
|
|
return &models.GarageBucketInfo{ID: "id-1"}, nil
|
|
}
|
|
body, _ := json.Marshal(models.UpdateBucketWebsiteRequest{Enabled: false})
|
|
req := httptest.NewRequest(http.MethodPut, "/buckets/alpha/website", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
}
|