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 := ¶ms.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 }