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, &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 (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: &params.ModelUserInfo{UserName: "one"}},
   309  					{Result: &params.ModelUserInfo{UserName: "two"}},
   310  					{Result: &params.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  }