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