github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/daemon/api_general_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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 daemon_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"net/url"
    30  	"time"
    31  
    32  	"gopkg.in/check.v1"
    33  
    34  	"github.com/snapcore/snapd/arch"
    35  	"github.com/snapcore/snapd/boot"
    36  	"github.com/snapcore/snapd/daemon"
    37  	"github.com/snapcore/snapd/dirs"
    38  	"github.com/snapcore/snapd/interfaces/ifacetest"
    39  	"github.com/snapcore/snapd/overlord/auth"
    40  	"github.com/snapcore/snapd/overlord/configstate/config"
    41  	"github.com/snapcore/snapd/overlord/state"
    42  	"github.com/snapcore/snapd/release"
    43  	"github.com/snapcore/snapd/sandbox"
    44  )
    45  
    46  var _ = check.Suite(&generalSuite{})
    47  
    48  type generalSuite struct {
    49  	apiBaseSuite
    50  }
    51  
    52  func (s *generalSuite) TestRoot(c *check.C) {
    53  	s.daemon(c)
    54  
    55  	req, err := http.NewRequest("GET", "/", nil)
    56  	c.Assert(err, check.IsNil)
    57  
    58  	// check it only does GET
    59  	s.checkGetOnly(c, req)
    60  
    61  	rec := httptest.NewRecorder()
    62  	s.req(c, req, nil).ServeHTTP(rec, nil)
    63  	c.Check(rec.Code, check.Equals, 200)
    64  	c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
    65  
    66  	expected := []interface{}{"TBD"}
    67  	var rsp daemon.RespJSON
    68  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
    69  	c.Check(rsp.Status, check.Equals, 200)
    70  	c.Check(rsp.Result, check.DeepEquals, expected)
    71  }
    72  
    73  func (s *generalSuite) TestSysInfo(c *check.C) {
    74  	req, err := http.NewRequest("GET", "/v2/system-info", nil)
    75  	c.Assert(err, check.IsNil)
    76  
    77  	d := s.daemon(c)
    78  	d.Version = "42b1"
    79  
    80  	// check it only does GET
    81  	s.checkGetOnly(c, req)
    82  
    83  	// set both legacy and new refresh schedules. new one takes priority
    84  	st := d.Overlord().State()
    85  	st.Lock()
    86  	tr := config.NewTransaction(st)
    87  	tr.Set("core", "refresh.schedule", "00:00-9:00/12:00-13:00")
    88  	tr.Set("core", "refresh.timer", "8:00~9:00/2")
    89  	tr.Commit()
    90  	st.Unlock()
    91  
    92  	restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"})
    93  	defer restore()
    94  	restore = release.MockOnClassic(true)
    95  	defer restore()
    96  	restore = sandbox.MockForceDevMode(true)
    97  	defer restore()
    98  	// reload dirs for release info to have effect
    99  	dirs.SetRootDir(dirs.GlobalRootDir)
   100  	restore = daemon.MockSystemdVirt("magic")
   101  	defer restore()
   102  
   103  	buildID := "this-is-my-build-id"
   104  	restore = daemon.MockBuildID(buildID)
   105  	defer restore()
   106  
   107  	rec := httptest.NewRecorder()
   108  	s.req(c, req, nil).ServeHTTP(rec, nil)
   109  	c.Check(rec.Code, check.Equals, 200)
   110  	c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
   111  
   112  	expected := map[string]interface{}{
   113  		"series":  "16",
   114  		"version": "42b1",
   115  		"os-release": map[string]interface{}{
   116  			"id":         "distro-id",
   117  			"version-id": "1.2",
   118  		},
   119  		"build-id":   buildID,
   120  		"on-classic": true,
   121  		"managed":    false,
   122  		"locations": map[string]interface{}{
   123  			"snap-mount-dir": dirs.SnapMountDir,
   124  			"snap-bin-dir":   dirs.SnapBinariesDir,
   125  		},
   126  		"refresh": map[string]interface{}{
   127  			// only the "timer" field
   128  			"timer": "8:00~9:00/2",
   129  		},
   130  		"confinement":      "partial",
   131  		"sandbox-features": map[string]interface{}{"confinement-options": []interface{}{"classic", "devmode"}},
   132  		"architecture":     arch.DpkgArchitecture(),
   133  		"virtualization":   "magic",
   134  		"system-mode":      "run",
   135  	}
   136  	var rsp daemon.RespJSON
   137  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
   138  	c.Check(rsp.Status, check.Equals, 200)
   139  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   140  	// Ensure that we had a kernel-verrsion but don't check the actual value.
   141  	const kernelVersionKey = "kernel-version"
   142  	c.Check(rsp.Result.(map[string]interface{})[kernelVersionKey], check.Not(check.Equals), "")
   143  	delete(rsp.Result.(map[string]interface{}), kernelVersionKey)
   144  	c.Check(rsp.Result, check.DeepEquals, expected)
   145  }
   146  
   147  func (s *generalSuite) TestSysInfoLegacyRefresh(c *check.C) {
   148  	req, err := http.NewRequest("GET", "/v2/system-info", nil)
   149  	c.Assert(err, check.IsNil)
   150  
   151  	d := s.daemon(c)
   152  	d.Version = "42b1"
   153  
   154  	restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"})
   155  	defer restore()
   156  	restore = release.MockOnClassic(true)
   157  	defer restore()
   158  	restore = sandbox.MockForceDevMode(true)
   159  	defer restore()
   160  	restore = daemon.MockSystemdVirt("kvm")
   161  	defer restore()
   162  	// reload dirs for release info to have effect
   163  	dirs.SetRootDir(dirs.GlobalRootDir)
   164  
   165  	// set the legacy refresh schedule
   166  	st := d.Overlord().State()
   167  	st.Lock()
   168  	tr := config.NewTransaction(st)
   169  	tr.Set("core", "refresh.schedule", "00:00-9:00/12:00-13:00")
   170  	tr.Set("core", "refresh.timer", "")
   171  	tr.Commit()
   172  	st.Unlock()
   173  
   174  	// add a test security backend
   175  	err = d.Overlord().InterfaceManager().Repository().AddBackend(&ifacetest.TestSecurityBackend{
   176  		BackendName:             "apparmor",
   177  		SandboxFeaturesCallback: func() []string { return []string{"feature-1", "feature-2"} },
   178  	})
   179  	c.Assert(err, check.IsNil)
   180  
   181  	buildID := "this-is-my-build-id"
   182  	restore = daemon.MockBuildID(buildID)
   183  	defer restore()
   184  
   185  	rec := httptest.NewRecorder()
   186  	s.req(c, req, nil).ServeHTTP(rec, nil)
   187  	c.Check(rec.Code, check.Equals, 200)
   188  	c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
   189  
   190  	expected := map[string]interface{}{
   191  		"series":  "16",
   192  		"version": "42b1",
   193  		"os-release": map[string]interface{}{
   194  			"id":         "distro-id",
   195  			"version-id": "1.2",
   196  		},
   197  		"build-id":   buildID,
   198  		"on-classic": true,
   199  		"managed":    false,
   200  		"locations": map[string]interface{}{
   201  			"snap-mount-dir": dirs.SnapMountDir,
   202  			"snap-bin-dir":   dirs.SnapBinariesDir,
   203  		},
   204  		"refresh": map[string]interface{}{
   205  			// only the "schedule" field
   206  			"schedule": "00:00-9:00/12:00-13:00",
   207  		},
   208  		"confinement": "partial",
   209  		"sandbox-features": map[string]interface{}{
   210  			"apparmor":            []interface{}{"feature-1", "feature-2"},
   211  			"confinement-options": []interface{}{"classic", "devmode"}, // we know it's this because of the release.Mock... calls above
   212  		},
   213  		"architecture":   arch.DpkgArchitecture(),
   214  		"virtualization": "kvm",
   215  		"system-mode":    "run",
   216  	}
   217  	var rsp daemon.RespJSON
   218  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
   219  	c.Check(rsp.Status, check.Equals, 200)
   220  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   221  	const kernelVersionKey = "kernel-version"
   222  	delete(rsp.Result.(map[string]interface{}), kernelVersionKey)
   223  	c.Check(rsp.Result, check.DeepEquals, expected)
   224  }
   225  
   226  func (s *generalSuite) testSysInfoSystemMode(c *check.C, mode string) {
   227  	req, err := http.NewRequest("GET", "/v2/system-info", nil)
   228  	c.Assert(err, check.IsNil)
   229  
   230  	c.Assert(mode != "", check.Equals, true, check.Commentf("mode is unset for the test"))
   231  
   232  	restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"})
   233  	defer restore()
   234  	restore = release.MockOnClassic(false)
   235  	defer restore()
   236  	restore = sandbox.MockForceDevMode(false)
   237  	defer restore()
   238  	restore = daemon.MockSystemdVirt("")
   239  	defer restore()
   240  
   241  	// reload dirs for release info to have effect on paths
   242  	dirs.SetRootDir(dirs.GlobalRootDir)
   243  
   244  	// mock the modeenv file
   245  	m := boot.Modeenv{
   246  		Mode:           mode,
   247  		RecoverySystem: "20191127",
   248  	}
   249  	c.Assert(m.WriteTo(""), check.IsNil)
   250  
   251  	d := s.daemon(c)
   252  	d.Version = "42b1"
   253  
   254  	// add a test security backend
   255  	err = d.Overlord().InterfaceManager().Repository().AddBackend(&ifacetest.TestSecurityBackend{
   256  		BackendName:             "apparmor",
   257  		SandboxFeaturesCallback: func() []string { return []string{"feature-1", "feature-2"} },
   258  	})
   259  	c.Assert(err, check.IsNil)
   260  
   261  	buildID := "this-is-my-build-id"
   262  	restore = daemon.MockBuildID(buildID)
   263  	defer restore()
   264  
   265  	rec := httptest.NewRecorder()
   266  	s.req(c, req, nil).ServeHTTP(rec, nil)
   267  	c.Check(rec.Code, check.Equals, 200)
   268  	c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json")
   269  
   270  	expected := map[string]interface{}{
   271  		"series":  "16",
   272  		"version": "42b1",
   273  		"os-release": map[string]interface{}{
   274  			"id":         "distro-id",
   275  			"version-id": "1.2",
   276  		},
   277  		"build-id":   buildID,
   278  		"on-classic": false,
   279  		"managed":    false,
   280  		"locations": map[string]interface{}{
   281  			"snap-mount-dir": dirs.SnapMountDir,
   282  			"snap-bin-dir":   dirs.SnapBinariesDir,
   283  		},
   284  		"refresh": map[string]interface{}{
   285  			"timer": "00:00~24:00/4",
   286  		},
   287  		"confinement": "strict",
   288  		"sandbox-features": map[string]interface{}{
   289  			"apparmor":            []interface{}{"feature-1", "feature-2"},
   290  			"confinement-options": []interface{}{"devmode", "strict"}, // we know it's this because of the release.Mock... calls above
   291  		},
   292  		"architecture": arch.DpkgArchitecture(),
   293  		"system-mode":  mode,
   294  	}
   295  	var rsp daemon.RespJSON
   296  	c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil)
   297  	c.Check(rsp.Status, check.Equals, 200)
   298  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   299  	const kernelVersionKey = "kernel-version"
   300  	delete(rsp.Result.(map[string]interface{}), kernelVersionKey)
   301  	c.Check(rsp.Result, check.DeepEquals, expected)
   302  }
   303  
   304  func (s *generalSuite) TestSysInfoSystemModeRun(c *check.C) {
   305  	s.testSysInfoSystemMode(c, "run")
   306  }
   307  
   308  func (s *generalSuite) TestSysInfoSystemModeRecover(c *check.C) {
   309  	s.testSysInfoSystemMode(c, "recover")
   310  }
   311  
   312  func (s *generalSuite) TestSysInfoSystemModeInstall(c *check.C) {
   313  	s.testSysInfoSystemMode(c, "install")
   314  }
   315  func (s *generalSuite) TestSysInfoIsManaged(c *check.C) {
   316  	d := s.daemon(c)
   317  
   318  	st := d.Overlord().State()
   319  	st.Lock()
   320  	_, err := auth.NewUser(st, "someuser", "mymail@test.com", "macaroon", []string{"discharge"})
   321  	st.Unlock()
   322  	c.Assert(err, check.IsNil)
   323  
   324  	req, err := http.NewRequest("GET", "/v2/system-info", nil)
   325  	c.Assert(err, check.IsNil)
   326  
   327  	rsp := s.syncReq(c, req, nil)
   328  	c.Check(rsp.Result.(map[string]interface{})["managed"], check.Equals, true)
   329  }
   330  
   331  func (s *generalSuite) TestSysInfoWorksDegraded(c *check.C) {
   332  	d := s.daemon(c)
   333  
   334  	d.SetDegradedMode(fmt.Errorf("some error"))
   335  
   336  	req, err := http.NewRequest("GET", "/v2/system-info", nil)
   337  	c.Assert(err, check.IsNil)
   338  
   339  	rsp := s.syncReq(c, req, nil)
   340  	c.Check(rsp.Status, check.Equals, 200)
   341  }
   342  
   343  func setupChanges(st *state.State) []string {
   344  	chg1 := st.NewChange("install", "install...")
   345  	chg1.Set("snap-names", []string{"funky-snap-name"})
   346  	t1 := st.NewTask("download", "1...")
   347  	t2 := st.NewTask("activate", "2...")
   348  	chg1.AddAll(state.NewTaskSet(t1, t2))
   349  	t1.Logf("l11")
   350  	t1.Logf("l12")
   351  	chg2 := st.NewChange("remove", "remove..")
   352  	t3 := st.NewTask("unlink", "1...")
   353  	chg2.AddTask(t3)
   354  	t3.SetStatus(state.ErrorStatus)
   355  	t3.Errorf("rm failed")
   356  
   357  	return []string{chg1.ID(), chg2.ID(), t1.ID(), t2.ID(), t3.ID()}
   358  }
   359  
   360  func (s *generalSuite) TestStateChangesDefaultToInProgress(c *check.C) {
   361  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   362  	defer restore()
   363  
   364  	// Setup
   365  	d := s.daemon(c)
   366  	st := d.Overlord().State()
   367  	st.Lock()
   368  	setupChanges(st)
   369  	st.Unlock()
   370  
   371  	// Execute
   372  	req, err := http.NewRequest("GET", "/v2/changes", nil)
   373  	c.Assert(err, check.IsNil)
   374  	rsp := s.syncReq(c, req, nil)
   375  
   376  	// Verify
   377  	c.Check(rsp.Status, check.Equals, 200)
   378  	c.Assert(rsp.Result, check.HasLen, 1)
   379  
   380  	rec := httptest.NewRecorder()
   381  	rsp.ServeHTTP(rec, nil)
   382  	c.Assert(rec.Code, check.Equals, 200)
   383  	res := rec.Body.Bytes()
   384  
   385  	c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*`)
   386  }
   387  
   388  func (s *generalSuite) TestStateChangesInProgress(c *check.C) {
   389  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   390  	defer restore()
   391  
   392  	// Setup
   393  	d := s.daemon(c)
   394  	st := d.Overlord().State()
   395  	st.Lock()
   396  	setupChanges(st)
   397  	st.Unlock()
   398  
   399  	// Execute
   400  	req, err := http.NewRequest("GET", "/v2/changes?select=in-progress", nil)
   401  	c.Assert(err, check.IsNil)
   402  	rsp := s.syncReq(c, req, nil)
   403  
   404  	// Verify
   405  	c.Check(rsp.Status, check.Equals, 200)
   406  	c.Assert(rsp.Result, check.HasLen, 1)
   407  
   408  	rec := httptest.NewRecorder()
   409  	rsp.ServeHTTP(rec, nil)
   410  	c.Assert(rec.Code, check.Equals, 200)
   411  	res := rec.Body.Bytes()
   412  
   413  	c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`)
   414  }
   415  
   416  func (s *generalSuite) TestStateChangesAll(c *check.C) {
   417  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   418  	defer restore()
   419  
   420  	// Setup
   421  	d := s.daemon(c)
   422  	st := d.Overlord().State()
   423  	st.Lock()
   424  	setupChanges(st)
   425  	st.Unlock()
   426  
   427  	// Execute
   428  	req, err := http.NewRequest("GET", "/v2/changes?select=all", nil)
   429  	c.Assert(err, check.IsNil)
   430  	rsp := s.syncReq(c, req, nil)
   431  
   432  	// Verify
   433  	c.Check(rsp.Status, check.Equals, 200)
   434  	c.Assert(rsp.Result, check.HasLen, 2)
   435  
   436  	rec := httptest.NewRecorder()
   437  	rsp.ServeHTTP(rec, nil)
   438  	c.Assert(rec.Code, check.Equals, 200)
   439  	res := rec.Body.Bytes()
   440  
   441  	c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`)
   442  	c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`)
   443  }
   444  
   445  func (s *generalSuite) TestStateChangesReady(c *check.C) {
   446  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   447  	defer restore()
   448  
   449  	// Setup
   450  	d := s.daemon(c)
   451  	st := d.Overlord().State()
   452  	st.Lock()
   453  	setupChanges(st)
   454  	st.Unlock()
   455  
   456  	// Execute
   457  	req, err := http.NewRequest("GET", "/v2/changes?select=ready", nil)
   458  	c.Assert(err, check.IsNil)
   459  	rsp := s.syncReq(c, req, nil)
   460  
   461  	// Verify
   462  	c.Check(rsp.Status, check.Equals, 200)
   463  	c.Assert(rsp.Result, check.HasLen, 1)
   464  
   465  	rec := httptest.NewRecorder()
   466  	rsp.ServeHTTP(rec, nil)
   467  	c.Assert(rec.Code, check.Equals, 200)
   468  	res := rec.Body.Bytes()
   469  
   470  	c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`)
   471  }
   472  
   473  func (s *generalSuite) TestStateChangesForSnapName(c *check.C) {
   474  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   475  	defer restore()
   476  
   477  	// Setup
   478  	d := s.daemon(c)
   479  	st := d.Overlord().State()
   480  	st.Lock()
   481  	setupChanges(st)
   482  	st.Unlock()
   483  
   484  	// Execute
   485  	req, err := http.NewRequest("GET", "/v2/changes?for=funky-snap-name&select=all", nil)
   486  	c.Assert(err, check.IsNil)
   487  	rsp := s.syncReq(c, req, nil)
   488  
   489  	// Verify
   490  	c.Check(rsp.Status, check.Equals, 200)
   491  	c.Assert(rsp.Result, check.FitsTypeOf, []*daemon.ChangeInfo(nil))
   492  
   493  	res := rsp.Result.([]*daemon.ChangeInfo)
   494  	c.Assert(res, check.HasLen, 1)
   495  	c.Check(res[0].Kind, check.Equals, `install`)
   496  
   497  	rec := httptest.NewRecorder()
   498  	rsp.ServeHTTP(rec, nil)
   499  	c.Assert(rec.Code, check.Equals, 200)
   500  }
   501  
   502  func (s *generalSuite) TestStateChangesForSnapNameWithApp(c *check.C) {
   503  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   504  	defer restore()
   505  
   506  	// Setup
   507  	d := s.daemon(c)
   508  	st := d.Overlord().State()
   509  	st.Lock()
   510  	chg1 := st.NewChange("service-control", "install...")
   511  	// as triggered by snap restart lxd.daemon
   512  	chg1.Set("snap-names", []string{"lxd.daemon"})
   513  	t1 := st.NewTask("exec-command", "1...")
   514  	chg1.AddAll(state.NewTaskSet(t1))
   515  	t1.Logf("foobar")
   516  
   517  	st.Unlock()
   518  
   519  	// Execute
   520  	req, err := http.NewRequest("GET", "/v2/changes?for=lxd&select=all", nil)
   521  	c.Assert(err, check.IsNil)
   522  	rsp := s.syncReq(c, req, nil)
   523  
   524  	// Verify
   525  	c.Check(rsp.Status, check.Equals, 200)
   526  	c.Assert(rsp.Result, check.FitsTypeOf, []*daemon.ChangeInfo(nil))
   527  
   528  	res := rsp.Result.([]*daemon.ChangeInfo)
   529  	c.Assert(res, check.HasLen, 1)
   530  	c.Check(res[0].Kind, check.Equals, `service-control`)
   531  
   532  	rec := httptest.NewRecorder()
   533  	rsp.ServeHTTP(rec, nil)
   534  	c.Assert(rec.Code, check.Equals, 200)
   535  }
   536  
   537  func (s *generalSuite) TestStateChange(c *check.C) {
   538  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   539  	defer restore()
   540  
   541  	// Setup
   542  	d := s.daemon(c)
   543  	st := d.Overlord().State()
   544  	st.Lock()
   545  	ids := setupChanges(st)
   546  	chg := st.Change(ids[0])
   547  	chg.Set("api-data", map[string]int{"n": 42})
   548  	st.Unlock()
   549  
   550  	// Execute
   551  	req, err := http.NewRequest("GET", "/v2/changes/"+ids[0], nil)
   552  	c.Assert(err, check.IsNil)
   553  	rsp := s.syncReq(c, req, nil)
   554  	rec := httptest.NewRecorder()
   555  	rsp.ServeHTTP(rec, req)
   556  
   557  	// Verify
   558  	c.Check(rec.Code, check.Equals, 200)
   559  	c.Check(rsp.Status, check.Equals, 200)
   560  	c.Check(rsp.Result, check.NotNil)
   561  
   562  	var body map[string]interface{}
   563  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   564  	c.Check(err, check.IsNil)
   565  	c.Check(body["result"], check.DeepEquals, map[string]interface{}{
   566  		"id":         ids[0],
   567  		"kind":       "install",
   568  		"summary":    "install...",
   569  		"status":     "Do",
   570  		"ready":      false,
   571  		"spawn-time": "2016-04-21T01:02:03Z",
   572  		"tasks": []interface{}{
   573  			map[string]interface{}{
   574  				"id":         ids[2],
   575  				"kind":       "download",
   576  				"summary":    "1...",
   577  				"status":     "Do",
   578  				"log":        []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"},
   579  				"progress":   map[string]interface{}{"label": "", "done": 0., "total": 1.},
   580  				"spawn-time": "2016-04-21T01:02:03Z",
   581  			},
   582  			map[string]interface{}{
   583  				"id":         ids[3],
   584  				"kind":       "activate",
   585  				"summary":    "2...",
   586  				"status":     "Do",
   587  				"progress":   map[string]interface{}{"label": "", "done": 0., "total": 1.},
   588  				"spawn-time": "2016-04-21T01:02:03Z",
   589  			},
   590  		},
   591  		"data": map[string]interface{}{
   592  			"n": float64(42),
   593  		},
   594  	})
   595  }
   596  
   597  func (s *generalSuite) expectManageAccess() {
   598  	s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage"})
   599  }
   600  
   601  func (s *generalSuite) TestStateChangeAbort(c *check.C) {
   602  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   603  	defer restore()
   604  
   605  	soon := 0
   606  	_, restore = daemon.MockEnsureStateSoon(func(st *state.State) {
   607  		soon++
   608  	})
   609  	defer restore()
   610  
   611  	// Setup
   612  	d := s.daemon(c)
   613  	st := d.Overlord().State()
   614  	st.Lock()
   615  	ids := setupChanges(st)
   616  	st.Unlock()
   617  
   618  	s.expectManageAccess()
   619  
   620  	buf := bytes.NewBufferString(`{"action": "abort"}`)
   621  
   622  	// Execute
   623  	req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf)
   624  	c.Assert(err, check.IsNil)
   625  	rsp := s.syncReq(c, req, nil)
   626  	rec := httptest.NewRecorder()
   627  	rsp.ServeHTTP(rec, req)
   628  
   629  	// Ensure scheduled
   630  	c.Check(soon, check.Equals, 1)
   631  
   632  	// Verify
   633  	c.Check(rec.Code, check.Equals, 200)
   634  	c.Check(rsp.Status, check.Equals, 200)
   635  	c.Check(rsp.Result, check.NotNil)
   636  
   637  	var body map[string]interface{}
   638  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   639  	c.Check(err, check.IsNil)
   640  	c.Check(body["result"], check.DeepEquals, map[string]interface{}{
   641  		"id":         ids[0],
   642  		"kind":       "install",
   643  		"summary":    "install...",
   644  		"status":     "Hold",
   645  		"ready":      true,
   646  		"spawn-time": "2016-04-21T01:02:03Z",
   647  		"ready-time": "2016-04-21T01:02:03Z",
   648  		"tasks": []interface{}{
   649  			map[string]interface{}{
   650  				"id":         ids[2],
   651  				"kind":       "download",
   652  				"summary":    "1...",
   653  				"status":     "Hold",
   654  				"log":        []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"},
   655  				"progress":   map[string]interface{}{"label": "", "done": 1., "total": 1.},
   656  				"spawn-time": "2016-04-21T01:02:03Z",
   657  				"ready-time": "2016-04-21T01:02:03Z",
   658  			},
   659  			map[string]interface{}{
   660  				"id":         ids[3],
   661  				"kind":       "activate",
   662  				"summary":    "2...",
   663  				"status":     "Hold",
   664  				"progress":   map[string]interface{}{"label": "", "done": 1., "total": 1.},
   665  				"spawn-time": "2016-04-21T01:02:03Z",
   666  				"ready-time": "2016-04-21T01:02:03Z",
   667  			},
   668  		},
   669  	})
   670  }
   671  
   672  func (s *generalSuite) TestStateChangeAbortIsReady(c *check.C) {
   673  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   674  	defer restore()
   675  
   676  	// Setup
   677  	d := s.daemon(c)
   678  	st := d.Overlord().State()
   679  	st.Lock()
   680  	ids := setupChanges(st)
   681  	st.Change(ids[0]).SetStatus(state.DoneStatus)
   682  	st.Unlock()
   683  
   684  	s.expectManageAccess()
   685  
   686  	buf := bytes.NewBufferString(`{"action": "abort"}`)
   687  
   688  	// Execute
   689  	req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf)
   690  	c.Assert(err, check.IsNil)
   691  	rspe := s.errorReq(c, req, nil)
   692  	rec := httptest.NewRecorder()
   693  	rspe.ServeHTTP(rec, req)
   694  
   695  	// Verify
   696  	c.Check(rec.Code, check.Equals, 400)
   697  	c.Check(rspe.Status, check.Equals, 400)
   698  
   699  	var body map[string]interface{}
   700  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   701  	c.Check(err, check.IsNil)
   702  	c.Check(body["result"], check.DeepEquals, map[string]interface{}{
   703  		"message": fmt.Sprintf("cannot abort change %s with nothing pending", ids[0]),
   704  	})
   705  }
   706  
   707  func (s *generalSuite) testWarnings(c *check.C, all bool, body io.Reader) (calls string, result interface{}) {
   708  	s.daemon(c)
   709  
   710  	s.expectManageAccess()
   711  
   712  	okayWarns := func(*state.State, time.Time) int { calls += "ok"; return 0 }
   713  	allWarns := func(*state.State) []*state.Warning { calls += "all"; return nil }
   714  	pendingWarns := func(*state.State) ([]*state.Warning, time.Time) { calls += "show"; return nil, time.Time{} }
   715  	restore := daemon.MockWarningsAccessors(okayWarns, allWarns, pendingWarns)
   716  	defer restore()
   717  
   718  	method := "GET"
   719  	if body != nil {
   720  		method = "POST"
   721  	}
   722  	q := url.Values{}
   723  	if all {
   724  		q.Set("select", "all")
   725  	}
   726  	req, err := http.NewRequest(method, "/v2/warnings?"+q.Encode(), body)
   727  	c.Assert(err, check.IsNil)
   728  
   729  	rsp := s.syncReq(c, req, nil)
   730  
   731  	c.Check(rsp.Status, check.Equals, 200)
   732  	c.Assert(rsp.Result, check.NotNil)
   733  	return calls, rsp.Result
   734  }
   735  
   736  func (s *generalSuite) TestAllWarnings(c *check.C) {
   737  	calls, result := s.testWarnings(c, true, nil)
   738  	c.Check(calls, check.Equals, "all")
   739  	c.Check(result, check.DeepEquals, []state.Warning{})
   740  }
   741  
   742  func (s *generalSuite) TestSomeWarnings(c *check.C) {
   743  	calls, result := s.testWarnings(c, false, nil)
   744  	c.Check(calls, check.Equals, "show")
   745  	c.Check(result, check.DeepEquals, []state.Warning{})
   746  }
   747  
   748  func (s *generalSuite) TestAckWarnings(c *check.C) {
   749  	calls, result := s.testWarnings(c, false, bytes.NewReader([]byte(`{"action": "okay", "timestamp": "2006-01-02T15:04:05Z"}`)))
   750  	c.Check(calls, check.Equals, "ok")
   751  	c.Check(result, check.DeepEquals, 0)
   752  }