github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/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.Resp
    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.Resp
   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.Resp
   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.Resp
   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  	res, err := rsp.MarshalJSON()
   381  	c.Assert(err, check.IsNil)
   382  
   383  	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"}.*`)
   384  }
   385  
   386  func (s *generalSuite) TestStateChangesInProgress(c *check.C) {
   387  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   388  	defer restore()
   389  
   390  	// Setup
   391  	d := s.daemon(c)
   392  	st := d.Overlord().State()
   393  	st.Lock()
   394  	setupChanges(st)
   395  	st.Unlock()
   396  
   397  	// Execute
   398  	req, err := http.NewRequest("GET", "/v2/changes?select=in-progress", nil)
   399  	c.Assert(err, check.IsNil)
   400  	rsp := s.syncReq(c, req, nil)
   401  
   402  	// Verify
   403  	c.Check(rsp.Status, check.Equals, 200)
   404  	c.Assert(rsp.Result, check.HasLen, 1)
   405  
   406  	res, err := rsp.MarshalJSON()
   407  	c.Assert(err, check.IsNil)
   408  
   409  	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"}.*`)
   410  }
   411  
   412  func (s *generalSuite) TestStateChangesAll(c *check.C) {
   413  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   414  	defer restore()
   415  
   416  	// Setup
   417  	d := s.daemon(c)
   418  	st := d.Overlord().State()
   419  	st.Lock()
   420  	setupChanges(st)
   421  	st.Unlock()
   422  
   423  	// Execute
   424  	req, err := http.NewRequest("GET", "/v2/changes?select=all", nil)
   425  	c.Assert(err, check.IsNil)
   426  	rsp := s.syncReq(c, req, nil)
   427  
   428  	// Verify
   429  	c.Check(rsp.Status, check.Equals, 200)
   430  	c.Assert(rsp.Result, check.HasLen, 2)
   431  
   432  	res, err := rsp.MarshalJSON()
   433  	c.Assert(err, check.IsNil)
   434  
   435  	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"}.*`)
   436  	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":"[^"]+".*`)
   437  }
   438  
   439  func (s *generalSuite) TestStateChangesReady(c *check.C) {
   440  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   441  	defer restore()
   442  
   443  	// Setup
   444  	d := s.daemon(c)
   445  	st := d.Overlord().State()
   446  	st.Lock()
   447  	setupChanges(st)
   448  	st.Unlock()
   449  
   450  	// Execute
   451  	req, err := http.NewRequest("GET", "/v2/changes?select=ready", nil)
   452  	c.Assert(err, check.IsNil)
   453  	rsp := s.syncReq(c, req, nil)
   454  
   455  	// Verify
   456  	c.Check(rsp.Status, check.Equals, 200)
   457  	c.Assert(rsp.Result, check.HasLen, 1)
   458  
   459  	res, err := rsp.MarshalJSON()
   460  	c.Assert(err, check.IsNil)
   461  
   462  	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":"[^"]+".*`)
   463  }
   464  
   465  func (s *generalSuite) TestStateChangesForSnapName(c *check.C) {
   466  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   467  	defer restore()
   468  
   469  	// Setup
   470  	d := s.daemon(c)
   471  	st := d.Overlord().State()
   472  	st.Lock()
   473  	setupChanges(st)
   474  	st.Unlock()
   475  
   476  	// Execute
   477  	req, err := http.NewRequest("GET", "/v2/changes?for=funky-snap-name&select=all", nil)
   478  	c.Assert(err, check.IsNil)
   479  	rsp := s.syncReq(c, req, nil)
   480  
   481  	// Verify
   482  	c.Check(rsp.Status, check.Equals, 200)
   483  	c.Assert(rsp.Result, check.FitsTypeOf, []*daemon.ChangeInfo(nil))
   484  
   485  	res := rsp.Result.([]*daemon.ChangeInfo)
   486  	c.Assert(res, check.HasLen, 1)
   487  	c.Check(res[0].Kind, check.Equals, `install`)
   488  
   489  	_, err = rsp.MarshalJSON()
   490  	c.Assert(err, check.IsNil)
   491  }
   492  
   493  func (s *generalSuite) TestStateChangesForSnapNameWithApp(c *check.C) {
   494  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   495  	defer restore()
   496  
   497  	// Setup
   498  	d := s.daemon(c)
   499  	st := d.Overlord().State()
   500  	st.Lock()
   501  	chg1 := st.NewChange("service-control", "install...")
   502  	// as triggered by snap restart lxd.daemon
   503  	chg1.Set("snap-names", []string{"lxd.daemon"})
   504  	t1 := st.NewTask("exec-command", "1...")
   505  	chg1.AddAll(state.NewTaskSet(t1))
   506  	t1.Logf("foobar")
   507  
   508  	st.Unlock()
   509  
   510  	// Execute
   511  	req, err := http.NewRequest("GET", "/v2/changes?for=lxd&select=all", nil)
   512  	c.Assert(err, check.IsNil)
   513  	rsp := s.syncReq(c, req, nil)
   514  
   515  	// Verify
   516  	c.Check(rsp.Status, check.Equals, 200)
   517  	c.Assert(rsp.Result, check.FitsTypeOf, []*daemon.ChangeInfo(nil))
   518  
   519  	res := rsp.Result.([]*daemon.ChangeInfo)
   520  	c.Assert(res, check.HasLen, 1)
   521  	c.Check(res[0].Kind, check.Equals, `service-control`)
   522  
   523  	_, err = rsp.MarshalJSON()
   524  	c.Assert(err, check.IsNil)
   525  }
   526  
   527  func (s *generalSuite) TestStateChange(c *check.C) {
   528  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   529  	defer restore()
   530  
   531  	// Setup
   532  	d := s.daemon(c)
   533  	st := d.Overlord().State()
   534  	st.Lock()
   535  	ids := setupChanges(st)
   536  	chg := st.Change(ids[0])
   537  	chg.Set("api-data", map[string]int{"n": 42})
   538  	st.Unlock()
   539  
   540  	// Execute
   541  	req, err := http.NewRequest("GET", "/v2/changes/"+ids[0], nil)
   542  	c.Assert(err, check.IsNil)
   543  	rsp := s.syncReq(c, req, nil)
   544  	rec := httptest.NewRecorder()
   545  	rsp.ServeHTTP(rec, req)
   546  
   547  	// Verify
   548  	c.Check(rec.Code, check.Equals, 200)
   549  	c.Check(rsp.Status, check.Equals, 200)
   550  	c.Check(rsp.Result, check.NotNil)
   551  
   552  	var body map[string]interface{}
   553  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   554  	c.Check(err, check.IsNil)
   555  	c.Check(body["result"], check.DeepEquals, map[string]interface{}{
   556  		"id":         ids[0],
   557  		"kind":       "install",
   558  		"summary":    "install...",
   559  		"status":     "Do",
   560  		"ready":      false,
   561  		"spawn-time": "2016-04-21T01:02:03Z",
   562  		"tasks": []interface{}{
   563  			map[string]interface{}{
   564  				"id":         ids[2],
   565  				"kind":       "download",
   566  				"summary":    "1...",
   567  				"status":     "Do",
   568  				"log":        []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"},
   569  				"progress":   map[string]interface{}{"label": "", "done": 0., "total": 1.},
   570  				"spawn-time": "2016-04-21T01:02:03Z",
   571  			},
   572  			map[string]interface{}{
   573  				"id":         ids[3],
   574  				"kind":       "activate",
   575  				"summary":    "2...",
   576  				"status":     "Do",
   577  				"progress":   map[string]interface{}{"label": "", "done": 0., "total": 1.},
   578  				"spawn-time": "2016-04-21T01:02:03Z",
   579  			},
   580  		},
   581  		"data": map[string]interface{}{
   582  			"n": float64(42),
   583  		},
   584  	})
   585  }
   586  
   587  func (s *generalSuite) TestStateChangeAbort(c *check.C) {
   588  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   589  	defer restore()
   590  
   591  	soon := 0
   592  	_, restore = daemon.MockEnsureStateSoon(func(st *state.State) {
   593  		soon++
   594  	})
   595  	defer restore()
   596  
   597  	// Setup
   598  	d := s.daemon(c)
   599  	st := d.Overlord().State()
   600  	st.Lock()
   601  	ids := setupChanges(st)
   602  	st.Unlock()
   603  
   604  	buf := bytes.NewBufferString(`{"action": "abort"}`)
   605  
   606  	// Execute
   607  	req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf)
   608  	c.Assert(err, check.IsNil)
   609  	rsp := s.syncReq(c, req, nil)
   610  	rec := httptest.NewRecorder()
   611  	rsp.ServeHTTP(rec, req)
   612  
   613  	// Ensure scheduled
   614  	c.Check(soon, check.Equals, 1)
   615  
   616  	// Verify
   617  	c.Check(rec.Code, check.Equals, 200)
   618  	c.Check(rsp.Status, check.Equals, 200)
   619  	c.Check(rsp.Result, check.NotNil)
   620  
   621  	var body map[string]interface{}
   622  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   623  	c.Check(err, check.IsNil)
   624  	c.Check(body["result"], check.DeepEquals, map[string]interface{}{
   625  		"id":         ids[0],
   626  		"kind":       "install",
   627  		"summary":    "install...",
   628  		"status":     "Hold",
   629  		"ready":      true,
   630  		"spawn-time": "2016-04-21T01:02:03Z",
   631  		"ready-time": "2016-04-21T01:02:03Z",
   632  		"tasks": []interface{}{
   633  			map[string]interface{}{
   634  				"id":         ids[2],
   635  				"kind":       "download",
   636  				"summary":    "1...",
   637  				"status":     "Hold",
   638  				"log":        []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"},
   639  				"progress":   map[string]interface{}{"label": "", "done": 1., "total": 1.},
   640  				"spawn-time": "2016-04-21T01:02:03Z",
   641  				"ready-time": "2016-04-21T01:02:03Z",
   642  			},
   643  			map[string]interface{}{
   644  				"id":         ids[3],
   645  				"kind":       "activate",
   646  				"summary":    "2...",
   647  				"status":     "Hold",
   648  				"progress":   map[string]interface{}{"label": "", "done": 1., "total": 1.},
   649  				"spawn-time": "2016-04-21T01:02:03Z",
   650  				"ready-time": "2016-04-21T01:02:03Z",
   651  			},
   652  		},
   653  	})
   654  }
   655  
   656  func (s *generalSuite) TestStateChangeAbortIsReady(c *check.C) {
   657  	restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC))
   658  	defer restore()
   659  
   660  	// Setup
   661  	d := s.daemon(c)
   662  	st := d.Overlord().State()
   663  	st.Lock()
   664  	ids := setupChanges(st)
   665  	st.Change(ids[0]).SetStatus(state.DoneStatus)
   666  	st.Unlock()
   667  
   668  	buf := bytes.NewBufferString(`{"action": "abort"}`)
   669  
   670  	// Execute
   671  	req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf)
   672  	c.Assert(err, check.IsNil)
   673  	rsp := s.errorReq(c, req, nil)
   674  	rec := httptest.NewRecorder()
   675  	rsp.ServeHTTP(rec, req)
   676  
   677  	// Verify
   678  	c.Check(rec.Code, check.Equals, 400)
   679  	c.Check(rsp.Status, check.Equals, 400)
   680  	c.Check(rsp.Result, check.NotNil)
   681  
   682  	var body map[string]interface{}
   683  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   684  	c.Check(err, check.IsNil)
   685  	c.Check(body["result"], check.DeepEquals, map[string]interface{}{
   686  		"message": fmt.Sprintf("cannot abort change %s with nothing pending", ids[0]),
   687  	})
   688  }
   689  
   690  func (s *generalSuite) testWarnings(c *check.C, all bool, body io.Reader) (calls string, result interface{}) {
   691  	s.daemon(c)
   692  
   693  	okayWarns := func(*state.State, time.Time) int { calls += "ok"; return 0 }
   694  	allWarns := func(*state.State) []*state.Warning { calls += "all"; return nil }
   695  	pendingWarns := func(*state.State) ([]*state.Warning, time.Time) { calls += "show"; return nil, time.Time{} }
   696  	restore := daemon.MockWarningsAccessors(okayWarns, allWarns, pendingWarns)
   697  	defer restore()
   698  
   699  	method := "GET"
   700  	if body != nil {
   701  		method = "POST"
   702  	}
   703  	q := url.Values{}
   704  	if all {
   705  		q.Set("select", "all")
   706  	}
   707  	req, err := http.NewRequest(method, "/v2/warnings?"+q.Encode(), body)
   708  	c.Assert(err, check.IsNil)
   709  
   710  	rsp := s.syncReq(c, req, nil)
   711  
   712  	c.Check(rsp.Status, check.Equals, 200)
   713  	c.Assert(rsp.Result, check.NotNil)
   714  	return calls, rsp.Result
   715  }
   716  
   717  func (s *generalSuite) TestAllWarnings(c *check.C) {
   718  	calls, result := s.testWarnings(c, true, nil)
   719  	c.Check(calls, check.Equals, "all")
   720  	c.Check(result, check.DeepEquals, []state.Warning{})
   721  }
   722  
   723  func (s *generalSuite) TestSomeWarnings(c *check.C) {
   724  	calls, result := s.testWarnings(c, false, nil)
   725  	c.Check(calls, check.Equals, "show")
   726  	c.Check(result, check.DeepEquals, []state.Warning{})
   727  }
   728  
   729  func (s *generalSuite) TestAckWarnings(c *check.C) {
   730  	calls, result := s.testWarnings(c, false, bytes.NewReader([]byte(`{"action": "okay", "timestamp": "2006-01-02T15:04:05Z"}`)))
   731  	c.Check(calls, check.Equals, "ok")
   732  	c.Check(result, check.DeepEquals, 0)
   733  }