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 }