// Copyright 2023 Versity Software // This file is licensed under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package controllers import ( "context" "encoding/xml" "errors" "net/http" "testing" "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", Secret: "secret", Role: auth.RoleAdmin, }) assert.NoError(t, err) invalidUserRoleBody, err := xml.Marshal(auth.Account{ Access: "access", Secret: "secret", Role: auth.Role("invalid_role"), }) assert.NoError(t, err) tests := []struct { name string input testInput output testOutput }{ { name: "invalid request body", input: testInput{ body: []byte("invalid_request_body"), }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrMalformedXML), }, }, { name: "invalid user role", input: testInput{ body: invalidUserRoleBody, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminInvalidUserRole), }, }, { name: "backend returns user exists error", input: testInput{ body: validBody, beErr: auth.ErrUserExists, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminUserExists), }, }, { name: "backend returns other error", input: testInput{ body: validBody, beErr: s3err.GetAPIError(s3err.ErrInvalidRequest), }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrInvalidRequest), }, }, { name: "successful response", input: testInput{ body: validBody, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{ Status: http.StatusCreated, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iam := &IAMServiceMock{ CreateAccountFunc: func(account auth.Account) error { return tt.input.beErr }, } ctrl := AdminController{ iam: iam, } testController( t, ctrl.CreateUser, tt.output.response, tt.output.err, ctxInputs{ body: tt.input.body, }) }) } } func TestAdminController_UpdateUser(t *testing.T) { validBody, err := xml.Marshal(auth.MutableProps{ Secret: utils.GetStringPtr("secret"), Role: auth.RoleAdmin, }) assert.NoError(t, err) invalidUserRoleBody, err := xml.Marshal(auth.MutableProps{ Secret: utils.GetStringPtr("secret"), Role: auth.Role("invalid_role"), }) assert.NoError(t, err) tests := []struct { name string input testInput output testOutput }{ { name: "missing user access key", output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminMissingUserAcess), }, }, { name: "invalid request body", input: testInput{ body: []byte("invalid_request_body"), queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrMalformedXML), }, }, { name: "invalid user role", input: testInput{ body: invalidUserRoleBody, queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminInvalidUserRole), }, }, { name: "backend returns user not found error", input: testInput{ body: validBody, beErr: auth.ErrNoSuchUser, queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminUserNotFound), }, }, { name: "backend returns other error", input: testInput{ body: validBody, beErr: s3err.GetAPIError(s3err.ErrInvalidRequest), queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrInvalidRequest), }, }, { name: "successful response", input: testInput{ body: validBody, queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iam := &IAMServiceMock{ UpdateUserAccountFunc: func(access string, props auth.MutableProps) error { return tt.input.beErr }, } ctrl := AdminController{ iam: iam, } testController( t, ctrl.UpdateUser, tt.output.response, tt.output.err, ctxInputs{ body: tt.input.body, queries: tt.input.queries, }) }) } } func TestAdminController_DeleteUser(t *testing.T) { tests := []struct { name string input testInput output testOutput }{ { name: "missing user access key", output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminMissingUserAcess), }, }, { name: "backend returns other error", input: testInput{ beErr: s3err.GetAPIError(s3err.ErrInvalidRequest), queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrInvalidRequest), }, }, { name: "successful response", input: testInput{ queries: map[string]string{ "access": "user", }, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iam := &IAMServiceMock{ DeleteUserAccountFunc: func(access string) error { return tt.input.beErr }, } ctrl := AdminController{ iam: iam, } testController( t, ctrl.DeleteUser, tt.output.response, tt.output.err, ctxInputs{ queries: tt.input.queries, }) }) } } func TestAdminController_ListUsers(t *testing.T) { accs := []auth.Account{ { Access: "access", Secret: "secret", }, { Access: "access", Secret: "secret", }, } tests := []struct { name string input testInput output testOutput }{ { name: "backend returns error", input: testInput{ beRes: []auth.Account{}, beErr: s3err.GetAPIError(s3err.ErrInternalError), }, output: testOutput{ response: &Response{ Data: auth.ListUserAccountsResult{ Accounts: []auth.Account{}, }, MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrInternalError), }, }, { name: "successful response", input: testInput{ beRes: accs, }, output: testOutput{ response: &Response{ Data: auth.ListUserAccountsResult{ Accounts: accs, }, MetaOpts: &MetaOptions{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iam := &IAMServiceMock{ ListUserAccountsFunc: func() ([]auth.Account, error) { return tt.input.beRes.([]auth.Account), tt.input.beErr }, } ctrl := AdminController{ iam: iam, } testController( t, ctrl.ListUsers, tt.output.response, tt.output.err, ctxInputs{ queries: tt.input.queries, }) }) } } func TestAdminController_ChangeBucketOwner(t *testing.T) { tests := []struct { name string input testInput output testOutput }{ { name: "fails to get user account", input: testInput{ extraMockErr: s3err.GetAPIError(s3err.ErrInternalError), }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: errors.New("check user account: "), }, }, { name: "user not found", input: testInput{ extraMockErr: auth.ErrNoSuchUser, }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminUserNotFound), }, }, { name: "backend returns error", input: testInput{ beErr: s3err.GetAPIError(s3err.ErrAdminMethodNotSupported), }, output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrAdminMethodNotSupported), }, }, { name: "successful response", output: testOutput{ response: &Response{ MetaOpts: &MetaOptions{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iam := &IAMServiceMock{ GetUserAccountFunc: func(access string) (auth.Account, error) { return auth.Account{}, tt.input.extraMockErr }, } be := &BackendMock{ ChangeBucketOwnerFunc: func(contextMoqParam context.Context, bucket, owner string) error { return tt.input.beErr }, } ctrl := AdminController{ iam: iam, be: be, } testController( t, ctrl.ChangeBucketOwner, tt.output.response, tt.output.err, ctxInputs{}, ) }) } } func TestAdminController_ListBuckets(t *testing.T) { res := []s3response.Bucket{ { Name: "bucket", Owner: "owner", }, } tests := []struct { name string input testInput output testOutput }{ { name: "backend returns other error", input: testInput{ beRes: []s3response.Bucket{}, beErr: s3err.GetAPIError(s3err.ErrNoSuchBucket), }, output: testOutput{ response: &Response{ Data: s3response.ListBucketsResult{ Buckets: []s3response.Bucket{}, }, MetaOpts: &MetaOptions{}, }, err: s3err.GetAPIError(s3err.ErrNoSuchBucket), }, }, { name: "successful response", input: testInput{ beRes: res, }, output: testOutput{ response: &Response{ Data: s3response.ListBucketsResult{ Buckets: res, }, MetaOpts: &MetaOptions{}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { be := &BackendMock{ ListBucketsAndOwnersFunc: func(contextMoqParam context.Context) ([]s3response.Bucket, error) { return tt.input.beRes.([]s3response.Bucket), tt.input.beErr }, } ctrl := AdminController{ be: be, } testController( t, ctrl.ListBuckets, tt.output.response, tt.output.err, ctxInputs{}, ) }) } }