github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/endpoint_test.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "fmt" 22 "net" 23 "net/url" 24 "path/filepath" 25 "reflect" 26 "strings" 27 "testing" 28 ) 29 30 func TestNewEndpoint(t *testing.T) { 31 u2, _ := url.Parse("https://example.org/path") 32 u4, _ := url.Parse("http://192.168.253.200/path") 33 rootSlashFoo, _ := filepath.Abs("/foo") 34 testCases := []struct { 35 arg string 36 expectedEndpoint Endpoint 37 expectedType EndpointType 38 expectedErr error 39 }{ 40 {"/foo", Endpoint{&url.URL{Path: rootSlashFoo}, true, -1, -1, -1}, PathEndpointType, nil}, 41 {"https://example.org/path", Endpoint{u2, false, -1, -1, -1}, URLEndpointType, nil}, 42 {"http://192.168.253.200/path", Endpoint{u4, false, -1, -1, -1}, URLEndpointType, nil}, 43 {"", Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")}, 44 {SlashSeparator, Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")}, 45 {`\`, Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")}, 46 {"c://foo", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format")}, 47 {"ftp://foo", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format")}, 48 {"http://server/path?location", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format")}, 49 {"http://:/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: invalid port number")}, 50 {"http://:8080/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: empty host name")}, 51 {"http://server:/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: invalid port number")}, 52 {"https://93.184.216.34:808080/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")}, 53 {"http://server:8080//", Endpoint{}, -1, fmt.Errorf("empty or root path is not supported in URL endpoint")}, 54 {"http://server:8080/", Endpoint{}, -1, fmt.Errorf("empty or root path is not supported in URL endpoint")}, 55 {"192.168.1.210:9000", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")}, 56 } 57 58 for i, test := range testCases { 59 t.Run(fmt.Sprint("case-", i), func(t *testing.T) { 60 endpoint, err := NewEndpoint(test.arg) 61 if err == nil { 62 err = endpoint.UpdateIsLocal() 63 } 64 65 switch { 66 case test.expectedErr == nil: 67 if err != nil { 68 t.Errorf("error: expected = <nil>, got = %v", err) 69 } 70 case err == nil: 71 t.Errorf("error: expected = %v, got = <nil>", test.expectedErr) 72 case test.expectedErr.Error() != err.Error(): 73 t.Errorf("error: expected = %v, got = %v", test.expectedErr, err) 74 } 75 76 if err == nil { 77 if (test.expectedEndpoint.URL == nil) != (endpoint.URL == nil) { 78 t.Errorf("endpoint url: expected = %#v, got = %#v", test.expectedEndpoint.URL, endpoint.URL) 79 return 80 } else if test.expectedEndpoint.URL.String() != endpoint.URL.String() { 81 t.Errorf("endpoint url: expected = %#v, got = %#v", test.expectedEndpoint.URL.String(), endpoint.URL.String()) 82 return 83 } 84 if !reflect.DeepEqual(test.expectedEndpoint, endpoint) { 85 t.Errorf("endpoint: expected = %#v, got = %#v", test.expectedEndpoint, endpoint) 86 } 87 } 88 89 if err == nil && test.expectedType != endpoint.Type() { 90 t.Errorf("type: expected = %+v, got = %+v", test.expectedType, endpoint.Type()) 91 } 92 }) 93 } 94 } 95 96 func TestNewEndpoints(t *testing.T) { 97 testCases := []struct { 98 args []string 99 expectedErr error 100 }{ 101 {[]string{"/d1", "/d2", "/d3", "/d4"}, nil}, 102 {[]string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d3", "http://localhost/d4"}, nil}, 103 {[]string{"http://example.org/d1", "http://example.com/d1", "http://example.net/d1", "http://example.edu/d1"}, nil}, 104 {[]string{"http://localhost/d1", "http://localhost/d2", "http://example.org/d1", "http://example.org/d2"}, nil}, 105 {[]string{"https://localhost:9000/d1", "https://localhost:9001/d2", "https://localhost:9002/d3", "https://localhost:9003/d4"}, nil}, 106 // // It is valid WRT endpoint list that same path is expected with different port on same server. 107 {[]string{"https://127.0.0.1:9000/d1", "https://127.0.0.1:9001/d1", "https://127.0.0.1:9002/d1", "https://127.0.0.1:9003/d1"}, nil}, 108 {[]string{"d1", "d2", "d3", "d1"}, fmt.Errorf("duplicate endpoints found")}, 109 {[]string{"d1", "d2", "d3", "./d1"}, fmt.Errorf("duplicate endpoints found")}, 110 {[]string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d1", "http://localhost/d4"}, fmt.Errorf("duplicate endpoints found")}, 111 {[]string{"ftp://server/d1", "http://server/d2", "http://server/d3", "http://server/d4"}, fmt.Errorf("'ftp://server/d1': invalid URL endpoint format")}, 112 {[]string{"d1", "http://localhost/d2", "d3", "d4"}, fmt.Errorf("mixed style endpoints are not supported")}, 113 {[]string{"http://example.org/d1", "https://example.com/d1", "http://example.net/d1", "https://example.edut/d1"}, fmt.Errorf("mixed scheme is not supported")}, 114 {[]string{"192.168.1.210:9000/tmp/dir0", "192.168.1.210:9000/tmp/dir1", "192.168.1.210:9000/tmp/dir2", "192.168.110:9000/tmp/dir3"}, fmt.Errorf("'192.168.1.210:9000/tmp/dir0': invalid URL endpoint format: missing scheme http or https")}, 115 } 116 117 for _, testCase := range testCases { 118 _, err := NewEndpoints(testCase.args...) 119 switch { 120 case testCase.expectedErr == nil: 121 if err != nil { 122 t.Fatalf("error: expected = <nil>, got = %v", err) 123 } 124 case err == nil: 125 t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr) 126 case testCase.expectedErr.Error() != err.Error(): 127 t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err) 128 } 129 } 130 } 131 132 func TestCreateEndpoints(t *testing.T) { 133 tempGlobalMinioPort := globalMinioPort 134 defer func() { 135 globalMinioPort = tempGlobalMinioPort 136 }() 137 globalMinioPort = "9000" 138 139 // Filter ipList by IPs those do not start with '127.'. 140 nonLoopBackIPs := localIP4.FuncMatch(func(ip string, matchString string) bool { 141 return !net.ParseIP(ip).IsLoopback() 142 }, "") 143 if len(nonLoopBackIPs) == 0 { 144 t.Fatalf("No non-loop back IP address found for this host") 145 } 146 nonLoopBackIP := nonLoopBackIPs.ToSlice()[0] 147 148 mustAbs := func(s string) string { 149 s, err := filepath.Abs(s) 150 if err != nil { 151 t.Fatal(err) 152 } 153 return s 154 } 155 getExpectedEndpoints := func(args []string, prefix string) ([]*url.URL, []bool) { 156 var URLs []*url.URL 157 var localFlags []bool 158 for _, arg := range args { 159 u, _ := url.Parse(arg) 160 URLs = append(URLs, u) 161 localFlags = append(localFlags, strings.HasPrefix(arg, prefix)) 162 } 163 164 return URLs, localFlags 165 } 166 167 case1Endpoint1 := "http://" + nonLoopBackIP + "/d1" 168 case1Endpoint2 := "http://" + nonLoopBackIP + "/d2" 169 args := []string{ 170 "http://" + nonLoopBackIP + ":10000/d1", 171 "http://" + nonLoopBackIP + ":10000/d2", 172 "http://example.org:10000/d3", 173 "http://example.com:10000/d4", 174 } 175 case1URLs, case1LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":10000/") 176 177 case2Endpoint1 := "http://" + nonLoopBackIP + "/d1" 178 case2Endpoint2 := "http://" + nonLoopBackIP + ":9000/d2" 179 args = []string{ 180 "http://" + nonLoopBackIP + ":10000/d1", 181 "http://" + nonLoopBackIP + ":9000/d2", 182 "http://example.org:10000/d3", 183 "http://example.com:10000/d4", 184 } 185 case2URLs, case2LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":10000/") 186 187 case3Endpoint1 := "http://" + nonLoopBackIP + "/d1" 188 args = []string{ 189 "http://" + nonLoopBackIP + ":80/d1", 190 "http://example.org:9000/d2", 191 "http://example.com:80/d3", 192 "http://example.net:80/d4", 193 } 194 case3URLs, case3LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":80/") 195 196 case4Endpoint1 := "http://" + nonLoopBackIP + "/d1" 197 args = []string{ 198 "http://" + nonLoopBackIP + ":9000/d1", 199 "http://example.org:9000/d2", 200 "http://example.com:9000/d3", 201 "http://example.net:9000/d4", 202 } 203 case4URLs, case4LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":9000/") 204 205 case5Endpoint1 := "http://" + nonLoopBackIP + ":9000/d1" 206 case5Endpoint2 := "http://" + nonLoopBackIP + ":9001/d2" 207 case5Endpoint3 := "http://" + nonLoopBackIP + ":9002/d3" 208 case5Endpoint4 := "http://" + nonLoopBackIP + ":9003/d4" 209 args = []string{ 210 case5Endpoint1, 211 case5Endpoint2, 212 case5Endpoint3, 213 case5Endpoint4, 214 } 215 case5URLs, case5LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":9000/") 216 217 case6Endpoint := "http://" + nonLoopBackIP + ":9003/d4" 218 args = []string{ 219 "http://localhost:9000/d1", 220 "http://localhost:9001/d2", 221 "http://127.0.0.1:9002/d3", 222 case6Endpoint, 223 } 224 case6URLs, case6LocalFlags := getExpectedEndpoints(args, "http://"+nonLoopBackIP+":9003/") 225 226 testCases := []struct { 227 serverAddr string 228 args []string 229 expectedServerAddr string 230 expectedEndpoints Endpoints 231 expectedSetupType SetupType 232 expectedErr error 233 }{ 234 {"localhost", []string{}, "", Endpoints{}, -1, fmt.Errorf("address localhost: missing port in address")}, 235 236 // Erasure Single Drive 237 {"localhost:9000", []string{"http://localhost/d1"}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for SD setup")}, 238 {":443", []string{"/d1"}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil}, 239 {"localhost:10000", []string{"/d1"}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil}, 240 {"localhost:9000", []string{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")}, 241 242 // Erasure Setup with PathEndpointType 243 { 244 ":1234", 245 []string{"/d1", "/d2", "/d3", "/d4"}, 246 ":1234", 247 Endpoints{ 248 Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}, 249 Endpoint{URL: &url.URL{Path: mustAbs("/d2")}, IsLocal: true}, 250 Endpoint{URL: &url.URL{Path: mustAbs("/d3")}, IsLocal: true}, 251 Endpoint{URL: &url.URL{Path: mustAbs("/d4")}, IsLocal: true}, 252 }, 253 ErasureSetupType, nil, 254 }, 255 // DistErasure Setup with URLEndpointType 256 {":9000", []string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d3", "http://localhost/d4"}, ":9000", Endpoints{ 257 Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d1"}, IsLocal: true}, 258 Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d2"}, IsLocal: true}, 259 Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d3"}, IsLocal: true}, 260 Endpoint{URL: &url.URL{Scheme: "http", Host: "localhost:9000", Path: "/d4"}, IsLocal: true}, 261 }, ErasureSetupType, nil}, 262 // DistErasure Setup with URLEndpointType having mixed naming to local host. 263 {"127.0.0.1:10000", []string{"http://localhost/d1", "http://localhost/d2", "http://127.0.0.1/d3", "http://127.0.0.1/d4"}, "", Endpoints{}, -1, fmt.Errorf("all local endpoints should not have different hostnames/ips")}, 264 265 {":9001", []string{"http://10.0.0.1:9000/export", "http://10.0.0.2:9000/export", "http://" + nonLoopBackIP + ":9001/export", "http://10.0.0.2:9001/export"}, "", Endpoints{}, -1, fmt.Errorf("path '/export' can not be served by different port on same address")}, 266 267 {":9000", []string{"http://127.0.0.1:9000/export", "http://" + nonLoopBackIP + ":9000/export", "http://10.0.0.1:9000/export", "http://10.0.0.2:9000/export"}, "", Endpoints{}, -1, fmt.Errorf("path '/export' cannot be served by different address on same server")}, 268 269 // DistErasure type 270 {"127.0.0.1:10000", []string{case1Endpoint1, case1Endpoint2, "http://example.org/d3", "http://example.com/d4"}, "127.0.0.1:10000", Endpoints{ 271 Endpoint{URL: case1URLs[0], IsLocal: case1LocalFlags[0]}, 272 Endpoint{URL: case1URLs[1], IsLocal: case1LocalFlags[1]}, 273 Endpoint{URL: case1URLs[2], IsLocal: case1LocalFlags[2]}, 274 Endpoint{URL: case1URLs[3], IsLocal: case1LocalFlags[3]}, 275 }, DistErasureSetupType, nil}, 276 277 {"127.0.0.1:10000", []string{case2Endpoint1, case2Endpoint2, "http://example.org/d3", "http://example.com/d4"}, "127.0.0.1:10000", Endpoints{ 278 Endpoint{URL: case2URLs[0], IsLocal: case2LocalFlags[0]}, 279 Endpoint{URL: case2URLs[1], IsLocal: case2LocalFlags[1]}, 280 Endpoint{URL: case2URLs[2], IsLocal: case2LocalFlags[2]}, 281 Endpoint{URL: case2URLs[3], IsLocal: case2LocalFlags[3]}, 282 }, DistErasureSetupType, nil}, 283 284 {":80", []string{case3Endpoint1, "http://example.org:9000/d2", "http://example.com/d3", "http://example.net/d4"}, ":80", Endpoints{ 285 Endpoint{URL: case3URLs[0], IsLocal: case3LocalFlags[0]}, 286 Endpoint{URL: case3URLs[1], IsLocal: case3LocalFlags[1]}, 287 Endpoint{URL: case3URLs[2], IsLocal: case3LocalFlags[2]}, 288 Endpoint{URL: case3URLs[3], IsLocal: case3LocalFlags[3]}, 289 }, DistErasureSetupType, nil}, 290 291 {":9000", []string{case4Endpoint1, "http://example.org/d2", "http://example.com/d3", "http://example.net/d4"}, ":9000", Endpoints{ 292 Endpoint{URL: case4URLs[0], IsLocal: case4LocalFlags[0]}, 293 Endpoint{URL: case4URLs[1], IsLocal: case4LocalFlags[1]}, 294 Endpoint{URL: case4URLs[2], IsLocal: case4LocalFlags[2]}, 295 Endpoint{URL: case4URLs[3], IsLocal: case4LocalFlags[3]}, 296 }, DistErasureSetupType, nil}, 297 298 {":9000", []string{case5Endpoint1, case5Endpoint2, case5Endpoint3, case5Endpoint4}, ":9000", Endpoints{ 299 Endpoint{URL: case5URLs[0], IsLocal: case5LocalFlags[0]}, 300 Endpoint{URL: case5URLs[1], IsLocal: case5LocalFlags[1]}, 301 Endpoint{URL: case5URLs[2], IsLocal: case5LocalFlags[2]}, 302 Endpoint{URL: case5URLs[3], IsLocal: case5LocalFlags[3]}, 303 }, DistErasureSetupType, nil}, 304 305 // DistErasure Setup using only local host. 306 {":9003", []string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://127.0.0.1:9002/d3", case6Endpoint}, ":9003", Endpoints{ 307 Endpoint{URL: case6URLs[0], IsLocal: case6LocalFlags[0]}, 308 Endpoint{URL: case6URLs[1], IsLocal: case6LocalFlags[1]}, 309 Endpoint{URL: case6URLs[2], IsLocal: case6LocalFlags[2]}, 310 Endpoint{URL: case6URLs[3], IsLocal: case6LocalFlags[3]}, 311 }, DistErasureSetupType, nil}, 312 } 313 314 for i, testCase := range testCases { 315 i := i 316 testCase := testCase 317 t.Run("", func(t *testing.T) { 318 var srvCtxt serverCtxt 319 err := mergeDisksLayoutFromArgs(testCase.args, &srvCtxt) 320 if err != nil && testCase.expectedErr == nil { 321 t.Errorf("Test %d: unexpected error: %v", i+1, err) 322 } 323 pools, setupType, err := CreatePoolEndpoints(testCase.serverAddr, srvCtxt.Layout.pools...) 324 if err == nil && testCase.expectedErr != nil { 325 t.Errorf("Test %d: expected = %v, got = <nil>", i+1, testCase.expectedErr) 326 } 327 if err == nil { 328 if setupType != testCase.expectedSetupType { 329 t.Errorf("Test %d: setupType: expected = %v, got = %v", i+1, testCase.expectedSetupType, setupType) 330 } 331 endpoints := pools[0] 332 if len(endpoints) != len(testCase.expectedEndpoints) { 333 t.Errorf("Test %d: endpoints: expected = %d, got = %d", i+1, len(testCase.expectedEndpoints), 334 len(endpoints)) 335 } else { 336 for i, endpoint := range endpoints { 337 if testCase.expectedEndpoints[i].String() != endpoint.String() { 338 t.Errorf("Test %d: endpoints: expected = %s, got = %s", 339 i+1, 340 testCase.expectedEndpoints[i], 341 endpoint) 342 } 343 } 344 } 345 } 346 if err != nil && testCase.expectedErr == nil { 347 t.Errorf("Test %d: error: expected = <nil>, got = %v, testCase: %v", i+1, err, testCase) 348 } 349 }) 350 } 351 } 352 353 // Tests get local peer functionality, local peer is supposed to only return one entry per minio service. 354 // So it means that if you have say localhost:9000 and localhost:9001 as endpointArgs then localhost:9001 355 // is considered a remote service from localhost:9000 perspective. 356 func TestGetLocalPeer(t *testing.T) { 357 tempGlobalMinioPort := globalMinioPort 358 defer func() { 359 globalMinioPort = tempGlobalMinioPort 360 }() 361 globalMinioPort = "9000" 362 363 testCases := []struct { 364 endpointArgs []string 365 expectedResult string 366 }{ 367 {[]string{"/d1", "/d2", "d3", "d4"}, "127.0.0.1:9000"}, 368 { 369 []string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, 370 "localhost:9000", 371 }, 372 { 373 []string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"}, 374 "localhost:9000", 375 }, 376 { 377 []string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"}, 378 "localhost:9000", 379 }, 380 } 381 382 for i, testCase := range testCases { 383 zendpoints := mustGetPoolEndpoints(0, testCase.endpointArgs...) 384 if !zendpoints[0].Endpoints[0].IsLocal { 385 if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil { 386 t.Fatalf("error: expected = <nil>, got = %v", err) 387 } 388 } 389 localPeer := GetLocalPeer(zendpoints, "", "9000") 390 if localPeer != testCase.expectedResult { 391 t.Fatalf("Test %d: expected: %v, got: %v", i+1, testCase.expectedResult, localPeer) 392 } 393 } 394 } 395 396 func TestGetRemotePeers(t *testing.T) { 397 tempGlobalMinioPort := globalMinioPort 398 defer func() { 399 globalMinioPort = tempGlobalMinioPort 400 }() 401 globalMinioPort = "9000" 402 403 testCases := []struct { 404 endpointArgs []string 405 expectedResult []string 406 expectedLocal string 407 }{ 408 {[]string{"/d1", "/d2", "d3", "d4"}, []string{}, ""}, 409 {[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:9000"}, "localhost:9000"}, 410 {[]string{"http://localhost:9000/d1", "http://localhost:10000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"}, []string{"example.com:9000", "example.org:9000", "localhost:10000", "localhost:9000"}, "localhost:9000"}, 411 {[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"}, []string{"example.com:9000", "example.net:9000", "example.org:9000", "localhost:9000"}, "localhost:9000"}, 412 {[]string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"}, []string{"localhost:9000", "localhost:9001", "localhost:9002", "localhost:9003"}, "localhost:9000"}, 413 } 414 415 for _, testCase := range testCases { 416 zendpoints := mustGetPoolEndpoints(0, testCase.endpointArgs...) 417 if !zendpoints[0].Endpoints[0].IsLocal { 418 if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil { 419 t.Errorf("error: expected = <nil>, got = %v", err) 420 } 421 } 422 remotePeers, local := zendpoints.peers() 423 if !reflect.DeepEqual(remotePeers, testCase.expectedResult) { 424 t.Errorf("expected: %v, got: %v", testCase.expectedResult, remotePeers) 425 } 426 if local != testCase.expectedLocal { 427 t.Errorf("expected: %v, got: %v", testCase.expectedLocal, local) 428 } 429 } 430 }