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