github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/api/client_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api_test 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net" 13 "net/http" 14 "net/url" 15 "path" 16 "strings" 17 18 "github.com/juju/errors" 19 "github.com/juju/httprequest" 20 "github.com/juju/loggo" 21 "github.com/juju/names" 22 jc "github.com/juju/testing/checkers" 23 "github.com/juju/version" 24 "golang.org/x/net/websocket" 25 gc "gopkg.in/check.v1" 26 "gopkg.in/juju/charm.v6-unstable" 27 28 "github.com/juju/juju/api" 29 "github.com/juju/juju/api/base" 30 "github.com/juju/juju/apiserver/params" 31 jujunames "github.com/juju/juju/juju/names" 32 jujutesting "github.com/juju/juju/juju/testing" 33 "github.com/juju/juju/rpc" 34 "github.com/juju/juju/state" 35 "github.com/juju/juju/testcharms" 36 coretesting "github.com/juju/juju/testing" 37 "github.com/juju/juju/testing/factory" 38 ) 39 40 type clientSuite struct { 41 jujutesting.JujuConnSuite 42 } 43 44 var _ = gc.Suite(&clientSuite{}) 45 46 // TODO(jam) 2013-08-27 http://pad.lv/1217282 47 // Right now most of the direct tests for api.Client behavior are in 48 // apiserver/client/*_test.go 49 50 func (s *clientSuite) TestCloseMultipleOk(c *gc.C) { 51 client := s.APIState.Client() 52 c.Assert(client.Close(), gc.IsNil) 53 c.Assert(client.Close(), gc.IsNil) 54 c.Assert(client.Close(), gc.IsNil) 55 } 56 57 func (s *clientSuite) TestUploadToolsOtherEnvironment(c *gc.C) { 58 otherSt, otherAPISt := s.otherEnviron(c) 59 defer otherSt.Close() 60 defer otherAPISt.Close() 61 client := otherAPISt.Client() 62 newVersion := version.MustParseBinary("5.4.3-quantal-amd64") 63 var called bool 64 65 // build fake tools 66 expectedTools, _ := coretesting.TarGz( 67 coretesting.NewTarFile(jujunames.Jujud, 0777, "jujud contents "+newVersion.String())) 68 69 // UploadTools does not use the facades, so instead of patching the 70 // facade call, we set up a fake endpoint to test. 71 defer fakeAPIEndpoint(c, client, envEndpoint(c, otherAPISt, "tools"), "POST", 72 func(w http.ResponseWriter, r *http.Request) { 73 called = true 74 75 c.Assert(r.URL.Query(), gc.DeepEquals, url.Values{ 76 "binaryVersion": []string{"5.4.3-quantal-amd64"}, 77 "series": []string{""}, 78 }) 79 defer r.Body.Close() 80 obtainedTools, err := ioutil.ReadAll(r.Body) 81 c.Assert(err, jc.ErrorIsNil) 82 c.Assert(obtainedTools, gc.DeepEquals, expectedTools) 83 }, 84 ).Close() 85 86 // We don't test the error or tools results as we only wish to assert that 87 // the API client POSTs the tools archive to the correct endpoint. 88 client.UploadTools(bytes.NewReader(expectedTools), newVersion) 89 c.Assert(called, jc.IsTrue) 90 } 91 92 func (s *clientSuite) TestAddLocalCharm(c *gc.C) { 93 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 94 curl := charm.MustParseURL( 95 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 96 ) 97 client := s.APIState.Client() 98 99 // Test the sanity checks first. 100 _, err := client.AddLocalCharm(charm.MustParseURL("cs:quantal/wordpress-1"), nil) 101 c.Assert(err, gc.ErrorMatches, `expected charm URL with local: schema, got "cs:quantal/wordpress-1"`) 102 103 // Upload an archive with its original revision. 104 savedURL, err := client.AddLocalCharm(curl, charmArchive) 105 c.Assert(err, jc.ErrorIsNil) 106 c.Assert(savedURL.String(), gc.Equals, curl.String()) 107 108 // Upload a charm directory with changed revision. 109 charmDir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy") 110 charmDir.SetDiskRevision(42) 111 savedURL, err = client.AddLocalCharm(curl, charmDir) 112 c.Assert(err, jc.ErrorIsNil) 113 c.Assert(savedURL.Revision, gc.Equals, 42) 114 115 // Upload a charm directory again, revision should be bumped. 116 savedURL, err = client.AddLocalCharm(curl, charmDir) 117 c.Assert(err, jc.ErrorIsNil) 118 c.Assert(savedURL.String(), gc.Equals, curl.WithRevision(43).String()) 119 } 120 121 func (s *clientSuite) TestAddLocalCharmOtherEnvironment(c *gc.C) { 122 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 123 curl := charm.MustParseURL( 124 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 125 ) 126 127 otherSt, otherAPISt := s.otherEnviron(c) 128 defer otherSt.Close() 129 defer otherAPISt.Close() 130 client := otherAPISt.Client() 131 132 // Upload an archive 133 savedURL, err := client.AddLocalCharm(curl, charmArchive) 134 c.Assert(err, jc.ErrorIsNil) 135 c.Assert(savedURL.String(), gc.Equals, curl.String()) 136 137 charm, err := otherSt.Charm(curl) 138 c.Assert(err, jc.ErrorIsNil) 139 c.Assert(charm.String(), gc.Equals, curl.String()) 140 } 141 142 func (s *clientSuite) otherEnviron(c *gc.C) (*state.State, api.Connection) { 143 otherSt := s.Factory.MakeModel(c, nil) 144 info := s.APIInfo(c) 145 info.ModelTag = otherSt.ModelTag() 146 apiState, err := api.Open(info, api.DefaultDialOpts()) 147 c.Assert(err, jc.ErrorIsNil) 148 return otherSt, apiState 149 } 150 151 func (s *clientSuite) TestAddLocalCharmError(c *gc.C) { 152 client := s.APIState.Client() 153 154 // AddLocalCharm does not use the facades, so instead of patching the 155 // facade call, we set up a fake endpoint to test. 156 defer fakeAPIEndpoint(c, client, envEndpoint(c, s.APIState, "charms"), "POST", 157 func(w http.ResponseWriter, r *http.Request) { 158 httprequest.WriteJSON(w, http.StatusMethodNotAllowed, ¶ms.CharmsResponse{ 159 Error: "the POST method is not allowed", 160 ErrorCode: params.CodeMethodNotAllowed, 161 }) 162 }, 163 ).Close() 164 165 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 166 curl := charm.MustParseURL( 167 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 168 ) 169 170 _, err := client.AddLocalCharm(curl, charmArchive) 171 c.Assert(err, gc.ErrorMatches, `POST http://.*/model/deadbeef-0bad-400d-8000-4b1d0d06f00d/charms\?series=quantal: the POST method is not allowed`) 172 } 173 174 func (s *clientSuite) TestMinVersionLocalCharm(c *gc.C) { 175 tests := []minverTest{ 176 {"2.0.0", "1.0.0", true}, 177 {"1.0.0", "2.0.0", false}, 178 {"1.25.0", "1.24.0", true}, 179 {"1.24.0", "1.25.0", false}, 180 {"1.25.1", "1.25.0", true}, 181 {"1.25.0", "1.25.1", false}, 182 {"1.25.0", "1.25.0", true}, 183 {"1.25.0", "1.25-alpha1", true}, 184 {"1.25-alpha1", "1.25.0", false}, 185 } 186 client := s.APIState.Client() 187 for _, t := range tests { 188 testMinVer(client, t, c) 189 } 190 } 191 192 type minverTest struct { 193 juju string 194 charm string 195 ok bool 196 } 197 198 func testMinVer(client *api.Client, t minverTest, c *gc.C) { 199 charmMinVer := version.MustParse(t.charm) 200 jujuVer := version.MustParse(t.juju) 201 202 cleanup := api.PatchClientFacadeCall(client, 203 func(request string, paramsIn interface{}, response interface{}) error { 204 c.Assert(paramsIn, gc.IsNil) 205 if response, ok := response.(*params.AgentVersionResult); ok { 206 response.Version = jujuVer 207 } else { 208 c.Log("wrong output structure") 209 c.Fail() 210 } 211 return nil 212 }, 213 ) 214 defer cleanup() 215 216 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 217 curl := charm.MustParseURL( 218 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 219 ) 220 charmArchive.Meta().MinJujuVersion = charmMinVer 221 222 _, err := client.AddLocalCharm(curl, charmArchive) 223 224 if t.ok { 225 if err != nil { 226 c.Errorf("Unexpected non-nil error for jujuver %v, minver %v: %#v", t.juju, t.charm, err) 227 } 228 } else { 229 if err == nil { 230 c.Errorf("Unexpected nil error for jujuver %v, minver %v", t.juju, t.charm) 231 } else if !api.IsMinVersionError(err) { 232 c.Errorf("Wrong error for jujuver %v, minver %v: expected minVersionError, got: %#v", t.juju, t.charm, err) 233 } 234 } 235 } 236 237 func (s *clientSuite) TestOpenCharmFound(c *gc.C) { 238 client := s.APIState.Client() 239 curl, ch := addLocalCharm(c, client, "dummy") 240 expected, err := ioutil.ReadFile(ch.Path) 241 c.Assert(err, jc.ErrorIsNil) 242 243 reader, err := client.OpenCharm(curl) 244 defer reader.Close() 245 c.Assert(err, jc.ErrorIsNil) 246 247 data, err := ioutil.ReadAll(reader) 248 c.Assert(err, jc.ErrorIsNil) 249 c.Check(data, jc.DeepEquals, expected) 250 } 251 252 func (s *clientSuite) TestOpenCharmMissing(c *gc.C) { 253 curl := charm.MustParseURL("cs:quantal/spam-3") 254 client := s.APIState.Client() 255 256 _, err := client.OpenCharm(curl) 257 258 c.Check(err, gc.ErrorMatches, `.*unable to retrieve and save the charm: cannot get charm from state: charm "cs:quantal/spam-3" not found`) 259 } 260 261 func addLocalCharm(c *gc.C, client *api.Client, name string) (*charm.URL, *charm.CharmArchive) { 262 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), name) 263 curl := charm.MustParseURL(fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision())) 264 _, err := client.AddLocalCharm(curl, charmArchive) 265 c.Assert(err, jc.ErrorIsNil) 266 return curl, charmArchive 267 } 268 269 func fakeAPIEndpoint(c *gc.C, client *api.Client, address, method string, handle func(http.ResponseWriter, *http.Request)) net.Listener { 270 lis, err := net.Listen("tcp", "127.0.0.1:0") 271 c.Assert(err, jc.ErrorIsNil) 272 273 mux := http.NewServeMux() 274 mux.HandleFunc(address, func(w http.ResponseWriter, r *http.Request) { 275 if r.Method == method { 276 handle(w, r) 277 } 278 }) 279 go func() { 280 http.Serve(lis, mux) 281 }() 282 api.SetServerAddress(client, "http", lis.Addr().String()) 283 return lis 284 } 285 286 // envEndpoint returns "/model/<model-uuid>/<destination>" 287 func envEndpoint(c *gc.C, apiState api.Connection, destination string) string { 288 modelTag, err := apiState.ModelTag() 289 c.Assert(err, jc.ErrorIsNil) 290 return path.Join("/model", modelTag.Id(), destination) 291 } 292 293 func (s *clientSuite) TestClientEnvironmentUUID(c *gc.C) { 294 environ, err := s.State.Model() 295 c.Assert(err, jc.ErrorIsNil) 296 297 client := s.APIState.Client() 298 c.Assert(client.ModelUUID(), gc.Equals, environ.Tag().Id()) 299 } 300 301 func (s *clientSuite) TestClientEnvironmentUsers(c *gc.C) { 302 client := s.APIState.Client() 303 cleanup := api.PatchClientFacadeCall(client, 304 func(request string, paramsIn interface{}, response interface{}) error { 305 c.Assert(paramsIn, gc.IsNil) 306 if response, ok := response.(*params.ModelUserInfoResults); ok { 307 response.Results = []params.ModelUserInfoResult{ 308 {Result: ¶ms.ModelUserInfo{UserName: "one"}}, 309 {Result: ¶ms.ModelUserInfo{UserName: "two"}}, 310 {Result: ¶ms.ModelUserInfo{UserName: "three"}}, 311 } 312 } else { 313 c.Log("wrong output structure") 314 c.Fail() 315 } 316 return nil 317 }, 318 ) 319 defer cleanup() 320 321 obtained, err := client.ModelUserInfo() 322 c.Assert(err, jc.ErrorIsNil) 323 324 c.Assert(obtained, jc.DeepEquals, []params.ModelUserInfo{ 325 {UserName: "one"}, 326 {UserName: "two"}, 327 {UserName: "three"}, 328 }) 329 } 330 331 func (s *clientSuite) TestDestroyEnvironment(c *gc.C) { 332 client := s.APIState.Client() 333 var called bool 334 cleanup := api.PatchClientFacadeCall(client, 335 func(req string, args interface{}, resp interface{}) error { 336 c.Assert(req, gc.Equals, "DestroyModel") 337 called = true 338 return nil 339 }) 340 defer cleanup() 341 342 err := client.DestroyModel() 343 c.Assert(err, jc.ErrorIsNil) 344 c.Assert(called, jc.IsTrue) 345 } 346 347 func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) { 348 client := s.APIState.Client() 349 // Use the no tail option so we don't try to start a tailing cursor 350 // on the oplog when there is no oplog configured in mongo as the tests 351 // don't set up mongo in replicaset mode. 352 reader, err := client.WatchDebugLog(api.DebugLogParams{NoTail: true}) 353 c.Assert(err, jc.ErrorIsNil) 354 c.Assert(reader, gc.NotNil) 355 reader.Close() 356 } 357 358 func (s *clientSuite) TestConnectStreamRequiresSlashPathPrefix(c *gc.C) { 359 reader, err := s.APIState.ConnectStream("foo", nil) 360 c.Assert(err, gc.ErrorMatches, `path must start with "/"`) 361 c.Assert(reader, gc.Equals, nil) 362 } 363 364 func (s *clientSuite) TestConnectStreamErrorBadConnection(c *gc.C) { 365 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 366 return nil, fmt.Errorf("bad connection") 367 }) 368 reader, err := s.APIState.ConnectStream("/", nil) 369 c.Assert(err, gc.ErrorMatches, "bad connection") 370 c.Assert(reader, gc.IsNil) 371 } 372 373 func (s *clientSuite) TestConnectStreamErrorNoData(c *gc.C) { 374 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 375 return fakeStreamReader{&bytes.Buffer{}}, nil 376 }) 377 reader, err := s.APIState.ConnectStream("/", nil) 378 c.Assert(err, gc.ErrorMatches, "unable to read initial response: EOF") 379 c.Assert(reader, gc.IsNil) 380 } 381 382 func (s *clientSuite) TestConnectStreamErrorBadData(c *gc.C) { 383 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 384 return fakeStreamReader{strings.NewReader("junk\n")}, nil 385 }) 386 reader, err := s.APIState.ConnectStream("/", nil) 387 c.Assert(err, gc.ErrorMatches, "unable to unmarshal initial response: .*") 388 c.Assert(reader, gc.IsNil) 389 } 390 391 func (s *clientSuite) TestConnectStreamErrorReadError(c *gc.C) { 392 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 393 err := fmt.Errorf("bad read") 394 return fakeStreamReader{&badReader{err}}, nil 395 }) 396 reader, err := s.APIState.ConnectStream("/", nil) 397 c.Assert(err, gc.ErrorMatches, "unable to read initial response: bad read") 398 c.Assert(reader, gc.IsNil) 399 } 400 401 func (s *clientSuite) TestWatchDebugLogParamsEncoded(c *gc.C) { 402 s.PatchValue(api.WebsocketDialConfig, echoURL(c)) 403 404 params := api.DebugLogParams{ 405 IncludeEntity: []string{"a", "b"}, 406 IncludeModule: []string{"c", "d"}, 407 ExcludeEntity: []string{"e", "f"}, 408 ExcludeModule: []string{"g", "h"}, 409 Limit: 100, 410 Backlog: 200, 411 Level: loggo.ERROR, 412 Replay: true, 413 NoTail: true, 414 } 415 416 client := s.APIState.Client() 417 reader, err := client.WatchDebugLog(params) 418 c.Assert(err, jc.ErrorIsNil) 419 420 connectURL := connectURLFromReader(c, reader) 421 values := connectURL.Query() 422 c.Assert(values, jc.DeepEquals, url.Values{ 423 "includeEntity": params.IncludeEntity, 424 "includeModule": params.IncludeModule, 425 "excludeEntity": params.ExcludeEntity, 426 "excludeModule": params.ExcludeModule, 427 "maxLines": {"100"}, 428 "backlog": {"200"}, 429 "level": {"ERROR"}, 430 "replay": {"true"}, 431 "noTail": {"true"}, 432 }) 433 } 434 435 func (s *clientSuite) TestConnectStreamAtUUIDPath(c *gc.C) { 436 s.PatchValue(api.WebsocketDialConfig, echoURL(c)) 437 // If the server supports it, we should log at "/model/UUID/log" 438 environ, err := s.State.Model() 439 c.Assert(err, jc.ErrorIsNil) 440 info := s.APIInfo(c) 441 info.ModelTag = environ.ModelTag() 442 apistate, err := api.Open(info, api.DialOpts{}) 443 c.Assert(err, jc.ErrorIsNil) 444 defer apistate.Close() 445 reader, err := apistate.ConnectStream("/path", nil) 446 c.Assert(err, jc.ErrorIsNil) 447 connectURL := connectURLFromReader(c, reader) 448 c.Assert(connectURL.Path, gc.Matches, fmt.Sprintf("/model/%s/path", environ.UUID())) 449 } 450 451 func (s *clientSuite) TestOpenUsesEnvironUUIDPaths(c *gc.C) { 452 info := s.APIInfo(c) 453 454 // Passing in the correct model UUID should work 455 environ, err := s.State.Model() 456 c.Assert(err, jc.ErrorIsNil) 457 info.ModelTag = environ.ModelTag() 458 apistate, err := api.Open(info, api.DialOpts{}) 459 c.Assert(err, jc.ErrorIsNil) 460 apistate.Close() 461 462 // Passing in a bad model UUID should fail with a known error 463 info.ModelTag = names.NewModelTag("dead-beef-123456") 464 apistate, err = api.Open(info, api.DialOpts{}) 465 c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ 466 Message: `unknown model: "dead-beef-123456"`, 467 Code: "not found", 468 }) 469 c.Check(err, jc.Satisfies, params.IsCodeNotFound) 470 c.Assert(apistate, gc.IsNil) 471 } 472 473 func (s *clientSuite) TestSetEnvironAgentVersionDuringUpgrade(c *gc.C) { 474 // This is an integration test which ensure that a test with the 475 // correct error code is seen by the client from the 476 // SetModelAgentVersion call when an upgrade is in progress. 477 envConfig, err := s.State.ModelConfig() 478 c.Assert(err, jc.ErrorIsNil) 479 agentVersion, ok := envConfig.AgentVersion() 480 c.Assert(ok, jc.IsTrue) 481 machine := s.Factory.MakeMachine(c, &factory.MachineParams{ 482 Jobs: []state.MachineJob{state.JobManageModel}, 483 }) 484 err = machine.SetAgentVersion(version.MustParseBinary(agentVersion.String() + "-quantal-amd64")) 485 c.Assert(err, jc.ErrorIsNil) 486 nextVersion := version.MustParse("9.8.7") 487 _, err = s.State.EnsureUpgradeInfo(machine.Id(), agentVersion, nextVersion) 488 c.Assert(err, jc.ErrorIsNil) 489 490 err = s.APIState.Client().SetModelAgentVersion(nextVersion) 491 492 // Expect an error with a error code that indicates this specific 493 // situation. The client needs to be able to reliably identify 494 // this error and handle it differently to other errors. 495 c.Assert(params.IsCodeUpgradeInProgress(err), jc.IsTrue) 496 } 497 498 func (s *clientSuite) TestAbortCurrentUpgrade(c *gc.C) { 499 client := s.APIState.Client() 500 someErr := errors.New("random") 501 cleanup := api.PatchClientFacadeCall(client, 502 func(request string, args interface{}, response interface{}) error { 503 c.Assert(request, gc.Equals, "AbortCurrentUpgrade") 504 c.Assert(args, gc.IsNil) 505 c.Assert(response, gc.IsNil) 506 return someErr 507 }, 508 ) 509 defer cleanup() 510 511 err := client.AbortCurrentUpgrade() 512 c.Assert(err, gc.Equals, someErr) // Confirms that the correct facade was called 513 } 514 515 func (s *clientSuite) TestEnvironmentGet(c *gc.C) { 516 client := s.APIState.Client() 517 env, err := client.ModelGet() 518 c.Assert(err, jc.ErrorIsNil) 519 // Check a known value, just checking that there is something there. 520 c.Assert(env["type"], gc.Equals, "dummy") 521 } 522 523 func (s *clientSuite) TestEnvironmentSet(c *gc.C) { 524 client := s.APIState.Client() 525 err := client.ModelSet(map[string]interface{}{ 526 "some-name": "value", 527 "other-name": true, 528 }) 529 c.Assert(err, jc.ErrorIsNil) 530 // Check them using ModelGet. 531 env, err := client.ModelGet() 532 c.Assert(err, jc.ErrorIsNil) 533 c.Assert(env["some-name"], gc.Equals, "value") 534 c.Assert(env["other-name"], gc.Equals, true) 535 } 536 537 func (s *clientSuite) TestEnvironmentUnset(c *gc.C) { 538 client := s.APIState.Client() 539 err := client.ModelSet(map[string]interface{}{ 540 "some-name": "value", 541 }) 542 c.Assert(err, jc.ErrorIsNil) 543 544 // Now unset it and make sure it isn't there. 545 err = client.ModelUnset("some-name") 546 c.Assert(err, jc.ErrorIsNil) 547 548 env, err := client.ModelGet() 549 c.Assert(err, jc.ErrorIsNil) 550 _, found := env["some-name"] 551 c.Assert(found, jc.IsFalse) 552 } 553 554 // badReader raises err when Read is called. 555 type badReader struct { 556 err error 557 } 558 559 func (r *badReader) Read(p []byte) (n int, err error) { 560 return 0, r.err 561 } 562 563 func echoURL(c *gc.C) func(*websocket.Config) (base.Stream, error) { 564 return func(config *websocket.Config) (base.Stream, error) { 565 pr, pw := io.Pipe() 566 go func() { 567 fmt.Fprintf(pw, "null\n") 568 fmt.Fprintf(pw, "%s\n", config.Location) 569 }() 570 return fakeStreamReader{pr}, nil 571 } 572 } 573 574 func connectURLFromReader(c *gc.C, rc io.ReadCloser) *url.URL { 575 bufReader := bufio.NewReader(rc) 576 location, err := bufReader.ReadString('\n') 577 c.Assert(err, jc.ErrorIsNil) 578 connectURL, err := url.Parse(strings.TrimSpace(location)) 579 c.Assert(err, jc.ErrorIsNil) 580 rc.Close() 581 return connectURL 582 } 583 584 type fakeStreamReader struct { 585 io.Reader 586 } 587 588 func (s fakeStreamReader) Close() error { 589 if c, ok := s.Reader.(io.Closer); ok { 590 return c.Close() 591 } 592 return nil 593 } 594 595 func (s fakeStreamReader) Write([]byte) (int, error) { 596 panic("not implemented") 597 } 598 599 func (s fakeStreamReader) ReadJSON(v interface{}) error { 600 panic("not implemented") 601 } 602 603 func (s fakeStreamReader) WriteJSON(v interface{}) error { 604 panic("not implemented") 605 }