github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/state/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  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net"
    14  	"net/http"
    15  	"net/url"
    16  	"strings"
    17  
    18  	"code.google.com/p/go.net/websocket"
    19  	"github.com/juju/charm"
    20  	charmtesting "github.com/juju/charm/testing"
    21  	"github.com/juju/loggo"
    22  	jc "github.com/juju/testing/checkers"
    23  	gc "launchpad.net/gocheck"
    24  
    25  	jujutesting "github.com/juju/juju/juju/testing"
    26  	"github.com/juju/juju/state/api"
    27  	"github.com/juju/juju/state/api/params"
    28  )
    29  
    30  type clientSuite struct {
    31  	jujutesting.JujuConnSuite
    32  }
    33  
    34  var _ = gc.Suite(&clientSuite{})
    35  
    36  // TODO(jam) 2013-08-27 http://pad.lv/1217282
    37  // Right now most of the direct tests for api.Client behavior are in
    38  // state/apiserver/client/*_test.go
    39  
    40  func (s *clientSuite) TestCloseMultipleOk(c *gc.C) {
    41  	client := s.APIState.Client()
    42  	c.Assert(client.Close(), gc.IsNil)
    43  	c.Assert(client.Close(), gc.IsNil)
    44  	c.Assert(client.Close(), gc.IsNil)
    45  }
    46  
    47  func (s *clientSuite) TestAddLocalCharm(c *gc.C) {
    48  	charmArchive := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
    49  	curl := charm.MustParseURL(
    50  		fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()),
    51  	)
    52  	client := s.APIState.Client()
    53  
    54  	// Test the sanity checks first.
    55  	_, err := client.AddLocalCharm(charm.MustParseURL("cs:quantal/wordpress-1"), nil)
    56  	c.Assert(err, gc.ErrorMatches, `expected charm URL with local: schema, got "cs:quantal/wordpress-1"`)
    57  
    58  	// Upload an archive with its original revision.
    59  	savedURL, err := client.AddLocalCharm(curl, charmArchive)
    60  	c.Assert(err, gc.IsNil)
    61  	c.Assert(savedURL.String(), gc.Equals, curl.String())
    62  
    63  	// Upload a charm directory with changed revision.
    64  	charmDir := charmtesting.Charms.ClonedDir(c.MkDir(), "dummy")
    65  	charmDir.SetDiskRevision(42)
    66  	savedURL, err = client.AddLocalCharm(curl, charmDir)
    67  	c.Assert(err, gc.IsNil)
    68  	c.Assert(savedURL.Revision, gc.Equals, 42)
    69  
    70  	// Upload a charm directory again, revision should be bumped.
    71  	savedURL, err = client.AddLocalCharm(curl, charmDir)
    72  	c.Assert(err, gc.IsNil)
    73  	c.Assert(savedURL.String(), gc.Equals, curl.WithRevision(43).String())
    74  
    75  	// Finally, try the NotImplementedError by mocking the server
    76  	// address to a handler that returns 405 Method Not Allowed for
    77  	// POST.
    78  	lis, err := net.Listen("tcp", "127.0.0.1:0")
    79  	c.Assert(err, gc.IsNil)
    80  	defer lis.Close()
    81  	url := fmt.Sprintf("http://%v", lis.Addr())
    82  	http.HandleFunc("/charms", func(w http.ResponseWriter, r *http.Request) {
    83  		if r.Method == "POST" {
    84  			http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
    85  		}
    86  	})
    87  	go func() {
    88  		http.Serve(lis, nil)
    89  	}()
    90  
    91  	api.SetServerRoot(client, url)
    92  	_, err = client.AddLocalCharm(curl, charmArchive)
    93  	c.Assert(err, jc.Satisfies, params.IsCodeNotImplemented)
    94  }
    95  
    96  func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) {
    97  	// Shows both the unmarshalling of a real error, and
    98  	// that the api server is connected.
    99  	client := s.APIState.Client()
   100  	reader, err := client.WatchDebugLog(api.DebugLogParams{})
   101  	c.Assert(err, gc.ErrorMatches, "cannot open log file: .*")
   102  	c.Assert(reader, gc.IsNil)
   103  }
   104  
   105  func (s *clientSuite) TestConnectionErrorBadConnection(c *gc.C) {
   106  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
   107  		return nil, fmt.Errorf("bad connection")
   108  	})
   109  	client := s.APIState.Client()
   110  	reader, err := client.WatchDebugLog(api.DebugLogParams{})
   111  	c.Assert(err, gc.ErrorMatches, "bad connection")
   112  	c.Assert(reader, gc.IsNil)
   113  }
   114  
   115  func (s *clientSuite) TestConnectionErrorNoData(c *gc.C) {
   116  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
   117  		return ioutil.NopCloser(&bytes.Buffer{}), nil
   118  	})
   119  	client := s.APIState.Client()
   120  	reader, err := client.WatchDebugLog(api.DebugLogParams{})
   121  	c.Assert(err, gc.ErrorMatches, "unable to read initial response: EOF")
   122  	c.Assert(reader, gc.IsNil)
   123  }
   124  
   125  func (s *clientSuite) TestConnectionErrorBadData(c *gc.C) {
   126  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
   127  		junk := strings.NewReader("junk\n")
   128  		return ioutil.NopCloser(junk), nil
   129  	})
   130  	client := s.APIState.Client()
   131  	reader, err := client.WatchDebugLog(api.DebugLogParams{})
   132  	c.Assert(err, gc.ErrorMatches, "unable to unmarshal initial response: .*")
   133  	c.Assert(reader, gc.IsNil)
   134  }
   135  
   136  func (s *clientSuite) TestConnectionErrorReadError(c *gc.C) {
   137  	s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
   138  		err := fmt.Errorf("bad read")
   139  		return ioutil.NopCloser(&badReader{err}), nil
   140  	})
   141  	client := s.APIState.Client()
   142  	reader, err := client.WatchDebugLog(api.DebugLogParams{})
   143  	c.Assert(err, gc.ErrorMatches, "unable to read initial response: bad read")
   144  	c.Assert(reader, gc.IsNil)
   145  }
   146  
   147  func (s *clientSuite) TestParamsEncoded(c *gc.C) {
   148  	s.PatchValue(api.WebsocketDialConfig, echoURL(c))
   149  
   150  	params := api.DebugLogParams{
   151  		IncludeEntity: []string{"a", "b"},
   152  		IncludeModule: []string{"c", "d"},
   153  		ExcludeEntity: []string{"e", "f"},
   154  		ExcludeModule: []string{"g", "h"},
   155  		Limit:         100,
   156  		Backlog:       200,
   157  		Level:         loggo.ERROR,
   158  		Replay:        true,
   159  	}
   160  
   161  	client := s.APIState.Client()
   162  	reader, err := client.WatchDebugLog(params)
   163  	c.Assert(err, gc.IsNil)
   164  
   165  	connectURL := connectURLFromReader(c, reader)
   166  
   167  	c.Assert(connectURL.Path, gc.Matches, "/log")
   168  	values := connectURL.Query()
   169  	c.Assert(values, jc.DeepEquals, url.Values{
   170  		"includeEntity": params.IncludeEntity,
   171  		"includeModule": params.IncludeModule,
   172  		"excludeEntity": params.ExcludeEntity,
   173  		"excludeModule": params.ExcludeModule,
   174  		"maxLines":      {"100"},
   175  		"backlog":       {"200"},
   176  		"level":         {"ERROR"},
   177  		"replay":        {"true"},
   178  	})
   179  }
   180  
   181  func (s *clientSuite) TestDebugLogRootPath(c *gc.C) {
   182  	s.PatchValue(api.WebsocketDialConfig, echoURL(c))
   183  
   184  	// If the server is old, we log at "/log"
   185  	info := s.APIInfo(c)
   186  	info.EnvironTag = ""
   187  	apistate, err := api.Open(info, api.DialOpts{})
   188  	c.Assert(err, gc.IsNil)
   189  	defer apistate.Close()
   190  	reader, err := apistate.Client().WatchDebugLog(api.DebugLogParams{})
   191  	c.Assert(err, gc.IsNil)
   192  	connectURL := connectURLFromReader(c, reader)
   193  	c.Assert(connectURL.Path, gc.Matches, "/log")
   194  }
   195  
   196  func (s *clientSuite) TestDebugLogAtUUIDLogPath(c *gc.C) {
   197  	s.PatchValue(api.WebsocketDialConfig, echoURL(c))
   198  	// If the server supports it, we should log at "/environment/UUID/log"
   199  	environ, err := s.State.Environment()
   200  	c.Assert(err, gc.IsNil)
   201  	info := s.APIInfo(c)
   202  	info.EnvironTag = environ.Tag()
   203  	apistate, err := api.Open(info, api.DialOpts{})
   204  	c.Assert(err, gc.IsNil)
   205  	defer apistate.Close()
   206  	reader, err := apistate.Client().WatchDebugLog(api.DebugLogParams{})
   207  	c.Assert(err, gc.IsNil)
   208  	connectURL := connectURLFromReader(c, reader)
   209  	c.ExpectFailure("debug log always goes to /log for compatibility http://pad.lv/1326799")
   210  	c.Assert(connectURL.Path, gc.Matches, fmt.Sprintf("/%s/log", environ.UUID()))
   211  }
   212  
   213  func (s *clientSuite) TestOpenUsesEnvironUUIDPaths(c *gc.C) {
   214  	info := s.APIInfo(c)
   215  	// Backwards compatibility, passing EnvironTag = "" should just work
   216  	info.EnvironTag = ""
   217  	apistate, err := api.Open(info, api.DialOpts{})
   218  	c.Assert(err, gc.IsNil)
   219  	apistate.Close()
   220  
   221  	// Passing in the correct environment UUID should also work
   222  	environ, err := s.State.Environment()
   223  	c.Assert(err, gc.IsNil)
   224  	info.EnvironTag = environ.Tag()
   225  	apistate, err = api.Open(info, api.DialOpts{})
   226  	c.Assert(err, gc.IsNil)
   227  	apistate.Close()
   228  
   229  	// Passing in a bad environment UUID should fail with a known error
   230  	info.EnvironTag = "environment-dead-beef-123456"
   231  	apistate, err = api.Open(info, api.DialOpts{})
   232  	c.Check(err, gc.ErrorMatches, `unknown environment: "dead-beef-123456"`)
   233  	c.Check(err, jc.Satisfies, params.IsCodeNotFound)
   234  	c.Assert(apistate, gc.IsNil)
   235  }
   236  
   237  // badReader raises err when Read is called.
   238  type badReader struct {
   239  	err error
   240  }
   241  
   242  func (r *badReader) Read(p []byte) (n int, err error) {
   243  	return 0, r.err
   244  }
   245  
   246  func echoURL(c *gc.C) func(*websocket.Config) (io.ReadCloser, error) {
   247  	response := &params.ErrorResult{}
   248  	message, err := json.Marshal(response)
   249  	c.Assert(err, gc.IsNil)
   250  	return func(config *websocket.Config) (io.ReadCloser, error) {
   251  		pr, pw := io.Pipe()
   252  		go func() {
   253  			fmt.Fprintf(pw, "%s\n", message)
   254  			fmt.Fprintf(pw, "%s\n", config.Location)
   255  		}()
   256  		return pr, nil
   257  	}
   258  }
   259  
   260  func connectURLFromReader(c *gc.C, rc io.ReadCloser) *url.URL {
   261  	bufReader := bufio.NewReader(rc)
   262  	location, err := bufReader.ReadString('\n')
   263  	c.Assert(err, gc.IsNil)
   264  	connectURL, err := url.Parse(strings.TrimSpace(location))
   265  	c.Assert(err, gc.IsNil)
   266  	rc.Close()
   267  	return connectURL
   268  }