github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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, &params.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 fakeAPIEndpoint(c *gc.C, client *api.Client, address, method string, handle func(http.ResponseWriter, *http.Request)) net.Listener {
   238  	lis, err := net.Listen("tcp", "127.0.0.1:0")
   239  	c.Assert(err, jc.ErrorIsNil)
   240  
   241  	mux := http.NewServeMux()
   242  	mux.HandleFunc(address, func(w http.ResponseWriter, r *http.Request) {
   243  		if r.Method == method {
   244  			handle(w, r)
   245  		}
   246  	})
   247  	go func() {
   248  		http.Serve(lis, mux)
   249  	}()
   250  	api.SetServerAddress(client, "http", lis.Addr().String())
   251  	return lis
   252  }
   253  
   254  // envEndpoint returns "/model/<model-uuid>/<destination>"
   255  func envEndpoint(c *gc.C, apiState api.Connection, destination string) string {
   256  	modelTag, err := apiState.ModelTag()
   257  	c.Assert(err, jc.ErrorIsNil)
   258  	return path.Join("/model", modelTag.Id(), destination)
   259  }
   260  
   261  func (s *clientSuite) TestClientEnvironmentUUID(c *gc.C) {
   262  	environ, err := s.State.Model()
   263  	c.Assert(err, jc.ErrorIsNil)
   264  
   265  	client := s.APIState.Client()
   266  	c.Assert(client.ModelUUID(), gc.Equals, environ.Tag().Id())
   267  }
   268  
   269  func (s *clientSuite) TestClientEnvironmentUsers(c *gc.C) {
   270  	client := s.APIState.Client()
   271  	cleanup := api.PatchClientFacadeCall(client,
   272  		func(request string, paramsIn interface{}, response interface{}) error {
   273  			c.Assert(paramsIn, gc.IsNil)
   274  			if response, ok := response.(*params.ModelUserInfoResults); ok {
   275  				response.Results = []params.ModelUserInfoResult{
   276  					{Result: &params.ModelUserInfo{UserName: "one"}},
   277  					{Result: &params.ModelUserInfo{UserName: "two"}},
   278  					{Result: &params.ModelUserInfo{UserName: "three"}},
   279  				}
   280  			} else {
   281  				c.Log("wrong output structure")
   282  				c.Fail()
   283  			}
   284  			return nil
   285  		},
   286  	)
   287  	defer cleanup()
   288  
   289  	obtained, err := client.ModelUserInfo()
   290  	c.Assert(err, jc.ErrorIsNil)
   291  
   292  	c.Assert(obtained, jc.DeepEquals, []params.ModelUserInfo{
   293  		{UserName: "one"},
   294  		{UserName: "two"},
   295  		{UserName: "three"},
   296  	})
   297  }
   298  
   299  func (s *clientSuite) TestDestroyEnvironment(c *gc.C) {
   300  	client := s.APIState.Client()
   301  	var called bool
   302  	cleanup := api.PatchClientFacadeCall(client,
   303  		func(req string, args interface{}, resp interface{}) error {
   304  			c.Assert(req, gc.Equals, "DestroyModel")
   305  			called = true
   306  			return nil
   307  		})
   308  	defer cleanup()
   309  
   310  	err := client.DestroyModel()
   311  	c.Assert(err, jc.ErrorIsNil)
   312  	c.Assert(called, jc.IsTrue)
   313  }
   314  
   315  func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) {
   316  	client := s.APIState.Client()
   317  	// Use the no tail option so we don't try to start a tailing cursor
   318  	// on the oplog when there is no oplog configured in mongo as the tests
   319  	// don't set up mongo in replicaset mode.
   320  	reader, err := client.WatchDebugLog(api.DebugLogParams{NoTail: true})
   321  	c.Assert(err, jc.ErrorIsNil)
   322  	c.Assert(reader, gc.NotNil)
   323  	reader.Close()
   324  }
   325  
   326  func (s *clientSuite) TestConnectStreamRequiresSlashPathPrefix(c *gc.C) {
   327  	reader, err := s.APIState.ConnectStream("foo", nil)
   328  	c.Assert(err, gc.ErrorMatches, `path must start with "/"`)
   329  	c.Assert(reader, gc.Equals, nil)
   330  }
   331  
   332  func (s *clientSuite) TestConnectStreamErrorBadConnection(c *gc.C) {
   333  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) {
   334  		return nil, fmt.Errorf("bad connection")
   335  	})
   336  	reader, err := s.APIState.ConnectStream("/", nil)
   337  	c.Assert(err, gc.ErrorMatches, "bad connection")
   338  	c.Assert(reader, gc.IsNil)
   339  }
   340  
   341  func (s *clientSuite) TestConnectStreamErrorNoData(c *gc.C) {
   342  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) {
   343  		return fakeStreamReader{&bytes.Buffer{}}, nil
   344  	})
   345  	reader, err := s.APIState.ConnectStream("/", nil)
   346  	c.Assert(err, gc.ErrorMatches, "unable to read initial response: EOF")
   347  	c.Assert(reader, gc.IsNil)
   348  }
   349  
   350  func (s *clientSuite) TestConnectStreamErrorBadData(c *gc.C) {
   351  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) {
   352  		return fakeStreamReader{strings.NewReader("junk\n")}, nil
   353  	})
   354  	reader, err := s.APIState.ConnectStream("/", nil)
   355  	c.Assert(err, gc.ErrorMatches, "unable to unmarshal initial response: .*")
   356  	c.Assert(reader, gc.IsNil)
   357  }
   358  
   359  func (s *clientSuite) TestConnectStreamErrorReadError(c *gc.C) {
   360  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) {
   361  		err := fmt.Errorf("bad read")
   362  		return fakeStreamReader{&badReader{err}}, nil
   363  	})
   364  	reader, err := s.APIState.ConnectStream("/", nil)
   365  	c.Assert(err, gc.ErrorMatches, "unable to read initial response: bad read")
   366  	c.Assert(reader, gc.IsNil)
   367  }
   368  
   369  func (s *clientSuite) TestWatchDebugLogParamsEncoded(c *gc.C) {
   370  	s.PatchValue(api.WebsocketDialConfig, echoURL(c))
   371  
   372  	params := api.DebugLogParams{
   373  		IncludeEntity: []string{"a", "b"},
   374  		IncludeModule: []string{"c", "d"},
   375  		ExcludeEntity: []string{"e", "f"},
   376  		ExcludeModule: []string{"g", "h"},
   377  		Limit:         100,
   378  		Backlog:       200,
   379  		Level:         loggo.ERROR,
   380  		Replay:        true,
   381  		NoTail:        true,
   382  	}
   383  
   384  	client := s.APIState.Client()
   385  	reader, err := client.WatchDebugLog(params)
   386  	c.Assert(err, jc.ErrorIsNil)
   387  
   388  	connectURL := connectURLFromReader(c, reader)
   389  	values := connectURL.Query()
   390  	c.Assert(values, jc.DeepEquals, url.Values{
   391  		"includeEntity": params.IncludeEntity,
   392  		"includeModule": params.IncludeModule,
   393  		"excludeEntity": params.ExcludeEntity,
   394  		"excludeModule": params.ExcludeModule,
   395  		"maxLines":      {"100"},
   396  		"backlog":       {"200"},
   397  		"level":         {"ERROR"},
   398  		"replay":        {"true"},
   399  		"noTail":        {"true"},
   400  	})
   401  }
   402  
   403  func (s *clientSuite) TestConnectStreamAtUUIDPath(c *gc.C) {
   404  	s.PatchValue(api.WebsocketDialConfig, echoURL(c))
   405  	// If the server supports it, we should log at "/model/UUID/log"
   406  	environ, err := s.State.Model()
   407  	c.Assert(err, jc.ErrorIsNil)
   408  	info := s.APIInfo(c)
   409  	info.ModelTag = environ.ModelTag()
   410  	apistate, err := api.Open(info, api.DialOpts{})
   411  	c.Assert(err, jc.ErrorIsNil)
   412  	defer apistate.Close()
   413  	reader, err := apistate.ConnectStream("/path", nil)
   414  	c.Assert(err, jc.ErrorIsNil)
   415  	connectURL := connectURLFromReader(c, reader)
   416  	c.Assert(connectURL.Path, gc.Matches, fmt.Sprintf("/model/%s/path", environ.UUID()))
   417  }
   418  
   419  func (s *clientSuite) TestOpenUsesEnvironUUIDPaths(c *gc.C) {
   420  	info := s.APIInfo(c)
   421  
   422  	// Passing in the correct model UUID should work
   423  	environ, err := s.State.Model()
   424  	c.Assert(err, jc.ErrorIsNil)
   425  	info.ModelTag = environ.ModelTag()
   426  	apistate, err := api.Open(info, api.DialOpts{})
   427  	c.Assert(err, jc.ErrorIsNil)
   428  	apistate.Close()
   429  
   430  	// Passing in a bad model UUID should fail with a known error
   431  	info.ModelTag = names.NewModelTag("dead-beef-123456")
   432  	apistate, err = api.Open(info, api.DialOpts{})
   433  	c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{
   434  		Message: `unknown model: "dead-beef-123456"`,
   435  		Code:    "not found",
   436  	})
   437  	c.Check(err, jc.Satisfies, params.IsCodeNotFound)
   438  	c.Assert(apistate, gc.IsNil)
   439  }
   440  
   441  func (s *clientSuite) TestSetEnvironAgentVersionDuringUpgrade(c *gc.C) {
   442  	// This is an integration test which ensure that a test with the
   443  	// correct error code is seen by the client from the
   444  	// SetModelAgentVersion call when an upgrade is in progress.
   445  	envConfig, err := s.State.ModelConfig()
   446  	c.Assert(err, jc.ErrorIsNil)
   447  	agentVersion, ok := envConfig.AgentVersion()
   448  	c.Assert(ok, jc.IsTrue)
   449  	machine := s.Factory.MakeMachine(c, &factory.MachineParams{
   450  		Jobs: []state.MachineJob{state.JobManageModel},
   451  	})
   452  	err = machine.SetAgentVersion(version.MustParseBinary(agentVersion.String() + "-quantal-amd64"))
   453  	c.Assert(err, jc.ErrorIsNil)
   454  	nextVersion := version.MustParse("9.8.7")
   455  	_, err = s.State.EnsureUpgradeInfo(machine.Id(), agentVersion, nextVersion)
   456  	c.Assert(err, jc.ErrorIsNil)
   457  
   458  	err = s.APIState.Client().SetModelAgentVersion(nextVersion)
   459  
   460  	// Expect an error with a error code that indicates this specific
   461  	// situation. The client needs to be able to reliably identify
   462  	// this error and handle it differently to other errors.
   463  	c.Assert(params.IsCodeUpgradeInProgress(err), jc.IsTrue)
   464  }
   465  
   466  func (s *clientSuite) TestAbortCurrentUpgrade(c *gc.C) {
   467  	client := s.APIState.Client()
   468  	someErr := errors.New("random")
   469  	cleanup := api.PatchClientFacadeCall(client,
   470  		func(request string, args interface{}, response interface{}) error {
   471  			c.Assert(request, gc.Equals, "AbortCurrentUpgrade")
   472  			c.Assert(args, gc.IsNil)
   473  			c.Assert(response, gc.IsNil)
   474  			return someErr
   475  		},
   476  	)
   477  	defer cleanup()
   478  
   479  	err := client.AbortCurrentUpgrade()
   480  	c.Assert(err, gc.Equals, someErr) // Confirms that the correct facade was called
   481  }
   482  
   483  func (s *clientSuite) TestEnvironmentGet(c *gc.C) {
   484  	client := s.APIState.Client()
   485  	env, err := client.ModelGet()
   486  	c.Assert(err, jc.ErrorIsNil)
   487  	// Check a known value, just checking that there is something there.
   488  	c.Assert(env["type"], gc.Equals, "dummy")
   489  }
   490  
   491  func (s *clientSuite) TestEnvironmentSet(c *gc.C) {
   492  	client := s.APIState.Client()
   493  	err := client.ModelSet(map[string]interface{}{
   494  		"some-name":  "value",
   495  		"other-name": true,
   496  	})
   497  	c.Assert(err, jc.ErrorIsNil)
   498  	// Check them using ModelGet.
   499  	env, err := client.ModelGet()
   500  	c.Assert(err, jc.ErrorIsNil)
   501  	c.Assert(env["some-name"], gc.Equals, "value")
   502  	c.Assert(env["other-name"], gc.Equals, true)
   503  }
   504  
   505  func (s *clientSuite) TestEnvironmentUnset(c *gc.C) {
   506  	client := s.APIState.Client()
   507  	err := client.ModelSet(map[string]interface{}{
   508  		"some-name": "value",
   509  	})
   510  	c.Assert(err, jc.ErrorIsNil)
   511  
   512  	// Now unset it and make sure it isn't there.
   513  	err = client.ModelUnset("some-name")
   514  	c.Assert(err, jc.ErrorIsNil)
   515  
   516  	env, err := client.ModelGet()
   517  	c.Assert(err, jc.ErrorIsNil)
   518  	_, found := env["some-name"]
   519  	c.Assert(found, jc.IsFalse)
   520  }
   521  
   522  // badReader raises err when Read is called.
   523  type badReader struct {
   524  	err error
   525  }
   526  
   527  func (r *badReader) Read(p []byte) (n int, err error) {
   528  	return 0, r.err
   529  }
   530  
   531  func echoURL(c *gc.C) func(*websocket.Config) (base.Stream, error) {
   532  	return func(config *websocket.Config) (base.Stream, error) {
   533  		pr, pw := io.Pipe()
   534  		go func() {
   535  			fmt.Fprintf(pw, "null\n")
   536  			fmt.Fprintf(pw, "%s\n", config.Location)
   537  		}()
   538  		return fakeStreamReader{pr}, nil
   539  	}
   540  }
   541  
   542  func connectURLFromReader(c *gc.C, rc io.ReadCloser) *url.URL {
   543  	bufReader := bufio.NewReader(rc)
   544  	location, err := bufReader.ReadString('\n')
   545  	c.Assert(err, jc.ErrorIsNil)
   546  	connectURL, err := url.Parse(strings.TrimSpace(location))
   547  	c.Assert(err, jc.ErrorIsNil)
   548  	rc.Close()
   549  	return connectURL
   550  }
   551  
   552  type fakeStreamReader struct {
   553  	io.Reader
   554  }
   555  
   556  func (s fakeStreamReader) Close() error {
   557  	if c, ok := s.Reader.(io.Closer); ok {
   558  		return c.Close()
   559  	}
   560  	return nil
   561  }
   562  
   563  func (s fakeStreamReader) Write([]byte) (int, error) {
   564  	panic("not implemented")
   565  }
   566  
   567  func (s fakeStreamReader) ReadJSON(v interface{}) error {
   568  	panic("not implemented")
   569  }
   570  
   571  func (s fakeStreamReader) WriteJSON(v interface{}) error {
   572  	panic("not implemented")
   573  }