| 
									
										
										
										
											2021-04-19 03:41:13 +08:00
										 |  |  | // Copyright (c) 2015-2021 MinIO, Inc.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This file is part of MinIO Object Storage stack
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This program is free software: you can redistribute it and/or modify
 | 
					
						
							|  |  |  | // it under the terms of the GNU Affero General Public License as published by
 | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or
 | 
					
						
							|  |  |  | // (at your option) any later version.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful
 | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					
						
							|  |  |  | // GNU Affero General Public License for more details.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // You should have received a copy of the GNU Affero General Public License
 | 
					
						
							|  |  |  | // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | package cmd | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2017-01-18 02:02:58 +08:00
										 |  |  | 	"bytes" | 
					
						
							| 
									
										
										
										
											2020-04-15 08:52:38 +08:00
										 |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	"encoding/json" | 
					
						
							| 
									
										
										
										
											2023-03-22 09:35:29 +08:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2017-03-18 00:25:49 +08:00
										 |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/http/httptest" | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | 	"net/url" | 
					
						
							| 
									
										
										
										
											2023-03-22 09:35:29 +08:00
										 |  |  | 	"sort" | 
					
						
							| 
									
										
										
										
											2018-09-28 08:16:30 +08:00
										 |  |  | 	"sync" | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	"testing" | 
					
						
							| 
									
										
										
										
											2021-11-12 13:03:02 +08:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-07 05:46:50 +08:00
										 |  |  | 	"github.com/minio/madmin-go/v2" | 
					
						
							| 
									
										
										
										
											2021-06-02 05:59:40 +08:00
										 |  |  | 	"github.com/minio/minio/internal/auth" | 
					
						
							| 
									
										
										
										
											2023-01-23 19:12:47 +08:00
										 |  |  | 	"github.com/minio/mux" | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | // adminErasureTestBed - encapsulates subsystems that need to be setup for
 | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | // admin-handler unit tests.
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | type adminErasureTestBed struct { | 
					
						
							|  |  |  | 	erasureDirs []string | 
					
						
							|  |  |  | 	objLayer    ObjectLayer | 
					
						
							|  |  |  | 	router      *mux.Router | 
					
						
							| 
									
										
										
										
											2021-12-03 03:29:16 +08:00
										 |  |  | 	done        context.CancelFunc | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | // prepareAdminErasureTestBed - helper function that setups a single-node
 | 
					
						
							|  |  |  | // Erasure backend for admin-handler tests.
 | 
					
						
							|  |  |  | func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) { | 
					
						
							| 
									
										
										
										
											2021-12-03 03:29:16 +08:00
										 |  |  | 	ctx, cancel := context.WithCancel(ctx) | 
					
						
							| 
									
										
										
										
											2020-04-15 08:52:38 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	// reset global variables to start afresh.
 | 
					
						
							|  |  |  | 	resetTestGlobals() | 
					
						
							| 
									
										
										
										
											2017-02-08 04:51:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	// Set globalIsErasure to indicate that the setup uses an erasure
 | 
					
						
							| 
									
										
										
										
											2020-01-16 10:30:32 +08:00
										 |  |  | 	// code backend.
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	globalIsErasure = true | 
					
						
							| 
									
										
										
										
											2020-01-16 10:30:32 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	// Initializing objectLayer for HealFormatHandler.
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx) | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	if xlErr != nil { | 
					
						
							| 
									
										
										
										
											2021-12-03 03:29:16 +08:00
										 |  |  | 		cancel() | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 		return nil, xlErr | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-15 12:41:47 +08:00
										 |  |  | 	// Initialize minio server config.
 | 
					
						
							|  |  |  | 	if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil { | 
					
						
							| 
									
										
										
										
											2021-12-03 03:29:16 +08:00
										 |  |  | 		cancel() | 
					
						
							| 
									
										
										
										
											2018-08-15 12:41:47 +08:00
										 |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-08 16:13:02 +08:00
										 |  |  | 	// Initialize boot time
 | 
					
						
							| 
									
										
										
										
											2017-03-19 02:28:41 +08:00
										 |  |  | 	globalBootTime = UTCNow() | 
					
						
							| 
									
										
										
										
											2017-02-08 16:13:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-27 12:47:42 +08:00
										 |  |  | 	globalEndpoints = mustGetPoolEndpoints(erasureDirs...) | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-14 18:08:40 +08:00
										 |  |  | 	initAllSubsystems(ctx) | 
					
						
							| 
									
										
										
										
											2019-02-12 17:25:52 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-18 05:42:08 +08:00
										 |  |  | 	initConfigSubsystem(ctx, objLayer) | 
					
						
							| 
									
										
										
										
											2018-04-25 06:53:30 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-08 02:39:57 +08:00
										 |  |  | 	globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) | 
					
						
							| 
									
										
										
										
											2021-05-09 23:14:19 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	// Setup admin mgmt REST API handlers.
 | 
					
						
							| 
									
										
										
										
											2018-04-22 10:23:54 +08:00
										 |  |  | 	adminRouter := mux.NewRouter() | 
					
						
							| 
									
										
										
										
											2021-07-10 23:32:52 +08:00
										 |  |  | 	registerAdminRouter(adminRouter, true) | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	return &adminErasureTestBed{ | 
					
						
							|  |  |  | 		erasureDirs: erasureDirs, | 
					
						
							|  |  |  | 		objLayer:    objLayer, | 
					
						
							|  |  |  | 		router:      adminRouter, | 
					
						
							| 
									
										
										
										
											2021-12-03 03:29:16 +08:00
										 |  |  | 		done:        cancel, | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	}, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // TearDown - method that resets the test bed for subsequent unit
 | 
					
						
							|  |  |  | // tests to start afresh.
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | func (atb *adminErasureTestBed) TearDown() { | 
					
						
							| 
									
										
										
										
											2021-12-03 03:29:16 +08:00
										 |  |  | 	atb.done() | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	removeRoots(atb.erasureDirs) | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	resetTestGlobals() | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | // initTestObjLayer - Helper function to initialize an Erasure-based object
 | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | // layer and set globalObjectAPI.
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error) { | 
					
						
							|  |  |  | 	erasureDirs, err := getRandomDisks(16) | 
					
						
							| 
									
										
										
										
											2018-02-16 09:45:57 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-01-27 12:47:42 +08:00
										 |  |  | 	endpoints := mustGetPoolEndpoints(erasureDirs...) | 
					
						
							| 
									
										
										
										
											2018-06-07 03:52:56 +08:00
										 |  |  | 	globalPolicySys = NewPolicySys() | 
					
						
							| 
									
										
										
										
											2021-01-17 04:08:02 +08:00
										 |  |  | 	objLayer, err := newErasureServerPools(ctx, endpoints) | 
					
						
							| 
									
										
										
										
											2018-02-16 09:45:57 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	// Make objLayer available to all internal services via globalObjectAPI.
 | 
					
						
							|  |  |  | 	globalObjLayerMutex.Lock() | 
					
						
							|  |  |  | 	globalObjectAPI = objLayer | 
					
						
							|  |  |  | 	globalObjLayerMutex.Unlock() | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	return objLayer, erasureDirs, nil | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | // cmdType - Represents different service subcomands like status, stop
 | 
					
						
							|  |  |  | // and restart.
 | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | type cmdType int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ( | 
					
						
							| 
									
										
										
										
											2019-08-29 06:04:43 +08:00
										 |  |  | 	restartCmd cmdType = iota | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	stopCmd | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | // toServiceSignal - Helper function that translates a given cmdType
 | 
					
						
							|  |  |  | // value to its corresponding serviceSignal value.
 | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | func (c cmdType) toServiceSignal() serviceSignal { | 
					
						
							|  |  |  | 	switch c { | 
					
						
							|  |  |  | 	case restartCmd: | 
					
						
							|  |  |  | 		return serviceRestart | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	case stopCmd: | 
					
						
							|  |  |  | 		return serviceStop | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-08-29 06:04:43 +08:00
										 |  |  | 	return serviceRestart | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | func (c cmdType) toServiceAction() madmin.ServiceAction { | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	switch c { | 
					
						
							|  |  |  | 	case restartCmd: | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | 		return madmin.ServiceActionRestart | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	case stopCmd: | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | 		return madmin.ServiceActionStop | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-08-29 06:04:43 +08:00
										 |  |  | 	return madmin.ServiceActionRestart | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | // testServiceSignalReceiver - Helper function that simulates a
 | 
					
						
							|  |  |  | // go-routine waiting on service signal.
 | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | func testServiceSignalReceiver(cmd cmdType, t *testing.T) { | 
					
						
							|  |  |  | 	expectedCmd := cmd.toServiceSignal() | 
					
						
							|  |  |  | 	serviceCmd := <-globalServiceSignalCh | 
					
						
							|  |  |  | 	if serviceCmd != expectedCmd { | 
					
						
							|  |  |  | 		t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | // getServiceCmdRequest - Constructs a management REST API request for service
 | 
					
						
							|  |  |  | // subcommands for a given cmdType value.
 | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) { | 
					
						
							|  |  |  | 	queryVal := url.Values{} | 
					
						
							|  |  |  | 	queryVal.Set("action", string(cmd.toServiceAction())) | 
					
						
							| 
									
										
										
										
											2019-11-05 01:30:59 +08:00
										 |  |  | 	resource := adminPathPrefix + adminAPIVersionPrefix + "/service?" + queryVal.Encode() | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | 	req, err := newTestRequest(http.MethodPost, resource, 0, nil) | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// management REST API uses signature V4 for authentication.
 | 
					
						
							| 
									
										
										
										
											2016-12-27 02:21:23 +08:00
										 |  |  | 	err = signRequestV4(req, cred.AccessKey, cred.SecretKey) | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return req, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | // testServicesCmdHandler - parametrizes service subcommand tests on
 | 
					
						
							|  |  |  | // cmdType value.
 | 
					
						
							| 
									
										
										
										
											2017-01-25 00:08:36 +08:00
										 |  |  | func testServicesCmdHandler(cmd cmdType, t *testing.T) { | 
					
						
							| 
									
										
										
										
											2020-04-15 08:52:38 +08:00
										 |  |  | 	ctx, cancel := context.WithCancel(context.Background()) | 
					
						
							|  |  |  | 	defer cancel() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	adminTestBed, err := prepareAdminErasureTestBed(ctx) | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2021-08-04 04:26:57 +08:00
										 |  |  | 		t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err) | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-01-23 16:32:55 +08:00
										 |  |  | 	defer adminTestBed.TearDown() | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | 	// Initialize admin peers to make admin RPC calls. Note: In a
 | 
					
						
							|  |  |  | 	// single node setup, this degenerates to a simple function
 | 
					
						
							|  |  |  | 	// call under the hood.
 | 
					
						
							| 
									
										
										
										
											2017-04-12 06:44:27 +08:00
										 |  |  | 	globalMinioAddr = "127.0.0.1:9000" | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-28 08:16:30 +08:00
										 |  |  | 	var wg sync.WaitGroup | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-22 10:23:54 +08:00
										 |  |  | 	// Setting up a go routine to simulate ServerRouter's
 | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	// handleServiceSignals for stop and restart commands.
 | 
					
						
							| 
									
										
										
										
											2017-01-18 06:25:59 +08:00
										 |  |  | 	if cmd == restartCmd { | 
					
						
							| 
									
										
										
										
											2018-09-28 08:16:30 +08:00
										 |  |  | 		wg.Add(1) | 
					
						
							|  |  |  | 		go func() { | 
					
						
							|  |  |  | 			defer wg.Done() | 
					
						
							|  |  |  | 			testServiceSignalReceiver(cmd, t) | 
					
						
							|  |  |  | 		}() | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-10-23 13:59:13 +08:00
										 |  |  | 	credentials := globalActiveCred | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | 	req, err := getServiceCmdRequest(cmd, credentials) | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		t.Fatalf("Failed to build service status request %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-01-18 06:25:59 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	rec := httptest.NewRecorder() | 
					
						
							| 
									
										
										
										
											2018-04-22 10:23:54 +08:00
										 |  |  | 	adminTestBed.router.ServeHTTP(rec, req) | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | 	if rec.Code != http.StatusOK { | 
					
						
							| 
									
										
										
										
											2022-09-20 02:05:16 +08:00
										 |  |  | 		resp, _ := io.ReadAll(rec.Body) | 
					
						
							| 
									
										
										
										
											2019-08-28 02:37:47 +08:00
										 |  |  | 		t.Errorf("Expected to receive %d status code but received %d. Body (%s)", | 
					
						
							|  |  |  | 			http.StatusOK, rec.Code, string(resp)) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-28 08:16:30 +08:00
										 |  |  | 	// Wait until testServiceSignalReceiver() called in a goroutine quits.
 | 
					
						
							|  |  |  | 	wg.Wait() | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-01-04 15:39:22 +08:00
										 |  |  | // Test for service restart management REST API.
 | 
					
						
							| 
									
										
										
										
											2016-12-16 14:26:15 +08:00
										 |  |  | func TestServiceRestartHandler(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2017-01-25 00:08:36 +08:00
										 |  |  | 	testServicesCmdHandler(restartCmd, t) | 
					
						
							| 
									
										
										
										
											2017-01-18 06:25:59 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-18 00:25:49 +08:00
										 |  |  | // buildAdminRequest - helper function to build an admin API request.
 | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | func buildAdminRequest(queryVal url.Values, method, path string, | 
					
						
							| 
									
										
										
										
											2022-01-03 01:15:06 +08:00
										 |  |  | 	contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error, | 
					
						
							|  |  |  | ) { | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	req, err := newTestRequest(method, | 
					
						
							| 
									
										
										
										
											2019-11-05 01:30:59 +08:00
										 |  |  | 		adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(), | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 		contentLength, bodySeeker) | 
					
						
							| 
									
										
										
										
											2017-03-18 00:25:49 +08:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2018-04-06 06:04:40 +08:00
										 |  |  | 		return nil, err | 
					
						
							| 
									
										
										
										
											2017-03-18 00:25:49 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-23 13:59:13 +08:00
										 |  |  | 	cred := globalActiveCred | 
					
						
							| 
									
										
										
										
											2017-03-18 00:25:49 +08:00
										 |  |  | 	err = signRequestV4(req, cred.AccessKey, cred.SecretKey) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2018-04-06 06:04:40 +08:00
										 |  |  | 		return nil, err | 
					
						
							| 
									
										
										
										
											2017-03-18 00:25:49 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return req, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | func TestAdminServerInfo(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2020-04-15 08:52:38 +08:00
										 |  |  | 	ctx, cancel := context.WithCancel(context.Background()) | 
					
						
							|  |  |  | 	defer cancel() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 	adminTestBed, err := prepareAdminErasureTestBed(ctx) | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2021-08-04 04:26:57 +08:00
										 |  |  | 		t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err) | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2020-03-03 08:29:30 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	defer adminTestBed.TearDown() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Initialize admin peers to make admin RPC calls.
 | 
					
						
							| 
									
										
										
										
											2017-04-12 06:44:27 +08:00
										 |  |  | 	globalMinioAddr = "127.0.0.1:9000" | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Prepare query params for set-config mgmt REST API.
 | 
					
						
							|  |  |  | 	queryVal := url.Values{} | 
					
						
							|  |  |  | 	queryVal.Set("info", "") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-23 06:54:55 +08:00
										 |  |  | 	req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil) | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		t.Fatalf("Failed to construct get-config object request - %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	rec := httptest.NewRecorder() | 
					
						
							| 
									
										
										
										
											2018-04-22 10:23:54 +08:00
										 |  |  | 	adminTestBed.router.ServeHTTP(rec, req) | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	if rec.Code != http.StatusOK { | 
					
						
							|  |  |  | 		t.Errorf("Expected to succeed but failed with %d", rec.Code) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-14 03:33:11 +08:00
										 |  |  | 	results := madmin.InfoMessage{} | 
					
						
							| 
									
										
										
										
											2017-04-21 22:15:53 +08:00
										 |  |  | 	err = json.NewDecoder(rec.Body).Decode(&results) | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		t.Fatalf("Failed to decode set config result json %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-14 03:33:11 +08:00
										 |  |  | 	if results.Region != globalMinioDefaultRegion { | 
					
						
							|  |  |  | 		t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, results.Region) | 
					
						
							| 
									
										
										
										
											2017-04-07 14:08:33 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-12 17:25:52 +08:00
										 |  |  | // TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
 | 
					
						
							|  |  |  | func TestToAdminAPIErrCode(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2017-02-28 03:40:27 +08:00
										 |  |  | 	testCases := []struct { | 
					
						
							|  |  |  | 		err            error | 
					
						
							|  |  |  | 		expectedAPIErr APIErrorCode | 
					
						
							|  |  |  | 	}{ | 
					
						
							|  |  |  | 		// 1. Server not in quorum.
 | 
					
						
							|  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2020-06-13 11:04:01 +08:00
										 |  |  | 			err:            errErasureWriteQuorum, | 
					
						
							| 
									
										
										
										
											2017-02-28 03:40:27 +08:00
										 |  |  | 			expectedAPIErr: ErrAdminConfigNoQuorum, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		// 2. No error.
 | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			err:            nil, | 
					
						
							|  |  |  | 			expectedAPIErr: ErrNone, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		// 3. Non-admin API specific error.
 | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			err:            errDiskNotFound, | 
					
						
							| 
									
										
										
										
											2020-04-10 00:30:02 +08:00
										 |  |  | 			expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound), | 
					
						
							| 
									
										
										
										
											2017-02-28 03:40:27 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, test := range testCases { | 
					
						
							| 
									
										
										
										
											2020-04-10 00:30:02 +08:00
										 |  |  | 		actualErr := toAdminAPIErrCode(GlobalContext, test.err) | 
					
						
							| 
									
										
										
										
											2017-02-28 03:40:27 +08:00
										 |  |  | 		if actualErr != test.expectedAPIErr { | 
					
						
							|  |  |  | 			t.Errorf("Test %d: Expected %v but received %v", | 
					
						
							|  |  |  | 				i+1, test.expectedAPIErr, actualErr) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func TestExtractHealInitParams(t *testing.T) { | 
					
						
							|  |  |  | 	mkParams := func(clientToken string, forceStart, forceStop bool) url.Values { | 
					
						
							|  |  |  | 		v := url.Values{} | 
					
						
							|  |  |  | 		if clientToken != "" { | 
					
						
							| 
									
										
										
										
											2020-09-24 23:40:21 +08:00
										 |  |  | 			v.Add(mgmtClientToken, clientToken) | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		if forceStart { | 
					
						
							| 
									
										
										
										
											2020-09-24 23:40:21 +08:00
										 |  |  | 			v.Add(mgmtForceStart, "") | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		if forceStop { | 
					
						
							| 
									
										
										
										
											2020-09-24 23:40:21 +08:00
										 |  |  | 			v.Add(mgmtForceStop, "") | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		return v | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	qParmsArr := []url.Values{ | 
					
						
							|  |  |  | 		// Invalid cases
 | 
					
						
							|  |  |  | 		mkParams("", true, true), | 
					
						
							|  |  |  | 		mkParams("111", true, true), | 
					
						
							|  |  |  | 		mkParams("111", true, false), | 
					
						
							|  |  |  | 		mkParams("111", false, true), | 
					
						
							|  |  |  | 		// Valid cases follow
 | 
					
						
							|  |  |  | 		mkParams("", true, false), | 
					
						
							|  |  |  | 		mkParams("", false, true), | 
					
						
							|  |  |  | 		mkParams("", false, false), | 
					
						
							|  |  |  | 		mkParams("111", false, false), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	varsArr := []map[string]string{ | 
					
						
							|  |  |  | 		// Invalid cases
 | 
					
						
							| 
									
										
										
										
											2020-09-24 23:40:21 +08:00
										 |  |  | 		{mgmtPrefix: "objprefix"}, | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 		// Valid cases
 | 
					
						
							|  |  |  | 		{}, | 
					
						
							| 
									
										
										
										
											2020-09-24 23:40:21 +08:00
										 |  |  | 		{mgmtBucket: "bucket"}, | 
					
						
							|  |  |  | 		{mgmtBucket: "bucket", mgmtPrefix: "objprefix"}, | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Body is always valid - we do not test JSON decoding.
 | 
					
						
							|  |  |  | 	body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Test all combinations!
 | 
					
						
							|  |  |  | 	for pIdx, parms := range qParmsArr { | 
					
						
							|  |  |  | 		for vIdx, vars := range varsArr { | 
					
						
							| 
									
										
										
										
											2020-12-27 14:58:06 +08:00
										 |  |  | 			_, err := extractHealInitParams(vars, parms, bytes.NewReader([]byte(body))) | 
					
						
							| 
									
										
										
										
											2019-08-30 04:53:27 +08:00
										 |  |  | 			isErrCase := false | 
					
						
							|  |  |  | 			if pIdx < 4 || vIdx < 1 { | 
					
						
							|  |  |  | 				isErrCase = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if err != ErrNone && !isErrCase { | 
					
						
							|  |  |  | 				t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err) | 
					
						
							|  |  |  | 			} else if err == ErrNone && isErrCase { | 
					
						
							|  |  |  | 				t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-03-22 09:35:29 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | type byResourceUID struct{ madmin.LockEntries } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (b byResourceUID) Less(i, j int) bool { | 
					
						
							|  |  |  | 	toUniqLock := func(entry madmin.LockEntry) string { | 
					
						
							|  |  |  | 		return fmt.Sprintf("%s/%s", entry.Resource, entry.ID) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j]) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestTopLockEntries(t *testing.T) { | 
					
						
							|  |  |  | 	locksHeld := make(map[string][]lockRequesterInfo) | 
					
						
							|  |  |  | 	var owners []string | 
					
						
							|  |  |  | 	for i := 0; i < 4; i++ { | 
					
						
							|  |  |  | 		owners = append(owners, fmt.Sprintf("node-%d", i)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Simulate DeleteObjects of 10 objects in a single request. i.e same lock
 | 
					
						
							|  |  |  | 	// request UID, but 10 different resource names associated with it.
 | 
					
						
							|  |  |  | 	var lris []lockRequesterInfo | 
					
						
							|  |  |  | 	uuid := mustGetUUID() | 
					
						
							|  |  |  | 	for i := 0; i < 10; i++ { | 
					
						
							|  |  |  | 		resource := fmt.Sprintf("bucket/delete-object-%d", i) | 
					
						
							|  |  |  | 		lri := lockRequesterInfo{ | 
					
						
							|  |  |  | 			Name:   resource, | 
					
						
							|  |  |  | 			Writer: true, | 
					
						
							|  |  |  | 			UID:    uuid, | 
					
						
							|  |  |  | 			Owner:  owners[i%len(owners)], | 
					
						
							|  |  |  | 			Group:  true, | 
					
						
							|  |  |  | 			Quorum: 3, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		lris = append(lris, lri) | 
					
						
							|  |  |  | 		locksHeld[resource] = []lockRequesterInfo{lri} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Add a few concurrent read locks to the mix
 | 
					
						
							|  |  |  | 	for i := 0; i < 50; i++ { | 
					
						
							|  |  |  | 		resource := fmt.Sprintf("bucket/get-object-%d", i) | 
					
						
							|  |  |  | 		lri := lockRequesterInfo{ | 
					
						
							|  |  |  | 			Name:   resource, | 
					
						
							|  |  |  | 			UID:    mustGetUUID(), | 
					
						
							|  |  |  | 			Owner:  owners[i%len(owners)], | 
					
						
							|  |  |  | 			Quorum: 2, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		lris = append(lris, lri) | 
					
						
							|  |  |  | 		locksHeld[resource] = append(locksHeld[resource], lri) | 
					
						
							|  |  |  | 		// concurrent read lock, same resource different uid
 | 
					
						
							|  |  |  | 		lri.UID = mustGetUUID() | 
					
						
							|  |  |  | 		lris = append(lris, lri) | 
					
						
							|  |  |  | 		locksHeld[resource] = append(locksHeld[resource], lri) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var peerLocks []*PeerLocks | 
					
						
							|  |  |  | 	for _, owner := range owners { | 
					
						
							|  |  |  | 		peerLocks = append(peerLocks, &PeerLocks{ | 
					
						
							|  |  |  | 			Addr:  owner, | 
					
						
							|  |  |  | 			Locks: locksHeld, | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	var exp madmin.LockEntries | 
					
						
							|  |  |  | 	for _, lri := range lris { | 
					
						
							|  |  |  | 		lockType := func(lri lockRequesterInfo) string { | 
					
						
							|  |  |  | 			if lri.Writer { | 
					
						
							|  |  |  | 				return "WRITE" | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return "READ" | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		exp = append(exp, madmin.LockEntry{ | 
					
						
							|  |  |  | 			Resource:   lri.Name, | 
					
						
							|  |  |  | 			Type:       lockType(lri), | 
					
						
							|  |  |  | 			ServerList: owners, | 
					
						
							|  |  |  | 			Owner:      lri.Owner, | 
					
						
							|  |  |  | 			ID:         lri.UID, | 
					
						
							|  |  |  | 			Quorum:     lri.Quorum, | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	testCases := []struct { | 
					
						
							|  |  |  | 		peerLocks []*PeerLocks | 
					
						
							|  |  |  | 		expected  madmin.LockEntries | 
					
						
							|  |  |  | 	}{ | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			peerLocks: peerLocks, | 
					
						
							|  |  |  | 			expected:  exp, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// printEntries := func(entries madmin.LockEntries) {
 | 
					
						
							|  |  |  | 	// 	for i, entry := range entries {
 | 
					
						
							|  |  |  | 	// 		fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
 | 
					
						
							|  |  |  | 	// 	}
 | 
					
						
							|  |  |  | 	// }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	check := func(exp, got madmin.LockEntries) (int, bool) { | 
					
						
							|  |  |  | 		if len(exp) != len(got) { | 
					
						
							|  |  |  | 			return 0, false | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-04-25 04:28:18 +08:00
										 |  |  | 		sort.Slice(exp, byResourceUID{exp}.Less) | 
					
						
							|  |  |  | 		sort.Slice(got, byResourceUID{got}.Less) | 
					
						
							| 
									
										
										
										
											2023-03-22 09:35:29 +08:00
										 |  |  | 		// printEntries(exp)
 | 
					
						
							|  |  |  | 		// printEntries(got)
 | 
					
						
							|  |  |  | 		for i, e := range exp { | 
					
						
							|  |  |  | 			if !e.Timestamp.Equal(got[i].Timestamp) { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			// Skip checking elapsed since it's time sensitive.
 | 
					
						
							|  |  |  | 			// if e.Elapsed != got[i].Elapsed {
 | 
					
						
							|  |  |  | 			// 	return false
 | 
					
						
							|  |  |  | 			// }
 | 
					
						
							|  |  |  | 			if e.Resource != got[i].Resource { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if e.Type != got[i].Type { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if e.Source != got[i].Source { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if e.Owner != got[i].Owner { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if e.ID != got[i].ID { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if len(e.ServerList) != len(got[i].ServerList) { | 
					
						
							|  |  |  | 				return i, false | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			for j := range e.ServerList { | 
					
						
							|  |  |  | 				if e.ServerList[j] != got[i].ServerList[j] { | 
					
						
							|  |  |  | 					return i, false | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return 0, true | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for i, tc := range testCases { | 
					
						
							|  |  |  | 		got := topLockEntries(tc.peerLocks, false) | 
					
						
							|  |  |  | 		if idx, ok := check(tc.expected, got); !ok { | 
					
						
							|  |  |  | 			t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx]) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |