mirror of
https://github.com/versity/versitygw.git
synced 2026-01-03 10:35:15 +00:00
feat: implements unit tests for controller utilities
This commit is contained in:
@@ -41,8 +41,14 @@ type Tag struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
// Manager is a manager of metrics plugins
|
||||
type Manager struct {
|
||||
// Manager is the interface definition for metrics manager
|
||||
type Manager interface {
|
||||
Send(ctx *fiber.Ctx, err error, action string, count int64, status int)
|
||||
Close()
|
||||
}
|
||||
|
||||
// manager is a manager of metrics plugins
|
||||
type manager struct {
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
|
||||
@@ -59,7 +65,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
// NewManager initializes metrics plugins and returns a new metrics manager
|
||||
func NewManager(ctx context.Context, conf Config) (*Manager, error) {
|
||||
func NewManager(ctx context.Context, conf Config) (Manager, error) {
|
||||
if len(conf.StatsdServers) == 0 && len(conf.DogStatsdServers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -74,7 +80,7 @@ func NewManager(ctx context.Context, conf Config) (*Manager, error) {
|
||||
|
||||
addDataChan := make(chan datapoint, dataItemCount)
|
||||
|
||||
mgr := &Manager{
|
||||
mgr := &manager{
|
||||
addDataChan: addDataChan,
|
||||
ctx: ctx,
|
||||
config: conf,
|
||||
@@ -112,7 +118,7 @@ func NewManager(ctx context.Context, conf Config) (*Manager, error) {
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Send(ctx *fiber.Ctx, err error, action string, count int64, status int) {
|
||||
func (m *manager) Send(ctx *fiber.Ctx, err error, action string, count int64, status int) {
|
||||
// In case of Authentication failures, url parsing ...
|
||||
if action == "" {
|
||||
action = ActionUndetected
|
||||
@@ -168,12 +174,12 @@ func (m *Manager) Send(ctx *fiber.Ctx, err error, action string, count int64, st
|
||||
}
|
||||
|
||||
// increment increments the key by one
|
||||
func (m *Manager) increment(key string, tags ...Tag) {
|
||||
func (m *manager) increment(key string, tags ...Tag) {
|
||||
m.add(key, 1, tags...)
|
||||
}
|
||||
|
||||
// add adds value to key
|
||||
func (m *Manager) add(key string, value int64, tags ...Tag) {
|
||||
func (m *manager) add(key string, value int64, tags ...Tag) {
|
||||
if m.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
@@ -192,7 +198,7 @@ func (m *Manager) add(key string, value int64, tags ...Tag) {
|
||||
}
|
||||
|
||||
// Close closes metrics channels, waits for data to complete, closes all plugins
|
||||
func (m *Manager) Close() {
|
||||
func (m *manager) Close() {
|
||||
// drain the datapoint channels
|
||||
close(m.addDataChan)
|
||||
m.wg.Wait()
|
||||
@@ -209,7 +215,7 @@ type publisher interface {
|
||||
Close()
|
||||
}
|
||||
|
||||
func (m *Manager) addForwarder(addChan <-chan datapoint) {
|
||||
func (m *manager) addForwarder(addChan <-chan datapoint) {
|
||||
for data := range addChan {
|
||||
for _, s := range m.publishers {
|
||||
s.Add(data.key, data.value, data.tags...)
|
||||
|
||||
@@ -23,11 +23,38 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
func TestNewAdminController(t *testing.T) {
|
||||
type args struct {
|
||||
iam auth.IAMService
|
||||
be backend.Backend
|
||||
l s3log.AuditLogger
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want AdminController
|
||||
}{
|
||||
{
|
||||
name: "initialize admin api",
|
||||
args: args{},
|
||||
want: AdminController{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := NewAdminController(tt.args.iam, tt.args.be, tt.args.l)
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminController_CreateUser(t *testing.T) {
|
||||
validBody, err := xml.Marshal(auth.Account{
|
||||
Access: "access",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,17 +16,27 @@ package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/versity/versitygw/auth"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/metrics"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
"github.com/versity/versitygw/s3event"
|
||||
"github.com/versity/versitygw/s3log"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -143,8 +153,572 @@ func buildRequest(bucket, object string, body []byte, headers, queries map[strin
|
||||
|
||||
// set the request headers
|
||||
for key, val := range headers {
|
||||
req.Header.Add(key, val)
|
||||
req.Header.Set(key, val)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
be backend.Backend
|
||||
iam auth.IAMService
|
||||
logger s3log.AuditLogger
|
||||
evs s3event.S3EventSender
|
||||
mm metrics.Manager
|
||||
debug bool
|
||||
readonly bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want S3ApiController
|
||||
}{
|
||||
{
|
||||
name: "debug enabled",
|
||||
args: args{
|
||||
debug: true,
|
||||
},
|
||||
want: S3ApiController{
|
||||
debug: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "debug disabled",
|
||||
args: args{
|
||||
debug: false,
|
||||
},
|
||||
want: S3ApiController{
|
||||
debug: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := New(tt.args.be, tt.args.iam, tt.args.logger, tt.args.evs, tt.args.mm, tt.args.debug, tt.args.readonly)
|
||||
assert.Equal(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3ApiController_HandleUnmatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input testInput
|
||||
output testOutput
|
||||
}{
|
||||
{
|
||||
name: "return method not allowed",
|
||||
output: testOutput{
|
||||
response: &Response{},
|
||||
err: s3err.GetAPIError(s3err.ErrMethodNotAllowed),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := S3ApiController{}
|
||||
|
||||
testController(
|
||||
t,
|
||||
ctrl.HandleUnmatch,
|
||||
tt.output.response,
|
||||
tt.output.err,
|
||||
ctxInputs{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetResponseHeaders(t *testing.T) {
|
||||
type args struct {
|
||||
headers map[string]*string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "should not set if map is nil",
|
||||
args: args{
|
||||
headers: nil,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "should set some headers",
|
||||
args: args{
|
||||
headers: map[string]*string{
|
||||
"x-amz-checksum-algorithm": utils.GetStringPtr("crc32"),
|
||||
"x-amz-meta-key": utils.GetStringPtr("meta_key"),
|
||||
"x-amz-mp-size": utils.GetStringPtr(""),
|
||||
"something": nil,
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"x-amz-checksum-algorithm": "crc32",
|
||||
"x-amz-meta-key": "meta_key",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
app := fiber.New()
|
||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
SetResponseHeaders(ctx, tt.args.headers)
|
||||
if tt.expected != nil {
|
||||
for key, val := range tt.expected {
|
||||
v := ctx.Response().Header.Peek(key)
|
||||
assert.Equal(t, val, string(v))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mock the audit logger
|
||||
type mockAuditLogger struct {
|
||||
}
|
||||
|
||||
func (m *mockAuditLogger) Log(_ *fiber.Ctx, _ error, _ []byte, _ s3log.LogMeta) {}
|
||||
func (m *mockAuditLogger) HangUp() error { return nil }
|
||||
func (m *mockAuditLogger) Shutdown() error { return nil }
|
||||
|
||||
// mock S3 event sender
|
||||
type mockEvSender struct {
|
||||
}
|
||||
|
||||
func (m *mockEvSender) SendEvent(_ *fiber.Ctx, _ s3event.EventMeta) {}
|
||||
func (m *mockEvSender) Close() error { return nil }
|
||||
|
||||
// mock metrics manager
|
||||
|
||||
type mockMetricsManager struct{}
|
||||
|
||||
func (m *mockMetricsManager) Send(_ *fiber.Ctx, _ error, _ string, _ int64, _ int) {}
|
||||
func (m *mockMetricsManager) Close() {}
|
||||
|
||||
func TestProcessController(t *testing.T) {
|
||||
payload, err := xml.Marshal(s3response.Bucket{
|
||||
Name: "something",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
payloadLen := len(payload) + len(xmlhdr)
|
||||
|
||||
services := &Services{
|
||||
Logger: &mockAuditLogger{},
|
||||
EventSender: &mockEvSender{},
|
||||
MetricsManager: &mockMetricsManager{},
|
||||
}
|
||||
type args struct {
|
||||
controller Controller
|
||||
svc *Services
|
||||
}
|
||||
type expected struct {
|
||||
status int
|
||||
headers map[string]string
|
||||
body []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected expected
|
||||
}{
|
||||
{
|
||||
name: "no services successfull response",
|
||||
args: args{
|
||||
svc: &Services{},
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handle api error",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{}, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusBadRequest,
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInvalidRequest), "", "", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handle custom error",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{}, errors.New("custom error")
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusInternalServerError,
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "body parsing fails",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
Data: make(chan int),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusInternalServerError,
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no data payload",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
ObjectCount: 2,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return 204 http status",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
Status: http.StatusNoContent,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already encoded payload",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
Data: []byte("encoded_data"),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusOK,
|
||||
body: []byte("encoded_data"),
|
||||
headers: map[string]string{
|
||||
"Content-Length": "12",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should set response headers",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
Headers: map[string]*string{
|
||||
"X-Amz-My-Custom-Header": utils.GetStringPtr("my_value"),
|
||||
"X-Amz-Meta-My-Meta": utils.GetStringPtr("my_meta"),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
status: http.StatusOK,
|
||||
headers: map[string]string{
|
||||
"X-Amz-My-Custom-Header": "my_value",
|
||||
"X-Amz-Meta-My-Meta": "my_meta",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "large paylod: should return internal error",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
type Item struct {
|
||||
Value string `xml:"value"`
|
||||
}
|
||||
|
||||
type payload struct {
|
||||
Items []Item `xml:"item"`
|
||||
}
|
||||
|
||||
const targetSize = 5 * 1024 * 1024 // 5 MiB
|
||||
const itemCount = 500
|
||||
const valueSize = targetSize / itemCount
|
||||
|
||||
p := payload{
|
||||
Items: make([]Item, itemCount),
|
||||
}
|
||||
|
||||
// Preallocate one shared string of desired size
|
||||
var sb strings.Builder
|
||||
sb.Grow(valueSize)
|
||||
for range valueSize {
|
||||
sb.WriteByte('A')
|
||||
}
|
||||
largeValue := sb.String()
|
||||
|
||||
for i := range p.Items {
|
||||
p.Items[i].Value = largeValue
|
||||
}
|
||||
|
||||
return &Response{
|
||||
Data: p,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
|
||||
status: http.StatusInternalServerError,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not encoded payload",
|
||||
args: args{
|
||||
svc: services,
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
Data: s3response.Bucket{
|
||||
Name: "something",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
headers: map[string]string{
|
||||
"Content-Length": fmt.Sprint(payloadLen),
|
||||
},
|
||||
body: append(xmlhdr, payload...),
|
||||
status: http.StatusOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := fiber.New().AcquireCtx(&fasthttp.RequestCtx{})
|
||||
err := ProcessController(ctx, tt.args.controller, metrics.ActionAbortMultipartUpload, tt.args.svc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check the status
|
||||
assert.Equal(t, tt.expected.status, ctx.Response().StatusCode())
|
||||
|
||||
// check the response headers to be set
|
||||
if tt.expected.headers != nil {
|
||||
for key, val := range tt.expected.headers {
|
||||
v := ctx.Response().Header.Peek(key)
|
||||
assert.Equal(t, val, string(v))
|
||||
}
|
||||
}
|
||||
|
||||
// check the response body
|
||||
if tt.expected.body != nil {
|
||||
assert.Equal(t, tt.expected.body, ctx.Response().Body())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessHandlers(t *testing.T) {
|
||||
payload, err := xml.Marshal(s3response.Checksum{
|
||||
CRC32: utils.GetStringPtr("crc32"),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
controller Controller
|
||||
svc *Services
|
||||
handlers []fiber.Handler
|
||||
locals map[utils.ContextKey]any
|
||||
}
|
||||
type expected struct {
|
||||
body []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected expected
|
||||
}{
|
||||
{
|
||||
name: "should skip the handlers",
|
||||
args: args{
|
||||
locals: map[utils.ContextKey]any{
|
||||
utils.ContextKeySkip: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handler returns error",
|
||||
args: args{
|
||||
handlers: []fiber.Handler{
|
||||
func(ctx *fiber.Ctx) error {
|
||||
return nil
|
||||
},
|
||||
func(ctx *fiber.Ctx) error {
|
||||
return s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
},
|
||||
},
|
||||
svc: &Services{},
|
||||
},
|
||||
expected: expected{
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrAccessDenied), "", "", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should process the controller",
|
||||
args: args{
|
||||
handlers: []fiber.Handler{
|
||||
func(ctx *fiber.Ctx) error {
|
||||
return nil
|
||||
},
|
||||
func(ctx *fiber.Ctx) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
svc: &Services{},
|
||||
controller: func(ctx *fiber.Ctx) (*Response, error) {
|
||||
return &Response{
|
||||
Data: s3response.Checksum{
|
||||
CRC32: utils.GetStringPtr("crc32"),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
body: append(xmlhdr, payload...),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mdlwr := ProcessHandlers(tt.args.controller, metrics.ActionCreateBucket, tt.args.svc, tt.args.handlers...)
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Post("/:bucket/*", func(ctx *fiber.Ctx) error {
|
||||
// set the request locals
|
||||
if tt.args.locals != nil {
|
||||
for key, val := range tt.args.locals {
|
||||
key.Set(ctx, val)
|
||||
}
|
||||
}
|
||||
|
||||
// call the controller by passing the ctx
|
||||
err := mdlwr(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check the response body
|
||||
if tt.expected.body != nil {
|
||||
assert.Equal(t, tt.expected.body, ctx.Response().Body())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
app.All("*", func(ctx *fiber.Ctx) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
req := buildRequest("bucket", "object", nil, nil, nil)
|
||||
|
||||
_, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapMiddleware(t *testing.T) {
|
||||
type args struct {
|
||||
handler fiber.Handler
|
||||
logger s3log.AuditLogger
|
||||
mm metrics.Manager
|
||||
}
|
||||
type expected struct {
|
||||
body []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected expected
|
||||
}{
|
||||
{
|
||||
name: "handler returns no error",
|
||||
args: args{
|
||||
handler: func(ctx *fiber.Ctx) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handler returns api error",
|
||||
args: args{
|
||||
handler: func(ctx *fiber.Ctx) error {
|
||||
return s3err.GetAPIError(s3err.ErrAclNotSupported)
|
||||
},
|
||||
mm: &mockMetricsManager{},
|
||||
logger: &mockAuditLogger{},
|
||||
},
|
||||
expected: expected{
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrAclNotSupported), "", "", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handler returns custom error",
|
||||
args: args{
|
||||
handler: func(ctx *fiber.Ctx) error {
|
||||
return errors.New("custom error")
|
||||
},
|
||||
},
|
||||
expected: expected{
|
||||
body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mdlwr := WrapMiddleware(tt.args.handler, tt.args.logger, tt.args.mm)
|
||||
app := fiber.New()
|
||||
|
||||
app.Post("/:bucket/*", func(ctx *fiber.Ctx) error {
|
||||
// call the controller by passing the ctx
|
||||
err := mdlwr(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check the response body
|
||||
if tt.expected.body != nil {
|
||||
assert.Equal(t, tt.expected.body, ctx.Response().Body())
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
app.All("*", func(ctx *fiber.Ctx) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
req := buildRequest("bucket", "object", nil, nil, nil)
|
||||
|
||||
_, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,24 +311,7 @@ func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
|
||||
grants := grantFullControl + grantRead + grantReadACP + granWrite + grantWriteACP
|
||||
var input *auth.PutBucketAclInput
|
||||
|
||||
ownership, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
|
||||
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)) {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
if ownership == types.ObjectOwnershipBucketOwnerEnforced {
|
||||
debuglogger.Logf("bucket acls are disabled")
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrAclNotSupported)
|
||||
}
|
||||
|
||||
err = auth.VerifyAccess(ctx.Context(), c.be,
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
@@ -346,6 +329,23 @@ func (c S3ApiController) PutBucketAcl(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
ownership, err := c.be.GetBucketOwnershipControls(ctx.Context(), bucket)
|
||||
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound)) {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
if ownership == types.ObjectOwnershipBucketOwnerEnforced {
|
||||
debuglogger.Logf("bucket acls are disabled")
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrAclNotSupported)
|
||||
}
|
||||
|
||||
if len(ctx.Body()) > 0 {
|
||||
var accessControlPolicy auth.AccessControlPolicy
|
||||
err := xml.Unmarshal(ctx.Body(), &accessControlPolicy)
|
||||
|
||||
@@ -17,6 +17,7 @@ package controllers
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
@@ -810,4 +811,291 @@ func TestS3ApiController_CreateBucket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add a test for PutBucketAcl
|
||||
func TestS3ApiController_PutBucketAcl(t *testing.T) {
|
||||
invalidBody, err := xml.Marshal(auth.AccessControlPolicy{
|
||||
Owner: &types.Owner{
|
||||
ID: utils.GetStringPtr("root"),
|
||||
},
|
||||
AccessControlList: auth.AccessControlList{
|
||||
Grants: []auth.Grant{
|
||||
{
|
||||
Permission: auth.Permission("invalid_permission"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
incorrectOwnerBody, err := xml.Marshal(auth.AccessControlPolicy{
|
||||
Owner: &types.Owner{
|
||||
ID: utils.GetStringPtr("user"),
|
||||
},
|
||||
AccessControlList: auth.AccessControlList{},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
validAccessControlPolicy, err := xml.Marshal(auth.AccessControlPolicy{
|
||||
Owner: &types.Owner{
|
||||
ID: utils.GetStringPtr("root"),
|
||||
},
|
||||
AccessControlList: auth.AccessControlList{},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input testInput
|
||||
output testOutput
|
||||
}{
|
||||
{
|
||||
name: "access denied",
|
||||
input: testInput{
|
||||
locals: accessDeniedLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrAccessDenied),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fails to get bucket ownership",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
extraMockErr: s3err.GetAPIError(s3err.ErrInternalError),
|
||||
extraMockResp: types.ObjectOwnership(""),
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrInternalError),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "acl not supported",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerEnforced,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrAclNotSupported),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid request body",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
body: []byte("invalid_body"),
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMalformedACL),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid access control policy",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
body: invalidBody,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMalformedACL),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "incorrect owner id",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
body: incorrectOwnerBody,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.APIError{
|
||||
Code: "InvalidArgument",
|
||||
Description: "Invalid id",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both access control policy and grants",
|
||||
input: testInput{
|
||||
body: validAccessControlPolicy,
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
headers: map[string]string{
|
||||
"X-Amz-Acl": "public-read",
|
||||
},
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrUnexpectedContent),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "access control policy success",
|
||||
input: testInput{
|
||||
body: validAccessControlPolicy,
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid canned acl",
|
||||
input: testInput{
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
headers: map[string]string{
|
||||
"X-Amz-Acl": "invalid_acl",
|
||||
},
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrInvalidRequest),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both canned acl and grants",
|
||||
input: testInput{
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
headers: map[string]string{
|
||||
"X-Amz-Acl": "public-read",
|
||||
"X-Amz-Grant-Read": "grt1",
|
||||
},
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrBothCannedAndHeaderGrants),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "canned acl success",
|
||||
input: testInput{
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
headers: map[string]string{
|
||||
"X-Amz-Acl": "public-read",
|
||||
},
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "grants update acl fails",
|
||||
input: testInput{
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
headers: map[string]string{
|
||||
"X-Amz-Grant-Read": "grt1",
|
||||
},
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: errors.New("accounts does not exist: grt1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no option provided",
|
||||
input: testInput{
|
||||
extraMockResp: types.ObjectOwnershipBucketOwnerPreferred,
|
||||
locals: defaultLocals,
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMissingSecurityHeader),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
be := &BackendMock{
|
||||
PutBucketAclFunc: func(contextMoqParam context.Context, bucket string, data []byte) error {
|
||||
return tt.input.beErr
|
||||
},
|
||||
GetBucketOwnershipControlsFunc: func(contextMoqParam context.Context, bucket string) (types.ObjectOwnership, error) {
|
||||
return tt.input.extraMockResp.(types.ObjectOwnership), tt.input.extraMockErr
|
||||
},
|
||||
GetBucketPolicyFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
return nil, s3err.GetAPIError(s3err.ErrAccessDenied)
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := S3ApiController{
|
||||
be: be,
|
||||
iam: auth.NewIAMServiceSingle(
|
||||
auth.Account{
|
||||
Access: "root",
|
||||
}),
|
||||
}
|
||||
|
||||
testController(t, ctrl.PutBucketAcl, tt.output.response, tt.output.err,
|
||||
ctxInputs{
|
||||
locals: tt.input.locals,
|
||||
body: tt.input.body,
|
||||
bucket: tt.input.bucket,
|
||||
headers: tt.input.headers,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
*res.ContentLength)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
ContentLength: getint64(res.ContentLength),
|
||||
ContentLength: utils.GetInt64(res.ContentLength),
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
Status: status,
|
||||
},
|
||||
@@ -501,7 +501,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
"Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
|
||||
},
|
||||
MetaOpts: &MetaOptions{
|
||||
ContentLength: getint64(res.ContentLength),
|
||||
ContentLength: utils.GetInt64(res.ContentLength),
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
Status: status,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ type S3ApiRouter struct {
|
||||
WithAdmSrv bool
|
||||
}
|
||||
|
||||
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm *metrics.Manager, debug bool, readonly bool, region string, root middlewares.RootUserConfig) {
|
||||
func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm metrics.Manager, debug bool, readonly bool, region string, root middlewares.RootUserConfig) {
|
||||
ctrl := controllers.New(be, iam, logger, evs, mm, debug, readonly)
|
||||
adminServices := &controllers.Services{
|
||||
Logger: aLogger,
|
||||
|
||||
@@ -51,7 +51,7 @@ func New(
|
||||
l s3log.AuditLogger,
|
||||
adminLogger s3log.AuditLogger,
|
||||
evs s3event.S3EventSender,
|
||||
mm *metrics.Manager,
|
||||
mm metrics.Manager,
|
||||
opts ...Option,
|
||||
) (*S3ApiServer, error) {
|
||||
server := &S3ApiServer{
|
||||
|
||||
@@ -704,3 +704,12 @@ func FormatDatePtrToString(date *time.Time, format string) *string {
|
||||
formatted := date.UTC().Format(format)
|
||||
return &formatted
|
||||
}
|
||||
|
||||
// GetInt64 returns the value of int64 pointer
|
||||
func GetInt64(n *int64) int64 {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return *n
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user