github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/usersession/agent/rest_api_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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 agent_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"os"
    30  	"path/filepath"
    31  	"time"
    32  
    33  	. "gopkg.in/check.v1"
    34  
    35  	"github.com/godbus/dbus"
    36  	"github.com/snapcore/snapd/dbusutil"
    37  	"github.com/snapcore/snapd/dbusutil/dbustest"
    38  	"github.com/snapcore/snapd/desktop/notification"
    39  	"github.com/snapcore/snapd/dirs"
    40  	"github.com/snapcore/snapd/systemd"
    41  	"github.com/snapcore/snapd/testutil"
    42  	"github.com/snapcore/snapd/usersession/agent"
    43  	"github.com/snapcore/snapd/usersession/client"
    44  )
    45  
    46  type restSuite struct {
    47  	testutil.BaseTest
    48  	testutil.DBusTest
    49  	sysdLog [][]string
    50  	agent   *agent.SessionAgent
    51  }
    52  
    53  var _ = Suite(&restSuite{})
    54  
    55  func (s *restSuite) SetUpTest(c *C) {
    56  	s.BaseTest.SetUpTest(c)
    57  	s.DBusTest.SetUpTest(c)
    58  	dirs.SetRootDir(c.MkDir())
    59  	xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
    60  	c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil)
    61  
    62  	s.sysdLog = nil
    63  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
    64  		s.sysdLog = append(s.sysdLog, cmd)
    65  		return []byte("ActiveState=inactive\n"), nil
    66  	})
    67  	s.AddCleanup(restore)
    68  	restore = systemd.MockStopDelays(time.Millisecond, 25*time.Second)
    69  	s.AddCleanup(restore)
    70  	restore = agent.MockStopTimeouts(20*time.Millisecond, time.Millisecond)
    71  	s.AddCleanup(restore)
    72  
    73  	var err error
    74  	s.agent, err = agent.New()
    75  	c.Assert(err, IsNil)
    76  	s.agent.Start()
    77  	s.AddCleanup(func() { s.agent.Stop() })
    78  }
    79  
    80  func (s *restSuite) TearDownTest(c *C) {
    81  	dirs.SetRootDir("")
    82  	s.DBusTest.TearDownTest(c)
    83  	s.BaseTest.TearDownTest(c)
    84  }
    85  
    86  type resp struct {
    87  	Type   agent.ResponseType `json:"type"`
    88  	Result interface{}        `json:"result"`
    89  }
    90  
    91  func (s *restSuite) TestSessionInfo(c *C) {
    92  	// the agent.SessionInfo end point only supports GET requests
    93  	c.Check(agent.SessionInfoCmd.PUT, IsNil)
    94  	c.Check(agent.SessionInfoCmd.POST, IsNil)
    95  	c.Check(agent.SessionInfoCmd.DELETE, IsNil)
    96  	c.Assert(agent.SessionInfoCmd.GET, NotNil)
    97  
    98  	c.Check(agent.SessionInfoCmd.Path, Equals, "/v1/session-info")
    99  
   100  	s.agent.Version = "42b1"
   101  	rec := httptest.NewRecorder()
   102  	agent.SessionInfoCmd.GET(agent.SessionInfoCmd, nil).ServeHTTP(rec, nil)
   103  	c.Check(rec.Code, Equals, 200)
   104  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   105  
   106  	var rsp resp
   107  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   108  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   109  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   110  		"version": "42b1",
   111  	})
   112  }
   113  
   114  func (s *restSuite) TestServiceControl(c *C) {
   115  	// the agent.Services end point only supports POST requests
   116  	c.Assert(agent.ServiceControlCmd.GET, IsNil)
   117  	c.Check(agent.ServiceControlCmd.PUT, IsNil)
   118  	c.Check(agent.ServiceControlCmd.POST, NotNil)
   119  	c.Check(agent.ServiceControlCmd.DELETE, IsNil)
   120  
   121  	c.Check(agent.ServiceControlCmd.Path, Equals, "/v1/service-control")
   122  }
   123  
   124  func (s *restSuite) TestServiceControlDaemonReload(c *C) {
   125  	s.testServiceControlDaemonReload(c, "application/json")
   126  }
   127  
   128  func (s *restSuite) TestServiceControlDaemonReloadComplexerContentType(c *C) {
   129  	s.testServiceControlDaemonReload(c, "application/json; charset=utf-8")
   130  }
   131  
   132  func (s *restSuite) TestServiceControlDaemonReloadInvalidCharset(c *C) {
   133  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"daemon-reload"}`))
   134  	req.Header.Set("Content-Type", "application/json; charset=iso-8859-1")
   135  	c.Assert(err, IsNil)
   136  	rec := httptest.NewRecorder()
   137  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   138  	c.Check(rec.Code, Equals, 400)
   139  	c.Check(rec.Body.String(), testutil.Contains,
   140  		"unknown charset in content type")
   141  }
   142  
   143  func (s *restSuite) testServiceControlDaemonReload(c *C, contentType string) {
   144  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"daemon-reload"}`))
   145  	req.Header.Set("Content-Type", contentType)
   146  	c.Assert(err, IsNil)
   147  	rec := httptest.NewRecorder()
   148  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   149  	c.Check(rec.Code, Equals, 200)
   150  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   151  
   152  	var rsp resp
   153  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   154  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   155  	c.Check(rsp.Result, IsNil)
   156  
   157  	c.Check(s.sysdLog, DeepEquals, [][]string{
   158  		{"--user", "daemon-reload"},
   159  	})
   160  }
   161  
   162  func (s *restSuite) TestServiceControlStart(c *C) {
   163  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "snap.bar.service"]}`))
   164  	req.Header.Set("Content-Type", "application/json")
   165  	c.Assert(err, IsNil)
   166  	rec := httptest.NewRecorder()
   167  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   168  	c.Check(rec.Code, Equals, 200)
   169  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   170  
   171  	var rsp resp
   172  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   173  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   174  	c.Check(rsp.Result, Equals, nil)
   175  
   176  	c.Check(s.sysdLog, DeepEquals, [][]string{
   177  		{"--user", "start", "snap.foo.service"},
   178  		{"--user", "start", "snap.bar.service"},
   179  	})
   180  }
   181  
   182  func (s *restSuite) TestServicesStartNonSnap(c *C) {
   183  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "not-snap.bar.service"]}`))
   184  	req.Header.Set("Content-Type", "application/json")
   185  	c.Assert(err, IsNil)
   186  	rec := httptest.NewRecorder()
   187  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   188  	c.Check(rec.Code, Equals, 500)
   189  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   190  
   191  	var rsp resp
   192  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   193  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   194  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   195  		"message": "cannot start non-snap service not-snap.bar.service",
   196  	})
   197  
   198  	// No services were started on the error.
   199  	c.Check(s.sysdLog, HasLen, 0)
   200  }
   201  
   202  func (s *restSuite) TestServicesStartFailureStopsServices(c *C) {
   203  	var sysdLog [][]string
   204  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
   205  		sysdLog = append(sysdLog, cmd)
   206  		if cmd[0] == "--user" && cmd[1] == "start" && cmd[2] == "snap.bar.service" {
   207  			return nil, fmt.Errorf("start failure")
   208  		}
   209  		return []byte("ActiveState=inactive\n"), nil
   210  	})
   211  	defer restore()
   212  
   213  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "snap.bar.service"]}`))
   214  	req.Header.Set("Content-Type", "application/json")
   215  	c.Assert(err, IsNil)
   216  	rec := httptest.NewRecorder()
   217  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   218  	c.Check(rec.Code, Equals, 500)
   219  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   220  
   221  	var rsp resp
   222  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   223  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   224  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   225  		"message": "some user services failed to start",
   226  		"kind":    "service-control",
   227  		"value": map[string]interface{}{
   228  			"start-errors": map[string]interface{}{
   229  				"snap.bar.service": "start failure",
   230  			},
   231  			"stop-errors": map[string]interface{}{},
   232  		},
   233  	})
   234  
   235  	c.Check(sysdLog, DeepEquals, [][]string{
   236  		{"--user", "start", "snap.foo.service"},
   237  		{"--user", "start", "snap.bar.service"},
   238  		{"--user", "stop", "snap.foo.service"},
   239  		{"--user", "show", "--property=ActiveState", "snap.foo.service"},
   240  	})
   241  }
   242  
   243  func (s *restSuite) TestServicesStartFailureReportsStopFailures(c *C) {
   244  	var sysdLog [][]string
   245  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
   246  		sysdLog = append(sysdLog, cmd)
   247  		if cmd[0] == "--user" && cmd[1] == "start" && cmd[2] == "snap.bar.service" {
   248  			return nil, fmt.Errorf("start failure")
   249  		}
   250  		if cmd[0] == "--user" && cmd[1] == "stop" && cmd[2] == "snap.foo.service" {
   251  			return nil, fmt.Errorf("stop failure")
   252  		}
   253  		return []byte("ActiveState=inactive\n"), nil
   254  	})
   255  	defer restore()
   256  
   257  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "snap.bar.service"]}`))
   258  	req.Header.Set("Content-Type", "application/json")
   259  	c.Assert(err, IsNil)
   260  	rec := httptest.NewRecorder()
   261  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   262  	c.Check(rec.Code, Equals, 500)
   263  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   264  
   265  	var rsp resp
   266  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   267  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   268  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   269  		"message": "some user services failed to start",
   270  		"kind":    "service-control",
   271  		"value": map[string]interface{}{
   272  			"start-errors": map[string]interface{}{
   273  				"snap.bar.service": "start failure",
   274  			},
   275  			"stop-errors": map[string]interface{}{
   276  				"snap.foo.service": "stop failure",
   277  			},
   278  		},
   279  	})
   280  
   281  	c.Check(sysdLog, DeepEquals, [][]string{
   282  		{"--user", "start", "snap.foo.service"},
   283  		{"--user", "start", "snap.bar.service"},
   284  		{"--user", "stop", "snap.foo.service"},
   285  	})
   286  }
   287  
   288  func (s *restSuite) TestServicesStop(c *C) {
   289  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "snap.bar.service"]}`))
   290  	req.Header.Set("Content-Type", "application/json")
   291  	c.Assert(err, IsNil)
   292  	rec := httptest.NewRecorder()
   293  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   294  	c.Check(rec.Code, Equals, 200)
   295  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   296  
   297  	var rsp resp
   298  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   299  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   300  	c.Check(rsp.Result, Equals, nil)
   301  
   302  	c.Check(s.sysdLog, DeepEquals, [][]string{
   303  		{"--user", "stop", "snap.foo.service"},
   304  		{"--user", "show", "--property=ActiveState", "snap.foo.service"},
   305  		{"--user", "stop", "snap.bar.service"},
   306  		{"--user", "show", "--property=ActiveState", "snap.bar.service"},
   307  	})
   308  }
   309  
   310  func (s *restSuite) TestServicesStopNonSnap(c *C) {
   311  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "not-snap.bar.service"]}`))
   312  	req.Header.Set("Content-Type", "application/json")
   313  	c.Assert(err, IsNil)
   314  	rec := httptest.NewRecorder()
   315  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   316  	c.Check(rec.Code, Equals, 500)
   317  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   318  
   319  	var rsp resp
   320  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   321  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   322  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   323  		"message": "cannot stop non-snap service not-snap.bar.service",
   324  	})
   325  
   326  	// No services were started on the error.
   327  	c.Check(s.sysdLog, HasLen, 0)
   328  }
   329  
   330  func (s *restSuite) TestServicesStopReportsTimeout(c *C) {
   331  	var sysdLog [][]string
   332  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
   333  		// Ignore "show" spam
   334  		if cmd[1] != "show" {
   335  			sysdLog = append(sysdLog, cmd)
   336  		}
   337  		if cmd[len(cmd)-1] == "snap.bar.service" {
   338  			return []byte("ActiveState=active\n"), nil
   339  		}
   340  		return []byte("ActiveState=inactive\n"), nil
   341  	})
   342  	defer restore()
   343  
   344  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "snap.bar.service"]}`))
   345  	req.Header.Set("Content-Type", "application/json")
   346  	c.Assert(err, IsNil)
   347  	rec := httptest.NewRecorder()
   348  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   349  	c.Check(rec.Code, Equals, 500)
   350  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   351  
   352  	var rsp resp
   353  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   354  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   355  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   356  		"message": "some user services failed to stop",
   357  		"kind":    "service-control",
   358  		"value": map[string]interface{}{
   359  			"stop-errors": map[string]interface{}{
   360  				"snap.bar.service": "snap.bar.service failed to stop: timeout",
   361  			},
   362  		},
   363  	})
   364  
   365  	c.Check(sysdLog, DeepEquals, [][]string{
   366  		{"--user", "stop", "snap.foo.service"},
   367  		{"--user", "stop", "snap.bar.service"},
   368  	})
   369  }
   370  
   371  func (s *restSuite) TestPostPendingRefreshNotificationMalformedContentType(c *C) {
   372  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBufferString(""))
   373  	req.Header.Set("Content-Type", "text/plain/joke")
   374  	c.Assert(err, IsNil)
   375  	rec := httptest.NewRecorder()
   376  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   377  	c.Check(rec.Code, Equals, 400)
   378  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   379  
   380  	var rsp resp
   381  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   382  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   383  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot parse content type: mime: unexpected content after media subtype"})
   384  }
   385  
   386  func (s *restSuite) TestPostPendingRefreshNotificationUnsupportedContentType(c *C) {
   387  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBufferString(""))
   388  	req.Header.Set("Content-Type", "text/plain")
   389  	c.Assert(err, IsNil)
   390  	rec := httptest.NewRecorder()
   391  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   392  	c.Check(rec.Code, Equals, 400)
   393  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   394  
   395  	var rsp resp
   396  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   397  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   398  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "unknown content type: text/plain"})
   399  }
   400  
   401  func (s *restSuite) TestPostPendingRefreshNotificationUnsupportedContentEncoding(c *C) {
   402  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBufferString(""))
   403  	req.Header.Set("Content-Type", "application/json; charset=EBCDIC")
   404  	c.Assert(err, IsNil)
   405  	rec := httptest.NewRecorder()
   406  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   407  	c.Check(rec.Code, Equals, 400)
   408  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   409  
   410  	var rsp resp
   411  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   412  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   413  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "unknown charset in content type: application/json; charset=EBCDIC"})
   414  }
   415  
   416  func (s *restSuite) TestPostPendingRefreshNotificationMalformedRequestBody(c *C) {
   417  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh",
   418  		bytes.NewBufferString(`{"instance-name":syntaxerror}`))
   419  	req.Header.Set("Content-Type", "application/json")
   420  	c.Assert(err, IsNil)
   421  	rec := httptest.NewRecorder()
   422  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   423  	c.Check(rec.Code, Equals, 400)
   424  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   425  
   426  	var rsp resp
   427  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   428  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   429  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot decode request body into pending snap refresh info: invalid character 's' looking for beginning of value"})
   430  }
   431  
   432  func (s *restSuite) TestPostPendingRefreshNotificationNoSessionBus(c *C) {
   433  	noDBus := func() (*dbus.Conn, error) {
   434  		return nil, fmt.Errorf("cannot find bus")
   435  	}
   436  	restore := dbusutil.MockConnections(noDBus, noDBus)
   437  	defer restore()
   438  
   439  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh",
   440  		bytes.NewBufferString(`{"instance-name":"pkg"}`))
   441  	req.Header.Set("Content-Type", "application/json")
   442  	c.Assert(err, IsNil)
   443  	rec := httptest.NewRecorder()
   444  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   445  	c.Check(rec.Code, Equals, 500)
   446  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   447  
   448  	var rsp resp
   449  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   450  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   451  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot connect to the session bus: cannot find bus"})
   452  }
   453  
   454  func (s *restSuite) testPostPendingRefreshNotificationBody(c *C, refreshInfo *client.PendingSnapRefreshInfo, checkMsg func(c *C, msg *dbus.Message)) {
   455  	conn, err := dbustest.Connection(func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   456  		if checkMsg != nil {
   457  			checkMsg(c, msg)
   458  		}
   459  		responseSig := dbus.SignatureOf(uint32(0))
   460  		response := &dbus.Message{
   461  			Type: dbus.TypeMethodReply,
   462  			Headers: map[dbus.HeaderField]dbus.Variant{
   463  				dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   464  				dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   465  				// dbus.FieldDestination is provided automatically by DBus test helper.
   466  				dbus.FieldSignature: dbus.MakeVariant(responseSig),
   467  			},
   468  			Body: []interface{}{uint32(7)}, // NotificationID (ignored for now)
   469  		}
   470  		return []*dbus.Message{response}, nil
   471  	})
   472  	c.Assert(err, IsNil)
   473  	restore := dbusutil.MockOnlySessionBusAvailable(conn)
   474  	defer restore()
   475  
   476  	reqBody, err := json.Marshal(refreshInfo)
   477  	c.Assert(err, IsNil)
   478  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBuffer(reqBody))
   479  	req.Header.Set("Content-Type", "application/json")
   480  	c.Assert(err, IsNil)
   481  	rec := httptest.NewRecorder()
   482  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   483  	c.Check(rec.Code, Equals, 200)
   484  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   485  
   486  	var rsp resp
   487  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   488  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   489  	c.Check(rsp.Result, IsNil)
   490  }
   491  
   492  func (s *restSuite) TestPostPendingRefreshNotificationHappeningNow(c *C) {
   493  	refreshInfo := &client.PendingSnapRefreshInfo{InstanceName: "pkg"}
   494  	s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) {
   495  		c.Check(msg.Body[0], Equals, "")
   496  		c.Check(msg.Body[1], Equals, uint32(0))
   497  		c.Check(msg.Body[2], Equals, "")
   498  		c.Check(msg.Body[3], Equals, `Snap "pkg" is refreshing now!`)
   499  		c.Check(msg.Body[4], Equals, "")
   500  		c.Check(msg.Body[5], HasLen, 0)
   501  		c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   502  			"urgency":       dbus.MakeVariant(byte(notification.CriticalUrgency)),
   503  			"desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"),
   504  		})
   505  		c.Check(msg.Body[7], Equals, int32(0))
   506  	})
   507  }
   508  
   509  func (s *restSuite) TestPostPendingRefreshNotificationFewDays(c *C) {
   510  	refreshInfo := &client.PendingSnapRefreshInfo{
   511  		InstanceName:  "pkg",
   512  		TimeRemaining: time.Hour * 72,
   513  	}
   514  	s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) {
   515  		c.Check(msg.Body[3], Equals, `Pending update of "pkg" snap`)
   516  		c.Check(msg.Body[4], Equals, "Close the app to avoid disruptions (3 days left)")
   517  		c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   518  			"urgency":       dbus.MakeVariant(byte(notification.LowUrgency)),
   519  			"desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"),
   520  		})
   521  		c.Check(msg.Body[7], Equals, int32(0))
   522  	})
   523  }
   524  
   525  func (s *restSuite) TestPostPendingRefreshNotificationFewHours(c *C) {
   526  	refreshInfo := &client.PendingSnapRefreshInfo{
   527  		InstanceName:  "pkg",
   528  		TimeRemaining: time.Hour * 7,
   529  	}
   530  	s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) {
   531  		// boring stuff is checked above
   532  		c.Check(msg.Body[3], Equals, `Pending update of "pkg" snap`)
   533  		c.Check(msg.Body[4], Equals, "Close the app to avoid disruptions (7 hours left)")
   534  		c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   535  			"urgency":       dbus.MakeVariant(byte(notification.NormalUrgency)),
   536  			"desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"),
   537  		})
   538  	})
   539  }
   540  
   541  func (s *restSuite) TestPostPendingRefreshNotificationFewMinutes(c *C) {
   542  	refreshInfo := &client.PendingSnapRefreshInfo{
   543  		InstanceName:  "pkg",
   544  		TimeRemaining: time.Minute * 15,
   545  	}
   546  	s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) {
   547  		// boring stuff is checked above
   548  		c.Check(msg.Body[3], Equals, `Pending update of "pkg" snap`)
   549  		c.Check(msg.Body[4], Equals, "Close the app to avoid disruptions (15 minutes left)")
   550  		c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   551  			"urgency":       dbus.MakeVariant(byte(notification.CriticalUrgency)),
   552  			"desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"),
   553  		})
   554  	})
   555  }
   556  
   557  func (s *restSuite) TestPostPendingRefreshNotificationBusyAppDesktopFile(c *C) {
   558  	refreshInfo := &client.PendingSnapRefreshInfo{
   559  		InstanceName:        "pkg",
   560  		BusyAppName:         "app",
   561  		BusyAppDesktopEntry: "pkg_app",
   562  	}
   563  	err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755)
   564  	c.Assert(err, IsNil)
   565  	desktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "pkg_app.desktop")
   566  	err = ioutil.WriteFile(desktopFilePath, []byte(`
   567  [Desktop Entry]
   568  Icon=app.png
   569  	`), 0644)
   570  	c.Assert(err, IsNil)
   571  
   572  	s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) {
   573  		// boring stuff is checked above
   574  		c.Check(msg.Body[2], Equals, "app.png")
   575  		c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   576  			"urgency":       dbus.MakeVariant(byte(notification.CriticalUrgency)),
   577  			"desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"),
   578  		})
   579  	})
   580  }
   581  
   582  func (s *restSuite) TestPostPendingRefreshNotificationBusyAppMalformedDesktopFile(c *C) {
   583  	refreshInfo := &client.PendingSnapRefreshInfo{
   584  		InstanceName:        "pkg",
   585  		BusyAppName:         "app",
   586  		BusyAppDesktopEntry: "pkg_app",
   587  	}
   588  	err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755)
   589  	c.Assert(err, IsNil)
   590  	desktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "pkg_app.desktop")
   591  	err = ioutil.WriteFile(desktopFilePath, []byte(`garbage!`), 0644)
   592  	c.Assert(err, IsNil)
   593  
   594  	s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) {
   595  		// boring stuff is checked above
   596  		c.Check(msg.Body[2], Equals, "") // Icon is not provided
   597  		c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   598  			"desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"),
   599  			"urgency":       dbus.MakeVariant(byte(notification.CriticalUrgency)),
   600  		})
   601  	})
   602  }
   603  
   604  func (s *restSuite) TestPostPendingRefreshNotificationNoNotificationServer(c *C) {
   605  	conn, err := dbustest.Connection(func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   606  		response := &dbus.Message{
   607  			Type: dbus.TypeError,
   608  			Headers: map[dbus.HeaderField]dbus.Variant{
   609  				dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   610  				dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   611  				// dbus.FieldDestination is provided automatically by DBus test helper.
   612  				dbus.FieldErrorName: dbus.MakeVariant("org.freedesktop.DBus.Error.NameHasNoOwner"),
   613  			},
   614  		}
   615  		return []*dbus.Message{response}, nil
   616  	})
   617  	c.Assert(err, IsNil)
   618  	restore := dbusutil.MockOnlySessionBusAvailable(conn)
   619  	defer restore()
   620  
   621  	refreshInfo := &client.PendingSnapRefreshInfo{
   622  		InstanceName: "pkg",
   623  	}
   624  	reqBody, err := json.Marshal(refreshInfo)
   625  	c.Assert(err, IsNil)
   626  	req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBuffer(reqBody))
   627  	req.Header.Set("Content-Type", "application/json")
   628  	c.Assert(err, IsNil)
   629  	rec := httptest.NewRecorder()
   630  	agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req)
   631  	c.Check(rec.Code, Equals, 500)
   632  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   633  
   634  	var rsp resp
   635  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   636  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   637  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot send notification message: org.freedesktop.DBus.Error.NameHasNoOwner"})
   638  }