github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap-recovery-chooser/main_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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  	"log/syslog"
    29  	"net/http"
    30  	"net/http/httptest"
    31  	"os"
    32  	"os/exec"
    33  	"path/filepath"
    34  	"testing"
    35  
    36  	. "gopkg.in/check.v1"
    37  
    38  	"github.com/snapcore/snapd/client"
    39  	main "github.com/snapcore/snapd/cmd/snap-recovery-chooser"
    40  	"github.com/snapcore/snapd/dirs"
    41  	"github.com/snapcore/snapd/logger"
    42  	"github.com/snapcore/snapd/testutil"
    43  )
    44  
    45  // Hook up check.v1 into the "go test" runner
    46  func Test(t *testing.T) { TestingT(t) }
    47  
    48  type baseCmdSuite struct {
    49  	testutil.BaseTest
    50  
    51  	stdout, stderr bytes.Buffer
    52  	markerFile     string
    53  }
    54  
    55  func (s *baseCmdSuite) SetUpTest(c *C) {
    56  	s.BaseTest.SetUpTest(c)
    57  	_, r := logger.MockLogger()
    58  	s.AddCleanup(r)
    59  	r = main.MockStdStreams(&s.stdout, &s.stderr)
    60  	s.AddCleanup(r)
    61  
    62  	d := c.MkDir()
    63  	s.markerFile = filepath.Join(d, "marker")
    64  	err := ioutil.WriteFile(s.markerFile, nil, 0644)
    65  	c.Assert(err, IsNil)
    66  }
    67  
    68  type cmdSuite struct {
    69  	baseCmdSuite
    70  }
    71  
    72  var _ = Suite(&cmdSuite{})
    73  
    74  var mockSystems = &main.ChooserSystems{
    75  	Systems: []client.System{
    76  		{
    77  			Label: "foo",
    78  			Actions: []client.SystemAction{
    79  				{Title: "reinstall", Mode: "install"},
    80  			},
    81  		},
    82  	},
    83  }
    84  
    85  func (s *cmdSuite) TestRunUIHappy(c *C) {
    86  	mockCmd := testutil.MockCommand(c, "tool", `
    87  echo '{}'
    88  `)
    89  	defer mockCmd.Restore()
    90  
    91  	rsp, err := main.RunUI(exec.Command(mockCmd.Exe()), mockSystems)
    92  	c.Assert(err, IsNil)
    93  	c.Assert(rsp, NotNil)
    94  }
    95  
    96  func (s *cmdSuite) TestRunUIBadJSON(c *C) {
    97  	mockCmd := testutil.MockCommand(c, "tool", `
    98  echo 'garbage'
    99  `)
   100  	defer mockCmd.Restore()
   101  
   102  	rsp, err := main.RunUI(exec.Command(mockCmd.Exe()), mockSystems)
   103  	c.Assert(err, ErrorMatches, "cannot decode response: .*")
   104  	c.Assert(rsp, IsNil)
   105  }
   106  
   107  func (s *cmdSuite) TestRunUIToolErr(c *C) {
   108  	mockCmd := testutil.MockCommand(c, "tool", `
   109  echo foo
   110  exit 22
   111  `)
   112  	defer mockCmd.Restore()
   113  
   114  	_, err := main.RunUI(exec.Command(mockCmd.Exe()), mockSystems)
   115  	c.Assert(err, ErrorMatches, "cannot collect output of the UI process: exit status 22")
   116  }
   117  
   118  func (s *cmdSuite) TestRunUIInputJSON(c *C) {
   119  	d := c.MkDir()
   120  	tf := filepath.Join(d, "json-input")
   121  	mockCmd := testutil.MockCommand(c, "tool", fmt.Sprintf(`
   122  cat > %s
   123  echo '{}'
   124  `, tf))
   125  	defer mockCmd.Restore()
   126  
   127  	_, err := main.RunUI(exec.Command(mockCmd.Exe()), mockSystems)
   128  	c.Assert(err, IsNil)
   129  
   130  	data, err := ioutil.ReadFile(tf)
   131  	c.Assert(err, IsNil)
   132  	var input *main.ChooserSystems
   133  	err = json.Unmarshal(data, &input)
   134  	c.Assert(err, IsNil)
   135  
   136  	c.Assert(input, DeepEquals, mockSystems)
   137  }
   138  
   139  func (s *cmdSuite) TestStdoutUI(c *C) {
   140  	var buf bytes.Buffer
   141  	err := main.OutputForUI(&buf, mockSystems)
   142  	c.Assert(err, IsNil)
   143  
   144  	var out *main.ChooserSystems
   145  
   146  	err = json.Unmarshal(buf.Bytes(), &out)
   147  	c.Assert(err, IsNil)
   148  	c.Assert(out, DeepEquals, mockSystems)
   149  }
   150  
   151  type mockedClientCmdSuite struct {
   152  	baseCmdSuite
   153  
   154  	config client.Config
   155  }
   156  
   157  var _ = Suite(&mockedClientCmdSuite{})
   158  
   159  func (s *mockedClientCmdSuite) SetUpTest(c *C) {
   160  	s.baseCmdSuite.SetUpTest(c)
   161  }
   162  
   163  func (s *mockedClientCmdSuite) RedirectClientToTestServer(handler func(http.ResponseWriter, *http.Request)) {
   164  	server := httptest.NewServer(http.HandlerFunc(handler))
   165  	s.BaseTest.AddCleanup(func() { server.Close() })
   166  	s.config.BaseURL = server.URL
   167  }
   168  
   169  type mockSystemRequestResponse struct {
   170  	label  string
   171  	code   int
   172  	reboot bool
   173  	expect map[string]interface{}
   174  }
   175  
   176  func (s *mockedClientCmdSuite) mockSuccessfulResponse(c *C, rspSystems *main.ChooserSystems, rspPostSystem *mockSystemRequestResponse) {
   177  	n := 0
   178  	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
   179  		switch n {
   180  		case 0:
   181  			c.Check(r.URL.Path, Equals, "/v2/systems")
   182  			err := json.NewEncoder(w).Encode(apiResponse{
   183  				Type:       "sync",
   184  				Result:     rspSystems,
   185  				StatusCode: 200,
   186  			})
   187  			c.Assert(err, IsNil)
   188  		case 1:
   189  			if rspPostSystem == nil {
   190  				c.Fatalf("unexpected request to %q", r.URL.Path)
   191  			}
   192  			c.Check(r.URL.Path, Equals, "/v2/systems/"+rspPostSystem.label)
   193  			c.Check(r.Method, Equals, "POST")
   194  
   195  			var data map[string]interface{}
   196  			err := json.NewDecoder(r.Body).Decode(&data)
   197  			c.Assert(err, IsNil)
   198  			c.Check(data, DeepEquals, rspPostSystem.expect)
   199  
   200  			rspType := "sync"
   201  			var rspData map[string]string
   202  			if rspPostSystem.code >= 400 {
   203  				rspType = "error"
   204  				rspData = map[string]string{"message": "failed in mock"}
   205  			}
   206  			var maintenance map[string]interface{}
   207  			if rspPostSystem.reboot {
   208  				maintenance = map[string]interface{}{
   209  					"kind":    client.ErrorKindSystemRestart,
   210  					"message": "system is restartring",
   211  				}
   212  			}
   213  			err = json.NewEncoder(w).Encode(apiResponse{
   214  				Type:        rspType,
   215  				Result:      rspData,
   216  				StatusCode:  rspPostSystem.code,
   217  				Maintenance: maintenance,
   218  			})
   219  			c.Assert(err, IsNil)
   220  		default:
   221  			c.Fatalf("expected to get 1 requests, now on %d", n+1)
   222  		}
   223  		n++
   224  	})
   225  }
   226  
   227  type apiResponse struct {
   228  	Type        string      `json:"type"`
   229  	Result      interface{} `json:"result"`
   230  	StatusCode  int         `json:"status-code"`
   231  	Maintenance interface{} `json:"maintenance"`
   232  }
   233  
   234  func (s *mockedClientCmdSuite) TestMainChooserWithTool(c *C) {
   235  	r := main.MockDefaultMarkerFile(s.markerFile)
   236  	defer r()
   237  	// sanity
   238  	c.Assert(s.markerFile, testutil.FilePresent)
   239  
   240  	capturedStdinPath := filepath.Join(c.MkDir(), "stdin")
   241  	mockCmd := testutil.MockCommand(c, "tool", fmt.Sprintf(`
   242  cat - > %s
   243  echo '{"label":"label","action":{"mode":"install","title":"reinstall"}}'
   244  `, capturedStdinPath))
   245  	defer mockCmd.Restore()
   246  	r = main.MockChooserTool(func() (*exec.Cmd, error) {
   247  		return exec.Command(mockCmd.Exe()), nil
   248  	})
   249  	defer r()
   250  
   251  	s.mockSuccessfulResponse(c, mockSystems, &mockSystemRequestResponse{
   252  		code:  200,
   253  		label: "label",
   254  		expect: map[string]interface{}{
   255  			"action": "do",
   256  			"mode":   "install",
   257  			"title":  "reinstall",
   258  		},
   259  		reboot: true,
   260  	})
   261  
   262  	rbt, err := main.Chooser(client.New(&s.config))
   263  	c.Assert(err, IsNil)
   264  	c.Assert(rbt, Equals, true)
   265  	c.Assert(mockCmd.Calls(), DeepEquals, [][]string{
   266  		{"tool"},
   267  	})
   268  
   269  	capturedStdin, err := ioutil.ReadFile(capturedStdinPath)
   270  	c.Assert(err, IsNil)
   271  	var stdoutSystems main.ChooserSystems
   272  	err = json.Unmarshal(capturedStdin, &stdoutSystems)
   273  	c.Assert(err, IsNil)
   274  	c.Check(&stdoutSystems, DeepEquals, mockSystems)
   275  
   276  	c.Assert(s.markerFile, testutil.FileAbsent)
   277  }
   278  
   279  func (s *mockedClientCmdSuite) TestMainChooserToolNotFound(c *C) {
   280  	r := main.MockDefaultMarkerFile(s.markerFile)
   281  	defer r()
   282  	// sanity
   283  	c.Assert(s.markerFile, testutil.FilePresent)
   284  
   285  	s.mockSuccessfulResponse(c, mockSystems, nil)
   286  
   287  	r = main.MockChooserTool(func() (*exec.Cmd, error) {
   288  		return nil, fmt.Errorf("tool not found")
   289  	})
   290  	defer r()
   291  
   292  	rbt, err := main.Chooser(client.New(&s.config))
   293  	c.Assert(err, ErrorMatches, "cannot locate the chooser UI tool: tool not found")
   294  	c.Assert(rbt, Equals, false)
   295  
   296  	c.Assert(s.markerFile, testutil.FileAbsent)
   297  }
   298  
   299  func (s *mockedClientCmdSuite) TestMainChooserBadAPI(c *C) {
   300  	r := main.MockDefaultMarkerFile(s.markerFile)
   301  	defer r()
   302  	// sanity
   303  	c.Assert(s.markerFile, testutil.FilePresent)
   304  
   305  	n := 0
   306  	s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
   307  		switch n {
   308  		case 0:
   309  			c.Check(r.URL.Path, Equals, "/v2/systems")
   310  			enc := json.NewEncoder(w)
   311  			err := enc.Encode(apiResponse{
   312  				Type: "error",
   313  				Result: map[string]string{
   314  					"message": "no systems for you",
   315  				},
   316  				StatusCode: 400,
   317  			})
   318  			c.Assert(err, IsNil)
   319  		default:
   320  			c.Fatalf("expected to get 1 requests, now on %d", n+1)
   321  		}
   322  		n++
   323  	})
   324  
   325  	rbt, err := main.Chooser(client.New(&s.config))
   326  	c.Assert(err, ErrorMatches, "cannot list recovery systems: no systems for you")
   327  	c.Assert(rbt, Equals, false)
   328  
   329  	c.Assert(s.markerFile, testutil.FileAbsent)
   330  }
   331  
   332  func (s *mockedClientCmdSuite) TestMainChooserDefaultsToConsoleConf(c *C) {
   333  	d := c.MkDir()
   334  	dirs.SetRootDir(d)
   335  	defer dirs.SetRootDir("/")
   336  
   337  	r := main.MockDefaultMarkerFile(s.markerFile)
   338  	defer r()
   339  	// sanity
   340  	c.Assert(s.markerFile, testutil.FilePresent)
   341  
   342  	s.mockSuccessfulResponse(c, mockSystems, &mockSystemRequestResponse{
   343  		code:  200,
   344  		label: "label",
   345  		expect: map[string]interface{}{
   346  			"action": "do",
   347  			"mode":   "install",
   348  			"title":  "reinstall",
   349  		},
   350  	})
   351  
   352  	mockCmd := testutil.MockCommand(c, filepath.Join(dirs.GlobalRootDir, "/usr/bin/console-conf"), `
   353  echo '{"label":"label","action":{"mode":"install","title":"reinstall"}}'
   354  `)
   355  	defer mockCmd.Restore()
   356  
   357  	rbt, err := main.Chooser(client.New(&s.config))
   358  	c.Assert(err, IsNil)
   359  	c.Assert(rbt, Equals, false)
   360  
   361  	c.Check(mockCmd.Calls(), DeepEquals, [][]string{
   362  		{"console-conf", "--recovery-chooser-mode"},
   363  	})
   364  
   365  	c.Assert(s.markerFile, testutil.FileAbsent)
   366  }
   367  
   368  func (s *mockedClientCmdSuite) TestMainChooserNoConsoleConf(c *C) {
   369  	d := c.MkDir()
   370  	dirs.SetRootDir(d)
   371  	defer dirs.SetRootDir("/")
   372  
   373  	r := main.MockDefaultMarkerFile(s.markerFile)
   374  	defer r()
   375  	// sanity
   376  	c.Assert(s.markerFile, testutil.FilePresent)
   377  
   378  	// not expecting a POST request
   379  	s.mockSuccessfulResponse(c, mockSystems, nil)
   380  
   381  	// tries to look up the console-conf binary but fails
   382  	rbt, err := main.Chooser(client.New(&s.config))
   383  	c.Assert(err, ErrorMatches, `cannot locate the chooser UI tool: chooser UI tool ".*/usr/bin/console-conf" does not exist`)
   384  	c.Assert(rbt, Equals, false)
   385  	c.Assert(s.markerFile, testutil.FileAbsent)
   386  }
   387  
   388  func (s *mockedClientCmdSuite) TestMainChooserGarbageNoActionRequested(c *C) {
   389  	d := c.MkDir()
   390  	dirs.SetRootDir(d)
   391  	defer dirs.SetRootDir("/")
   392  
   393  	r := main.MockDefaultMarkerFile(s.markerFile)
   394  	defer r()
   395  	// sanity
   396  	c.Assert(s.markerFile, testutil.FilePresent)
   397  
   398  	// not expecting a POST request
   399  	s.mockSuccessfulResponse(c, mockSystems, nil)
   400  
   401  	mockCmd := testutil.MockCommand(c, filepath.Join(dirs.GlobalRootDir, "/usr/bin/console-conf"), `
   402  echo 'garbage'
   403  `)
   404  	defer mockCmd.Restore()
   405  
   406  	rbt, err := main.Chooser(client.New(&s.config))
   407  	c.Assert(err, ErrorMatches, "UI process failed: cannot decode response: .*")
   408  	c.Assert(rbt, Equals, false)
   409  
   410  	c.Check(mockCmd.Calls(), DeepEquals, [][]string{
   411  		{"console-conf", "--recovery-chooser-mode"},
   412  	})
   413  
   414  	c.Assert(s.markerFile, testutil.FileAbsent)
   415  }
   416  
   417  func (s *mockedClientCmdSuite) TestMainChooserNoMarkerNoCalls(c *C) {
   418  	r := main.MockDefaultMarkerFile(s.markerFile + ".notfound")
   419  	defer r()
   420  
   421  	mockCmd := testutil.MockCommand(c, "tool", `
   422  exit 123
   423  `)
   424  	defer mockCmd.Restore()
   425  	r = main.MockChooserTool(func() (*exec.Cmd, error) {
   426  		return exec.Command(mockCmd.Exe()), nil
   427  	})
   428  	defer r()
   429  
   430  	rbt, err := main.Chooser(client.New(&s.config))
   431  	c.Assert(err, ErrorMatches, "cannot run chooser without the marker file")
   432  	c.Assert(rbt, Equals, false)
   433  
   434  	c.Assert(mockCmd.Calls(), HasLen, 0)
   435  }
   436  
   437  func (s *mockedClientCmdSuite) TestMainChooserSnapdAPIBad(c *C) {
   438  	r := main.MockDefaultMarkerFile(s.markerFile)
   439  	defer r()
   440  	// sanity
   441  	c.Assert(s.markerFile, testutil.FilePresent)
   442  
   443  	mockCmd := testutil.MockCommand(c, "tool", `
   444  echo '{"label":"label","action":{"mode":"install","title":"reinstall"}}'
   445  `)
   446  	defer mockCmd.Restore()
   447  	r = main.MockChooserTool(func() (*exec.Cmd, error) {
   448  		return exec.Command(mockCmd.Exe()), nil
   449  	})
   450  	defer r()
   451  
   452  	s.mockSuccessfulResponse(c, mockSystems, &mockSystemRequestResponse{
   453  		code:  400,
   454  		label: "label",
   455  		expect: map[string]interface{}{
   456  			"action": "do",
   457  			"mode":   "install",
   458  			"title":  "reinstall",
   459  		},
   460  	})
   461  
   462  	rbt, err := main.Chooser(client.New(&s.config))
   463  	c.Assert(err, ErrorMatches, "cannot request system action: .* failed in mock")
   464  	c.Assert(rbt, Equals, false)
   465  	c.Assert(mockCmd.Calls(), DeepEquals, [][]string{
   466  		{"tool"},
   467  	})
   468  
   469  	c.Assert(s.markerFile, testutil.FileAbsent)
   470  
   471  }
   472  
   473  type mockedSyslogCmdSuite struct {
   474  	baseCmdSuite
   475  
   476  	term string
   477  }
   478  
   479  var _ = Suite(&mockedSyslogCmdSuite{})
   480  
   481  func (s *mockedSyslogCmdSuite) SetUpTest(c *C) {
   482  	s.baseCmdSuite.SetUpTest(c)
   483  
   484  	s.term = os.Getenv("TERM")
   485  	s.AddCleanup(func() { os.Setenv("TERM", s.term) })
   486  
   487  	r := main.MockSyslogNew(func(p syslog.Priority, t string) (io.Writer, error) {
   488  		c.Fatal("not mocked")
   489  		return nil, fmt.Errorf("not mocked")
   490  	})
   491  	s.AddCleanup(r)
   492  }
   493  
   494  func (s *mockedSyslogCmdSuite) TestNoSyslogFallback(c *C) {
   495  	err := os.Setenv("TERM", "someterm")
   496  	c.Assert(err, IsNil)
   497  
   498  	called := false
   499  	r := main.MockSyslogNew(func(_ syslog.Priority, _ string) (io.Writer, error) {
   500  		called = true
   501  		return nil, fmt.Errorf("no syslog")
   502  	})
   503  	defer r()
   504  	err = main.LoggerWithSyslogMaybe()
   505  	c.Assert(err, IsNil)
   506  	c.Check(called, Equals, true)
   507  	// this likely goes to stderr
   508  	logger.Noticef("ping")
   509  }
   510  
   511  func (s *mockedSyslogCmdSuite) TestWithSyslog(c *C) {
   512  	err := os.Setenv("TERM", "someterm")
   513  	c.Assert(err, IsNil)
   514  
   515  	called := false
   516  	tag := ""
   517  	prio := syslog.Priority(0)
   518  	buf := bytes.Buffer{}
   519  	r := main.MockSyslogNew(func(p syslog.Priority, tg string) (io.Writer, error) {
   520  		tag = tg
   521  		prio = p
   522  		called = true
   523  		return &buf, nil
   524  	})
   525  	defer r()
   526  	err = main.LoggerWithSyslogMaybe()
   527  	c.Assert(err, IsNil)
   528  	c.Check(called, Equals, true)
   529  	c.Check(tag, Equals, "snap-recovery-chooser")
   530  	c.Check(prio, Equals, syslog.LOG_INFO|syslog.LOG_DAEMON)
   531  
   532  	logger.Noticef("ping")
   533  	c.Check(buf.String(), testutil.Contains, "ping")
   534  }
   535  
   536  func (s *mockedSyslogCmdSuite) TestSimple(c *C) {
   537  	err := os.Unsetenv("TERM")
   538  	c.Assert(err, IsNil)
   539  
   540  	r := main.MockSyslogNew(func(p syslog.Priority, tg string) (io.Writer, error) {
   541  		c.Fatalf("unexpected call")
   542  		return nil, fmt.Errorf("unexpected call")
   543  	})
   544  	defer r()
   545  	err = main.LoggerWithSyslogMaybe()
   546  	c.Assert(err, IsNil)
   547  }