github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/cmd/snap/cmd_services_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main_test 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "net/http" 26 "sort" 27 "strings" 28 "time" 29 30 "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/client" 33 snap "github.com/snapcore/snapd/cmd/snap" 34 ) 35 36 type appOpSuite struct { 37 BaseSnapSuite 38 } 39 40 var _ = check.Suite(&appOpSuite{}) 41 42 func (s *appOpSuite) SetUpTest(c *check.C) { 43 s.BaseSnapSuite.SetUpTest(c) 44 45 restoreClientRetry := client.MockDoTimings(time.Millisecond, time.Second) 46 restorePollTime := snap.MockPollTime(time.Millisecond) 47 s.AddCleanup(restoreClientRetry) 48 s.AddCleanup(restorePollTime) 49 } 50 51 func (s *appOpSuite) TearDownTest(c *check.C) { 52 s.BaseSnapSuite.TearDownTest(c) 53 } 54 55 func (s *appOpSuite) expectedBody(op string, names []string, extra []string) map[string]interface{} { 56 inames := make([]interface{}, len(names)) 57 for i, name := range names { 58 inames[i] = name 59 } 60 expectedBody := map[string]interface{}{ 61 "action": op, 62 "names": inames, 63 } 64 for _, x := range extra { 65 expectedBody[x] = true 66 } 67 return expectedBody 68 } 69 70 func (s *appOpSuite) args(op string, names []string, extra []string, noWait bool) []string { 71 args := []string{op} 72 if noWait { 73 args = append(args, "--no-wait") 74 } 75 for _, x := range extra { 76 args = append(args, "--"+x) 77 } 78 args = append(args, names...) 79 return args 80 } 81 82 func (s *appOpSuite) testOpNoArgs(c *check.C, op string) { 83 s.RedirectClientToTestServer(nil) 84 _, err := snap.Parser(snap.Client()).ParseArgs([]string{op}) 85 c.Assert(err, check.ErrorMatches, `.* required argument .* not provided`) 86 } 87 88 func (s *appOpSuite) testOpErrorResponse(c *check.C, op string, names []string, extra []string, noWait bool) { 89 n := 0 90 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 91 switch n { 92 case 0: 93 c.Check(r.Method, check.Equals, "POST") 94 c.Check(r.URL.Path, check.Equals, "/v2/apps") 95 c.Check(r.URL.Query(), check.HasLen, 0) 96 c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) 97 w.WriteHeader(400) 98 fmt.Fprintln(w, `{"type": "error", "result": {"message": "error"}, "status-code": 400}`) 99 default: 100 c.Fatalf("expected to get 1 requests, now on %d", n+1) 101 } 102 103 n++ 104 }) 105 106 _, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) 107 c.Assert(err, check.ErrorMatches, "error") 108 c.Check(n, check.Equals, 1) 109 } 110 111 func (s *appOpSuite) testOp(c *check.C, op, summary string, names []string, extra []string, noWait bool) { 112 n := 0 113 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 114 switch n { 115 case 0: 116 c.Check(r.URL.Path, check.Equals, "/v2/apps") 117 c.Check(r.URL.Query(), check.HasLen, 0) 118 c.Check(DecodedRequestBody(c, r), check.DeepEquals, s.expectedBody(op, names, extra)) 119 c.Check(r.Method, check.Equals, "POST") 120 w.WriteHeader(202) 121 fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`) 122 case 1: 123 c.Check(r.Method, check.Equals, "GET") 124 c.Check(r.URL.Path, check.Equals, "/v2/changes/42") 125 fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`) 126 case 2: 127 c.Check(r.Method, check.Equals, "GET") 128 c.Check(r.URL.Path, check.Equals, "/v2/changes/42") 129 fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) 130 default: 131 c.Fatalf("expected to get 2 requests, now on %d", n+1) 132 } 133 134 n++ 135 }) 136 rest, err := snap.Parser(snap.Client()).ParseArgs(s.args(op, names, extra, noWait)) 137 c.Assert(err, check.IsNil) 138 c.Assert(rest, check.HasLen, 0) 139 c.Check(s.Stderr(), check.Equals, "") 140 expectedN := 3 141 if noWait { 142 summary = "42" 143 expectedN = 1 144 } 145 c.Check(s.Stdout(), check.Equals, summary+"\n") 146 // ensure that the fake server api was actually hit 147 c.Check(n, check.Equals, expectedN) 148 } 149 150 func (s *appOpSuite) TestAppOps(c *check.C) { 151 extras := []string{"enable", "disable", "reload"} 152 summaries := []string{"Started.", "Stopped.", "Restarted."} 153 for i, op := range []string{"start", "stop", "restart"} { 154 s.testOpNoArgs(c, op) 155 for _, extra := range [][]string{nil, {extras[i]}} { 156 for _, noWait := range []bool{false, true} { 157 for _, names := range [][]string{ 158 {"foo"}, 159 {"foo", "bar"}, 160 {"foo", "bar.baz"}, 161 } { 162 s.testOpErrorResponse(c, op, names, extra, noWait) 163 s.testOp(c, op, summaries[i], names, extra, noWait) 164 s.stdout.Reset() 165 } 166 } 167 } 168 } 169 } 170 171 func (s *appOpSuite) TestAppStatus(c *check.C) { 172 n := 0 173 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 174 switch n { 175 case 0: 176 c.Check(r.URL.Path, check.Equals, "/v2/apps") 177 c.Check(r.URL.Query(), check.HasLen, 1) 178 c.Check(r.URL.Query().Get("select"), check.Equals, "service") 179 c.Check(r.Method, check.Equals, "GET") 180 w.WriteHeader(200) 181 enc := json.NewEncoder(w) 182 enc.Encode(map[string]interface{}{ 183 "type": "sync", 184 "result": []map[string]interface{}{ 185 { 186 "snap": "foo", 187 "name": "bar", 188 "daemon": "oneshot", 189 "daemon-scope": "system", 190 "active": false, 191 "enabled": true, 192 "activators": []map[string]interface{}{ 193 {"name": "bar", "type": "timer", "active": true, "enabled": true}, 194 }, 195 }, { 196 "snap": "foo", 197 "name": "baz", 198 "daemon": "oneshot", 199 "daemon-scope": "system", 200 "active": false, 201 "enabled": true, 202 "activators": []map[string]interface{}{ 203 {"name": "baz-sock1", "type": "socket", "active": true, "enabled": true}, 204 {"name": "baz-sock2", "type": "socket", "active": false, "enabled": true}, 205 }, 206 }, { 207 "snap": "foo", 208 "name": "qux", 209 "daemon": "simple", 210 "daemon-scope": "user", 211 "active": false, 212 "enabled": true, 213 }, { 214 "snap": "foo", 215 "name": "zed", 216 "active": true, 217 "enabled": true, 218 }, 219 }, 220 "status": "OK", 221 "status-code": 200, 222 }) 223 default: 224 c.Fatalf("expected to get 1 requests, now on %d", n+1) 225 } 226 227 n++ 228 }) 229 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) 230 c.Assert(err, check.IsNil) 231 c.Assert(rest, check.HasLen, 0) 232 c.Check(s.Stderr(), check.Equals, "") 233 c.Check(s.Stdout(), check.Equals, `Service Startup Current Notes 234 foo.bar enabled inactive timer-activated 235 foo.baz enabled inactive socket-activated 236 foo.qux enabled - user 237 foo.zed enabled active - 238 `) 239 // ensure that the fake server api was actually hit 240 c.Check(n, check.Equals, 1) 241 } 242 243 func (s *appOpSuite) TestServiceCompletion(c *check.C) { 244 n := 0 245 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 246 c.Check(r.URL.Path, check.Equals, "/v2/apps") 247 c.Check(r.URL.Query(), check.HasLen, 1) 248 c.Check(r.URL.Query().Get("select"), check.Equals, "service") 249 c.Check(r.Method, check.Equals, "GET") 250 w.WriteHeader(200) 251 enc := json.NewEncoder(w) 252 enc.Encode(map[string]interface{}{ 253 "type": "sync", 254 "result": []map[string]interface{}{ 255 {"snap": "a-snap", "name": "foo", "daemon": "simple"}, 256 {"snap": "a-snap", "name": "bar", "daemon": "simple"}, 257 {"snap": "b-snap", "name": "baz", "daemon": "simple"}, 258 }, 259 "status": "OK", 260 "status-code": 200, 261 }) 262 263 n++ 264 }) 265 266 var comp = func(s string) string { 267 comps := snap.ServiceName("").Complete(s) 268 as := make([]string, len(comps)) 269 for i := range comps { 270 as[i] = comps[i].Item 271 } 272 sort.Strings(as) 273 return strings.Join(as, " ") 274 } 275 276 c.Check(comp(""), check.Equals, "a-snap a-snap.bar a-snap.foo b-snap.baz") 277 c.Check(comp("a"), check.Equals, "a-snap a-snap.bar a-snap.foo") 278 c.Check(comp("a-snap"), check.Equals, "a-snap a-snap.bar a-snap.foo") 279 c.Check(comp("a-snap."), check.Equals, "a-snap.bar a-snap.foo") 280 c.Check(comp("a-snap.b"), check.Equals, "a-snap.bar") 281 c.Check(comp("b"), check.Equals, "b-snap.baz") 282 c.Check(comp("c"), check.Equals, "") 283 284 // ensure that the fake server api was actually hit 285 c.Check(n, check.Equals, 7) 286 } 287 288 func (s *appOpSuite) TestAppStatusNoServices(c *check.C) { 289 n := 0 290 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 291 switch n { 292 case 0: 293 c.Check(r.URL.Path, check.Equals, "/v2/apps") 294 c.Check(r.URL.Query(), check.HasLen, 1) 295 c.Check(r.URL.Query().Get("select"), check.Equals, "service") 296 c.Check(r.Method, check.Equals, "GET") 297 w.WriteHeader(200) 298 enc := json.NewEncoder(w) 299 enc.Encode(map[string]interface{}{ 300 "type": "sync", 301 "result": []map[string]interface{}{}, 302 "status": "OK", 303 "status-code": 200, 304 }) 305 default: 306 c.Fatalf("expected to get 1 requests, now on %d", n+1) 307 } 308 n++ 309 }) 310 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"services"}) 311 c.Assert(err, check.IsNil) 312 c.Assert(rest, check.HasLen, 0) 313 c.Check(s.Stdout(), check.Equals, "") 314 c.Check(s.Stderr(), check.Equals, "There are no services provided by installed snaps.\n") 315 // ensure that the fake server api was actually hit 316 c.Check(n, check.Equals, 1) 317 } 318 319 func (s *appOpSuite) TestLogsCommand(c *check.C) { 320 n := 0 321 timestamp := "2021-08-16T17:33:55Z" 322 message := "Thing occurred\n" 323 sid := "service1" 324 pid := "1000" 325 326 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 327 switch n { 328 case 0: 329 c.Check(r.URL.Path, check.Equals, "/v2/logs") 330 c.Check(r.Method, check.Equals, "GET") 331 w.WriteHeader(200) 332 _, err := w.Write([]byte{0x1E}) 333 c.Assert(err, check.IsNil) 334 335 enc := json.NewEncoder(w) 336 err = enc.Encode(map[string]interface{}{ 337 "timestamp": timestamp, 338 "message": message, 339 "sid": sid, 340 "pid": pid, 341 }) 342 c.Assert(err, check.IsNil) 343 344 default: 345 c.Fatalf("expected to get 1 requests, now on %d", n+1) 346 } 347 n++ 348 }) 349 350 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"logs", "snap"}) 351 c.Assert(err, check.IsNil) 352 c.Assert(rest, check.HasLen, 0) 353 354 utcTime, err := time.Parse(time.RFC3339, timestamp) 355 c.Assert(err, check.IsNil) 356 localTime := utcTime.In(time.Local).Format(time.RFC3339) 357 358 c.Check(s.Stdout(), check.Equals, fmt.Sprintf("%s %s[%s]: %s\n", localTime, sid, pid, message)) 359 c.Check(s.Stderr(), check.Equals, "") 360 // ensure that the fake server api was actually hit 361 c.Check(n, check.Equals, 1) 362 } 363 364 func (s *appOpSuite) TestLogsCommandWithAbsTimeFlag(c *check.C) { 365 n := 0 366 timestamp := "2021-08-16T17:33:55Z" 367 message := "Thing occurred" 368 sid := "service1" 369 pid := "1000" 370 371 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { 372 switch n { 373 case 0: 374 c.Check(r.URL.Path, check.Equals, "/v2/logs") 375 c.Check(r.Method, check.Equals, "GET") 376 w.WriteHeader(200) 377 _, err := w.Write([]byte{0x1E}) 378 c.Assert(err, check.IsNil) 379 380 enc := json.NewEncoder(w) 381 err = enc.Encode(map[string]interface{}{ 382 "timestamp": timestamp, 383 "message": message, 384 "sid": sid, 385 "pid": pid, 386 }) 387 c.Assert(err, check.IsNil) 388 389 default: 390 c.Fatalf("expected to get 1 requests, now on %d", n+1) 391 } 392 n++ 393 }) 394 395 rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"logs", "snap", "--abs-time"}) 396 c.Assert(err, check.IsNil) 397 c.Assert(rest, check.HasLen, 0) 398 399 c.Check(s.Stdout(), check.Equals, fmt.Sprintf("%s %s[%s]: %s\n", timestamp, sid, pid, message)) 400 c.Check(s.Stderr(), check.Equals, "") 401 402 // ensure that the fake server api was actually hit 403 c.Check(n, check.Equals, 1) 404 }