github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/cmd/snap-repair/runner_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017-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 main_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"net/http/httptest"
    30  	"net/url"
    31  	"os"
    32  	"path/filepath"
    33  	"strconv"
    34  	"strings"
    35  	"time"
    36  
    37  	. "gopkg.in/check.v1"
    38  	"gopkg.in/retry.v1"
    39  
    40  	"github.com/snapcore/snapd/arch"
    41  	"github.com/snapcore/snapd/asserts"
    42  	"github.com/snapcore/snapd/asserts/assertstest"
    43  	"github.com/snapcore/snapd/asserts/sysdb"
    44  	repair "github.com/snapcore/snapd/cmd/snap-repair"
    45  	"github.com/snapcore/snapd/dirs"
    46  	"github.com/snapcore/snapd/logger"
    47  	"github.com/snapcore/snapd/osutil"
    48  	"github.com/snapcore/snapd/snapdenv"
    49  	"github.com/snapcore/snapd/testutil"
    50  )
    51  
    52  type baseRunnerSuite struct {
    53  	tmpdir string
    54  
    55  	seedTime time.Time
    56  	t0       time.Time
    57  
    58  	storeSigning *assertstest.StoreStack
    59  
    60  	brandSigning *assertstest.SigningDB
    61  	brandAcct    *asserts.Account
    62  	brandAcctKey *asserts.AccountKey
    63  
    64  	modelAs *asserts.Model
    65  
    66  	seedAssertsDir string
    67  
    68  	repairRootAcctKey *asserts.AccountKey
    69  	repairsAcctKey    *asserts.AccountKey
    70  
    71  	repairsSigning *assertstest.SigningDB
    72  
    73  	restoreLogger func()
    74  }
    75  
    76  func (s *baseRunnerSuite) SetUpSuite(c *C) {
    77  	s.storeSigning = assertstest.NewStoreStack("canonical", nil)
    78  
    79  	brandPrivKey, _ := assertstest.GenerateKey(752)
    80  
    81  	s.brandAcct = assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
    82  		"account-id": "my-brand",
    83  	}, "")
    84  	s.brandAcctKey = assertstest.NewAccountKey(s.storeSigning, s.brandAcct, nil, brandPrivKey.PublicKey(), "")
    85  	s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey)
    86  
    87  	modelAs, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
    88  		"series":       "16",
    89  		"brand-id":     "my-brand",
    90  		"model":        "my-model-2",
    91  		"architecture": "armhf",
    92  		"gadget":       "gadget",
    93  		"kernel":       "kernel",
    94  		"timestamp":    time.Now().UTC().Format(time.RFC3339),
    95  	}, nil, "")
    96  	c.Assert(err, IsNil)
    97  	s.modelAs = modelAs.(*asserts.Model)
    98  
    99  	repairRootKey, _ := assertstest.GenerateKey(1024)
   100  
   101  	s.repairRootAcctKey = assertstest.NewAccountKey(s.storeSigning.RootSigning, s.storeSigning.TrustedAccount, nil, repairRootKey.PublicKey(), "")
   102  
   103  	repairsKey, _ := assertstest.GenerateKey(752)
   104  
   105  	repairRootSigning := assertstest.NewSigningDB("canonical", repairRootKey)
   106  
   107  	s.repairsAcctKey = assertstest.NewAccountKey(repairRootSigning, s.storeSigning.TrustedAccount, nil, repairsKey.PublicKey(), "")
   108  
   109  	s.repairsSigning = assertstest.NewSigningDB("canonical", repairsKey)
   110  }
   111  
   112  func (s *baseRunnerSuite) SetUpTest(c *C) {
   113  	_, s.restoreLogger = logger.MockLogger()
   114  
   115  	s.tmpdir = c.MkDir()
   116  	dirs.SetRootDir(s.tmpdir)
   117  
   118  	s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions")
   119  
   120  	// dummy seed yaml
   121  	err := os.MkdirAll(dirs.SnapSeedDir, 0755)
   122  	c.Assert(err, IsNil)
   123  	seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml")
   124  	err = ioutil.WriteFile(seedYamlFn, nil, 0644)
   125  	c.Assert(err, IsNil)
   126  	seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z")
   127  	c.Assert(err, IsNil)
   128  	err = os.Chtimes(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), seedTime, seedTime)
   129  	c.Assert(err, IsNil)
   130  	s.seedTime = seedTime
   131  
   132  	s.t0 = time.Now().UTC().Truncate(time.Minute)
   133  }
   134  
   135  func (s *baseRunnerSuite) TearDownTest(c *C) {
   136  	dirs.SetRootDir("/")
   137  	s.restoreLogger()
   138  }
   139  
   140  func (s *baseRunnerSuite) signSeqRepairs(c *C, repairs []string) []string {
   141  	var seq []string
   142  	for _, rpr := range repairs {
   143  		decoded, err := asserts.Decode([]byte(rpr))
   144  		c.Assert(err, IsNil)
   145  		signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "")
   146  		c.Assert(err, IsNil)
   147  		buf := &bytes.Buffer{}
   148  		enc := asserts.NewEncoder(buf)
   149  		enc.Encode(signed)
   150  		enc.Encode(s.repairsAcctKey)
   151  		seq = append(seq, buf.String())
   152  	}
   153  	return seq
   154  }
   155  
   156  const freshStateJSON = `{"device":{"brand":"my-brand","model":"my-model"},"time-lower-bound":"2017-08-11T15:49:49Z"}`
   157  
   158  func (s *baseRunnerSuite) freshState(c *C) {
   159  	err := os.MkdirAll(dirs.SnapRepairDir, 0775)
   160  	c.Assert(err, IsNil)
   161  	err = ioutil.WriteFile(dirs.SnapRepairStateFile, []byte(freshStateJSON), 0600)
   162  	c.Assert(err, IsNil)
   163  }
   164  
   165  type runnerSuite struct {
   166  	baseRunnerSuite
   167  
   168  	restore func()
   169  }
   170  
   171  func (s *runnerSuite) SetUpSuite(c *C) {
   172  	s.baseRunnerSuite.SetUpSuite(c)
   173  	s.restore = snapdenv.SetUserAgentFromVersion("1", nil, "snap-repair")
   174  }
   175  
   176  func (s *runnerSuite) TearDownSuite(c *C) {
   177  	s.restore()
   178  }
   179  
   180  var _ = Suite(&runnerSuite{})
   181  
   182  var (
   183  	testKey = `type: account-key
   184  authority-id: canonical
   185  account-id: canonical
   186  name: repair
   187  public-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   188  since: 2015-11-16T15:04:00Z
   189  body-length: 149
   190  sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
   191  
   192  AcZrBFaFwYABAvCX5A8dTcdLdhdiuy2YRHO5CAfM5InQefkKOhNMUq2yfi3Sk6trUHxskhZkPnm4
   193  NKx2yRr332q7AJXQHLX+DrZ29ycyoQ2NQGO3eAfQ0hjAAQFYBF8SSh5SutPu5XCVABEBAAE=
   194  
   195  AXNpZw==
   196  `
   197  
   198  	testRepair = `type: repair
   199  authority-id: canonical
   200  brand-id: canonical
   201  repair-id: 2
   202  summary: repair two
   203  architectures:
   204    - amd64
   205    - arm64
   206  series:
   207    - 16
   208  models:
   209    - xyz/frobinator
   210  timestamp: 2017-03-30T12:22:16Z
   211  body-length: 7
   212  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   213  
   214  script
   215  
   216  
   217  AXNpZw==
   218  `
   219  	testHeadersResp = `{"headers":
   220  {"architectures":["amd64","arm64"],"authority-id":"canonical","body-length":"7","brand-id":"canonical","models":["xyz/frobinator"],"repair-id":"2","series":["16"],"sign-key-sha3-384":"KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj","timestamp":"2017-03-30T12:22:16Z","type":"repair"}}`
   221  )
   222  
   223  func mustParseURL(s string) *url.URL {
   224  	u, err := url.Parse(s)
   225  	if err != nil {
   226  		panic(err)
   227  	}
   228  	return u
   229  }
   230  
   231  func (s *runnerSuite) mockBrokenTimeNowSetToEpoch(c *C, runner *repair.Runner) (restore func()) {
   232  	epoch := time.Unix(0, 0)
   233  	r := repair.MockTimeNow(func() time.Time {
   234  		return epoch
   235  	})
   236  	c.Check(runner.TLSTime().Equal(epoch), Equals, true)
   237  	return r
   238  }
   239  
   240  func (s *runnerSuite) checkBrokenTimeNowMitigated(c *C, runner *repair.Runner) {
   241  	c.Check(runner.TLSTime().Before(s.t0), Equals, false)
   242  }
   243  
   244  func (s *runnerSuite) TestFetchJustRepair(c *C) {
   245  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   246  		ua := r.Header.Get("User-Agent")
   247  		c.Check(strings.Contains(ua, "snap-repair"), Equals, true)
   248  		c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
   249  		c.Check(r.URL.Path, Equals, "/repairs/canonical/2")
   250  		io.WriteString(w, testRepair)
   251  	}))
   252  
   253  	c.Assert(mockServer, NotNil)
   254  	defer mockServer.Close()
   255  
   256  	runner := repair.NewRunner()
   257  	runner.BaseURL = mustParseURL(mockServer.URL)
   258  
   259  	r := s.mockBrokenTimeNowSetToEpoch(c, runner)
   260  	defer r()
   261  
   262  	repair, aux, err := runner.Fetch("canonical", 2, -1)
   263  	c.Assert(err, IsNil)
   264  	c.Check(repair, NotNil)
   265  	c.Check(aux, HasLen, 0)
   266  	c.Check(repair.BrandID(), Equals, "canonical")
   267  	c.Check(repair.RepairID(), Equals, 2)
   268  	c.Check(repair.Body(), DeepEquals, []byte("script\n"))
   269  
   270  	s.checkBrokenTimeNowMitigated(c, runner)
   271  }
   272  
   273  func (s *runnerSuite) TestFetchScriptTooBig(c *C) {
   274  	restore := repair.MockMaxRepairScriptSize(4)
   275  	defer restore()
   276  
   277  	n := 0
   278  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   279  		n++
   280  		c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
   281  		c.Check(r.URL.Path, Equals, "/repairs/canonical/2")
   282  		io.WriteString(w, testRepair)
   283  	}))
   284  
   285  	c.Assert(mockServer, NotNil)
   286  	defer mockServer.Close()
   287  
   288  	runner := repair.NewRunner()
   289  	runner.BaseURL = mustParseURL(mockServer.URL)
   290  
   291  	_, _, err := runner.Fetch("canonical", 2, -1)
   292  	c.Assert(err, ErrorMatches, `assertion body length 7 exceeds maximum body size 4 for "repair".*`)
   293  	c.Assert(n, Equals, 1)
   294  }
   295  
   296  var (
   297  	testRetryStrategy = retry.LimitCount(5, retry.LimitTime(1*time.Second,
   298  		retry.Exponential{
   299  			Initial: 1 * time.Millisecond,
   300  			Factor:  1,
   301  		},
   302  	))
   303  )
   304  
   305  func (s *runnerSuite) TestFetch500(c *C) {
   306  	restore := repair.MockFetchRetryStrategy(testRetryStrategy)
   307  	defer restore()
   308  
   309  	n := 0
   310  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   311  		n++
   312  		w.WriteHeader(500)
   313  	}))
   314  
   315  	c.Assert(mockServer, NotNil)
   316  	defer mockServer.Close()
   317  
   318  	runner := repair.NewRunner()
   319  	runner.BaseURL = mustParseURL(mockServer.URL)
   320  
   321  	_, _, err := runner.Fetch("canonical", 2, -1)
   322  	c.Assert(err, ErrorMatches, "cannot fetch repair, unexpected status 500")
   323  	c.Assert(n, Equals, 5)
   324  }
   325  
   326  func (s *runnerSuite) TestFetchEmpty(c *C) {
   327  	restore := repair.MockFetchRetryStrategy(testRetryStrategy)
   328  	defer restore()
   329  
   330  	n := 0
   331  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   332  		n++
   333  		w.WriteHeader(200)
   334  	}))
   335  
   336  	c.Assert(mockServer, NotNil)
   337  	defer mockServer.Close()
   338  
   339  	runner := repair.NewRunner()
   340  	runner.BaseURL = mustParseURL(mockServer.URL)
   341  
   342  	_, _, err := runner.Fetch("canonical", 2, -1)
   343  	c.Assert(err, Equals, io.ErrUnexpectedEOF)
   344  	c.Assert(n, Equals, 5)
   345  }
   346  
   347  func (s *runnerSuite) TestFetchBroken(c *C) {
   348  	restore := repair.MockFetchRetryStrategy(testRetryStrategy)
   349  	defer restore()
   350  
   351  	n := 0
   352  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   353  		n++
   354  		w.WriteHeader(200)
   355  		io.WriteString(w, "xyz:")
   356  	}))
   357  
   358  	c.Assert(mockServer, NotNil)
   359  	defer mockServer.Close()
   360  
   361  	runner := repair.NewRunner()
   362  	runner.BaseURL = mustParseURL(mockServer.URL)
   363  
   364  	_, _, err := runner.Fetch("canonical", 2, -1)
   365  	c.Assert(err, Equals, io.ErrUnexpectedEOF)
   366  	c.Assert(n, Equals, 5)
   367  }
   368  
   369  func (s *runnerSuite) TestFetchNotFound(c *C) {
   370  	restore := repair.MockFetchRetryStrategy(testRetryStrategy)
   371  	defer restore()
   372  
   373  	n := 0
   374  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   375  		n++
   376  		w.WriteHeader(404)
   377  	}))
   378  
   379  	c.Assert(mockServer, NotNil)
   380  	defer mockServer.Close()
   381  
   382  	runner := repair.NewRunner()
   383  	runner.BaseURL = mustParseURL(mockServer.URL)
   384  
   385  	r := s.mockBrokenTimeNowSetToEpoch(c, runner)
   386  	defer r()
   387  
   388  	_, _, err := runner.Fetch("canonical", 2, -1)
   389  	c.Assert(err, Equals, repair.ErrRepairNotFound)
   390  	c.Assert(n, Equals, 1)
   391  
   392  	s.checkBrokenTimeNowMitigated(c, runner)
   393  }
   394  
   395  func (s *runnerSuite) TestFetchIfNoneMatchNotModified(c *C) {
   396  	restore := repair.MockFetchRetryStrategy(testRetryStrategy)
   397  	defer restore()
   398  
   399  	n := 0
   400  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   401  		n++
   402  		c.Check(r.Header.Get("If-None-Match"), Equals, `"0"`)
   403  		w.WriteHeader(304)
   404  	}))
   405  
   406  	c.Assert(mockServer, NotNil)
   407  	defer mockServer.Close()
   408  
   409  	runner := repair.NewRunner()
   410  	runner.BaseURL = mustParseURL(mockServer.URL)
   411  
   412  	r := s.mockBrokenTimeNowSetToEpoch(c, runner)
   413  	defer r()
   414  
   415  	_, _, err := runner.Fetch("canonical", 2, 0)
   416  	c.Assert(err, Equals, repair.ErrRepairNotModified)
   417  	c.Assert(n, Equals, 1)
   418  
   419  	s.checkBrokenTimeNowMitigated(c, runner)
   420  }
   421  
   422  func (s *runnerSuite) TestFetchIgnoreSupersededRevision(c *C) {
   423  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   424  		io.WriteString(w, testRepair)
   425  	}))
   426  
   427  	c.Assert(mockServer, NotNil)
   428  	defer mockServer.Close()
   429  
   430  	runner := repair.NewRunner()
   431  	runner.BaseURL = mustParseURL(mockServer.URL)
   432  
   433  	_, _, err := runner.Fetch("canonical", 2, 2)
   434  	c.Assert(err, Equals, repair.ErrRepairNotModified)
   435  }
   436  
   437  func (s *runnerSuite) TestFetchIdMismatch(c *C) {
   438  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   439  		c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
   440  		io.WriteString(w, testRepair)
   441  	}))
   442  
   443  	c.Assert(mockServer, NotNil)
   444  	defer mockServer.Close()
   445  
   446  	runner := repair.NewRunner()
   447  	runner.BaseURL = mustParseURL(mockServer.URL)
   448  
   449  	_, _, err := runner.Fetch("canonical", 4, -1)
   450  	c.Assert(err, ErrorMatches, `cannot fetch repair, repair id mismatch canonical/2 != canonical/4`)
   451  }
   452  
   453  func (s *runnerSuite) TestFetchWrongFirstType(c *C) {
   454  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   455  		c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
   456  		c.Check(r.URL.Path, Equals, "/repairs/canonical/2")
   457  		io.WriteString(w, testKey)
   458  	}))
   459  
   460  	c.Assert(mockServer, NotNil)
   461  	defer mockServer.Close()
   462  
   463  	runner := repair.NewRunner()
   464  	runner.BaseURL = mustParseURL(mockServer.URL)
   465  
   466  	_, _, err := runner.Fetch("canonical", 2, -1)
   467  	c.Assert(err, ErrorMatches, `cannot fetch repair, unexpected first assertion "account-key"`)
   468  }
   469  
   470  func (s *runnerSuite) TestFetchRepairPlusKey(c *C) {
   471  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   472  		c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
   473  		c.Check(r.URL.Path, Equals, "/repairs/canonical/2")
   474  		io.WriteString(w, testRepair)
   475  		io.WriteString(w, "\n")
   476  		io.WriteString(w, testKey)
   477  	}))
   478  
   479  	c.Assert(mockServer, NotNil)
   480  	defer mockServer.Close()
   481  
   482  	runner := repair.NewRunner()
   483  	runner.BaseURL = mustParseURL(mockServer.URL)
   484  
   485  	repair, aux, err := runner.Fetch("canonical", 2, -1)
   486  	c.Assert(err, IsNil)
   487  	c.Check(repair, NotNil)
   488  	c.Check(aux, HasLen, 1)
   489  	_, ok := aux[0].(*asserts.AccountKey)
   490  	c.Check(ok, Equals, true)
   491  }
   492  
   493  func (s *runnerSuite) TestPeek(c *C) {
   494  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   495  		ua := r.Header.Get("User-Agent")
   496  		c.Check(strings.Contains(ua, "snap-repair"), Equals, true)
   497  		c.Check(r.Header.Get("Accept"), Equals, "application/json")
   498  		c.Check(r.URL.Path, Equals, "/repairs/canonical/2")
   499  		io.WriteString(w, testHeadersResp)
   500  	}))
   501  
   502  	c.Assert(mockServer, NotNil)
   503  	defer mockServer.Close()
   504  
   505  	runner := repair.NewRunner()
   506  	runner.BaseURL = mustParseURL(mockServer.URL)
   507  
   508  	r := s.mockBrokenTimeNowSetToEpoch(c, runner)
   509  	defer r()
   510  
   511  	h, err := runner.Peek("canonical", 2)
   512  	c.Assert(err, IsNil)
   513  	c.Check(h["series"], DeepEquals, []interface{}{"16"})
   514  	c.Check(h["architectures"], DeepEquals, []interface{}{"amd64", "arm64"})
   515  	c.Check(h["models"], DeepEquals, []interface{}{"xyz/frobinator"})
   516  
   517  	s.checkBrokenTimeNowMitigated(c, runner)
   518  }
   519  
   520  func (s *runnerSuite) TestPeek500(c *C) {
   521  	restore := repair.MockPeekRetryStrategy(testRetryStrategy)
   522  	defer restore()
   523  
   524  	n := 0
   525  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   526  		n++
   527  		w.WriteHeader(500)
   528  	}))
   529  
   530  	c.Assert(mockServer, NotNil)
   531  	defer mockServer.Close()
   532  
   533  	runner := repair.NewRunner()
   534  	runner.BaseURL = mustParseURL(mockServer.URL)
   535  
   536  	_, err := runner.Peek("canonical", 2)
   537  	c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500")
   538  	c.Assert(n, Equals, 5)
   539  }
   540  
   541  func (s *runnerSuite) TestPeekInvalid(c *C) {
   542  	restore := repair.MockPeekRetryStrategy(testRetryStrategy)
   543  	defer restore()
   544  
   545  	n := 0
   546  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   547  		n++
   548  		w.WriteHeader(200)
   549  		io.WriteString(w, "{")
   550  	}))
   551  
   552  	c.Assert(mockServer, NotNil)
   553  	defer mockServer.Close()
   554  
   555  	runner := repair.NewRunner()
   556  	runner.BaseURL = mustParseURL(mockServer.URL)
   557  
   558  	_, err := runner.Peek("canonical", 2)
   559  	c.Assert(err, Equals, io.ErrUnexpectedEOF)
   560  	c.Assert(n, Equals, 5)
   561  }
   562  
   563  func (s *runnerSuite) TestPeekNotFound(c *C) {
   564  	n := 0
   565  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   566  		n++
   567  		w.WriteHeader(404)
   568  	}))
   569  
   570  	c.Assert(mockServer, NotNil)
   571  	defer mockServer.Close()
   572  
   573  	runner := repair.NewRunner()
   574  	runner.BaseURL = mustParseURL(mockServer.URL)
   575  
   576  	r := s.mockBrokenTimeNowSetToEpoch(c, runner)
   577  	defer r()
   578  
   579  	_, err := runner.Peek("canonical", 2)
   580  	c.Assert(err, Equals, repair.ErrRepairNotFound)
   581  	c.Assert(n, Equals, 1)
   582  
   583  	s.checkBrokenTimeNowMitigated(c, runner)
   584  }
   585  
   586  func (s *runnerSuite) TestPeekIdMismatch(c *C) {
   587  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   588  		c.Check(r.Header.Get("Accept"), Equals, "application/json")
   589  		io.WriteString(w, testHeadersResp)
   590  	}))
   591  
   592  	c.Assert(mockServer, NotNil)
   593  	defer mockServer.Close()
   594  
   595  	runner := repair.NewRunner()
   596  	runner.BaseURL = mustParseURL(mockServer.URL)
   597  
   598  	_, err := runner.Peek("canonical", 4)
   599  	c.Assert(err, ErrorMatches, `cannot peek repair headers, repair id mismatch canonical/2 != canonical/4`)
   600  }
   601  
   602  func (s *runnerSuite) TestLoadState(c *C) {
   603  	s.freshState(c)
   604  
   605  	runner := repair.NewRunner()
   606  	err := runner.LoadState()
   607  	c.Assert(err, IsNil)
   608  	brand, model := runner.BrandModel()
   609  	c.Check(brand, Equals, "my-brand")
   610  	c.Check(model, Equals, "my-model")
   611  }
   612  
   613  func (s *runnerSuite) initSeed(c *C) {
   614  	err := os.MkdirAll(s.seedAssertsDir, 0775)
   615  	c.Assert(err, IsNil)
   616  }
   617  
   618  func (s *runnerSuite) writeSeedAssert(c *C, fname string, a asserts.Assertion) {
   619  	err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, fname), asserts.Encode(a), 0644)
   620  	c.Assert(err, IsNil)
   621  }
   622  
   623  func (s *runnerSuite) rmSeedAssert(c *C, fname string) {
   624  	err := os.Remove(filepath.Join(s.seedAssertsDir, fname))
   625  	c.Assert(err, IsNil)
   626  }
   627  
   628  func (s *runnerSuite) TestLoadStateInitState(c *C) {
   629  	// sanity
   630  	c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false)
   631  	c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false)
   632  	// setup realistic seed/assertions
   633  	r := sysdb.InjectTrusted(s.storeSigning.Trusted)
   634  	defer r()
   635  	s.initSeed(c)
   636  	s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey(""))
   637  	s.writeSeedAssert(c, "brand.account", s.brandAcct)
   638  	s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey)
   639  	s.writeSeedAssert(c, "model", s.modelAs)
   640  
   641  	runner := repair.NewRunner()
   642  	err := runner.LoadState()
   643  	c.Assert(err, IsNil)
   644  	c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, true)
   645  
   646  	brand, model := runner.BrandModel()
   647  	c.Check(brand, Equals, "my-brand")
   648  	c.Check(model, Equals, "my-model-2")
   649  
   650  	c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true)
   651  }
   652  
   653  func (s *runnerSuite) TestLoadStateInitDeviceInfoFail(c *C) {
   654  	// sanity
   655  	c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false)
   656  	c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false)
   657  	// setup realistic seed/assertions
   658  	r := sysdb.InjectTrusted(s.storeSigning.Trusted)
   659  	defer r()
   660  	s.initSeed(c)
   661  
   662  	const errPrefix = "cannot set device information: "
   663  	tests := []struct {
   664  		breakFunc   func()
   665  		expectedErr string
   666  	}{
   667  		{func() { s.rmSeedAssert(c, "model") }, errPrefix + "no model assertion in seed data"},
   668  		{func() { s.rmSeedAssert(c, "brand.account") }, errPrefix + "no brand account assertion in seed data"},
   669  		{func() { s.rmSeedAssert(c, "brand.account-key") }, errPrefix + `cannot find public key.*`},
   670  		{func() {
   671  			// broken signature
   672  			blob := asserts.Encode(s.brandAcct)
   673  			err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, "brand.account"), blob[:len(blob)-3], 0644)
   674  			c.Assert(err, IsNil)
   675  		}, errPrefix + "cannot decode signature:.*"},
   676  		{func() { s.writeSeedAssert(c, "model2", s.modelAs) }, errPrefix + "multiple models in seed assertions"},
   677  	}
   678  
   679  	for _, test := range tests {
   680  		s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey(""))
   681  		s.writeSeedAssert(c, "brand.account", s.brandAcct)
   682  		s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey)
   683  		s.writeSeedAssert(c, "model", s.modelAs)
   684  
   685  		test.breakFunc()
   686  
   687  		runner := repair.NewRunner()
   688  		err := runner.LoadState()
   689  		c.Check(err, ErrorMatches, test.expectedErr)
   690  	}
   691  }
   692  
   693  func (s *runnerSuite) TestTLSTime(c *C) {
   694  	s.freshState(c)
   695  	runner := repair.NewRunner()
   696  	err := runner.LoadState()
   697  	c.Assert(err, IsNil)
   698  	epoch := time.Unix(0, 0)
   699  	r := repair.MockTimeNow(func() time.Time {
   700  		return epoch
   701  	})
   702  	defer r()
   703  	c.Check(runner.TLSTime().Equal(s.seedTime), Equals, true)
   704  }
   705  
   706  func makeReadOnly(c *C, dir string) (restore func()) {
   707  	// skip tests that need this because uid==0 does not honor
   708  	// write permissions in directories (yay, unix)
   709  	if os.Getuid() == 0 {
   710  		// FIXME: we could use osutil.Chattr() here
   711  		c.Skip("too lazy to make path readonly as root")
   712  	}
   713  	err := os.Chmod(dir, 0555)
   714  	c.Assert(err, IsNil)
   715  	return func() {
   716  		err := os.Chmod(dir, 0755)
   717  		c.Assert(err, IsNil)
   718  	}
   719  }
   720  
   721  func (s *runnerSuite) TestLoadStateInitStateFail(c *C) {
   722  	restore := makeReadOnly(c, filepath.Dir(dirs.SnapSeedDir))
   723  	defer restore()
   724  
   725  	runner := repair.NewRunner()
   726  	err := runner.LoadState()
   727  	c.Check(err, ErrorMatches, `cannot create repair state directory:.*`)
   728  }
   729  
   730  func (s *runnerSuite) TestSaveStateFail(c *C) {
   731  	s.freshState(c)
   732  
   733  	runner := repair.NewRunner()
   734  	err := runner.LoadState()
   735  	c.Assert(err, IsNil)
   736  
   737  	restore := makeReadOnly(c, dirs.SnapRepairDir)
   738  	defer restore()
   739  
   740  	// no error because this is a no-op
   741  	err = runner.SaveState()
   742  	c.Check(err, IsNil)
   743  
   744  	// mark as modified
   745  	runner.SetStateModified(true)
   746  
   747  	err = runner.SaveState()
   748  	c.Check(err, ErrorMatches, `cannot save repair state:.*`)
   749  }
   750  
   751  func (s *runnerSuite) TestSaveState(c *C) {
   752  	s.freshState(c)
   753  
   754  	runner := repair.NewRunner()
   755  	err := runner.LoadState()
   756  	c.Assert(err, IsNil)
   757  
   758  	runner.SetSequence("canonical", []*repair.RepairState{
   759  		{Sequence: 1, Revision: 3},
   760  	})
   761  	// mark as modified
   762  	runner.SetStateModified(true)
   763  
   764  	err = runner.SaveState()
   765  	c.Assert(err, IsNil)
   766  
   767  	c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, `{"device":{"brand":"my-brand","model":"my-model"},"sequences":{"canonical":[{"sequence":1,"revision":3,"status":0}]},"time-lower-bound":"2017-08-11T15:49:49Z"}`)
   768  }
   769  
   770  func (s *runnerSuite) TestApplicable(c *C) {
   771  	s.freshState(c)
   772  	runner := repair.NewRunner()
   773  	err := runner.LoadState()
   774  	c.Assert(err, IsNil)
   775  
   776  	scenarios := []struct {
   777  		headers    map[string]interface{}
   778  		applicable bool
   779  	}{
   780  		{nil, true},
   781  		{map[string]interface{}{"series": []interface{}{"18"}}, false},
   782  		{map[string]interface{}{"series": []interface{}{"18", "16"}}, true},
   783  		{map[string]interface{}{"series": "18"}, false},
   784  		{map[string]interface{}{"series": []interface{}{18}}, false},
   785  		{map[string]interface{}{"architectures": []interface{}{arch.DpkgArchitecture()}}, true},
   786  		{map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false},
   787  		{map[string]interface{}{"architectures": []interface{}{"other-arch", arch.DpkgArchitecture()}}, true},
   788  		{map[string]interface{}{"architectures": arch.DpkgArchitecture()}, false},
   789  		{map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true},
   790  		{map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false},
   791  		{map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true},
   792  		{map[string]interface{}{"models": "my-brand/my-model"}, false},
   793  		// model prefix matches
   794  		{map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true},
   795  		{map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true},
   796  		{map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false},
   797  		{map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true},
   798  		{map[string]interface{}{"models": []interface{}{"my*"}}, false},
   799  		{map[string]interface{}{"disabled": "true"}, false},
   800  		{map[string]interface{}{"disabled": "false"}, true},
   801  	}
   802  
   803  	for _, scen := range scenarios {
   804  		ok := runner.Applicable(scen.headers)
   805  		c.Check(ok, Equals, scen.applicable, Commentf("%v", scen))
   806  	}
   807  }
   808  
   809  var (
   810  	nextRepairs = []string{`type: repair
   811  authority-id: canonical
   812  brand-id: canonical
   813  repair-id: 1
   814  summary: repair one
   815  timestamp: 2017-07-01T12:00:00Z
   816  body-length: 8
   817  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   818  
   819  scriptA
   820  
   821  
   822  AXNpZw==`,
   823  		`type: repair
   824  authority-id: canonical
   825  brand-id: canonical
   826  repair-id: 2
   827  summary: repair two
   828  series:
   829    - 33
   830  timestamp: 2017-07-02T12:00:00Z
   831  body-length: 8
   832  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   833  
   834  scriptB
   835  
   836  
   837  AXNpZw==`,
   838  		`type: repair
   839  revision: 2
   840  authority-id: canonical
   841  brand-id: canonical
   842  repair-id: 3
   843  summary: repair three rev2
   844  series:
   845    - 16
   846  timestamp: 2017-07-03T12:00:00Z
   847  body-length: 8
   848  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   849  
   850  scriptC
   851  
   852  
   853  AXNpZw==
   854  `}
   855  
   856  	repair3Rev4 = `type: repair
   857  revision: 4
   858  authority-id: canonical
   859  brand-id: canonical
   860  repair-id: 3
   861  summary: repair three rev4
   862  series:
   863    - 16
   864  timestamp: 2017-07-03T12:00:00Z
   865  body-length: 9
   866  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   867  
   868  scriptC2
   869  
   870  
   871  AXNpZw==
   872  `
   873  
   874  	repair4 = `type: repair
   875  authority-id: canonical
   876  brand-id: canonical
   877  repair-id: 4
   878  summary: repair four
   879  timestamp: 2017-07-03T12:00:00Z
   880  body-length: 8
   881  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
   882  
   883  scriptD
   884  
   885  
   886  AXNpZw==
   887  `
   888  )
   889  
   890  func makeMockServer(c *C, seqRepairs *[]string, redirectFirst bool) *httptest.Server {
   891  	var mockServer *httptest.Server
   892  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   893  		ua := r.Header.Get("User-Agent")
   894  		c.Check(strings.Contains(ua, "snap-repair"), Equals, true)
   895  
   896  		urlPath := r.URL.Path
   897  		if redirectFirst && r.Header.Get("Accept") == asserts.MediaType {
   898  			if !strings.HasPrefix(urlPath, "/final/") {
   899  				// redirect
   900  				finalURL := mockServer.URL + "/final" + r.URL.Path
   901  				w.Header().Set("Location", finalURL)
   902  				w.WriteHeader(302)
   903  				return
   904  			}
   905  			urlPath = strings.TrimPrefix(urlPath, "/final")
   906  		}
   907  
   908  		c.Check(strings.HasPrefix(urlPath, "/repairs/canonical/"), Equals, true)
   909  
   910  		seq, err := strconv.Atoi(strings.TrimPrefix(urlPath, "/repairs/canonical/"))
   911  		c.Assert(err, IsNil)
   912  
   913  		if seq > len(*seqRepairs) {
   914  			w.WriteHeader(404)
   915  			return
   916  		}
   917  
   918  		rpr := []byte((*seqRepairs)[seq-1])
   919  		dec := asserts.NewDecoder(bytes.NewBuffer(rpr))
   920  		repair, err := dec.Decode()
   921  		c.Assert(err, IsNil)
   922  
   923  		switch r.Header.Get("Accept") {
   924  		case "application/json":
   925  			b, err := json.Marshal(map[string]interface{}{
   926  				"headers": repair.Headers(),
   927  			})
   928  			c.Assert(err, IsNil)
   929  			w.Write(b)
   930  		case asserts.MediaType:
   931  			etag := fmt.Sprintf(`"%d"`, repair.Revision())
   932  			if strings.Contains(r.Header.Get("If-None-Match"), etag) {
   933  				w.WriteHeader(304)
   934  				return
   935  			}
   936  			w.Write(rpr)
   937  		}
   938  	}))
   939  
   940  	c.Assert(mockServer, NotNil)
   941  
   942  	return mockServer
   943  }
   944  
   945  func (s *runnerSuite) TestTrustedRepairRootKeys(c *C) {
   946  	acctKeys := repair.TrustedRepairRootKeys()
   947  	c.Check(acctKeys, HasLen, 1)
   948  	c.Check(acctKeys[0].AccountID(), Equals, "canonical")
   949  	c.Check(acctKeys[0].PublicKeyID(), Equals, "nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t")
   950  }
   951  
   952  func (s *runnerSuite) TestVerify(c *C) {
   953  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
   954  	defer r1()
   955  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
   956  	defer r2()
   957  
   958  	runner := repair.NewRunner()
   959  
   960  	a, err := s.repairsSigning.Sign(asserts.RepairType, map[string]interface{}{
   961  		"brand-id":  "canonical",
   962  		"repair-id": "2",
   963  		"summary":   "repair two",
   964  		"timestamp": time.Now().UTC().Format(time.RFC3339),
   965  	}, []byte("#script"), "")
   966  	c.Assert(err, IsNil)
   967  	rpr := a.(*asserts.Repair)
   968  
   969  	err = runner.Verify(rpr, []asserts.Assertion{s.repairsAcctKey})
   970  	c.Check(err, IsNil)
   971  }
   972  
   973  func (s *runnerSuite) signSeqRepairs(c *C, repairs []string) []string {
   974  	var seq []string
   975  	for _, rpr := range repairs {
   976  		decoded, err := asserts.Decode([]byte(rpr))
   977  		c.Assert(err, IsNil)
   978  		signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "")
   979  		c.Assert(err, IsNil)
   980  		buf := &bytes.Buffer{}
   981  		enc := asserts.NewEncoder(buf)
   982  		enc.Encode(signed)
   983  		enc.Encode(s.repairsAcctKey)
   984  		seq = append(seq, buf.String())
   985  	}
   986  	return seq
   987  }
   988  
   989  func (s *runnerSuite) loadSequences(c *C) map[string][]*repair.RepairState {
   990  	data, err := ioutil.ReadFile(dirs.SnapRepairStateFile)
   991  	c.Assert(err, IsNil)
   992  	var x struct {
   993  		Sequences map[string][]*repair.RepairState `json:"sequences"`
   994  	}
   995  	err = json.Unmarshal(data, &x)
   996  	c.Assert(err, IsNil)
   997  	return x.Sequences
   998  }
   999  
  1000  func (s *runnerSuite) testNext(c *C, redirectFirst bool) {
  1001  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1002  	defer r1()
  1003  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1004  	defer r2()
  1005  
  1006  	seqRepairs := s.signSeqRepairs(c, nextRepairs)
  1007  
  1008  	mockServer := makeMockServer(c, &seqRepairs, redirectFirst)
  1009  	defer mockServer.Close()
  1010  
  1011  	runner := repair.NewRunner()
  1012  	runner.BaseURL = mustParseURL(mockServer.URL)
  1013  	runner.LoadState()
  1014  
  1015  	rpr, err := runner.Next("canonical")
  1016  	c.Assert(err, IsNil)
  1017  	c.Check(rpr.RepairID(), Equals, 1)
  1018  	c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "1", "r0.repair")), Equals, true)
  1019  
  1020  	rpr, err = runner.Next("canonical")
  1021  	c.Assert(err, IsNil)
  1022  	c.Check(rpr.RepairID(), Equals, 3)
  1023  	c.Check(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "3", "r2.repair"), testutil.FileEquals, seqRepairs[2])
  1024  
  1025  	// no more
  1026  	rpr, err = runner.Next("canonical")
  1027  	c.Check(err, Equals, repair.ErrRepairNotFound)
  1028  
  1029  	expectedSeq := []*repair.RepairState{
  1030  		{Sequence: 1},
  1031  		{Sequence: 2, Status: repair.SkipStatus},
  1032  		{Sequence: 3, Revision: 2},
  1033  	}
  1034  	c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq)
  1035  	// on disk
  1036  	seqs := s.loadSequences(c)
  1037  	c.Check(seqs["canonical"], DeepEquals, expectedSeq)
  1038  
  1039  	// start fresh run with new runner
  1040  	// will refetch repair 3
  1041  	signed := s.signSeqRepairs(c, []string{repair3Rev4, repair4})
  1042  	seqRepairs[2] = signed[0]
  1043  	seqRepairs = append(seqRepairs, signed[1])
  1044  
  1045  	runner = repair.NewRunner()
  1046  	runner.BaseURL = mustParseURL(mockServer.URL)
  1047  	runner.LoadState()
  1048  
  1049  	rpr, err = runner.Next("canonical")
  1050  	c.Assert(err, IsNil)
  1051  	c.Check(rpr.RepairID(), Equals, 1)
  1052  
  1053  	rpr, err = runner.Next("canonical")
  1054  	c.Assert(err, IsNil)
  1055  	c.Check(rpr.RepairID(), Equals, 3)
  1056  	// refetched new revision!
  1057  	c.Check(rpr.Revision(), Equals, 4)
  1058  	c.Check(rpr.Body(), DeepEquals, []byte("scriptC2\n"))
  1059  
  1060  	// new repair
  1061  	rpr, err = runner.Next("canonical")
  1062  	c.Assert(err, IsNil)
  1063  	c.Check(rpr.RepairID(), Equals, 4)
  1064  	c.Check(rpr.Body(), DeepEquals, []byte("scriptD\n"))
  1065  
  1066  	// no more
  1067  	rpr, err = runner.Next("canonical")
  1068  	c.Check(err, Equals, repair.ErrRepairNotFound)
  1069  
  1070  	c.Check(runner.Sequence("canonical"), DeepEquals, []*repair.RepairState{
  1071  		{Sequence: 1},
  1072  		{Sequence: 2, Status: repair.SkipStatus},
  1073  		{Sequence: 3, Revision: 4},
  1074  		{Sequence: 4},
  1075  	})
  1076  }
  1077  
  1078  func (s *runnerSuite) TestNext(c *C) {
  1079  	redirectFirst := false
  1080  	s.testNext(c, redirectFirst)
  1081  }
  1082  
  1083  func (s *runnerSuite) TestNextRedirect(c *C) {
  1084  	redirectFirst := true
  1085  	s.testNext(c, redirectFirst)
  1086  }
  1087  
  1088  func (s *runnerSuite) TestNextImmediateSkip(c *C) {
  1089  	seqRepairs := []string{`type: repair
  1090  authority-id: canonical
  1091  brand-id: canonical
  1092  repair-id: 1
  1093  summary: repair one
  1094  series:
  1095    - 33
  1096  timestamp: 2017-07-02T12:00:00Z
  1097  body-length: 8
  1098  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1099  
  1100  scriptB
  1101  
  1102  
  1103  AXNpZw==`}
  1104  
  1105  	mockServer := makeMockServer(c, &seqRepairs, false)
  1106  	defer mockServer.Close()
  1107  
  1108  	runner := repair.NewRunner()
  1109  	runner.BaseURL = mustParseURL(mockServer.URL)
  1110  	runner.LoadState()
  1111  
  1112  	// not applicable => not returned
  1113  	_, err := runner.Next("canonical")
  1114  	c.Check(err, Equals, repair.ErrRepairNotFound)
  1115  
  1116  	expectedSeq := []*repair.RepairState{
  1117  		{Sequence: 1, Status: repair.SkipStatus},
  1118  	}
  1119  	c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq)
  1120  	// on disk
  1121  	seqs := s.loadSequences(c)
  1122  	c.Check(seqs["canonical"], DeepEquals, expectedSeq)
  1123  }
  1124  
  1125  func (s *runnerSuite) TestNextRefetchSkip(c *C) {
  1126  	seqRepairs := []string{`type: repair
  1127  authority-id: canonical
  1128  brand-id: canonical
  1129  repair-id: 1
  1130  summary: repair one
  1131  series:
  1132    - 16
  1133  timestamp: 2017-07-02T12:00:00Z
  1134  body-length: 8
  1135  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1136  
  1137  scriptB
  1138  
  1139  
  1140  AXNpZw==`}
  1141  
  1142  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1143  	defer r1()
  1144  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1145  	defer r2()
  1146  
  1147  	seqRepairs = s.signSeqRepairs(c, seqRepairs)
  1148  
  1149  	mockServer := makeMockServer(c, &seqRepairs, false)
  1150  	defer mockServer.Close()
  1151  
  1152  	runner := repair.NewRunner()
  1153  	runner.BaseURL = mustParseURL(mockServer.URL)
  1154  	runner.LoadState()
  1155  
  1156  	_, err := runner.Next("canonical")
  1157  	c.Assert(err, IsNil)
  1158  
  1159  	expectedSeq := []*repair.RepairState{
  1160  		{Sequence: 1},
  1161  	}
  1162  	c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq)
  1163  	// on disk
  1164  	seqs := s.loadSequences(c)
  1165  	c.Check(seqs["canonical"], DeepEquals, expectedSeq)
  1166  
  1167  	// new fresh run, repair becomes now unapplicable
  1168  	seqRepairs[0] = `type: repair
  1169  authority-id: canonical
  1170  revision: 1
  1171  brand-id: canonical
  1172  repair-id: 1
  1173  summary: repair one rev1
  1174  series:
  1175    - 16
  1176  disabled: true
  1177  timestamp: 2017-07-02T12:00:00Z
  1178  body-length: 7
  1179  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1180  
  1181  scriptX
  1182  
  1183  AXNpZw==`
  1184  
  1185  	seqRepairs = s.signSeqRepairs(c, seqRepairs)
  1186  
  1187  	runner = repair.NewRunner()
  1188  	runner.BaseURL = mustParseURL(mockServer.URL)
  1189  	runner.LoadState()
  1190  
  1191  	_, err = runner.Next("canonical")
  1192  	c.Check(err, Equals, repair.ErrRepairNotFound)
  1193  
  1194  	expectedSeq = []*repair.RepairState{
  1195  		{Sequence: 1, Revision: 1, Status: repair.SkipStatus},
  1196  	}
  1197  	c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq)
  1198  	// on disk
  1199  	seqs = s.loadSequences(c)
  1200  	c.Check(seqs["canonical"], DeepEquals, expectedSeq)
  1201  }
  1202  
  1203  func (s *runnerSuite) TestNext500(c *C) {
  1204  	restore := repair.MockPeekRetryStrategy(testRetryStrategy)
  1205  	defer restore()
  1206  
  1207  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1208  		w.WriteHeader(500)
  1209  	}))
  1210  
  1211  	c.Assert(mockServer, NotNil)
  1212  	defer mockServer.Close()
  1213  
  1214  	runner := repair.NewRunner()
  1215  	runner.BaseURL = mustParseURL(mockServer.URL)
  1216  	runner.LoadState()
  1217  
  1218  	_, err := runner.Next("canonical")
  1219  	c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500")
  1220  }
  1221  
  1222  func (s *runnerSuite) TestNextNotFound(c *C) {
  1223  	s.freshState(c)
  1224  
  1225  	restore := repair.MockPeekRetryStrategy(testRetryStrategy)
  1226  	defer restore()
  1227  
  1228  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1229  		w.WriteHeader(404)
  1230  	}))
  1231  
  1232  	c.Assert(mockServer, NotNil)
  1233  	defer mockServer.Close()
  1234  
  1235  	runner := repair.NewRunner()
  1236  	runner.BaseURL = mustParseURL(mockServer.URL)
  1237  	runner.LoadState()
  1238  
  1239  	// sanity
  1240  	c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, freshStateJSON)
  1241  
  1242  	_, err := runner.Next("canonical")
  1243  	c.Assert(err, Equals, repair.ErrRepairNotFound)
  1244  
  1245  	// we saved new time lower bound
  1246  	t1 := runner.TimeLowerBound()
  1247  	expected := strings.Replace(freshStateJSON, "2017-08-11T15:49:49Z", t1.Format(time.RFC3339), 1)
  1248  	c.Check(expected, Not(Equals), freshStateJSON)
  1249  	c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, expected)
  1250  }
  1251  
  1252  func (s *runnerSuite) TestNextSaveStateError(c *C) {
  1253  	seqRepairs := []string{`type: repair
  1254  authority-id: canonical
  1255  brand-id: canonical
  1256  repair-id: 1
  1257  summary: repair one
  1258  series:
  1259    - 33
  1260  timestamp: 2017-07-02T12:00:00Z
  1261  body-length: 8
  1262  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1263  
  1264  scriptB
  1265  
  1266  
  1267  AXNpZw==`}
  1268  
  1269  	mockServer := makeMockServer(c, &seqRepairs, false)
  1270  	defer mockServer.Close()
  1271  
  1272  	runner := repair.NewRunner()
  1273  	runner.BaseURL = mustParseURL(mockServer.URL)
  1274  	runner.LoadState()
  1275  
  1276  	// break SaveState
  1277  	restore := makeReadOnly(c, dirs.SnapRepairDir)
  1278  	defer restore()
  1279  
  1280  	_, err := runner.Next("canonical")
  1281  	c.Check(err, ErrorMatches, `cannot save repair state:.*`)
  1282  }
  1283  
  1284  func (s *runnerSuite) TestNextVerifyNoKey(c *C) {
  1285  	seqRepairs := []string{`type: repair
  1286  authority-id: canonical
  1287  brand-id: canonical
  1288  repair-id: 1
  1289  summary: repair one
  1290  timestamp: 2017-07-02T12:00:00Z
  1291  body-length: 8
  1292  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1293  
  1294  scriptB
  1295  
  1296  
  1297  AXNpZw==`}
  1298  
  1299  	mockServer := makeMockServer(c, &seqRepairs, false)
  1300  	defer mockServer.Close()
  1301  
  1302  	runner := repair.NewRunner()
  1303  	runner.BaseURL = mustParseURL(mockServer.URL)
  1304  	runner.LoadState()
  1305  
  1306  	_, err := runner.Next("canonical")
  1307  	c.Check(err, ErrorMatches, `cannot verify repair canonical-1: cannot find public key.*`)
  1308  
  1309  	c.Check(runner.Sequence("canonical"), HasLen, 0)
  1310  }
  1311  
  1312  func (s *runnerSuite) TestNextVerifySelfSigned(c *C) {
  1313  	randoKey, _ := assertstest.GenerateKey(752)
  1314  
  1315  	randomSigning := assertstest.NewSigningDB("canonical", randoKey)
  1316  	randoKeyEncoded, err := asserts.EncodePublicKey(randoKey.PublicKey())
  1317  	c.Assert(err, IsNil)
  1318  	acctKey, err := randomSigning.Sign(asserts.AccountKeyType, map[string]interface{}{
  1319  		"authority-id":        "canonical",
  1320  		"account-id":          "canonical",
  1321  		"public-key-sha3-384": randoKey.PublicKey().ID(),
  1322  		"name":                "repairs",
  1323  		"since":               time.Now().UTC().Format(time.RFC3339),
  1324  	}, randoKeyEncoded, "")
  1325  	c.Assert(err, IsNil)
  1326  
  1327  	rpr, err := randomSigning.Sign(asserts.RepairType, map[string]interface{}{
  1328  		"brand-id":  "canonical",
  1329  		"repair-id": "1",
  1330  		"summary":   "repair one",
  1331  		"timestamp": time.Now().UTC().Format(time.RFC3339),
  1332  	}, []byte("scriptB\n"), "")
  1333  	c.Assert(err, IsNil)
  1334  
  1335  	buf := &bytes.Buffer{}
  1336  	enc := asserts.NewEncoder(buf)
  1337  	enc.Encode(rpr)
  1338  	enc.Encode(acctKey)
  1339  	seqRepairs := []string{buf.String()}
  1340  
  1341  	mockServer := makeMockServer(c, &seqRepairs, false)
  1342  	defer mockServer.Close()
  1343  
  1344  	runner := repair.NewRunner()
  1345  	runner.BaseURL = mustParseURL(mockServer.URL)
  1346  	runner.LoadState()
  1347  
  1348  	_, err = runner.Next("canonical")
  1349  	c.Check(err, ErrorMatches, `cannot verify repair canonical-1: circular assertions`)
  1350  
  1351  	c.Check(runner.Sequence("canonical"), HasLen, 0)
  1352  }
  1353  
  1354  func (s *runnerSuite) TestNextVerifyAllKeysOK(c *C) {
  1355  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1356  	defer r1()
  1357  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1358  	defer r2()
  1359  
  1360  	decoded, err := asserts.Decode([]byte(nextRepairs[0]))
  1361  	c.Assert(err, IsNil)
  1362  	signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "")
  1363  	c.Assert(err, IsNil)
  1364  
  1365  	// stream with all keys (any order) works as well
  1366  	buf := &bytes.Buffer{}
  1367  	enc := asserts.NewEncoder(buf)
  1368  	enc.Encode(signed)
  1369  	enc.Encode(s.storeSigning.TrustedKey)
  1370  	enc.Encode(s.repairRootAcctKey)
  1371  	enc.Encode(s.repairsAcctKey)
  1372  	seqRepairs := []string{buf.String()}
  1373  
  1374  	mockServer := makeMockServer(c, &seqRepairs, false)
  1375  	defer mockServer.Close()
  1376  
  1377  	runner := repair.NewRunner()
  1378  	runner.BaseURL = mustParseURL(mockServer.URL)
  1379  	runner.LoadState()
  1380  
  1381  	rpr, err := runner.Next("canonical")
  1382  	c.Assert(err, IsNil)
  1383  	c.Check(rpr.RepairID(), Equals, 1)
  1384  }
  1385  
  1386  func (s *runnerSuite) TestRepairSetStatus(c *C) {
  1387  	seqRepairs := []string{`type: repair
  1388  authority-id: canonical
  1389  brand-id: canonical
  1390  repair-id: 1
  1391  summary: repair one
  1392  timestamp: 2017-07-02T12:00:00Z
  1393  body-length: 8
  1394  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1395  
  1396  scriptB
  1397  
  1398  
  1399  AXNpZw==`}
  1400  
  1401  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1402  	defer r1()
  1403  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1404  	defer r2()
  1405  
  1406  	seqRepairs = s.signSeqRepairs(c, seqRepairs)
  1407  
  1408  	mockServer := makeMockServer(c, &seqRepairs, false)
  1409  	defer mockServer.Close()
  1410  
  1411  	runner := repair.NewRunner()
  1412  	runner.BaseURL = mustParseURL(mockServer.URL)
  1413  	runner.LoadState()
  1414  
  1415  	rpr, err := runner.Next("canonical")
  1416  	c.Assert(err, IsNil)
  1417  
  1418  	rpr.SetStatus(repair.DoneStatus)
  1419  
  1420  	expectedSeq := []*repair.RepairState{
  1421  		{Sequence: 1, Status: repair.DoneStatus},
  1422  	}
  1423  	c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq)
  1424  	// on disk
  1425  	seqs := s.loadSequences(c)
  1426  	c.Check(seqs["canonical"], DeepEquals, expectedSeq)
  1427  }
  1428  
  1429  func (s *runnerSuite) TestRepairBasicRun(c *C) {
  1430  	seqRepairs := []string{`type: repair
  1431  authority-id: canonical
  1432  brand-id: canonical
  1433  repair-id: 1
  1434  summary: repair one
  1435  series:
  1436    - 16
  1437  timestamp: 2017-07-02T12:00:00Z
  1438  body-length: 17
  1439  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1440  
  1441  #!/bin/sh
  1442  exit 0
  1443  
  1444  
  1445  AXNpZw==`}
  1446  
  1447  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1448  	defer r1()
  1449  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1450  	defer r2()
  1451  
  1452  	seqRepairs = s.signSeqRepairs(c, seqRepairs)
  1453  
  1454  	mockServer := makeMockServer(c, &seqRepairs, false)
  1455  	defer mockServer.Close()
  1456  
  1457  	runner := repair.NewRunner()
  1458  	runner.BaseURL = mustParseURL(mockServer.URL)
  1459  	runner.LoadState()
  1460  
  1461  	rpr, err := runner.Next("canonical")
  1462  	c.Assert(err, IsNil)
  1463  
  1464  	err = rpr.Run()
  1465  	c.Assert(err, IsNil)
  1466  	c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, "#!/bin/sh\nexit 0\n")
  1467  }
  1468  
  1469  func makeMockRepair(script string) string {
  1470  	return fmt.Sprintf(`type: repair
  1471  authority-id: canonical
  1472  brand-id: canonical
  1473  repair-id: 1
  1474  summary: repair one
  1475  series:
  1476    - 16
  1477  timestamp: 2017-07-02T12:00:00Z
  1478  body-length: %d
  1479  sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj
  1480  
  1481  %s
  1482  
  1483  AXNpZw==`, len(script), script)
  1484  }
  1485  
  1486  func verifyRepairStatus(c *C, status repair.RepairStatus) {
  1487  	c.Check(dirs.SnapRepairStateFile, testutil.FileContains, fmt.Sprintf(`{"device":{"brand":"","model":""},"sequences":{"canonical":[{"sequence":1,"revision":0,"status":%d}`, status))
  1488  }
  1489  
  1490  // tests related to correct execution of script
  1491  type runScriptSuite struct {
  1492  	baseRunnerSuite
  1493  
  1494  	seqRepairs []string
  1495  
  1496  	mockServer *httptest.Server
  1497  	runner     *repair.Runner
  1498  
  1499  	runDir string
  1500  
  1501  	restoreErrTrackerReportRepair func()
  1502  	errReport                     struct {
  1503  		repair string
  1504  		errMsg string
  1505  		dupSig string
  1506  		extra  map[string]string
  1507  	}
  1508  }
  1509  
  1510  var _ = Suite(&runScriptSuite{})
  1511  
  1512  func (s *runScriptSuite) SetUpTest(c *C) {
  1513  	s.baseRunnerSuite.SetUpTest(c)
  1514  
  1515  	s.mockServer = makeMockServer(c, &s.seqRepairs, false)
  1516  
  1517  	s.runner = repair.NewRunner()
  1518  	s.runner.BaseURL = mustParseURL(s.mockServer.URL)
  1519  	s.runner.LoadState()
  1520  
  1521  	s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1")
  1522  
  1523  	s.restoreErrTrackerReportRepair = repair.MockErrtrackerReportRepair(s.errtrackerReportRepair)
  1524  }
  1525  
  1526  func (s *runScriptSuite) TearDownTest(c *C) {
  1527  	s.baseRunnerSuite.TearDownTest(c)
  1528  
  1529  	s.restoreErrTrackerReportRepair()
  1530  	s.mockServer.Close()
  1531  }
  1532  
  1533  func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) {
  1534  	s.errReport.repair = repair
  1535  	s.errReport.errMsg = errMsg
  1536  	s.errReport.dupSig = dupSig
  1537  	s.errReport.extra = extra
  1538  
  1539  	return "some-oops-id", nil
  1540  }
  1541  
  1542  func (s *runScriptSuite) testScriptRun(c *C, mockScript string) *repair.Repair {
  1543  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1544  	defer r1()
  1545  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1546  	defer r2()
  1547  
  1548  	s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs)
  1549  
  1550  	rpr, err := s.runner.Next("canonical")
  1551  	c.Assert(err, IsNil)
  1552  
  1553  	err = rpr.Run()
  1554  	c.Assert(err, IsNil)
  1555  
  1556  	c.Check(filepath.Join(s.runDir, "r0.script"), testutil.FileEquals, mockScript)
  1557  
  1558  	return rpr
  1559  }
  1560  
  1561  func (s *runScriptSuite) verifyRundir(c *C, names []string) {
  1562  	dirents, err := ioutil.ReadDir(s.runDir)
  1563  	c.Assert(err, IsNil)
  1564  	c.Assert(dirents, HasLen, len(names))
  1565  	for i := range dirents {
  1566  		c.Check(dirents[i].Name(), Matches, names[i])
  1567  	}
  1568  }
  1569  
  1570  type byMtime []os.FileInfo
  1571  
  1572  func (m byMtime) Len() int           { return len(m) }
  1573  func (m byMtime) Less(i, j int) bool { return m[i].ModTime().Before(m[j].ModTime()) }
  1574  func (m byMtime) Swap(i, j int)      { m[i], m[j] = m[j], m[i] }
  1575  
  1576  func (s *runScriptSuite) verifyOutput(c *C, name, expectedOutput string) {
  1577  	c.Check(filepath.Join(s.runDir, name), testutil.FileEquals, expectedOutput)
  1578  	// ensure correct permissions
  1579  	fi, err := os.Stat(filepath.Join(s.runDir, name))
  1580  	c.Assert(err, IsNil)
  1581  	c.Check(fi.Mode(), Equals, os.FileMode(0600))
  1582  }
  1583  
  1584  func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) {
  1585  	script := `#!/bin/sh
  1586  echo "happy output"
  1587  echo "done" >&$SNAP_REPAIR_STATUS_FD
  1588  exit 0
  1589  `
  1590  	s.seqRepairs = []string{makeMockRepair(script)}
  1591  	s.testScriptRun(c, script)
  1592  	// verify
  1593  	s.verifyRundir(c, []string{
  1594  		`^r0.done$`,
  1595  		`^r0.script$`,
  1596  		`^work$`,
  1597  	})
  1598  	s.verifyOutput(c, "r0.done", `repair: canonical-1
  1599  revision: 0
  1600  summary: repair one
  1601  output:
  1602  happy output
  1603  `)
  1604  	verifyRepairStatus(c, repair.DoneStatus)
  1605  }
  1606  
  1607  func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) {
  1608  	script := `#!/bin/sh
  1609  echo "unhappy output"
  1610  exit 1
  1611  `
  1612  	s.seqRepairs = []string{makeMockRepair(script)}
  1613  	s.testScriptRun(c, script)
  1614  	// verify
  1615  	s.verifyRundir(c, []string{
  1616  		`^r0.retry$`,
  1617  		`^r0.script$`,
  1618  		`^work$`,
  1619  	})
  1620  	s.verifyOutput(c, "r0.retry", `repair: canonical-1
  1621  revision: 0
  1622  summary: repair one
  1623  output:
  1624  unhappy output
  1625  
  1626  repair canonical-1 revision 0 failed: exit status 1`)
  1627  	verifyRepairStatus(c, repair.RetryStatus)
  1628  
  1629  	c.Check(s.errReport.repair, Equals, "canonical/1")
  1630  	c.Check(s.errReport.errMsg, Equals, `repair canonical-1 revision 0 failed: exit status 1`)
  1631  	c.Check(s.errReport.dupSig, Equals, `canonical/1
  1632  repair canonical-1 revision 0 failed: exit status 1
  1633  output:
  1634  repair: canonical-1
  1635  revision: 0
  1636  summary: repair one
  1637  output:
  1638  unhappy output
  1639  `)
  1640  	c.Check(s.errReport.extra, DeepEquals, map[string]string{
  1641  		"Revision": "0",
  1642  		"RepairID": "1",
  1643  		"BrandID":  "canonical",
  1644  		"Status":   "retry",
  1645  	})
  1646  }
  1647  
  1648  func (s *runScriptSuite) TestRepairBasicSkip(c *C) {
  1649  	script := `#!/bin/sh
  1650  echo "other output"
  1651  echo "skip" >&$SNAP_REPAIR_STATUS_FD
  1652  exit 0
  1653  `
  1654  	s.seqRepairs = []string{makeMockRepair(script)}
  1655  	s.testScriptRun(c, script)
  1656  	// verify
  1657  	s.verifyRundir(c, []string{
  1658  		`^r0.script$`,
  1659  		`^r0.skip$`,
  1660  		`^work$`,
  1661  	})
  1662  	s.verifyOutput(c, "r0.skip", `repair: canonical-1
  1663  revision: 0
  1664  summary: repair one
  1665  output:
  1666  other output
  1667  `)
  1668  	verifyRepairStatus(c, repair.SkipStatus)
  1669  }
  1670  
  1671  func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) {
  1672  	script := `#!/bin/sh
  1673  if [ -f zzz-ran-once ]; then
  1674      echo "happy now"
  1675      echo "done" >&$SNAP_REPAIR_STATUS_FD
  1676      exit 0
  1677  fi
  1678  echo "unhappy output"
  1679  touch zzz-ran-once
  1680  exit 1
  1681  `
  1682  	s.seqRepairs = []string{makeMockRepair(script)}
  1683  	rpr := s.testScriptRun(c, script)
  1684  	s.verifyRundir(c, []string{
  1685  		`^r0.retry$`,
  1686  		`^r0.script$`,
  1687  		`^work$`,
  1688  	})
  1689  	s.verifyOutput(c, "r0.retry", `repair: canonical-1
  1690  revision: 0
  1691  summary: repair one
  1692  output:
  1693  unhappy output
  1694  
  1695  repair canonical-1 revision 0 failed: exit status 1`)
  1696  	verifyRepairStatus(c, repair.RetryStatus)
  1697  
  1698  	// run again, it will be happy this time
  1699  	err := rpr.Run()
  1700  	c.Assert(err, IsNil)
  1701  
  1702  	s.verifyRundir(c, []string{
  1703  		`^r0.done$`,
  1704  		`^r0.retry$`,
  1705  		`^r0.script$`,
  1706  		`^work$`,
  1707  	})
  1708  	s.verifyOutput(c, "r0.done", `repair: canonical-1
  1709  revision: 0
  1710  summary: repair one
  1711  output:
  1712  happy now
  1713  `)
  1714  	verifyRepairStatus(c, repair.DoneStatus)
  1715  }
  1716  
  1717  func (s *runScriptSuite) TestRepairHitsTimeout(c *C) {
  1718  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1719  	defer r1()
  1720  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1721  	defer r2()
  1722  
  1723  	restore := repair.MockDefaultRepairTimeout(100 * time.Millisecond)
  1724  	defer restore()
  1725  
  1726  	script := `#!/bin/sh
  1727  echo "output before timeout"
  1728  sleep 100
  1729  `
  1730  	s.seqRepairs = []string{makeMockRepair(script)}
  1731  	s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs)
  1732  
  1733  	rpr, err := s.runner.Next("canonical")
  1734  	c.Assert(err, IsNil)
  1735  
  1736  	err = rpr.Run()
  1737  	c.Assert(err, IsNil)
  1738  
  1739  	s.verifyRundir(c, []string{
  1740  		`^r0.retry$`,
  1741  		`^r0.script$`,
  1742  		`^work$`,
  1743  	})
  1744  	s.verifyOutput(c, "r0.retry", `repair: canonical-1
  1745  revision: 0
  1746  summary: repair one
  1747  output:
  1748  output before timeout
  1749  
  1750  repair canonical-1 revision 0 failed: repair did not finish within 100ms`)
  1751  	verifyRepairStatus(c, repair.RetryStatus)
  1752  }
  1753  
  1754  func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) {
  1755  	r1 := sysdb.InjectTrusted(s.storeSigning.Trusted)
  1756  	defer r1()
  1757  	r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey})
  1758  	defer r2()
  1759  
  1760  	script := `#!/bin/sh
  1761  echo PATH=$PATH
  1762  ls -l ${PATH##*:}/repair
  1763  `
  1764  	s.seqRepairs = []string{makeMockRepair(script)}
  1765  	s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs)
  1766  
  1767  	rpr, err := s.runner.Next("canonical")
  1768  	c.Assert(err, IsNil)
  1769  
  1770  	err = rpr.Run()
  1771  	c.Assert(err, IsNil)
  1772  
  1773  	c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileMatches, fmt.Sprintf(`(?ms).*^PATH=.*:.*/run/snapd/repair/tools.*`))
  1774  	c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileContains, `/repair -> /usr/lib/snapd/snap-repair`)
  1775  
  1776  	// run again and ensure no error happens
  1777  	err = rpr.Run()
  1778  	c.Assert(err, IsNil)
  1779  
  1780  }