github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  	"net/http"
    27  	"net/http/httptest"
    28  	"os"
    29  	"time"
    30  
    31  	. "gopkg.in/check.v1"
    32  
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/systemd"
    35  	"github.com/snapcore/snapd/testutil"
    36  	"github.com/snapcore/snapd/usersession/agent"
    37  )
    38  
    39  type restSuite struct {
    40  	testutil.BaseTest
    41  	sysdLog [][]string
    42  }
    43  
    44  var _ = Suite(&restSuite{})
    45  
    46  func (s *restSuite) SetUpTest(c *C) {
    47  	s.BaseTest.SetUpTest(c)
    48  	dirs.SetRootDir(c.MkDir())
    49  	xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
    50  	c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil)
    51  
    52  	s.sysdLog = nil
    53  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
    54  		s.sysdLog = append(s.sysdLog, cmd)
    55  		return []byte("ActiveState=inactive\n"), nil
    56  	})
    57  	s.AddCleanup(restore)
    58  	restore = systemd.MockStopDelays(time.Millisecond, 25*time.Second)
    59  	s.AddCleanup(restore)
    60  	restore = agent.MockStopTimeouts(20*time.Millisecond, time.Millisecond)
    61  	s.AddCleanup(restore)
    62  }
    63  
    64  func (s *restSuite) TearDownTest(c *C) {
    65  	dirs.SetRootDir("")
    66  	s.BaseTest.TearDownTest(c)
    67  }
    68  
    69  type resp struct {
    70  	Type   agent.ResponseType `json:"type"`
    71  	Result interface{}        `json:"result"`
    72  }
    73  
    74  func (s *restSuite) TestSessionInfo(c *C) {
    75  	// the agent.SessionInfo end point only supports GET requests
    76  	c.Check(agent.SessionInfoCmd.PUT, IsNil)
    77  	c.Check(agent.SessionInfoCmd.POST, IsNil)
    78  	c.Check(agent.SessionInfoCmd.DELETE, IsNil)
    79  	c.Assert(agent.SessionInfoCmd.GET, NotNil)
    80  
    81  	c.Check(agent.SessionInfoCmd.Path, Equals, "/v1/session-info")
    82  
    83  	a, err := agent.New()
    84  	c.Assert(err, IsNil)
    85  	a.Version = "42b1"
    86  	rec := httptest.NewRecorder()
    87  	agent.SessionInfoCmd.GET(agent.SessionInfoCmd, nil).ServeHTTP(rec, nil)
    88  	c.Check(rec.Code, Equals, 200)
    89  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
    90  
    91  	var rsp resp
    92  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
    93  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
    94  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
    95  		"version": "42b1",
    96  	})
    97  }
    98  
    99  func (s *restSuite) TestServiceControl(c *C) {
   100  	// the agent.Services end point only supports POST requests
   101  	c.Assert(agent.ServiceControlCmd.GET, IsNil)
   102  	c.Check(agent.ServiceControlCmd.PUT, IsNil)
   103  	c.Check(agent.ServiceControlCmd.POST, NotNil)
   104  	c.Check(agent.ServiceControlCmd.DELETE, IsNil)
   105  
   106  	c.Check(agent.ServiceControlCmd.Path, Equals, "/v1/service-control")
   107  }
   108  
   109  func (s *restSuite) TestServiceControlDaemonReload(c *C) {
   110  	s.testServiceControlDaemonReload(c, "application/json")
   111  }
   112  
   113  func (s *restSuite) TestServiceControlDaemonReloadComplexerContentType(c *C) {
   114  	s.testServiceControlDaemonReload(c, "application/json; charset=utf-8")
   115  }
   116  
   117  func (s *restSuite) TestServiceControlDaemonReloadInvalidCharset(c *C) {
   118  	_, err := agent.New()
   119  	c.Assert(err, IsNil)
   120  
   121  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"daemon-reload"}`))
   122  	req.Header.Set("Content-Type", "application/json; charset=iso-8859-1")
   123  	c.Assert(err, IsNil)
   124  	rec := httptest.NewRecorder()
   125  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   126  	c.Check(rec.Code, Equals, 400)
   127  	c.Check(rec.Body.String(), testutil.Contains,
   128  		"unknown charset in content type")
   129  }
   130  
   131  func (s *restSuite) testServiceControlDaemonReload(c *C, contentType string) {
   132  	_, err := agent.New()
   133  	c.Assert(err, IsNil)
   134  
   135  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"daemon-reload"}`))
   136  	req.Header.Set("Content-Type", contentType)
   137  	c.Assert(err, IsNil)
   138  	rec := httptest.NewRecorder()
   139  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   140  	c.Check(rec.Code, Equals, 200)
   141  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   142  
   143  	var rsp resp
   144  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   145  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   146  	c.Check(rsp.Result, IsNil)
   147  
   148  	c.Check(s.sysdLog, DeepEquals, [][]string{
   149  		{"--user", "daemon-reload"},
   150  	})
   151  }
   152  
   153  func (s *restSuite) TestServiceControlStart(c *C) {
   154  	_, err := agent.New()
   155  	c.Assert(err, IsNil)
   156  
   157  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "snap.bar.service"]}`))
   158  	req.Header.Set("Content-Type", "application/json")
   159  	c.Assert(err, IsNil)
   160  	rec := httptest.NewRecorder()
   161  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   162  	c.Check(rec.Code, Equals, 200)
   163  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   164  
   165  	var rsp resp
   166  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   167  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   168  	c.Check(rsp.Result, Equals, nil)
   169  
   170  	c.Check(s.sysdLog, DeepEquals, [][]string{
   171  		{"--user", "start", "snap.foo.service"},
   172  		{"--user", "start", "snap.bar.service"},
   173  	})
   174  }
   175  
   176  func (s *restSuite) TestServicesStartNonSnap(c *C) {
   177  	_, err := agent.New()
   178  	c.Assert(err, IsNil)
   179  
   180  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "not-snap.bar.service"]}`))
   181  	req.Header.Set("Content-Type", "application/json")
   182  	c.Assert(err, IsNil)
   183  	rec := httptest.NewRecorder()
   184  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   185  	c.Check(rec.Code, Equals, 500)
   186  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   187  
   188  	var rsp resp
   189  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   190  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   191  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   192  		"message": "cannot start non-snap service not-snap.bar.service",
   193  	})
   194  
   195  	// No services were started on the error.
   196  	c.Check(s.sysdLog, HasLen, 0)
   197  }
   198  
   199  func (s *restSuite) TestServicesStartFailureStopsServices(c *C) {
   200  	var sysdLog [][]string
   201  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
   202  		sysdLog = append(sysdLog, cmd)
   203  		if cmd[0] == "--user" && cmd[1] == "start" && cmd[2] == "snap.bar.service" {
   204  			return nil, fmt.Errorf("start failure")
   205  		}
   206  		return []byte("ActiveState=inactive\n"), nil
   207  	})
   208  	defer restore()
   209  
   210  	_, err := agent.New()
   211  	c.Assert(err, IsNil)
   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  	_, err := agent.New()
   258  	c.Assert(err, IsNil)
   259  
   260  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "snap.bar.service"]}`))
   261  	req.Header.Set("Content-Type", "application/json")
   262  	c.Assert(err, IsNil)
   263  	rec := httptest.NewRecorder()
   264  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   265  	c.Check(rec.Code, Equals, 500)
   266  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   267  
   268  	var rsp resp
   269  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   270  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   271  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   272  		"message": "some user services failed to start",
   273  		"kind":    "service-control",
   274  		"value": map[string]interface{}{
   275  			"start-errors": map[string]interface{}{
   276  				"snap.bar.service": "start failure",
   277  			},
   278  			"stop-errors": map[string]interface{}{
   279  				"snap.foo.service": "stop failure",
   280  			},
   281  		},
   282  	})
   283  
   284  	c.Check(sysdLog, DeepEquals, [][]string{
   285  		{"--user", "start", "snap.foo.service"},
   286  		{"--user", "start", "snap.bar.service"},
   287  		{"--user", "stop", "snap.foo.service"},
   288  	})
   289  }
   290  
   291  func (s *restSuite) TestServicesStop(c *C) {
   292  	_, err := agent.New()
   293  	c.Assert(err, IsNil)
   294  
   295  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "snap.bar.service"]}`))
   296  	req.Header.Set("Content-Type", "application/json")
   297  	c.Assert(err, IsNil)
   298  	rec := httptest.NewRecorder()
   299  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   300  	c.Check(rec.Code, Equals, 200)
   301  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   302  
   303  	var rsp resp
   304  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   305  	c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
   306  	c.Check(rsp.Result, Equals, nil)
   307  
   308  	c.Check(s.sysdLog, DeepEquals, [][]string{
   309  		{"--user", "stop", "snap.foo.service"},
   310  		{"--user", "show", "--property=ActiveState", "snap.foo.service"},
   311  		{"--user", "stop", "snap.bar.service"},
   312  		{"--user", "show", "--property=ActiveState", "snap.bar.service"},
   313  	})
   314  }
   315  
   316  func (s *restSuite) TestServicesStopNonSnap(c *C) {
   317  	_, err := agent.New()
   318  	c.Assert(err, IsNil)
   319  
   320  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "not-snap.bar.service"]}`))
   321  	req.Header.Set("Content-Type", "application/json")
   322  	c.Assert(err, IsNil)
   323  	rec := httptest.NewRecorder()
   324  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   325  	c.Check(rec.Code, Equals, 500)
   326  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   327  
   328  	var rsp resp
   329  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   330  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   331  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   332  		"message": "cannot stop non-snap service not-snap.bar.service",
   333  	})
   334  
   335  	// No services were started on the error.
   336  	c.Check(s.sysdLog, HasLen, 0)
   337  }
   338  
   339  func (s *restSuite) TestServicesStopReportsTimeout(c *C) {
   340  	var sysdLog [][]string
   341  	restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
   342  		// Ignore "show" spam
   343  		if cmd[1] != "show" {
   344  			sysdLog = append(sysdLog, cmd)
   345  		}
   346  		if cmd[len(cmd)-1] == "snap.bar.service" {
   347  			return []byte("ActiveState=active\n"), nil
   348  		}
   349  		return []byte("ActiveState=inactive\n"), nil
   350  	})
   351  	defer restore()
   352  
   353  	_, err := agent.New()
   354  	c.Assert(err, IsNil)
   355  
   356  	req, err := http.NewRequest("POST", "/v1/service-control", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "snap.bar.service"]}`))
   357  	req.Header.Set("Content-Type", "application/json")
   358  	c.Assert(err, IsNil)
   359  	rec := httptest.NewRecorder()
   360  	agent.ServiceControlCmd.POST(agent.ServiceControlCmd, req).ServeHTTP(rec, req)
   361  	c.Check(rec.Code, Equals, 500)
   362  	c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")
   363  
   364  	var rsp resp
   365  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
   366  	c.Check(rsp.Type, Equals, agent.ResponseTypeError)
   367  	c.Check(rsp.Result, DeepEquals, map[string]interface{}{
   368  		"message": "some user services failed to stop",
   369  		"kind":    "service-control",
   370  		"value": map[string]interface{}{
   371  			"stop-errors": map[string]interface{}{
   372  				"snap.bar.service": "snap.bar.service failed to stop: timeout",
   373  			},
   374  		},
   375  	})
   376  
   377  	c.Check(sysdLog, DeepEquals, [][]string{
   378  		{"--user", "stop", "snap.foo.service"},
   379  		{"--user", "stop", "snap.bar.service"},
   380  	})
   381  }