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  }